From ed99c06c8262ccdf3d693a8d6af26c24cab98003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?= Date: Sat, 3 May 2025 13:54:16 +0200 Subject: [PATCH 1/2] POC for serving the markdown pages (useful for LLMs) --- app/api.ts | 7 ++ .../$version.docs.framework.$framework.$.tsx | 16 +++- .../$version.docs.framework.$framework.$.ts | 38 ++++++++ app/utils/docs.ts | 89 +++++++++++-------- 4 files changed, 111 insertions(+), 39 deletions(-) create mode 100644 app/api.ts create mode 100644 app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts diff --git a/app/api.ts b/app/api.ts new file mode 100644 index 00000000..e81a9e72 --- /dev/null +++ b/app/api.ts @@ -0,0 +1,7 @@ +// app/api.ts +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from '@tanstack/start/api' + +export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/app/routes/$libraryId/$version.docs.framework.$framework.$.tsx b/app/routes/$libraryId/$version.docs.framework.$framework.$.tsx index 8f2c6d07..0683f5af 100644 --- a/app/routes/$libraryId/$version.docs.framework.$framework.$.tsx +++ b/app/routes/$libraryId/$version.docs.framework.$framework.$.tsx @@ -1,5 +1,5 @@ import { seo } from '~/utils/seo' -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' import { Doc } from '~/components/Doc' import { loadDocs } from '~/utils/docs' import { getBranch, getLibrary } from '~/libraries' @@ -15,6 +15,20 @@ export const Route = createFileRoute( const library = getLibrary(libraryId) + const isMarkdown = !!docsPath?.endsWith('.md') + + if (isMarkdown) { + const href = + '/api/md' + + ctx.location.pathname.slice( + 0, + ctx.location.pathname.length - '.md'.length + ) + throw redirect({ + href, + }) + } + return loadDocs({ repo: library.repo, branch: getBranch(library, version), diff --git a/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts b/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts new file mode 100644 index 00000000..98aa7567 --- /dev/null +++ b/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts @@ -0,0 +1,38 @@ +import { createAPIRoute } from '@tanstack/start/api' +import { getBranch, getLibrary } from '~/libraries' +import { loadDocs } from '~/utils/docs' + +export const APIRoute = createAPIRoute('/api/md/$libraryId/$version/docs/framework/$framework/$')({ + GET: async ({ params, request }) => { + const { libraryId, version, framework, _splat: docsPath } = params + const library = getLibrary(libraryId) + + const location = new URL(request.url) + + const loadDocsArgs = { + repo: library.repo, + branch: getBranch(library, version), + docsPath: `${ + library.docsRoot || 'docs' + }/framework/${framework}/${docsPath}`, + currentPath: location.pathname.slice('/api/md'.length), + redirectPath: `/${library.id}/${version}/docs/overview`, + useServerFn: false + } + + const res = await loadDocs(loadDocsArgs) + + + const { content, description, title} = res + + // Generate or fetch the Markdown content dynamically + const markdownContent = `# ${title}\n\n> ${description}\n\n${content}` + + // Return the Markdown content as a response + return new Response(markdownContent, { + headers: { + 'Content-Type': 'text/markdown', + }, + }) + }, +}) diff --git a/app/utils/docs.ts b/app/utils/docs.ts index 56dbb937..243c6a4e 100644 --- a/app/utils/docs.ts +++ b/app/utils/docs.ts @@ -9,18 +9,27 @@ import { createServerFn } from '@tanstack/start' import { z } from 'zod' import { setHeader } from 'vinxi/http' +const FetchDocsDataSchema = z.object({ + repo: z.string(), + branch: z.string(), + filePath: z.string(), +}) +type FetchDocsData = z.infer + export const loadDocs = async ({ repo, branch, // currentPath, // redirectPath, docsPath, + useServerFn = true, }: { repo: string branch: string docsPath: string currentPath: string redirectPath: string + useServerFn?: boolean }) => { if (!branch) { throw new Error('Invalid branch') @@ -32,55 +41,59 @@ export const loadDocs = async ({ const filePath = `${docsPath}.md` - return await fetchDocs({ + const params: { data: FetchDocsData } = { data: { repo, branch, filePath, - // currentPath, - // redirectPath, }, - }) + } + + return useServerFn ? fetchDocs(params) : fetchDocsFunction(params) } -export const fetchDocs = createServerFn({ method: 'GET' }) - .validator( - z.object({ repo: z.string(), branch: z.string(), filePath: z.string() }) - ) - .handler(async ({ data: { repo, branch, filePath } }) => { - const file = await fetchRepoFile(repo, branch, filePath) +const fetchDocsFunction = async ({ + data: { repo, branch, filePath }, +}: { + data: FetchDocsData +}) => { + const file = await fetchRepoFile(repo, branch, filePath) + + if (!file) { + throw notFound() + // if (currentPath === redirectPath) { + // // console.log('not found') + // throw notFound() + // } else { + // // console.log('redirect') + // throw redirect({ + // to: redirectPath, + // }) + // } + } - if (!file) { - throw notFound() - // if (currentPath === redirectPath) { - // // console.log('not found') - // throw notFound() - // } else { - // // console.log('redirect') - // throw redirect({ - // to: redirectPath, - // }) - // } - } + const frontMatter = extractFrontMatter(file) + const description = removeMarkdown(frontMatter.excerpt ?? '') - const frontMatter = extractFrontMatter(file) - const description = removeMarkdown(frontMatter.excerpt ?? '') + // Cache for 5 minutes on shared cache + // Revalidate in the background + setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + setHeader( + 'CDN-Cache-Control', + 'max-age=300, stale-while-revalidate=300, durable' + ) - // Cache for 5 minutes on shared cache - // Revalidate in the background - setHeader('Cache-Control', 'public, max-age=0, must-revalidate') - setHeader( - 'CDN-Cache-Control', - 'max-age=300, stale-while-revalidate=300, durable' - ) + return { + title: frontMatter.data?.title, + description, + filePath, + content: frontMatter.content, + } +} - return { - title: frontMatter.data?.title, - description, - filePath, - content: frontMatter.content, - } - }) +export const fetchDocs = createServerFn({ method: 'GET' }) + .validator(FetchDocsDataSchema) + .handler(fetchDocsFunction) export const fetchFile = createServerFn({ method: 'GET' }) .validator( From f1d6aa4b7879dde550ca7c7d4f56909b1f14030e Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 7 May 2025 14:29:07 +1200 Subject: [PATCH 2/2] add `Content-Disposition` headers --- .../api/md/$libraryId/$version.docs.framework.$framework.$.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts b/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts index 98aa7567..6ce78ac9 100644 --- a/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts +++ b/app/routes/api/md/$libraryId/$version.docs.framework.$framework.$.ts @@ -22,16 +22,18 @@ export const APIRoute = createAPIRoute('/api/md/$libraryId/$version/docs/framewo const res = await loadDocs(loadDocsArgs) - const { content, description, title} = res // Generate or fetch the Markdown content dynamically const markdownContent = `# ${title}\n\n> ${description}\n\n${content}` + const filename = (docsPath || 'file').split('/').join('-') + // Return the Markdown content as a response return new Response(markdownContent, { headers: { 'Content-Type': 'text/markdown', + 'Content-Disposition': `inline; filename="${filename}.md"`, }, }) },