diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 717472b6..a6223bba 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -2,6 +2,11 @@ import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs', + docs: { + postprocess: { + includeProcessedMarkdown: true, + }, + }, }); export default defineConfig(); diff --git a/apps/docs/src/components/page-actions.tsx b/apps/docs/src/components/page-actions.tsx new file mode 100644 index 00000000..5b6d3c03 --- /dev/null +++ b/apps/docs/src/components/page-actions.tsx @@ -0,0 +1,61 @@ +import { buttonVariants } from 'fumadocs-ui/components/ui/button'; +import { CheckIcon, CopyIcon, ExternalLinkIcon, PencilIcon } from 'lucide-react'; +import { useState } from 'react'; + +interface PageActionsProps { + markdownUrl: string; + githubUrl: string; +} + +export function PageActions({ markdownUrl, githubUrl }: PageActionsProps) { + return ( +
+ + + + View as Markdown + + + + Edit on GitHub + +
+ ); +} + +function CopyMarkdownButton({ markdownUrl }: { markdownUrl: string }) { + const [copied, setCopied] = useState(false); + + const onCopy = async () => { + try { + const res = await fetch(markdownUrl); + const text = await res.text(); + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // swallow — clipboard or fetch can fail in restrictive contexts + } + }; + + return ( + + ); +} diff --git a/apps/docs/src/lib/get-llm-text.ts b/apps/docs/src/lib/get-llm-text.ts new file mode 100644 index 00000000..bbb76b12 --- /dev/null +++ b/apps/docs/src/lib/get-llm-text.ts @@ -0,0 +1,7 @@ +import type { InferPageType } from 'fumadocs-core/source'; +import type { source } from './source'; + +export async function getLLMText(page: InferPageType) { + const processed = await page.data.getText('processed'); + return `# ${page.data.title} (${page.url})\n\n${processed}`; +} diff --git a/apps/docs/src/routes/docs/$.tsx b/apps/docs/src/routes/docs/$.tsx index 3e222c75..7cae02a5 100644 --- a/apps/docs/src/routes/docs/$.tsx +++ b/apps/docs/src/routes/docs/$.tsx @@ -10,10 +10,13 @@ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layo import defaultMdxComponents from 'fumadocs-ui/mdx'; import { Suspense } from 'react'; import browserCollections from '../../../.source/browser'; +import { PageActions } from '../../components/page-actions'; import { baseOptions } from '../../lib/layout.shared'; import { source } from '../../lib/source'; import { getStoryPayloads } from '../../lib/story'; +const GITHUB_DOCS_URL = 'https://github.com/lukebennett88/luke-ui/blob/main/apps/docs/content/docs'; + const storyModules = import.meta.glob<{ story: Story }>('../../*/*.story.tsx', { eager: true }); const clientStoryModules = import.meta.glob<{ storyClient: StoryClient }>( '../../*/*.story.client.tsx', @@ -50,7 +53,14 @@ const loader = createServerFn({ const page = source.getPage(slugs); if (!page) throw notFound(); + // Index pages (page.slugs === []) map to /llms.mdx/docs/index.md so that + // every prerendered file lives under /llms.mdx/docs/.md and the + // route's required `.md` suffix matches. + const markdownSlug = page.slugs.length > 0 ? page.slugs.join('/') : 'index'; + return { + githubUrl: `${GITHUB_DOCS_URL}/${page.path}`, + markdownUrl: `/llms.mdx/docs/${markdownSlug}.md`, pageTree: await source.serializePageTree(source.getPageTree()), path: page.path, storyPayloads: await getStoryPayloads(stories), @@ -62,13 +72,17 @@ const clientLoader = browserCollections.docs.createClientLoader({ { toc, frontmatter, default: MDX }, props: { className?: string; + githubUrl: string; + markdownUrl: string; }, ) { + const { githubUrl, markdownUrl, ...pageProps } = props; return ( - + {frontmatter.title} {frontmatter.description} + {clientLoader.useContent(data.path, { className: '', + githubUrl: data.githubUrl, + markdownUrl: data.markdownUrl, })} diff --git a/apps/docs/src/routes/llms-full[.]txt.ts b/apps/docs/src/routes/llms-full[.]txt.ts new file mode 100644 index 00000000..dc9d6dc5 --- /dev/null +++ b/apps/docs/src/routes/llms-full[.]txt.ts @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { getLLMText } from '../lib/get-llm-text'; +import { source } from '../lib/source'; + +export const Route = createFileRoute('/llms-full.txt')({ + server: { + handlers: { + GET: async () => { + const scanned = await Promise.all(source.getPages().map(getLLMText)); + return new Response(scanned.join('\n\n'), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); + }, + }, + }, +}); diff --git a/apps/docs/src/routes/llms[.]mdx/docs/$.ts b/apps/docs/src/routes/llms[.]mdx/docs/$.ts new file mode 100644 index 00000000..9ab4347d --- /dev/null +++ b/apps/docs/src/routes/llms[.]mdx/docs/$.ts @@ -0,0 +1,27 @@ +import { createFileRoute, notFound } from '@tanstack/react-router'; +import { getLLMText } from '../../../lib/get-llm-text'; +import { source } from '../../../lib/source'; + +export const Route = createFileRoute('/llms.mdx/docs/$')({ + server: { + handlers: { + GET: async ({ params }) => { + // Splat captures e.g. `index.md`, `getting-started.md`, or + // `components/actions/button.md`. We require the trailing `.md` so the + // prerendered file gets a markdown extension on disk and is served as + // text/plain by static hosts. The docs index lives at `index.md`. + const splat = params._splat ?? ''; + if (!splat.endsWith('.md')) throw notFound(); + const segments = splat.slice(0, -3).split('/').filter(Boolean); + const slugs = segments.length === 1 && segments[0] === 'index' ? [] : segments; + + const page = source.getPage(slugs); + if (!page) throw notFound(); + + return new Response(await getLLMText(page), { + headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, + }); + }, + }, + }, +}); diff --git a/apps/docs/src/routes/llms[.]txt.ts b/apps/docs/src/routes/llms[.]txt.ts new file mode 100644 index 00000000..c2adbdf0 --- /dev/null +++ b/apps/docs/src/routes/llms[.]txt.ts @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { source } from '../lib/source'; + +export const Route = createFileRoute('/llms.txt')({ + server: { + handlers: { + GET: () => { + const lines = ['# Luke UI Docs', '', '> Documentation for the @luke-ui/react package.', '']; + + for (const page of source.getPages()) { + const description = page.data.description ? `: ${page.data.description}` : ''; + lines.push(`- [${page.data.title}](${page.url})${description}`); + } + + return new Response(`${lines.join('\n')}\n`, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); + }, + }, + }, +}); diff --git a/apps/docs/vite.config.ts b/apps/docs/vite.config.ts index 51c5f38d..27dc1480 100644 --- a/apps/docs/vite.config.ts +++ b/apps/docs/vite.config.ts @@ -85,7 +85,7 @@ export default defineConfig(async () => ({ enabled: true, crawlLinks: true, }, - pages: [{ path: '/api/search' }], + pages: [{ path: '/api/search' }, { path: '/llms.txt' }, { path: '/llms-full.txt' }], }), react(), nitro(),