diff --git a/CLAUDE.md b/CLAUDE.md index cf27304..6ef0354 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ export const metadata = { } ``` -3. Add navigation entry in `/src/components/Navigation.jsx` +3. Add navigation entry in `/src/lib/navigation.ts` ### MDX Page Structure @@ -124,7 +124,7 @@ Store in `/public/images/` and reference as: ### Navigation -Navigation structure is defined in `/src/components/Navigation.jsx`. Each section contains an array of pages with `title` and `href`. +Navigation structure is defined in `/src/lib/navigation.ts` and rendered by `/src/components/Navigation.tsx`. Each section contains an array of pages with `title` and `href`. ### Search Index @@ -160,7 +160,7 @@ The documentation site uses a sophisticated MDX processing pipeline: The navigation is centrally managed through several interconnected components: -1. **Navigation Data**: Defined in `/src/components/Navigation.jsx` as a static structure +1. **Navigation Data**: Defined in `/src/lib/navigation.ts` as a static structure, shared by the sidebar, search, footer and the Markdown/llms.txt routes 2. **Dynamic Sections**: The `layout.jsx` file extracts sections from all MDX files at build time 3. **Section Provider**: Uses Zustand to manage active section state for smooth scrolling 4. **Mobile/Desktop Split**: Separate navigation components for responsive design @@ -182,7 +182,7 @@ Documentation follows a hierarchical structure: 2. Use two-column layout with `` and `` 3. Document properties with `` component 4. Include practical code examples -5. Add to navigation in `/src/components/Navigation.jsx` +5. Add to navigation in `/src/lib/navigation.ts` ### Updating Existing Documentation @@ -206,5 +206,5 @@ Documentation follows a hierarchical structure: - Keep code examples practical and directly runnable in Inkdrop - The site is deployed at https://developers.inkdrop.app/ - All MDX files are automatically indexed for search during the build process -- Navigation is centrally managed in `/src/components/Navigation.jsx` +- Navigation is centrally managed in `/src/lib/navigation.ts` diff --git a/next.config.mjs b/next.config.mjs index 7072ebb..4a29ba7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,6 +23,13 @@ const nextConfig = { outputFileTracingRoot: __dirname, outputFileTracingIncludes: { '/**/*': ['./src/app/**/*.mdx'] + }, + // Serve a plain Markdown version of every page at `.md` for agents + async rewrites() { + return [ + { source: '/index.md', destination: '/raw-md' }, + { source: '/:path+.md', destination: '/raw-md/:path+' } + ] } } diff --git a/src/app/api/markdown/route.ts b/src/app/api/markdown/route.ts deleted file mode 100644 index 016f5f0..0000000 --- a/src/app/api/markdown/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readFile } from 'node:fs/promises' -import path from 'node:path' - -import { mdxToMarkdown } from '@/lib/mdxToMarkdown' - -const APP_DIR = path.join(process.cwd(), 'src', 'app') - -/** Normalizes the `path` query param to a clean absolute page path like `/states/db`. */ -function normalizePagePath(raw: string | null): string | null { - if (raw == null || raw === '' || raw === '/') return '/' - // Allow only safe characters and reject any traversal attempt. - if (!/^\/[\w\-/]*$/.test(raw) || raw.includes('..')) return null - return raw.replace(/\/+$/, '') || '/' -} - -/** Resolves a page path to its `page.mdx` file, ensuring it stays within src/app. */ -function resolveMdxFile(pagePath: string): string | null { - const relative = pagePath === '/' ? 'page.mdx' : `${pagePath.slice(1)}/page.mdx` - const filePath = path.join(APP_DIR, relative) - if (filePath !== APP_DIR && !filePath.startsWith(APP_DIR + path.sep)) { - return null - } - return filePath -} - -export async function GET(request: Request): Promise { - const url = new URL(request.url) - const pagePath = normalizePagePath(url.searchParams.get('path')) - - if (!pagePath) { - return new Response('Invalid path', { status: 400 }) - } - - const filePath = resolveMdxFile(pagePath) - if (!filePath) { - return new Response('Invalid path', { status: 400 }) - } - - let source: string - try { - source = await readFile(filePath, 'utf8') - } catch { - return new Response('Not found', { status: 404 }) - } - - try { - const markdown = await mdxToMarkdown(source, { origin: url.origin, pagePath }) - return new Response(markdown, { - status: 200, - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Cache-Control': 'public, max-age=0, s-maxage=3600' - } - }) - } catch (error) { - console.error('Failed to convert MDX to Markdown:', error) - return new Response('Failed to render Markdown', { status: 500 }) - } -} diff --git a/src/app/llms-full.txt/route.ts b/src/app/llms-full.txt/route.ts new file mode 100644 index 0000000..1d99458 --- /dev/null +++ b/src/app/llms-full.txt/route.ts @@ -0,0 +1,9 @@ +import { getLlmsFullTxt } from '@/lib/markdown' + +export const dynamic = 'force-static' + +export async function GET() { + return new Response(await getLlmsFullTxt(), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }) +} diff --git a/src/app/llms.txt/route.ts b/src/app/llms.txt/route.ts new file mode 100644 index 0000000..f65c8dc --- /dev/null +++ b/src/app/llms.txt/route.ts @@ -0,0 +1,9 @@ +import { getLlmsTxt } from '@/lib/markdown' + +export const dynamic = 'force-static' + +export async function GET() { + return new Response(await getLlmsTxt(), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }) +} diff --git a/src/app/raw-md/[[...slug]]/route.ts b/src/app/raw-md/[[...slug]]/route.ts new file mode 100644 index 0000000..966742c --- /dev/null +++ b/src/app/raw-md/[[...slug]]/route.ts @@ -0,0 +1,23 @@ +import { getMarkdownSlugs, getPageMarkdown } from '@/lib/markdown' + +// Serves each docs page as plain Markdown, e.g. `/states/db.md`. +// Requests are rewritten here from `*.md` URLs in next.config.mjs. +export const dynamic = 'force-static' +export const dynamicParams = false + +export async function generateStaticParams() { + let slugs = await getMarkdownSlugs() + return slugs.map(slug => ({ slug })) +} + +export async function GET( + _request: Request, + { params }: { params: Promise<{ slug?: Array }> } +) { + let { slug = [] } = await params + let markdown = await getPageMarkdown(slug) + + return new Response(markdown, { + headers: { 'Content-Type': 'text/markdown; charset=utf-8' } + }) +} diff --git a/src/components/CopyMarkdownButton.tsx b/src/components/CopyMarkdownButton.tsx deleted file mode 100644 index 96240a4..0000000 --- a/src/components/CopyMarkdownButton.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { usePathname } from 'next/navigation' -import clsx from 'clsx' - -function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - ) -} - -type CopyState = 'idle' | 'copied' | 'error' - -export function CopyMarkdownButton() { - const pathname = usePathname() - const [state, setState] = useState('idle') - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (state === 'idle') return - const timeout = setTimeout(() => setState('idle'), 2000) - return () => clearTimeout(timeout) - }, [state]) - - async function handleCopy() { - if (loading) return - setLoading(true) - try { - const response = await fetch( - `/api/markdown?path=${encodeURIComponent(pathname)}` - ) - if (!response.ok) throw new Error(`Request failed: ${response.status}`) - const markdown = await response.text() - await window.navigator.clipboard.writeText(markdown) - setState('copied') - } catch (error) { - console.error('Failed to copy Markdown:', error) - setState('error') - } finally { - setLoading(false) - } - } - - const label = - state === 'copied' ? 'Copied!' : state === 'error' ? 'Failed' : 'Copy as Markdown' - - return ( - - ) -} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 6a8da52..ac8b3fb 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import { Button } from '@/components/Button' -import { navigation } from '@/components/Navigation' +import { navigation } from '@/lib/navigation' function PageLink({ label, diff --git a/src/components/Guides.tsx b/src/components/Guides.tsx index 65e0aad..03b6b91 100644 --- a/src/components/Guides.tsx +++ b/src/components/Guides.tsx @@ -1,29 +1,6 @@ import { Button } from '@/components/Button' import { Heading } from '@/components/Heading' - -const guides = [ - { - href: '/guides/create-a-theme', - name: 'Create a theme', - description: 'A tutorial on how to create a theme.' - }, - { - href: '/guides/plugin-word-count', - name: 'Create a plugin', - description: 'A tutorial on how to create a plugin.' - }, - { - href: '/guides/access-the-local-database', - name: 'Access the local database', - description: - 'Learn how to access the local database from plugins and external programs.' - }, - { - href: '/guides/create-a-blog-using-astro', - name: 'Create a blog using Astro', - description: 'A tutorial on how to create a blog using Astro.' - } -] +import { guides } from '@/lib/guides' export function Guides() { return ( diff --git a/src/components/ListNavigationSection.tsx b/src/components/ListNavigationSection.tsx index d9d93b4..19efcee 100644 --- a/src/components/ListNavigationSection.tsx +++ b/src/components/ListNavigationSection.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' -import { navigation } from '@/components/Navigation' +import { navigation } from '@/lib/navigation' export function ListNavigationSection() { const currentPath = usePathname() diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 58e7a61..a534bc1 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -11,17 +11,9 @@ import { Button } from '@/components/Button' import { useIsInsideMobileNavigation } from '@/components/MobileNavigation' import { useSectionStore } from '@/components/SectionProvider' import { Tag } from '@/components/Tag' +import { navigation, type NavGroup } from '@/lib/navigation' import { remToPx } from '@/lib/remToPx' -interface NavGroup { - title: string - link?: string - links: Array<{ - title: string - href: string - }> -} - function useInitialValue(value: T, condition = true) { let initialValue = useRef(value).current return condition ? initialValue : value @@ -227,149 +219,6 @@ function NavigationGroup({ ) } -export const navigation: Array = [ - { - title: 'Guides', - links: [ - { title: 'Introduction', href: '/' }, - { title: 'The init file', href: '/guides/the-init-file' }, - { title: 'Style tweaks', href: '/guides/style-tweaks' }, - { title: 'Create a theme', href: '/guides/create-a-theme' }, - { title: 'Plugin: Word count', href: '/guides/plugin-word-count' }, - { - title: 'Getting & modifying the app state', - href: '/guides/flux-architecture' - }, - { - title: 'Access the local database', - href: '/guides/access-the-local-database' - }, - { - title: 'Integrate with external programs', - href: '/guides/integrate-with-external-programs' - }, - { title: 'Customize the editor', href: '/guides/customize-the-editor' }, - { title: 'Extend the UI', href: '/guides/extend-the-ui' }, - { - title: 'Create a note template', - href: '/guides/create-a-note-template' - }, - { - title: 'Add styles for text annotations', - href: '/guides/text-annotations' - }, - { - title: 'Create a blog using Astro', - href: '/guides/create-a-blog-using-astro' - }, - { - title: 'Use ES modules in your plugin', - href: '/guides/es-modules' - }, - { - title: 'List of commands', - href: '/guides/list-of-commands' - } - ] - }, - { - title: 'Resources', - links: [ - { title: 'Notes', href: '/data-access/notes' }, - { title: 'Notebooks', href: '/data-access/books' }, - { title: 'Tags', href: '/data-access/tags' }, - { title: 'Files', href: '/data-access/files' }, - { title: 'Utilities', href: '/data-access/utils' }, - { title: 'Events', href: '/data-access/events' }, - { title: 'Local HTTP Server', href: '/data-access/local-http-server' } - ] - }, - { - title: 'Core Modules', - links: [ - { title: 'Config', href: '/modules/config' }, - { title: 'Command Registry', href: '/modules/command-registry' }, - { title: 'Context Menu Manager', href: '/modules/context-menu-manager' }, - { title: 'Component Manager', href: '/modules/component-manager' }, - { title: 'Environment', href: '/modules/environment' }, - { title: 'Keymap Manager', href: '/modules/keymap-manager' }, - { title: 'Layout Manager', href: '/modules/layout-manager' }, - { title: 'Markdown Renderer', href: '/modules/markdown-renderer' }, - { title: 'Markdown Editor', href: '/modules/markdown-editor' }, - { title: 'Menu Manager', href: '/modules/menu-manager' }, - { title: 'Notification', href: '/modules/notification' }, - { title: 'Notification Manager', href: '/modules/notification-manager' }, - { title: 'Package', href: '/modules/package' }, - { title: 'Package Manager', href: '/modules/package-manager' }, - { title: 'Style Manager', href: '/modules/style-manager' }, - { title: 'Telescope Manager', href: '/modules/telescope-manager' }, - { title: 'Theme Manager', href: '/modules/theme-manager' } - ] - }, - { - title: 'App States', - link: '/states', - links: [ - { title: 'Notebooks', href: '/states/books' }, - { title: 'Database', href: '/states/db' }, - { title: 'Editing note', href: '/states/editing-note' }, - { title: 'Editor', href: '/states/editor' }, - { title: 'Layouts', href: '/states/layouts' }, - { title: 'Local config', href: '/states/local-config' }, - { title: 'Main layout', href: '/states/main-layout' }, - { title: 'Note list bar', href: '/states/note-list-bar' }, - { title: 'Notes', href: '/states/notes' }, - { title: 'Preview', href: '/states/preview' }, - { title: 'Query context', href: '/states/query-context' }, - { title: 'Tags', href: '/states/tags' }, - { title: 'Stats', href: '/states/stats' } - ] - }, - { - title: 'App Actions', - link: '/actions', - links: [ - { title: 'Editing note', href: '/actions/editing-note' }, - { title: 'Editor', href: '/actions/editor' } - ] - }, - { - title: 'Components', - links: [ - { title: 'Modal', href: '/components/modal' }, - { title: 'Dialog', href: '/components/dialog' }, - { title: 'Button', href: '/components/button' }, - { title: 'CommandButton', href: '/components/command-button' }, - { title: 'NotebookListBar', href: '/components/notebook-list-bar' }, - { title: 'MessageDialog', href: '/components/message-dialog' } - ] - }, - { - title: 'Event Subscription (event-kit)', - links: [ - { title: 'Disposable', href: '/event-subscription/disposable' }, - { - title: 'Composite Disposable', - href: '/event-subscription/composite-disposable' - }, - { title: 'Emitter', href: '/event-subscription/emitter' } - ] - }, - { - title: 'Appendix', - links: [ - { - title: 'Migration Guide from v5 to v6', - href: '/appendix/plugin-migration-from-v5-to-v6' - }, - { - title: 'Migration Guide from v3 to v4', - href: '/appendix/plugin-migration-from-v3-to-v4' - } - ] - } -] - export function Navigation(props: React.ComponentPropsWithoutRef<'nav'>) { return (