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 (
+
+ );
+}
+
+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(),