From 8c704e94fe725d0c3524fdff5fd2fda6e90d7691 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 14 Jan 2026 22:52:32 +0100 Subject: [PATCH 01/24] Add LLM affordances based on Fuma docs (will not work yet) --- packages/documentation/source.config.ts | 34 +++-- .../src/components/not-found.tsx | 10 +- .../documentation/src/lib/get-llm-text.ts | 10 ++ .../documentation/src/lib/layout.shared.tsx | 4 +- packages/documentation/src/lib/source.ts | 8 +- .../src/nextjs-proxy-ts-broken.ts | 19 +++ packages/documentation/src/routeTree.gen.ts | 122 +++++++++--------- packages/documentation/src/router.tsx | 8 +- packages/documentation/src/routes/__root.tsx | 23 ++-- packages/documentation/src/routes/docs/$.tsx | 33 +++-- packages/documentation/src/routes/index.tsx | 14 +- .../src/routes/llms-full[.]txt.ts | 15 +++ .../src/routes/llms[.]mdx.docs.$.ts | 19 +++ packages/documentation/src/start.ts | 25 ++++ packages/documentation/todo.md | 1 - 15 files changed, 226 insertions(+), 119 deletions(-) create mode 100644 packages/documentation/src/lib/get-llm-text.ts create mode 100644 packages/documentation/src/nextjs-proxy-ts-broken.ts create mode 100644 packages/documentation/src/routes/llms-full[.]txt.ts create mode 100644 packages/documentation/src/routes/llms[.]mdx.docs.$.ts create mode 100644 packages/documentation/src/start.ts diff --git a/packages/documentation/source.config.ts b/packages/documentation/source.config.ts index dc89622a..b53a9c9c 100644 --- a/packages/documentation/source.config.ts +++ b/packages/documentation/source.config.ts @@ -1,10 +1,15 @@ -import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; -import { transformerTwoslash } from 'fumadocs-twoslash'; -import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; -import rehypeMermaid, { type RehypeMermaidOptions } from 'rehype-mermaid'; +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; +import { transformerTwoslash } from "fumadocs-twoslash"; +import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; +import rehypeMermaid, { type RehypeMermaidOptions } from "rehype-mermaid"; export const docs = defineDocs({ - dir: 'content/docs', + dir: "content/docs", + docs: { + postprocess: { + includeProcessedMarkdown: true, + }, + }, }); export default defineConfig({ @@ -13,11 +18,14 @@ export default defineConfig({ rehypePlugins: (plugins) => [mermaidConfig(), ...plugins], rehypeCodeOptions: { themes: { - light: 'github-light', - dark: 'github-dark', + light: "github-light", + dark: "github-dark", }, - transformers: [...(rehypeCodeDefaultOptions.transformers ?? []), transformerTwoslash()], - langs: ['js', 'jsx', 'ts', 'tsx'], + transformers: [ + ...(rehypeCodeDefaultOptions.transformers ?? []), + transformerTwoslash(), + ], + langs: ["js", "jsx", "ts", "tsx"], }, }, }); @@ -27,11 +35,11 @@ function mermaidConfig(): [typeof rehypeMermaid, RehypeMermaidOptions] { rehypeMermaid, { mermaidConfig: { - fontFamily: 'var(--font-sans)', - theme: 'neutral', - look: 'classic', + fontFamily: "var(--font-sans)", + theme: "neutral", + look: "classic", flowchart: { - defaultRenderer: 'elk', + defaultRenderer: "elk", padding: 6, }, themeCSS: ` diff --git a/packages/documentation/src/components/not-found.tsx b/packages/documentation/src/components/not-found.tsx index 04d2267f..d54fb5e2 100644 --- a/packages/documentation/src/components/not-found.tsx +++ b/packages/documentation/src/components/not-found.tsx @@ -1,11 +1,11 @@ -import { Link } from '@tanstack/react-router'; -import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { Link } from "@tanstack/react-router"; +import { HomeLayout } from "fumadocs-ui/layouts/home"; export function NotFound() { return ( @@ -13,8 +13,8 @@ export function NotFound() {

404

Page Not Found

- The page you are looking for might have been removed, had its name changed, or is - temporarily unavailable. + The page you are looking for might have been removed, had its name + changed, or is temporarily unavailable.

) { + const processed = await page.data.getText("processed"); + + return `# ${page.data.title} (${page.url}) + +${processed}`; +} diff --git a/packages/documentation/src/lib/layout.shared.tsx b/packages/documentation/src/lib/layout.shared.tsx index 6131ac6e..63bed523 100644 --- a/packages/documentation/src/lib/layout.shared.tsx +++ b/packages/documentation/src/lib/layout.shared.tsx @@ -1,9 +1,9 @@ -import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; export function baseOptions(): BaseLayoutProps { return { nav: { - title: 'Tanstack Start', + title: "Tanstack Start", }, }; } diff --git a/packages/documentation/src/lib/source.ts b/packages/documentation/src/lib/source.ts index 505a9a51..f8753bd4 100644 --- a/packages/documentation/src/lib/source.ts +++ b/packages/documentation/src/lib/source.ts @@ -1,9 +1,9 @@ -import { loader } from 'fumadocs-core/source'; -import { docs } from 'fumadocs-mdx:collections/server'; -import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; +import { loader } from "fumadocs-core/source"; +import { docs } from "fumadocs-mdx:collections/server"; +import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons"; export const source = loader({ source: docs.toFumadocsSource(), - baseUrl: '/docs', + baseUrl: "/docs", plugins: [lucideIconsPlugin()], }); diff --git a/packages/documentation/src/nextjs-proxy-ts-broken.ts b/packages/documentation/src/nextjs-proxy-ts-broken.ts new file mode 100644 index 00000000..19618ec7 --- /dev/null +++ b/packages/documentation/src/nextjs-proxy-ts-broken.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; + +const { rewrite: rewriteLLM } = rewritePath( + "/docs{/*path}", + "/llms.mdx/docs{/*path}", +); + +export default function proxy(request: NextRequest) { + if (isMarkdownPreferred(request)) { + const result = rewriteLLM(request.nextUrl.pathname); + + if (result) { + return NextResponse.rewrite(new URL(result, request.nextUrl)); + } + } + + return NextResponse.next(); +} diff --git a/packages/documentation/src/routeTree.gen.ts b/packages/documentation/src/routeTree.gen.ts index 6a5e345a..bd617e9e 100644 --- a/packages/documentation/src/routeTree.gen.ts +++ b/packages/documentation/src/routeTree.gen.ts @@ -8,80 +8,80 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from './routes/__root' -import { Route as IndexRouteImport } from './routes/index' -import { Route as DocsSplatRouteImport } from './routes/docs/$' -import { Route as ApiSearchRouteImport } from './routes/api/search' +import { Route as rootRouteImport } from "./routes/__root"; +import { Route as IndexRouteImport } from "./routes/index"; +import { Route as DocsSplatRouteImport } from "./routes/docs/$"; +import { Route as ApiSearchRouteImport } from "./routes/api/search"; const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => rootRouteImport, -} as any) +} as any); const DocsSplatRoute = DocsSplatRouteImport.update({ - id: '/docs/$', - path: '/docs/$', + id: "/docs/$", + path: "/docs/$", getParentRoute: () => rootRouteImport, -} as any) +} as any); const ApiSearchRoute = ApiSearchRouteImport.update({ - id: '/api/search', - path: '/api/search', + id: "/api/search", + path: "/api/search", getParentRoute: () => rootRouteImport, -} as any) +} as any); export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute + "/": typeof IndexRoute; + "/api/search": typeof ApiSearchRoute; + "/docs/$": typeof DocsSplatRoute; } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute + "/": typeof IndexRoute; + "/api/search": typeof ApiSearchRoute; + "/docs/$": typeof DocsSplatRoute; } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute + __root__: typeof rootRouteImport; + "/": typeof IndexRoute; + "/api/search": typeof ApiSearchRoute; + "/docs/$": typeof DocsSplatRoute; } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/api/search' | '/docs/$' - fileRoutesByTo: FileRoutesByTo - to: '/' | '/api/search' | '/docs/$' - id: '__root__' | '/' | '/api/search' | '/docs/$' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: "/" | "/api/search" | "/docs/$"; + fileRoutesByTo: FileRoutesByTo; + to: "/" | "/api/search" | "/docs/$"; + id: "__root__" | "/" | "/api/search" | "/docs/$"; + fileRoutesById: FileRoutesById; } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - ApiSearchRoute: typeof ApiSearchRoute - DocsSplatRoute: typeof DocsSplatRoute + IndexRoute: typeof IndexRoute; + ApiSearchRoute: typeof ApiSearchRoute; + DocsSplatRoute: typeof DocsSplatRoute; } -declare module '@tanstack/react-router' { +declare module "@tanstack/react-router" { interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - '/docs/$': { - id: '/docs/$' - path: '/docs/$' - fullPath: '/docs/$' - preLoaderRoute: typeof DocsSplatRouteImport - parentRoute: typeof rootRouteImport - } - '/api/search': { - id: '/api/search' - path: '/api/search' - fullPath: '/api/search' - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport - } + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/docs/$": { + id: "/docs/$"; + path: "/docs/$"; + fullPath: "/docs/$"; + preLoaderRoute: typeof DocsSplatRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/api/search": { + id: "/api/search"; + path: "/api/search"; + fullPath: "/api/search"; + preLoaderRoute: typeof ApiSearchRouteImport; + parentRoute: typeof rootRouteImport; + }; } } @@ -89,16 +89,16 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ApiSearchRoute: ApiSearchRoute, DocsSplatRoute: DocsSplatRoute, -} +}; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileTypes(); -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/react-start"; +declare module "@tanstack/react-start" { interface Register { - ssr: true - router: Awaited> + ssr: true; + router: Awaited>; } } diff --git a/packages/documentation/src/router.tsx b/packages/documentation/src/router.tsx index d6f335da..556bea3e 100644 --- a/packages/documentation/src/router.tsx +++ b/packages/documentation/src/router.tsx @@ -1,11 +1,11 @@ -import { createRouter as createTanStackRouter } from '@tanstack/react-router'; -import { routeTree } from './routeTree.gen'; -import { NotFound } from '@/components/not-found'; +import { createRouter as createTanStackRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; +import { NotFound } from "@/components/not-found"; export function getRouter() { return createTanStackRouter({ routeTree, - defaultPreload: 'intent', + defaultPreload: "intent", scrollRestoration: true, defaultNotFoundComponent: NotFound, }); diff --git a/packages/documentation/src/routes/__root.tsx b/packages/documentation/src/routes/__root.tsx index a02c242a..cf41584c 100644 --- a/packages/documentation/src/routes/__root.tsx +++ b/packages/documentation/src/routes/__root.tsx @@ -1,23 +1,28 @@ -import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'; -import * as React from 'react'; -import appCss from '@/styles/app.css?url'; -import { RootProvider } from 'fumadocs-ui/provider/tanstack'; +import { + createRootRoute, + HeadContent, + Outlet, + Scripts, +} from "@tanstack/react-router"; +import * as React from "react"; +import appCss from "@/styles/app.css?url"; +import { RootProvider } from "fumadocs-ui/provider/tanstack"; export const Route = createRootRoute({ head: () => ({ meta: [ { - charSet: 'utf-8', + charSet: "utf-8", }, { - name: 'viewport', - content: 'width=device-width, initial-scale=1', + name: "viewport", + content: "width=device-width, initial-scale=1", }, { - title: 'Fumadocs on TanStack Start', + title: "Fumadocs on TanStack Start", }, ], - links: [{ rel: 'stylesheet', href: appCss }], + links: [{ rel: "stylesheet", href: appCss }], }), component: RootComponent, }); diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index 69161c40..443de263 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -1,18 +1,23 @@ -import { createFileRoute, notFound } from '@tanstack/react-router'; -import { DocsLayout } from 'fumadocs-ui/layouts/docs'; -import { createServerFn } from '@tanstack/react-start'; -import { source } from '@/lib/source'; -import browserCollections from 'fumadocs-mdx:collections/browser'; -import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; -import * as Twoslash from 'fumadocs-twoslash/ui'; -import { baseOptions } from '@/lib/layout.shared'; -import { useFumadocsLoader } from 'fumadocs-core/source/client'; +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { DocsLayout } from "fumadocs-ui/layouts/docs"; +import { createServerFn } from "@tanstack/react-start"; +import { source } from "@/lib/source"; +import browserCollections from "fumadocs-mdx:collections/browser"; +import { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, +} from "fumadocs-ui/layouts/docs/page"; +import defaultMdxComponents from "fumadocs-ui/mdx"; +import * as Twoslash from "fumadocs-twoslash/ui"; +import { baseOptions } from "@/lib/layout.shared"; +import { useFumadocsLoader } from "fumadocs-core/source/client"; -export const Route = createFileRoute('/docs/$')({ +export const Route = createFileRoute("/docs/$")({ component: Page, loader: async ({ params }) => { - const slugs = params._splat?.split('/') ?? []; + const slugs = params._splat?.split("/") ?? []; const data = await serverLoader({ data: slugs }); await clientLoader.preload(data.path); return data; @@ -20,7 +25,7 @@ export const Route = createFileRoute('/docs/$')({ }); const serverLoader = createServerFn({ - method: 'GET', + method: "GET", }) .inputValidator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { @@ -64,7 +69,7 @@ function Page() { return ( {clientLoader.useContent(data.path, { - className: '', + className: "", })} ); diff --git a/packages/documentation/src/routes/index.tsx b/packages/documentation/src/routes/index.tsx index b5f6ac2b..9847acd8 100644 --- a/packages/documentation/src/routes/index.tsx +++ b/packages/documentation/src/routes/index.tsx @@ -1,8 +1,8 @@ -import { createFileRoute, Link } from '@tanstack/react-router'; -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import { baseOptions } from '@/lib/layout.shared'; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { HomeLayout } from "fumadocs-ui/layouts/home"; +import { baseOptions } from "@/lib/layout.shared"; -export const Route = createFileRoute('/')({ +export const Route = createFileRoute("/")({ component: Home, }); @@ -10,11 +10,13 @@ function Home() { return (
-

Fumadocs on Tanstack Start.

+

+ Fumadocs on Tanstack Start. +

diff --git a/packages/documentation/src/routes/llms-full[.]txt.ts b/packages/documentation/src/routes/llms-full[.]txt.ts new file mode 100644 index 00000000..f3ee9d49 --- /dev/null +++ b/packages/documentation/src/routes/llms-full[.]txt.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { source } from "@/lib/source"; +import { getLLMText } from "@/lib/get-llm-text"; + +export const Route = createFileRoute("/llms-full.txt")({ + server: { + handlers: { + GET: async () => { + const scan = source.getPages().map(getLLMText); + const scanned = await Promise.all(scan); + return new Response(scanned.join("\n\n")); + }, + }, + }, +}); diff --git a/packages/documentation/src/routes/llms[.]mdx.docs.$.ts b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts new file mode 100644 index 00000000..7ae20208 --- /dev/null +++ b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { source } from "@/lib/source"; + +export const Route = createFileRoute("/llms.mdx/docs/$")({ + server: { + handlers: { + GET: async ({ params }) => { + const slugs = params._splat?.split("/") ?? []; + const page = source.getPage(slugs); + if (!page) throw notFound(); + return new Response(await page.data.getText("raw"), { + headers: { + "Content-Type": "text/markdown", + }, + }); + }, + }, + }, +}); diff --git a/packages/documentation/src/start.ts b/packages/documentation/src/start.ts new file mode 100644 index 00000000..27e3292c --- /dev/null +++ b/packages/documentation/src/start.ts @@ -0,0 +1,25 @@ +import { createMiddleware, createStart } from "@tanstack/react-start"; +import { rewritePath } from "fumadocs-core/negotiation"; +import { redirect } from "@tanstack/react-router"; + +const { rewrite: rewriteLLM } = rewritePath( + "/docs{/*path}.mdx", + "llms.mdx/docs{/*path}", +); + +const llmMiddleware = createMiddleware().server(({ next, request }) => { + const url = new URL(request.url); + const path = rewriteLLM(url.pathname); + + if (path) { + throw redirect(new URL(path, url)); + } + + return next(); +}); + +export const startInstance = createStart(() => { + return { + requestMiddleware: [llmMiddleware], + }; +}); diff --git a/packages/documentation/todo.md b/packages/documentation/todo.md index 6cbaccb6..dbe026b0 100644 --- a/packages/documentation/todo.md +++ b/packages/documentation/todo.md @@ -1,3 +1,2 @@ -- - SEO https://tanstack.com/start/latest/docs/framework/react/guide/seo - LLMO https://tanstack.com/start/latest/docs/framework/react/guide/llmo From 14e5aa4dd53cc50282ba562609a91fc34aca6146 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 14 Jan 2026 23:27:33 +0100 Subject: [PATCH 02/24] Serve markdown to agents --- .prettierignore | 1 + .../src/nextjs-proxy-ts-broken.ts | 19 -- packages/documentation/src/routeTree.gen.ts | 170 +++++++++++------- packages/documentation/src/routes/docs/$.tsx | 32 ++++ .../documentation/src/routes/docs/-$.test.ts | 44 +++++ packages/documentation/tsconfig.json | 1 + 6 files changed, 187 insertions(+), 80 deletions(-) create mode 100644 .prettierignore delete mode 100644 packages/documentation/src/nextjs-proxy-ts-broken.ts create mode 100644 packages/documentation/src/routes/docs/-$.test.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..89421c20 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.gen.ts diff --git a/packages/documentation/src/nextjs-proxy-ts-broken.ts b/packages/documentation/src/nextjs-proxy-ts-broken.ts deleted file mode 100644 index 19618ec7..00000000 --- a/packages/documentation/src/nextjs-proxy-ts-broken.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; - -const { rewrite: rewriteLLM } = rewritePath( - "/docs{/*path}", - "/llms.mdx/docs{/*path}", -); - -export default function proxy(request: NextRequest) { - if (isMarkdownPreferred(request)) { - const result = rewriteLLM(request.nextUrl.pathname); - - if (result) { - return NextResponse.rewrite(new URL(result, request.nextUrl)); - } - } - - return NextResponse.next(); -} diff --git a/packages/documentation/src/routeTree.gen.ts b/packages/documentation/src/routeTree.gen.ts index bd617e9e..f09aca0b 100644 --- a/packages/documentation/src/routeTree.gen.ts +++ b/packages/documentation/src/routeTree.gen.ts @@ -8,97 +8,145 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root"; -import { Route as IndexRouteImport } from "./routes/index"; -import { Route as DocsSplatRouteImport } from "./routes/docs/$"; -import { Route as ApiSearchRouteImport } from "./routes/api/search"; +import { Route as rootRouteImport } from './routes/__root' +import { Route as LlmsFullDottxtRouteImport } from './routes/llms-full[.]txt' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSplatRouteImport } from './routes/docs/$' +import { Route as ApiSearchRouteImport } from './routes/api/search' +import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' +const LlmsFullDottxtRoute = LlmsFullDottxtRouteImport.update({ + id: '/llms-full.txt', + path: '/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ - id: "/", - path: "/", + id: '/', + path: '/', getParentRoute: () => rootRouteImport, -} as any); +} as any) const DocsSplatRoute = DocsSplatRouteImport.update({ - id: "/docs/$", - path: "/docs/$", + id: '/docs/$', + path: '/docs/$', getParentRoute: () => rootRouteImport, -} as any); +} as any) const ApiSearchRoute = ApiSearchRouteImport.update({ - id: "/api/search", - path: "/api/search", + id: '/api/search', + path: '/api/search', + getParentRoute: () => rootRouteImport, +} as any) +const LlmsDotmdxDocsSplatRoute = LlmsDotmdxDocsSplatRouteImport.update({ + id: '/llms.mdx/docs/$', + path: '/llms.mdx/docs/$', getParentRoute: () => rootRouteImport, -} as any); +} as any) export interface FileRoutesByFullPath { - "/": typeof IndexRoute; - "/api/search": typeof ApiSearchRoute; - "/docs/$": typeof DocsSplatRoute; + '/': typeof IndexRoute + '/llms-full.txt': typeof LlmsFullDottxtRoute + '/api/search': typeof ApiSearchRoute + '/docs/$': typeof DocsSplatRoute + '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute; - "/api/search": typeof ApiSearchRoute; - "/docs/$": typeof DocsSplatRoute; + '/': typeof IndexRoute + '/llms-full.txt': typeof LlmsFullDottxtRoute + '/api/search': typeof ApiSearchRoute + '/docs/$': typeof DocsSplatRoute + '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport; - "/": typeof IndexRoute; - "/api/search": typeof ApiSearchRoute; - "/docs/$": typeof DocsSplatRoute; + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/llms-full.txt': typeof LlmsFullDottxtRoute + '/api/search': typeof ApiSearchRoute + '/docs/$': typeof DocsSplatRoute + '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/" | "/api/search" | "/docs/$"; - fileRoutesByTo: FileRoutesByTo; - to: "/" | "/api/search" | "/docs/$"; - id: "__root__" | "/" | "/api/search" | "/docs/$"; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/llms-full.txt' + | '/api/search' + | '/docs/$' + | '/llms.mdx/docs/$' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/llms-full.txt' | '/api/search' | '/docs/$' | '/llms.mdx/docs/$' + id: + | '__root__' + | '/' + | '/llms-full.txt' + | '/api/search' + | '/docs/$' + | '/llms.mdx/docs/$' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute; - ApiSearchRoute: typeof ApiSearchRoute; - DocsSplatRoute: typeof DocsSplatRoute; + IndexRoute: typeof IndexRoute + LlmsFullDottxtRoute: typeof LlmsFullDottxtRoute + ApiSearchRoute: typeof ApiSearchRoute + DocsSplatRoute: typeof DocsSplatRoute + LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute } -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/": { - id: "/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof IndexRouteImport; - parentRoute: typeof rootRouteImport; - }; - "/docs/$": { - id: "/docs/$"; - path: "/docs/$"; - fullPath: "/docs/$"; - preLoaderRoute: typeof DocsSplatRouteImport; - parentRoute: typeof rootRouteImport; - }; - "/api/search": { - id: "/api/search"; - path: "/api/search"; - fullPath: "/api/search"; - preLoaderRoute: typeof ApiSearchRouteImport; - parentRoute: typeof rootRouteImport; - }; + '/llms-full.txt': { + id: '/llms-full.txt' + path: '/llms-full.txt' + fullPath: '/llms-full.txt' + preLoaderRoute: typeof LlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$': { + id: '/docs/$' + path: '/docs/$' + fullPath: '/docs/$' + preLoaderRoute: typeof DocsSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/api/search': { + id: '/api/search' + path: '/api/search' + fullPath: '/api/search' + preLoaderRoute: typeof ApiSearchRouteImport + parentRoute: typeof rootRouteImport + } + '/llms.mdx/docs/$': { + id: '/llms.mdx/docs/$' + path: '/llms.mdx/docs/$' + fullPath: '/llms.mdx/docs/$' + preLoaderRoute: typeof LlmsDotmdxDocsSplatRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + LlmsFullDottxtRoute: LlmsFullDottxtRoute, ApiSearchRoute: ApiSearchRoute, DocsSplatRoute: DocsSplatRoute, -}; + LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute, +} export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) - ._addFileTypes(); + ._addFileTypes() -import type { getRouter } from "./router.tsx"; -import type { createStart } from "@tanstack/react-start"; -declare module "@tanstack/react-start" { +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.ts' +declare module '@tanstack/react-start' { interface Register { - ssr: true; - router: Awaited>; + ssr: true + router: Awaited> + config: Awaited> } } diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index 443de263..ea57c547 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -13,6 +13,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import * as Twoslash from "fumadocs-twoslash/ui"; import { baseOptions } from "@/lib/layout.shared"; import { useFumadocsLoader } from "fumadocs-core/source/client"; +import { isMarkdownPreferred } from "fumadocs-core/negotiation"; export const Route = createFileRoute("/docs/$")({ component: Page, @@ -22,6 +23,37 @@ export const Route = createFileRoute("/docs/$")({ await clientLoader.preload(data.path); return data; }, + server: { + handlers: { + GET: async ({ request, params }) => { + if (!isMarkdownPreferred(request)) return; + + const slugs = params._splat?.split("/") ?? []; + const page = source.getPage(slugs); + if (!page) { + return new Response("Not found", { + status: 404, + headers: { "Content-Type": "text/plain" }, + }); + } + + const getText = ( + page.data as { getText?: (mode: string) => Promise } + ).getText; + if (!getText) { + return new Response("getText not available", { status: 500 }); + } + + const content = await getText("raw"); + return new Response(content, { + headers: { + "Content-Type": "text/markdown", + "Content-Length": String(new TextEncoder().encode(content).length), + }, + }); + }, + }, + }, }); const serverLoader = createServerFn({ diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts new file mode 100644 index 00000000..64386ce2 --- /dev/null +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -0,0 +1,44 @@ +/** + * Integration test for markdown content negotiation. + * + * Run with: bun test src/routes/docs/-$.test.ts + * Requires dev server running: bun run dev + */ +import { test, expect, describe } from "bun:test"; + +const BASE_URL = process.env["TEST_URL"] || "http://localhost:1440"; + +describe("markdown content negotiation", () => { + test("returns markdown when Accept: text/markdown", async () => { + const res = await fetch(`${BASE_URL}/docs`, { + headers: { Accept: "text/markdown" }, + }); + expect(res.headers.get("content-type")).toBe("text/markdown"); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text).toContain("---"); // frontmatter + expect(text).toContain("title:"); // has frontmatter field + }); + + test("returns markdown when Accept: text/plain", async () => { + const res = await fetch(`${BASE_URL}/docs`, { + headers: { Accept: "text/plain" }, + }); + expect(res.headers.get("content-type")).toBe("text/markdown"); + expect(res.status).toBe(200); + }); + + test("returns 404 for non-existent page with markdown accept", async () => { + const res = await fetch(`${BASE_URL}/docs/non-existent-page-xyz`, { + headers: { Accept: "text/markdown" }, + }); + expect(res.status).toBe(404); + }); + + test("does not intercept requests without markdown Accept header", async () => { + const res = await fetch(`${BASE_URL}/docs`); + // Should NOT return text/markdown (falls through to normal page render) + expect(res.headers.get("content-type")).not.toBe("text/markdown"); + expect(res.status).toBe(200); + }); +}); diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index 35c013f2..08fdf678 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -8,6 +8,7 @@ "@/*": ["./src/*"], "fumadocs-mdx:collections/*": [".source/*"] }, + "jsx": "react-jsx", "noEmit": true } } From 17890e9453628ea44ffa71842aa8be7da2d29d9b Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Thu, 15 Jan 2026 00:00:35 +0100 Subject: [PATCH 03/24] Fix type errors --- .gitignore | 1 + packages/design-system/tsconfig.json | 2 +- packages/documentation/src/lib/source.ts | 13 +++++++++++-- packages/documentation/src/routes/docs/$.tsx | 17 +++++++++-------- .../src/types/fumadocs-mdx-collections.d.ts | 15 +++++++++++++++ packages/documentation/tsconfig.json | 10 +++++----- tsconfig.json | 10 ++++++++-- 7 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 packages/documentation/src/types/fumadocs-mdx-collections.d.ts diff --git a/.gitignore b/.gitignore index dd38b978..ebdc6410 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ coverage out/ build dist +*.tsbuildinfo # Debug npm-debug.log* diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json index 55d35163..7836c99f 100644 --- a/packages/design-system/tsconfig.json +++ b/packages/design-system/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "jsx": "react-jsx", "outDir": "dist", - "noEmit": true + "composite": true }, "include": ["src", "eslint.config.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/documentation/src/lib/source.ts b/packages/documentation/src/lib/source.ts index f8753bd4..1a2e562b 100644 --- a/packages/documentation/src/lib/source.ts +++ b/packages/documentation/src/lib/source.ts @@ -1,9 +1,18 @@ -import { loader } from "fumadocs-core/source"; +import { loader, type Source, type PageData, type MetaData } from "fumadocs-core/source"; import { docs } from "fumadocs-mdx:collections/server"; import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons"; +import type { + DocCollectionEntry, + MetaCollectionEntry, +} from "fumadocs-mdx/runtime/server"; + +const docsSource = docs.toFumadocsSource() as Source<{ + pageData: DocCollectionEntry<"docs", PageData>; + metaData: MetaCollectionEntry; +}>; export const source = loader({ - source: docs.toFumadocsSource(), + source: docsSource, baseUrl: "/docs", plugins: [lucideIconsPlugin()], }); diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index ea57c547..de10fb4c 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -70,14 +70,15 @@ const serverLoader = createServerFn({ }; }); -const clientLoader = browserCollections.docs.createClientLoader({ - component( - { toc, frontmatter, default: MDX }, - // you can define props for the component - props: { - className?: string; - }, - ) { +const clientLoader = browserCollections.docs.createClientLoader<{ + className?: string; +}>({ + component(loaded, props) { + const { toc, default: MDX } = loaded; + const frontmatter = loaded.frontmatter as { + title: string; + description?: string; + }; return ( {frontmatter.title} diff --git a/packages/documentation/src/types/fumadocs-mdx-collections.d.ts b/packages/documentation/src/types/fumadocs-mdx-collections.d.ts new file mode 100644 index 00000000..f24428d8 --- /dev/null +++ b/packages/documentation/src/types/fumadocs-mdx-collections.d.ts @@ -0,0 +1,15 @@ +declare module "fumadocs-mdx:collections/server" { + import type { DocsCollectionEntry } from "fumadocs-mdx/runtime/server"; + + export const docs: DocsCollectionEntry; +} + +declare module "fumadocs-mdx:collections/browser" { + import type { DocCollectionEntry } from "fumadocs-mdx/runtime/browser"; + + const browserCollections: { + docs: DocCollectionEntry; + }; + + export default browserCollections; +} diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index 08fdf678..86bda3e5 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -1,14 +1,14 @@ { "extends": "../../tsconfig.json", - "include": ["**/*.ts", "**/*.tsx"], + "include": ["src/**/*.ts", "src/**/*.tsx"], "compilerOptions": { - "types": ["vite/client"], + "types": ["vite/client", "bun"], + "rootDir": ".", "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "fumadocs-mdx:collections/*": [".source/*"] + "@/*": ["./src/*"] }, "jsx": "react-jsx", - "noEmit": true + "composite": true } } diff --git a/tsconfig.json b/tsconfig.json index 3a57b1a3..ca2310b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,12 @@ "module": "ESNext", "moduleResolution": "Bundler", "target": "ESNext", - "noEmit": true - } + "noEmit": true, + "jsx": "react-jsx" + }, + "exclude": ["node_modules"], + "references": [ + { "path": "./packages/design-system" }, + { "path": "./packages/documentation" } + ] } From ff78d360904baa38a94d44a57243d39615da53ed Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Thu, 15 Jan 2026 00:24:37 +0100 Subject: [PATCH 04/24] Support brief llms.txt --- packages/documentation/src/routeTree.gen.ts | 28 ++++++++++++++++- .../documentation/src/routes/llms[.]txt.ts | 30 +++++++++++++++++++ packages/documentation/src/start.ts | 8 +++-- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 packages/documentation/src/routes/llms[.]txt.ts diff --git a/packages/documentation/src/routeTree.gen.ts b/packages/documentation/src/routeTree.gen.ts index f09aca0b..737c52b3 100644 --- a/packages/documentation/src/routeTree.gen.ts +++ b/packages/documentation/src/routeTree.gen.ts @@ -9,12 +9,18 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt' import { Route as LlmsFullDottxtRouteImport } from './routes/llms-full[.]txt' import { Route as IndexRouteImport } from './routes/index' import { Route as DocsSplatRouteImport } from './routes/docs/$' import { Route as ApiSearchRouteImport } from './routes/api/search' import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' +const LlmsDottxtRoute = LlmsDottxtRouteImport.update({ + id: '/llms.txt', + path: '/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) const LlmsFullDottxtRoute = LlmsFullDottxtRouteImport.update({ id: '/llms-full.txt', path: '/llms-full.txt', @@ -44,6 +50,7 @@ const LlmsDotmdxDocsSplatRoute = LlmsDotmdxDocsSplatRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/llms-full.txt': typeof LlmsFullDottxtRoute + '/llms.txt': typeof LlmsDottxtRoute '/api/search': typeof ApiSearchRoute '/docs/$': typeof DocsSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -51,6 +58,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/llms-full.txt': typeof LlmsFullDottxtRoute + '/llms.txt': typeof LlmsDottxtRoute '/api/search': typeof ApiSearchRoute '/docs/$': typeof DocsSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -59,6 +67,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/llms-full.txt': typeof LlmsFullDottxtRoute + '/llms.txt': typeof LlmsDottxtRoute '/api/search': typeof ApiSearchRoute '/docs/$': typeof DocsSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -68,15 +77,23 @@ export interface FileRouteTypes { fullPaths: | '/' | '/llms-full.txt' + | '/llms.txt' | '/api/search' | '/docs/$' | '/llms.mdx/docs/$' fileRoutesByTo: FileRoutesByTo - to: '/' | '/llms-full.txt' | '/api/search' | '/docs/$' | '/llms.mdx/docs/$' + to: + | '/' + | '/llms-full.txt' + | '/llms.txt' + | '/api/search' + | '/docs/$' + | '/llms.mdx/docs/$' id: | '__root__' | '/' | '/llms-full.txt' + | '/llms.txt' | '/api/search' | '/docs/$' | '/llms.mdx/docs/$' @@ -85,6 +102,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute LlmsFullDottxtRoute: typeof LlmsFullDottxtRoute + LlmsDottxtRoute: typeof LlmsDottxtRoute ApiSearchRoute: typeof ApiSearchRoute DocsSplatRoute: typeof DocsSplatRoute LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute @@ -92,6 +110,13 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/llms.txt': { + id: '/llms.txt' + path: '/llms.txt' + fullPath: '/llms.txt' + preLoaderRoute: typeof LlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/llms-full.txt': { id: '/llms-full.txt' path: '/llms-full.txt' @@ -133,6 +158,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LlmsFullDottxtRoute: LlmsFullDottxtRoute, + LlmsDottxtRoute: LlmsDottxtRoute, ApiSearchRoute: ApiSearchRoute, DocsSplatRoute: DocsSplatRoute, LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute, diff --git a/packages/documentation/src/routes/llms[.]txt.ts b/packages/documentation/src/routes/llms[.]txt.ts new file mode 100644 index 00000000..c7695a24 --- /dev/null +++ b/packages/documentation/src/routes/llms[.]txt.ts @@ -0,0 +1,30 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { source } from "@/lib/source"; + +export const Route = createFileRoute("/llms.txt")({ + server: { + handlers: { + GET: async ({ request }) => { + const baseUrl = new URL(request.url).origin; + const pages = source.getPages(); + + const lines = [ + "# GraphQL Hive", + "", + "> GraphQL Hive documentation", + "", + "## Docs", + "", + ...pages.map((page) => { + const desc = page.data.description ? `: ${page.data.description}` : ""; + return `- [${page.data.title}](${baseUrl}${page.url}.md)${desc}`; + }), + ]; + + return new Response(lines.join("\n"), { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); + }, + }, + }, +}); diff --git a/packages/documentation/src/start.ts b/packages/documentation/src/start.ts index 27e3292c..1e8f00be 100644 --- a/packages/documentation/src/start.ts +++ b/packages/documentation/src/start.ts @@ -2,14 +2,18 @@ import { createMiddleware, createStart } from "@tanstack/react-start"; import { rewritePath } from "fumadocs-core/negotiation"; import { redirect } from "@tanstack/react-router"; -const { rewrite: rewriteLLM } = rewritePath( +const { rewrite: rewriteMdx } = rewritePath( "/docs{/*path}.mdx", "llms.mdx/docs{/*path}", ); +const { rewrite: rewriteMd } = rewritePath( + "/docs{/*path}.md", + "llms.mdx/docs{/*path}", +); const llmMiddleware = createMiddleware().server(({ next, request }) => { const url = new URL(request.url); - const path = rewriteLLM(url.pathname); + const path = rewriteMdx(url.pathname) ?? rewriteMd(url.pathname); if (path) { throw redirect(new URL(path, url)); From 4b46452f7c9a13a410b6467da1f9122e053b88f9 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Thu, 15 Jan 2026 01:29:46 +0100 Subject: [PATCH 05/24] Add page actions --- bun.lock | 129 ++++++++++++++++-- cli.json | 13 ++ package.json | 9 ++ .../src/components/page-actions.tsx | 64 +++++++++ .../src/components/ui/button.tsx | 28 ++++ .../src/components/ui/popover.tsx | 35 +++++ packages/documentation/src/lib/cn.ts | 1 + packages/documentation/src/routes/docs/$.tsx | 28 +++- packages/documentation/tsconfig.json | 3 +- packages/documentation/vite.config.ts | 2 +- turbo.json | 2 +- 11 files changed, 294 insertions(+), 20 deletions(-) create mode 100644 cli.json create mode 100644 packages/documentation/src/components/page-actions.tsx create mode 100644 packages/documentation/src/components/ui/button.tsx create mode 100644 packages/documentation/src/components/ui/popover.tsx create mode 100644 packages/documentation/src/lib/cn.ts diff --git a/bun.lock b/bun.lock index e4b66b2b..773b4e40 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,16 @@ "workspaces": { "": { "name": "scriptorium", + "dependencies": { + "@radix-ui/react-popover": "^1.1.15", + "class-variance-authority": "^0.7.1", + "fumadocs-ui": "^16.4.7", + "lucide-react": "^0.562.0", + "react": "^19.2.3", + "tailwind-merge": "^3.4.0", + }, "devDependencies": { + "@fumadocs/cli": "^1.2.2", "@hasparus/eslint-config": "2.0.12", "@tsconfig/strictest": "2.0.8", "@types/bun": "latest", @@ -124,6 +133,10 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.1", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -216,6 +229,8 @@ "@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.7.2", "", {}, "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA=="], + "@fumadocs/cli": ["@fumadocs/cli@1.2.2", "", { "dependencies": { "@clack/prompts": "^0.11.0", "commander": "^14.0.2", "magic-string": "^0.30.21", "oxc-parser": "^0.107.0", "oxc-resolver": "^11.16.2", "package-manager-detector": "^1.6.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.2", "zod": "^4.3.5" }, "bin": { "fumadocs": "dist/index.js" } }, "sha512-BjXhjV9oZc8MHaxVQUmmbjoS0at3Sz5wWhIbHAnYLEEyRivak5s5OKcJqAWQ2YY1aF+QQ32cghq/b8+Ld2dV+Q=="], + "@fumadocs/ui": ["@fumadocs/ui@16.4.7", "", { "dependencies": { "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.1", "tailwind-merge": "^3.4.0" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "16.4.7", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-NnkMIN5BzBRh2OzA9rp2SgbGEkEwfCfq0sE4vq2n+GkIDIggicGYUNgSl2gtIBQsKYKP/a4/0wrkQKdq4eUJlw=="], "@hasparus/eslint-config": ["@hasparus/eslint-config@2.0.12", "", { "dependencies": { "@eslint/eslintrc": "3.3.3", "@eslint/js": "9.17.0", "@theguild/eslint-config": "0.13.4", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsonc": "2.21.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-mdx": "3.6.2", "eslint-plugin-n": "17.23.1", "eslint-plugin-perfectionist": "4.6.0", "eslint-plugin-promise": "7.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-sonarjs": "3.0.5", "eslint-plugin-unicorn": "62.0.0", "eslint-plugin-yml": "1.19.1", "globals": "15.14.0", "typescript-eslint": "8.18.2" }, "peerDependencies": { "eslint": "^9.0.0", "typescript": "^5.0.0" } }, "sha512-5WfvspuBeu6bxVbi7YT7wQE9NJM+RGoKlzzSGleSPXTaxTiY5ywcfjprFc0fogsFLmwfrzyhDZrtQrX3qZkEEQ=="], @@ -320,6 +335,88 @@ "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.107.0", "", { "os": "android", "cpu": "arm" }, "sha512-Fhap02+E3+tBDLsBZcsr7289kCfR3hyQnBAjhi7RSTHc7Ikydh1hS5cIzjOtlidFZJ1Vz5edbfoKGWO3/DqJNw=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.107.0", "", { "os": "android", "cpu": "arm64" }, "sha512-3gXyxBdwNzOCSdbzN3FSncilXUe/OJP0SAovRz+e20q5FInUYfVvOZUJfpII01anSmg+7KWY7p69IAgDYZZepw=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.107.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-i8W2krLmBd6jWldW1Y4/12zke+euEYZGuUggijJhEFy5xTQbwOhgVDWpdUx3CgZ17Plzjkd/dB/Ga0b13i0kAg=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.107.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JwDxozL+IPXeiP57GyRmC3coIKR7Duit69aHvhf63NZqMClnglI0gR8mI+JH4lNBP/o6AGaY22+8/rlfiMW5Pg=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.107.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m8h7qkymDLqxRGARWPJQH9x/I4ZLlwMhigj9iVkKZ7db/J1wl9ha+a9DCBrm5kRYikl4dSwu7wZXykKmrOzVVA=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.107.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QI9b9BvWcIk/vuBUGgas4eZZCXikd7yfXTppIFM2hNZN+omd2nCDMGZ5yMHy1r+TJw1hdxei8f8xzwmO1nTq3A=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.107.0", "", { "os": "linux", "cpu": "arm" }, "sha512-VMoeP+VZegiqRqcUa0RzopOErELVTSNDfdVIX/8No3ieZdxdHqvGlBmdCqqxIYZEYif2IZJ3VcIr2RvX4y8k9w=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.107.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-01yvXlhCB8aCu9xftIQCI9TGvVb2+md4ULJYmDSil4Qr4XfXa8soEJxfS/ywe+RiDnW7w8qomtz0DI+HT5sHRw=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.107.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pp2ovq2qxqGTyRclBe65/VD3IL0fwT+X5XJSKhdhO94BtNOPCcW0bZAgG3ILkoWPPdmtWUXT/y59cCkK+QNEYg=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.107.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1AgcnFazS00KBq38eQ8EW/vwjgtcNvVdbR/SnteVDY4j0klgSxaYe2/CQXnww4wVh8UjE3IHYYAfsudhggET0Q=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.107.0", "", { "os": "linux", "cpu": "none" }, "sha512-Yg/YyeaV9RiStZG2Rc50xhzrBIG2w1PuKJjlbVtJ+Mb2kY0zxhg2Pnifjt85ZKJqqJ9Bfao1LVXNweV2HYRAJA=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.107.0", "", { "os": "linux", "cpu": "none" }, "sha512-/KGiC2Ko1k0rQxTYqTP1MDipV5LCw5by9Yx+qUy5LL0eHtI06CkIZ9mPMua5+hwLygwMrv7Ry8MjpeTQ0qHpcQ=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.107.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-F4UKJ19+vTHTA7miSt7DWG04NwMGbLj4C7BfWY8V3LMX5zp68py/rcKYBusC7hcJQ4YBUKQzl1WLx9PMzyWiXg=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.107.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vF4vemHhzCsKQhfaV/j7xS7AavMVkHy29zhlAE03r61lvKK4lQBr2VvT6qgSTn4eYGNEHEZbRoFNOcmtaPGjtA=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.107.0", "", { "os": "linux", "cpu": "x64" }, "sha512-p6jxLjIMiySYclrRuVQELSm6wT5lTfkPRmcZKbtmLhyMlAR2rhuILnoZ/iVoE3Ib/hpE4G6XkLhRZLvp6ZVazw=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.107.0", "", { "os": "none", "cpu": "arm64" }, "sha512-iCUiKTYwqSmA/qgBR300fmXLVVi9tmk43O2B4oeMaydvnqUNWmZTNciOPwAFfc6024ISxZ77y4ISHTE0plX3LQ=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.107.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-VxrwctWEUSI3eJkRAGHISNlikcx8xAoglvAYAW4cdC5HfXbwRMuEunzzXMNXpNUMrdlqjf25Ay6OaxaztAOKgQ=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.107.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-zJlOsumV4JpUs0PGMF0ycjfCcV91Tpr81N7Qn5O00+MjFxI3AlHmrkhYTFA2cFicUW6XXSPe6KvEG8v46BCIBA=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.107.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vH44IYIiqzAxq7la/O+IRNdB3XqgdMRjVVT1UqA4rmyHUEQcfmCYy6cbbP07m5eLY2xAHAmuDqxBJEnQDGGGJQ=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.107.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8x6u+nIKEFR3WT5oHhSP7oPZGI8VLq3iVxOEeV75NfB5ubGUA7sNHcssZ37jmUfhYnkYzBiCGhEAIRa9bUMzBw=="], + + "@oxc-project/types": ["@oxc-project/types@0.107.0", "", {}, "sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.16.3", "", { "os": "android", "cpu": "arm" }, "sha512-CVyWHu6ACDqDcJxR4nmGiG8vDF4TISJHqRNzac5z/gPQycs/QrP/1pDsJBy0MD7jSw8nVq2E5WqeHQKabBG/Jg=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.16.3", "", { "os": "android", "cpu": "arm64" }, "sha512-tTIoB7plLeh2o6Ay7NnV5CJb6QUXdxI7Shnsp2ECrLSV81k+oVE3WXYrQSh4ltWL75i0OgU5Bj3bsuyg5SMepw=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.16.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXKVH7uwYd3Rbw1s2yJZd6/w+6b01iaokZubYhDAq4tOYArr+YCS+lr81q1hsTPPRZeIsWE+rJLulmf1qHdYZA=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.16.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WwjQ4WdnCxVYZYd3e3oY5XbV3JeLy9pPMK+eQQ2m8DtqUtbxnvPpAYC2Knv/2bS6q5JiktqOVJ2Hfia3OSo0/A=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.16.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4OHKFGJBBfOnuJnelbCS4eBorI6cj54FUxcZJwEXPeoLc8yzORBoJ2w+fQbwjlQcUUZLEg92uGhKCRiUoqznjg=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.16.3", "", { "os": "linux", "cpu": "arm" }, "sha512-OM3W0NLt9u7uKwG/yZbeXABansZC0oZeDF1nKgvcZoRw4/Yak6/l4S0onBfDFeYMY94eYeAt2bl60e30lgsb5A=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.16.3", "", { "os": "linux", "cpu": "arm" }, "sha512-MRs7D7i1t7ACsAdTuP81gLZES918EpBmiUyEl8fu302yQB+4L7L7z0Ui8BWnthUTQd3nAU9dXvENLK/SqRVH8A=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.16.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0eVYZxSceNqGADzhlV4ZRqkHF0fjWxRXQOB7Qwl5y1gN/XYUDvMfip+ngtzj4dM7zQT4U97hUhJ7PUKSy/JIGQ=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.16.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-B1BvLeZbgDdVN0FvU40l5Q7lej8310WlabCBaouk8jY7H7xbI8phtomTtk3Efmevgfy5hImaQJu6++OmcFb2NQ=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.16.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-q7khglic3Jqak7uDgA3MFnjDeI7krQT595GDZpvFq785fmFYSx8rlTkoHzmhQtUisYtl4XG7WUscwsoidFUI4w=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.16.3", "", { "os": "linux", "cpu": "none" }, "sha512-aFRNmQNPzDgQEbw2s3c8yJYRimacSDI+u9df8rn5nSKzTVitHmbEpZqfxpwNLCKIuLSNmozHR1z1OT+oZVeYqg=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.16.3", "", { "os": "linux", "cpu": "none" }, "sha512-vZI85SvSMADcEL9G1TIrV0Rlkc1fY5Mup0DdlVC5EHPysZB4hXXHpr+h09pjlK5y+5om5foIzDRxE1baUCaWOA=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.16.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-xiLBnaUlddFEzRHiHiSGEMbkg8EwZY6VD8F+3GfnFsiK3xg/4boaUV2bwXd+nUzl3UDQOMW1QcZJ4jJSb0qiJA=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.16.3", "", { "os": "linux", "cpu": "x64" }, "sha512-6y0b05wIazJJgwu7yU/AYGFswzQQudYJBOb/otDhiDacp1+6ye8egoxx63iVo9lSpDbipL++54AJQFlcOHCB+g=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.16.3", "", { "os": "linux", "cpu": "x64" }, "sha512-RmMgwuMa42c9logS7Pjprf5KCp8J1a1bFiuBFtG9/+yMu0BhY2t+0VR/um7pwtkNFvIQqAVh6gDOg/PnoKRcdQ=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.16.3", "", { "os": "none", "cpu": "arm64" }, "sha512-/7AYRkjjW7xu1nrHgWUFy99Duj4/ydOBVaHtODie9/M6fFngo+8uQDFFnzmr4q//sd/cchIerISp/8CQ5TsqIA=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.16.3", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-urM6aIPbi5di4BSlnpd/TWtDJgG6RD06HvLBuNM+qOYuFtY1/xPbzQ2LanBI2ycpqIoIZwsChyplALwAMdyfCQ=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.16.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-QuvLqGKf7frxWHQ5TnrcY0C/hJpANsaez99Q4dAk1hen7lDTD4FBPtBzPnntLFXeaVG3PnSmnVjlv0vMILwU7Q=="], + + "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.16.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QR/witXK6BmYTlEP8CCjC5fxeG5U9A6a50pNpC1nLnhAcJjtzFG8KcQ5etVy/XvCLiDc7fReaAWRNWtCaIhM8Q=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.16.3", "", { "os": "win32", "cpu": "x64" }, "sha512-bFuJRKOscsDAEZ/a8BezcTMAe2BQ/OBRfuMLFUuINfTR5qGVcm4a3xBIrQVepBaPxFj16SJdRjGe05vDiwZmFw=="], + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ=="], "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw=="], @@ -434,7 +531,7 @@ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], @@ -990,7 +1087,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], @@ -1954,6 +2051,10 @@ "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], + "oxc-parser": ["oxc-parser@0.107.0", "", { "dependencies": { "@oxc-project/types": "^0.107.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.107.0", "@oxc-parser/binding-android-arm64": "0.107.0", "@oxc-parser/binding-darwin-arm64": "0.107.0", "@oxc-parser/binding-darwin-x64": "0.107.0", "@oxc-parser/binding-freebsd-x64": "0.107.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.107.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.107.0", "@oxc-parser/binding-linux-arm64-gnu": "0.107.0", "@oxc-parser/binding-linux-arm64-musl": "0.107.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.107.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.107.0", "@oxc-parser/binding-linux-riscv64-musl": "0.107.0", "@oxc-parser/binding-linux-s390x-gnu": "0.107.0", "@oxc-parser/binding-linux-x64-gnu": "0.107.0", "@oxc-parser/binding-linux-x64-musl": "0.107.0", "@oxc-parser/binding-openharmony-arm64": "0.107.0", "@oxc-parser/binding-wasm32-wasi": "0.107.0", "@oxc-parser/binding-win32-arm64-msvc": "0.107.0", "@oxc-parser/binding-win32-ia32-msvc": "0.107.0", "@oxc-parser/binding-win32-x64-msvc": "0.107.0" } }, "sha512-3HuDitM2UIEDbCjEhXyLAC8LuQvneDq/0eioczXZFeY4f4ee91tUcavZ9U7s4ZIFZOoHmNtOyOCB6kOM4OAtOA=="], + + "oxc-resolver": ["oxc-resolver@11.16.3", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.16.3", "@oxc-resolver/binding-android-arm64": "11.16.3", "@oxc-resolver/binding-darwin-arm64": "11.16.3", "@oxc-resolver/binding-darwin-x64": "11.16.3", "@oxc-resolver/binding-freebsd-x64": "11.16.3", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.16.3", "@oxc-resolver/binding-linux-arm-musleabihf": "11.16.3", "@oxc-resolver/binding-linux-arm64-gnu": "11.16.3", "@oxc-resolver/binding-linux-arm64-musl": "11.16.3", "@oxc-resolver/binding-linux-ppc64-gnu": "11.16.3", "@oxc-resolver/binding-linux-riscv64-gnu": "11.16.3", "@oxc-resolver/binding-linux-riscv64-musl": "11.16.3", "@oxc-resolver/binding-linux-s390x-gnu": "11.16.3", "@oxc-resolver/binding-linux-x64-gnu": "11.16.3", "@oxc-resolver/binding-linux-x64-musl": "11.16.3", "@oxc-resolver/binding-openharmony-arm64": "11.16.3", "@oxc-resolver/binding-wasm32-wasi": "11.16.3", "@oxc-resolver/binding-win32-arm64-msvc": "11.16.3", "@oxc-resolver/binding-win32-ia32-msvc": "11.16.3", "@oxc-resolver/binding-win32-x64-msvc": "11.16.3" } }, "sha512-goLOJH3x69VouGWGp5CgCIHyksmOZzXr36lsRmQz1APg3SPFORrvV2q7nsUHMzLVa6ZJgNwkgUSJFsbCpAWkCA=="], + "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -2212,6 +2313,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], @@ -2544,7 +2647,7 @@ "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -2588,14 +2691,6 @@ "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@repo/ui/typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -2620,14 +2715,20 @@ "@tanstack/devtools-vite/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-plugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], "@tanstack/start-plugin-core/srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="], + "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@theguild/eslint-config/eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], "@theguild/eslint-config/eslint-plugin-jsonc": ["eslint-plugin-jsonc@2.19.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "eslint-compat-utils": "^0.6.0", "eslint-json-compat-utils": "^0.2.1", "espree": "^9.6.1", "graphemer": "^1.4.0", "jsonc-eslint-parser": "^2.0.4", "natural-compare": "^1.4.0", "synckit": "^0.6.0" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-MmlAOaZK1+Lg7YoCZPGRjb88ZjT+ct/KTsvcsbZdBm+w8WMzGx+XEmexk0m40P1WV9G2rFV7X3klyRGRpFXEjA=="], @@ -2708,8 +2809,6 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint-plugin-react-hooks/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "eslint-plugin-sonarjs/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], "eslint-plugin-sonarjs/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2724,7 +2823,7 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "fumadocs-mdx/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], @@ -2808,6 +2907,8 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.18.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.18.2", "@typescript-eslint/type-utils": "8.18.2", "@typescript-eslint/utils": "8.18.2", "@typescript-eslint/visitor-keys": "8.18.2", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg=="], diff --git a/cli.json b/cli.json new file mode 100644 index 00000000..4849e337 --- /dev/null +++ b/cli.json @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/@fumadocs/cli/dist/schema/default.json", + "aliases": { + "uiDir": "./components/ui", + "componentsDir": "./components", + "blockDir": "./components", + "cssDir": "./styles", + "libDir": "./lib" + }, + "baseDir": "", + "uiLibrary": "radix-ui", + "commands": {} +} \ No newline at end of file diff --git a/package.json b/package.json index bc6c3b69..2f4a5568 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "check-types": "turbo run check-types" }, "devDependencies": { + "@fumadocs/cli": "^1.2.2", "@hasparus/eslint-config": "2.0.12", "@tsconfig/strictest": "2.0.8", "@types/bun": "latest", @@ -28,5 +29,13 @@ }, "patchedDependencies": { "mermaid-isomorphic@3.0.4": "patches/mermaid-isomorphic@3.0.4.patch" + }, + "dependencies": { + "@radix-ui/react-popover": "^1.1.15", + "class-variance-authority": "^0.7.1", + "fumadocs-ui": "^16.4.7", + "lucide-react": "^0.562.0", + "react": "^19.2.3", + "tailwind-merge": "^3.4.0" } } diff --git a/packages/documentation/src/components/page-actions.tsx b/packages/documentation/src/components/page-actions.tsx new file mode 100644 index 00000000..fe64f27e --- /dev/null +++ b/packages/documentation/src/components/page-actions.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy, Github } from "lucide-react"; +import { useCopyButton } from "fumadocs-ui/utils/use-copy-button"; + +const cache = new Map(); +const actionClass = + "inline-flex items-center gap-2 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring disabled:pointer-events-none disabled:opacity-50"; + +export function LLMCopyButton({ markdownUrl }: { markdownUrl: string }) { + const [isLoading, setLoading] = useState(false); + const [checked, onClick] = useCopyButton(async () => { + const cached = cache.get(markdownUrl); + if (cached) return navigator.clipboard.writeText(cached); + + setLoading(true); + + try { + await navigator.clipboard.write([ + new ClipboardItem({ + "text/plain": fetch(markdownUrl).then(async (res) => { + const content = await res.text(); + cache.set(markdownUrl, content); + + return content; + }), + }), + ]); + } finally { + setLoading(false); + } + }); + + return ( + + ); +} + +export function PageActions({ + markdownUrl, + githubUrl, +}: { + markdownUrl: string; + githubUrl: string; +}) { + return ( + + ); +} diff --git a/packages/documentation/src/components/ui/button.tsx b/packages/documentation/src/components/ui/button.tsx new file mode 100644 index 00000000..b427d4e0 --- /dev/null +++ b/packages/documentation/src/components/ui/button.tsx @@ -0,0 +1,28 @@ +import { cva, type VariantProps } from 'class-variance-authority'; + +const variants = { + primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80', + outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground', + ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground', + secondary: + 'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground', +} as const; + +export const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring', + { + variants: { + variant: variants, + // fumadocs use `color` instead of `variant` + color: variants, + size: { + sm: 'gap-1 px-2 py-1.5 text-xs', + icon: 'p-1.5 [&_svg]:size-5', + 'icon-sm': 'p-1.5 [&_svg]:size-4.5', + 'icon-xs': 'p-1 [&_svg]:size-4', + }, + }, + }, +); + +export type ButtonProps = VariantProps; diff --git a/packages/documentation/src/components/ui/popover.tsx b/packages/documentation/src/components/ui/popover.tsx new file mode 100644 index 00000000..9257aa47 --- /dev/null +++ b/packages/documentation/src/components/ui/popover.tsx @@ -0,0 +1,35 @@ +'use client'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import * as React from 'react'; +import { cn } from '../../lib/cn'; + +const Popover: typeof PopoverPrimitive.Root = PopoverPrimitive.Root; + +const PopoverTrigger: typeof PopoverPrimitive.Trigger = PopoverPrimitive.Trigger; + +const PopoverContent: React.ForwardRefExoticComponent< + React.ComponentPropsWithoutRef & + React.RefAttributes> +> = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +const PopoverClose: typeof PopoverPrimitive.PopoverClose = PopoverPrimitive.PopoverClose; + +export { Popover, PopoverTrigger, PopoverContent, PopoverClose }; diff --git a/packages/documentation/src/lib/cn.ts b/packages/documentation/src/lib/cn.ts new file mode 100644 index 00000000..ba66fd25 --- /dev/null +++ b/packages/documentation/src/lib/cn.ts @@ -0,0 +1 @@ +export { twMerge as cn } from 'tailwind-merge'; diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index de10fb4c..4148e01d 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -14,6 +14,7 @@ import * as Twoslash from "fumadocs-twoslash/ui"; import { baseOptions } from "@/lib/layout.shared"; import { useFumadocsLoader } from "fumadocs-core/source/client"; import { isMarkdownPreferred } from "fumadocs-core/negotiation"; +import { PageActions } from "@/components/page-actions"; export const Route = createFileRoute("/docs/$")({ component: Page, @@ -26,9 +27,14 @@ export const Route = createFileRoute("/docs/$")({ server: { handlers: { GET: async ({ request, params }) => { - if (!isMarkdownPreferred(request)) return; + const rawSplat = params._splat ?? ""; + const isMdxRequest = rawSplat.endsWith(".mdx"); + const slugs = (isMdxRequest ? rawSplat.slice(0, -4) : rawSplat) + .split("/") + .filter(Boolean); + + if (!isMdxRequest && !isMarkdownPreferred(request)) return; - const slugs = params._splat?.split("/") ?? []; const page = source.getPage(slugs); if (!page) { return new Response("Not found", { @@ -66,12 +72,15 @@ const serverLoader = createServerFn({ return { path: page.path, + url: page.url, pageTree: await source.serializePageTree(source.getPageTree()), }; }); const clientLoader = browserCollections.docs.createClientLoader<{ className?: string; + markdownUrl: string; + githubUrl: string; }>({ component(loaded, props) { const { toc, default: MDX } = loaded; @@ -80,7 +89,18 @@ const clientLoader = browserCollections.docs.createClientLoader<{ description?: string; }; return ( - + + ), + }} + {...props} + > {frontmatter.title} {frontmatter.description} @@ -103,6 +123,8 @@ function Page() { {clientLoader.useContent(data.path, { className: "", + markdownUrl: `${data.url}.mdx`, + githubUrl: `https://github.com/graphql-hive/docs/blob/main/packages/documentation/content/docs/${data.path}`, })} ); diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index 86bda3e5..a2fe1346 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -6,7 +6,8 @@ "rootDir": ".", "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "fumadocs-mdx:collections/*": ["./.source/*"] }, "jsx": "react-jsx", "composite": true diff --git a/packages/documentation/vite.config.ts b/packages/documentation/vite.config.ts index 9196e0ef..42278a3c 100644 --- a/packages/documentation/vite.config.ts +++ b/packages/documentation/vite.config.ts @@ -13,9 +13,9 @@ export default defineConfig({ port: 1440, }, plugins: [ + mdx(await import("./source.config")), devtools(), nitro(), - mdx(await import("./source.config")), tailwindcss(), tsConfigPaths({ projects: ["./tsconfig.json"], diff --git a/turbo.json b/turbo.json index 1e0979ba..756dd1d8 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,6 @@ { "$schema": "https://turborepo.dev/schema.json", - "ui": "tui", + "ui": "stream", "tasks": { "build": { "dependsOn": ["^build"], From cd1d7e300428dc6e6b750ecc8fe605a5a48a13e7 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Thu, 15 Jan 2026 12:46:15 +0100 Subject: [PATCH 06/24] Fix a type error --- bun.lock | 1 + package.json | 3 +- packages/design-system/tsconfig.json | 5 +-- packages/documentation/package.json | 3 +- .../src/components/ui/popover.tsx | 35 ------------------- packages/documentation/tsconfig.json | 6 ++-- 6 files changed, 12 insertions(+), 41 deletions(-) delete mode 100644 packages/documentation/src/components/ui/popover.tsx diff --git a/bun.lock b/bun.lock index 773b4e40..a7fc6533 100644 --- a/bun.lock +++ b/bun.lock @@ -59,6 +59,7 @@ "tailwind-merge": "^3.4.0", "twoslash": "^0.3.6", "vite": "^7.3.1", + "zod": "^4.3.5", }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/package.json b/package.json index 2f4a5568..fe53ebb2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "dev": "turbo run dev", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "check-types": "turbo run check-types" + "check-types": "turbo run check-types", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@fumadocs/cli": "^1.2.2", diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json index 7836c99f..ad4fb9d6 100644 --- a/packages/design-system/tsconfig.json +++ b/packages/design-system/tsconfig.json @@ -2,8 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "jsx": "react-jsx", - "outDir": "dist", - "composite": true + "composite": true, + "noEmit": false, + "outDir": "dist" }, "include": ["src", "eslint.config.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/documentation/package.json b/packages/documentation/package.json index e1037b49..bcfaf1b1 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -30,7 +30,8 @@ "rehype-mermaid": "^3.0.0", "tailwind-merge": "^3.4.0", "twoslash": "^0.3.6", - "vite": "^7.3.1" + "vite": "^7.3.1", + "zod": "^4.3.5" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/packages/documentation/src/components/ui/popover.tsx b/packages/documentation/src/components/ui/popover.tsx deleted file mode 100644 index 9257aa47..00000000 --- a/packages/documentation/src/components/ui/popover.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; -import * as PopoverPrimitive from '@radix-ui/react-popover'; -import * as React from 'react'; -import { cn } from '../../lib/cn'; - -const Popover: typeof PopoverPrimitive.Root = PopoverPrimitive.Root; - -const PopoverTrigger: typeof PopoverPrimitive.Trigger = PopoverPrimitive.Trigger; - -const PopoverContent: React.ForwardRefExoticComponent< - React.ComponentPropsWithoutRef & - React.RefAttributes> -> = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( - - - -)); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -const PopoverClose: typeof PopoverPrimitive.PopoverClose = PopoverPrimitive.PopoverClose; - -export { Popover, PopoverTrigger, PopoverContent, PopoverClose }; diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index a2fe1346..06589865 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["src/**/*.ts", "src/**/*.tsx", ".source/**/*.ts"], "compilerOptions": { "types": ["vite/client", "bun"], "rootDir": ".", @@ -10,6 +10,8 @@ "fumadocs-mdx:collections/*": ["./.source/*"] }, "jsx": "react-jsx", - "composite": true + "composite": true, + "noEmit": false, + "outDir": "dist" } } From 42f7775d840781f55bb085933437b4e3173058cc Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Tue, 20 Jan 2026 22:56:14 +0100 Subject: [PATCH 07/24] Recover a comment lost in merge --- packages/documentation/source.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/documentation/source.config.ts b/packages/documentation/source.config.ts index b1736184..29575ba3 100644 --- a/packages/documentation/source.config.ts +++ b/packages/documentation/source.config.ts @@ -14,7 +14,11 @@ export const docs = defineDocs({ export default defineConfig({ mdxOptions: { - rehypePlugins: (plugins) => [mermaidConfig(), ...plugins], + rehypePlugins: (plugins) => [ + /** + * Mermaid must run before Shiki to find unprocessed code blocks. + */ + mermaidConfig(), ...plugins], rehypeCodeOptions: { langs: ["js", "jsx", "ts", "tsx"], themes: { From 5c5bb6b0898af3e3a9fd9c773d05c4cf93b8568a Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Tue, 20 Jan 2026 22:59:20 +0100 Subject: [PATCH 08/24] Format --- packages/documentation/source.config.ts | 4 +++- .../src/components/ui/button.tsx | 20 +++++++++---------- packages/documentation/src/lib/cn.ts | 2 +- packages/documentation/src/routes/docs/$.tsx | 4 +++- .../documentation/src/routes/llms[.]txt.ts | 4 +++- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/documentation/source.config.ts b/packages/documentation/source.config.ts index 29575ba3..b11fa77d 100644 --- a/packages/documentation/source.config.ts +++ b/packages/documentation/source.config.ts @@ -18,7 +18,9 @@ export default defineConfig({ /** * Mermaid must run before Shiki to find unprocessed code blocks. */ - mermaidConfig(), ...plugins], + mermaidConfig(), + ...plugins, + ], rehypeCodeOptions: { langs: ["js", "jsx", "ts", "tsx"], themes: { diff --git a/packages/documentation/src/components/ui/button.tsx b/packages/documentation/src/components/ui/button.tsx index a33033d8..7cb8cd69 100644 --- a/packages/documentation/src/components/ui/button.tsx +++ b/packages/documentation/src/components/ui/button.tsx @@ -1,25 +1,25 @@ -import { cva, type VariantProps } from 'class-variance-authority'; +import { cva, type VariantProps } from "class-variance-authority"; const variants = { - ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground', - outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground', - primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80', + ghost: "hover:bg-fd-accent hover:text-fd-accent-foreground", + outline: "border hover:bg-fd-accent hover:text-fd-accent-foreground", + primary: "bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80", secondary: - 'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground', + "border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground", } as const; export const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring', + "inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring", { variants: { variant: variants, // fumadocs use `color` instead of `variant` color: variants, size: { - icon: 'p-1.5 [&_svg]:size-5', - 'icon-sm': 'p-1.5 [&_svg]:size-4.5', - 'icon-xs': 'p-1 [&_svg]:size-4', - sm: 'gap-1 px-2 py-1.5 text-xs', + icon: "p-1.5 [&_svg]:size-5", + "icon-sm": "p-1.5 [&_svg]:size-4.5", + "icon-xs": "p-1 [&_svg]:size-4", + sm: "gap-1 px-2 py-1.5 text-xs", }, }, }, diff --git a/packages/documentation/src/lib/cn.ts b/packages/documentation/src/lib/cn.ts index ba66fd25..8e473dac 100644 --- a/packages/documentation/src/lib/cn.ts +++ b/packages/documentation/src/lib/cn.ts @@ -1 +1 @@ -export { twMerge as cn } from 'tailwind-merge'; +export { twMerge as cn } from "tailwind-merge"; diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index 18dcb107..5a094b50 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -43,7 +43,9 @@ export const Route = createFileRoute("/docs/$")({ }); } - const {getText} = (page.data as { getText?: (mode: string) => Promise }); + const { getText } = page.data as { + getText?: (mode: string) => Promise; + }; if (!getText) { return new Response("getText not available", { status: 500 }); } diff --git a/packages/documentation/src/routes/llms[.]txt.ts b/packages/documentation/src/routes/llms[.]txt.ts index 0abd134d..f8fcf95d 100644 --- a/packages/documentation/src/routes/llms[.]txt.ts +++ b/packages/documentation/src/routes/llms[.]txt.ts @@ -16,7 +16,9 @@ export const Route = createFileRoute("/llms.txt")({ "## Docs", "", ...pages.map((page) => { - const desc = page.data.description ? `: ${page.data.description}` : ""; + const desc = page.data.description + ? `: ${page.data.description}` + : ""; return `- [${page.data.title}](${baseUrl}${page.url}.md)${desc}`; }), ]; From 2cfe00d5676909bcf4c126541952060a77604446 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Tue, 20 Jan 2026 23:31:19 +0100 Subject: [PATCH 09/24] Disable unicorn/filename-case in routes dir --- bun.lock | 4 ++-- package.json | 2 +- packages/design-system/eslint.config.ts | 19 ++++++++++++++++++- packages/design-system/tsconfig.json | 2 +- packages/documentation/eslint.config.ts | 19 ++++++++++++++++++- packages/documentation/source.config.ts | 14 +++++++------- .../src/components/page-actions.tsx | 4 ++-- packages/documentation/src/routes/__root.tsx | 1 - 8 files changed, 49 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index eed712bd..54fbe0b0 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@scriptorium/monorepo", "devDependencies": { "@fumadocs/cli": "^1.2.2", - "@hasparus/eslint-config": "2.0.12", + "@hasparus/eslint-config": "2.0.17", "@tsconfig/strictest": "2.0.8", "@types/bun": "latest", "docs": "workspace:*", @@ -231,7 +231,7 @@ "@fumadocs/ui": ["@fumadocs/ui@16.4.7", "", { "dependencies": { "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.1", "tailwind-merge": "^3.4.0" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "16.4.7", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-NnkMIN5BzBRh2OzA9rp2SgbGEkEwfCfq0sE4vq2n+GkIDIggicGYUNgSl2gtIBQsKYKP/a4/0wrkQKdq4eUJlw=="], - "@hasparus/eslint-config": ["@hasparus/eslint-config@2.0.12", "", { "dependencies": { "@eslint/eslintrc": "3.3.3", "@eslint/js": "9.17.0", "@theguild/eslint-config": "0.13.4", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsonc": "2.21.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-mdx": "3.6.2", "eslint-plugin-n": "17.23.1", "eslint-plugin-perfectionist": "4.6.0", "eslint-plugin-promise": "7.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-sonarjs": "3.0.5", "eslint-plugin-unicorn": "62.0.0", "eslint-plugin-yml": "1.19.1", "globals": "15.14.0", "typescript-eslint": "8.18.2" }, "peerDependencies": { "eslint": "^9.0.0", "typescript": "^5.0.0" } }, "sha512-5WfvspuBeu6bxVbi7YT7wQE9NJM+RGoKlzzSGleSPXTaxTiY5ywcfjprFc0fogsFLmwfrzyhDZrtQrX3qZkEEQ=="], + "@hasparus/eslint-config": ["@hasparus/eslint-config@2.0.17", "", { "dependencies": { "@eslint/eslintrc": "3.3.3", "@eslint/js": "9.17.0", "@theguild/eslint-config": "0.13.4", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsonc": "2.21.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-mdx": "3.6.2", "eslint-plugin-n": "17.23.1", "eslint-plugin-perfectionist": "4.6.0", "eslint-plugin-promise": "7.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-sonarjs": "3.0.5", "eslint-plugin-unicorn": "62.0.0", "eslint-plugin-yml": "1.19.1", "globals": "15.14.0", "typescript-eslint": "8.18.2" }, "peerDependencies": { "eslint": "^9.0.0", "typescript": "^5.0.0" } }, "sha512-4EO7HPWHcgK3QV6Oa/rCrTzFUh/k/RIofCUP1voSicUCRaxsoStiWp12lD3vjfLwJhc562r7RfhA4Sr07QMNiQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], diff --git a/package.json b/package.json index 16b05cdb..45fff7d3 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@fumadocs/cli": "^1.2.2", - "@hasparus/eslint-config": "2.0.12", + "@hasparus/eslint-config": "2.0.17", "@tsconfig/strictest": "2.0.8", "@types/bun": "latest", "docs": "workspace:*", diff --git a/packages/design-system/eslint.config.ts b/packages/design-system/eslint.config.ts index 8ae5cb3a..95d65083 100644 --- a/packages/design-system/eslint.config.ts +++ b/packages/design-system/eslint.config.ts @@ -1 +1,18 @@ -export { default } from "@hasparus/eslint-config/the-guild"; +import config from "@hasparus/eslint-config/the-guild"; + +export default [ + ...config, + { + files: ["src/routes/**/*.tsx"], + rules: { + "unicorn/filename-case": "off", + }, + }, + { + languageOptions: { + parserOptions: { + project: "./tsconfig.json", + }, + }, + }, +]; diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json index ad4fb9d6..4389207a 100644 --- a/packages/design-system/tsconfig.json +++ b/packages/design-system/tsconfig.json @@ -6,6 +6,6 @@ "noEmit": false, "outDir": "dist" }, - "include": ["src", "eslint.config.ts"], + "include": ["src",], "exclude": ["node_modules", "dist"] } diff --git a/packages/documentation/eslint.config.ts b/packages/documentation/eslint.config.ts index 8ae5cb3a..2cbfb585 100644 --- a/packages/documentation/eslint.config.ts +++ b/packages/documentation/eslint.config.ts @@ -1 +1,18 @@ -export { default } from "@hasparus/eslint-config/the-guild"; +import config from "@hasparus/eslint-config/the-guild"; + +export default [ + ...config, + { + files: ["./src/routes/**/*.ts", "./src/routes/**/*.tsx"], + rules: { + "unicorn/filename-case": "off", + }, + }, + { + languageOptions: { + parserOptions: { + project: "./tsconfig.json", + }, + }, + }, +]; diff --git a/packages/documentation/source.config.ts b/packages/documentation/source.config.ts index b11fa77d..f84cb843 100644 --- a/packages/documentation/source.config.ts +++ b/packages/documentation/source.config.ts @@ -14,13 +14,6 @@ export const docs = defineDocs({ export default defineConfig({ mdxOptions: { - rehypePlugins: (plugins) => [ - /** - * Mermaid must run before Shiki to find unprocessed code blocks. - */ - mermaidConfig(), - ...plugins, - ], rehypeCodeOptions: { langs: ["js", "jsx", "ts", "tsx"], themes: { @@ -32,6 +25,13 @@ export default defineConfig({ transformerTwoslash(), ], }, + rehypePlugins: (plugins) => [ + /** + * Mermaid must run before Shiki to find unprocessed code blocks. + */ + mermaidConfig(), + ...plugins, + ], }, }); diff --git a/packages/documentation/src/components/page-actions.tsx b/packages/documentation/src/components/page-actions.tsx index 0157964e..66b53376 100644 --- a/packages/documentation/src/components/page-actions.tsx +++ b/packages/documentation/src/components/page-actions.tsx @@ -9,7 +9,7 @@ const actionClass = "inline-flex items-center gap-2 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring disabled:pointer-events-none disabled:opacity-50"; export function LLMCopyButton({ markdownUrl }: { markdownUrl: string }) { - const [isLoading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const [checked, onClick] = useCopyButton(async () => { const cached = cache.get(markdownUrl); if (cached) return navigator.clipboard.writeText(cached); @@ -33,7 +33,7 @@ export function LLMCopyButton({ markdownUrl }: { markdownUrl: string }) { }); return ( - diff --git a/packages/documentation/src/routes/__root.tsx b/packages/documentation/src/routes/__root.tsx index 9d1101e4..958505f5 100644 --- a/packages/documentation/src/routes/__root.tsx +++ b/packages/documentation/src/routes/__root.tsx @@ -6,7 +6,6 @@ import { Scripts, } from "@tanstack/react-router"; import { RootProvider } from "fumadocs-ui/provider/tanstack"; -import * as React from "react"; export const Route = createRootRoute({ component: RootComponent, From e3a8e46403026cee537b831ec4a8f7e2c75f16ef Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 00:16:39 +0100 Subject: [PATCH 10/24] Fix blank page --- packages/documentation/src/routes/docs/$.tsx | 39 +------------------- packages/documentation/src/start.ts | 23 +++++++++--- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/packages/documentation/src/routes/docs/$.tsx b/packages/documentation/src/routes/docs/$.tsx index 5a094b50..11aa5d20 100644 --- a/packages/documentation/src/routes/docs/$.tsx +++ b/packages/documentation/src/routes/docs/$.tsx @@ -3,7 +3,6 @@ import { baseOptions } from "@/lib/layout.shared"; import { source } from "@/lib/source"; import { createFileRoute, notFound } from "@tanstack/react-router"; import { createServerFn } from "@tanstack/react-start"; -import { isMarkdownPreferred } from "fumadocs-core/negotiation"; import { useFumadocsLoader } from "fumadocs-core/source/client"; import browserCollections from "fumadocs-mdx:collections/browser"; import * as Twoslash from "fumadocs-twoslash/ui"; @@ -19,47 +18,11 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; export const Route = createFileRoute("/docs/$")({ component: Page, loader: async ({ params }) => { - const slugs = params._splat?.split("/") ?? []; + const slugs = params._splat?.split("/").filter(Boolean) ?? []; const data = await serverLoader({ data: slugs }); await clientLoader.preload(data.path); return data; }, - server: { - handlers: { - GET: async ({ params, request }) => { - const rawSplat = params._splat ?? ""; - const isMdxRequest = rawSplat.endsWith(".mdx"); - const slugs = (isMdxRequest ? rawSplat.slice(0, -4) : rawSplat) - .split("/") - .filter(Boolean); - - if (!isMdxRequest && !isMarkdownPreferred(request)) return; - - const page = source.getPage(slugs); - if (!page) { - return new Response("Not found", { - headers: { "Content-Type": "text/plain" }, - status: 404, - }); - } - - const { getText } = page.data as { - getText?: (mode: string) => Promise; - }; - if (!getText) { - return new Response("getText not available", { status: 500 }); - } - - const content = await getText("raw"); - return new Response(content, { - headers: { - "Content-Length": String(new TextEncoder().encode(content).length), - "Content-Type": "text/markdown", - }, - }); - }, - }, - }, }); const serverLoader = createServerFn({ diff --git a/packages/documentation/src/start.ts b/packages/documentation/src/start.ts index 9db8f75e..1780a41c 100644 --- a/packages/documentation/src/start.ts +++ b/packages/documentation/src/start.ts @@ -1,22 +1,33 @@ import { redirect } from "@tanstack/react-router"; import { createMiddleware, createStart } from "@tanstack/react-start"; -import { rewritePath } from "fumadocs-core/negotiation"; +import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; const { rewrite: rewriteMdx } = rewritePath( "/docs{/*path}.mdx", - "llms.mdx/docs{/*path}", + "/llms.mdx/docs{/*path}", ); const { rewrite: rewriteMd } = rewritePath( "/docs{/*path}.md", - "llms.mdx/docs{/*path}", + "/llms.mdx/docs{/*path}", +); +const { rewrite: rewriteAccept } = rewritePath( + "/docs{/*path}", + "/llms.mdx/docs{/*path}", ); const llmMiddleware = createMiddleware().server(({ next, request }) => { const url = new URL(request.url); - const path = rewriteMdx(url.pathname) ?? rewriteMd(url.pathname); - if (path) { - throw redirect(new URL(path, url)); + const extensionPath = rewriteMdx(url.pathname) ?? rewriteMd(url.pathname); + if (extensionPath) { + throw redirect({ href: new URL(extensionPath, url).href }); + } + + if (isMarkdownPreferred(request)) { + const acceptPath = rewriteAccept(url.pathname); + if (acceptPath) { + throw redirect({ href: new URL(acceptPath, url).href }); + } } return next(); From ca53f87dae918e42bffd998634e96f4ef5bfdb76 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 00:46:57 +0100 Subject: [PATCH 11/24] Add more tests and CI workflow to run them --- .github/workflows/pr.yml | 5 +- .github/workflows/test.yml | 19 +++ package.json | 3 +- packages/documentation/package.json | 3 +- .../documentation/src/routes/docs/-$.test.ts | 112 +++++++++++++++--- packages/documentation/src/start.ts | 36 +++--- turbo.json | 3 + 7 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6033d3df..40f56bb5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,9 +13,8 @@ jobs: typescript: uses: ./.github/workflows/typescript-typecheck.yml - # todo: add tests - # tests: - # uses: ./.github/workflows/tests.yml + test: + uses: ./.github/workflows/test.yml website-preview: uses: ./.github/workflows/website.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..663b9899 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +on: + workflow_call: + +jobs: + unit: + runs-on: ubuntu-24.04 + steps: + - name: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 2 + + - name: setup environment + uses: ./.github/actions/setup + with: + actor: test + + - name: run tests + run: pnpm test diff --git a/package.json b/package.json index 45fff7d3..5fd3cafc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "format": "prettier --write \"**/*.{ts,tsx,md}\"", "format:check": "prettier --check \"**/*.{ts,tsx,md}\"", "typecheck": "turbo run typecheck", - "postinstall": "node scripts/fix-nitro-nightly.mjs" + "postinstall": "node scripts/fix-nitro-nightly.mjs", + "test": "turbo run test" }, "devDependencies": { "@fumadocs/cli": "^1.2.2", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 09426dd2..86967cab 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -10,7 +10,8 @@ "typecheck": "bun --bun fumadocs-mdx && bun --bun tsc --noEmit", "postinstall": "bun --bun fumadocs-mdx && bun playwright install chromium", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "test": "bun test" }, "dependencies": { "@sparticuz/chromium": "^143.0.4", diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts index 643b4e42..95705e9c 100644 --- a/packages/documentation/src/routes/docs/-$.test.ts +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -1,44 +1,126 @@ /** - * Integration test for markdown content negotiation. + * Integration tests for AI/LLM features. * - * Run with: bun test src/routes/docs/-$.test.ts + * Run with: bun test src/routes/docs/-llm.test.ts * Requires dev server running: bun run dev */ import { describe, expect, test } from "bun:test"; const BASE_URL = process.env["TEST_URL"] || "http://localhost:1440"; -describe("markdown content negotiation", () => { - test("returns markdown when Accept: text/markdown", async () => { +describe("llms.txt", () => { + test("returns index of docs", async () => { + const res = await fetch(`${BASE_URL}/llms.txt`); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/plain"); + const text = await res.text(); + expect(text).toContain("# GraphQL Hive"); + expect(text).toContain(".md)"); + }); +}); + +describe("llms-full.txt", () => { + test("returns all docs as markdown", async () => { + const res = await fetch(`${BASE_URL}/llms-full.txt`); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text).toContain("# "); + expect(text).toContain("(/docs"); + }); +}); + +describe(".mdx extension", () => { + test("/docs.mdx returns markdown for root docs page", async () => { + const res = await fetch(`${BASE_URL}/docs.mdx`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + expect(text).toContain("title:"); + }); + + test("/docs/test.mdx returns markdown for nested page", async () => { + const res = await fetch(`${BASE_URL}/docs/test.mdx`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + expect(text).toContain("title:"); + }); +}); + +describe(".md extension", () => { + test("/docs.md returns markdown for root docs page", async () => { + const res = await fetch(`${BASE_URL}/docs.md`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + }); + + test("/docs/test.md returns markdown for nested page", async () => { + const res = await fetch(`${BASE_URL}/docs/test.md`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + }); +}); + +describe("Accept header negotiation", () => { + test("Accept: text/markdown returns markdown", async () => { const res = await fetch(`${BASE_URL}/docs`, { headers: { Accept: "text/markdown" }, + redirect: "follow", }); - expect(res.headers.get("content-type")).toBe("text/markdown"); expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); const text = await res.text(); - expect(text).toContain("---"); // frontmatter - expect(text).toContain("title:"); // has frontmatter field + expect(text).toContain("---"); }); - test("returns markdown when Accept: text/plain", async () => { + test("Accept: text/plain returns markdown", async () => { const res = await fetch(`${BASE_URL}/docs`, { headers: { Accept: "text/plain" }, + redirect: "follow", }); - expect(res.headers.get("content-type")).toBe("text/markdown"); expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); }); - test("returns 404 for non-existent page with markdown accept", async () => { - const res = await fetch(`${BASE_URL}/docs/non-existent-page-xyz`, { + test("Accept: text/markdown works for nested pages", async () => { + const res = await fetch(`${BASE_URL}/docs/test`, { headers: { Accept: "text/markdown" }, + redirect: "follow", }); - expect(res.status).toBe(404); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); }); - test("does not intercept requests without markdown Accept header", async () => { + test("regular browser request returns HTML", async () => { const res = await fetch(`${BASE_URL}/docs`); - // Should NOT return text/markdown (falls through to normal page render) - expect(res.headers.get("content-type")).not.toBe("text/markdown"); expect(res.status).toBe(200); + expect(res.headers.get("content-type")).not.toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain(""); + }); +}); + +describe("404 handling", () => { + test(".mdx extension returns notFound for non-existent page", async () => { + const res = await fetch(`${BASE_URL}/docs/non-existent-xyz.mdx`, { + redirect: "follow", + }); + const json = await res.json(); + expect(json).toEqual({ isNotFound: true }); + }); + + test("Accept header returns notFound for non-existent page", async () => { + const res = await fetch(`${BASE_URL}/docs/non-existent-xyz`, { + headers: { Accept: "text/markdown" }, + redirect: "follow", + }); + const json = await res.json(); + expect(json).toEqual({ isNotFound: true }); }); }); diff --git a/packages/documentation/src/start.ts b/packages/documentation/src/start.ts index 1780a41c..4a97b697 100644 --- a/packages/documentation/src/start.ts +++ b/packages/documentation/src/start.ts @@ -2,29 +2,37 @@ import { redirect } from "@tanstack/react-router"; import { createMiddleware, createStart } from "@tanstack/react-start"; import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; -const { rewrite: rewriteMdx } = rewritePath( - "/docs{/*path}.mdx", - "/llms.mdx/docs{/*path}", -); -const { rewrite: rewriteMd } = rewritePath( - "/docs{/*path}.md", - "/llms.mdx/docs{/*path}", -); -const { rewrite: rewriteAccept } = rewritePath( - "/docs{/*path}", - "/llms.mdx/docs{/*path}", -); +const extensionRewrites = [ + rewritePath("/docs.:ext", "/llms.mdx/docs"), + rewritePath("/docs/*path.:ext", "/llms.mdx/docs/*path"), +]; + +const acceptRewrites = [ + rewritePath("/docs", "/llms.mdx/docs"), + rewritePath("/docs{/*path}", "/llms.mdx/docs{/*path}"), +]; + +function tryRewrite( + pathname: string, + configs: { rewrite: (path: string) => false | string }[], +): false | string { + for (const { rewrite } of configs) { + const result = rewrite(pathname); + if (result) return result; + } + return false; +} const llmMiddleware = createMiddleware().server(({ next, request }) => { const url = new URL(request.url); - const extensionPath = rewriteMdx(url.pathname) ?? rewriteMd(url.pathname); + const extensionPath = tryRewrite(url.pathname, extensionRewrites); if (extensionPath) { throw redirect({ href: new URL(extensionPath, url).href }); } if (isMarkdownPreferred(request)) { - const acceptPath = rewriteAccept(url.pathname); + const acceptPath = tryRewrite(url.pathname, acceptRewrites); if (acceptPath) { throw redirect({ href: new URL(acceptPath, url).href }); } diff --git a/turbo.json b/turbo.json index 6ab62393..c39f388c 100644 --- a/turbo.json +++ b/turbo.json @@ -19,6 +19,9 @@ "dev": { "cache": false, "persistent": true + }, + "test": { + "dependsOn": ["^test"] } } } From 3e171d8c6e4af941ad4ed1248333472ebf2d8bbd Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 01:00:36 +0100 Subject: [PATCH 12/24] Improve tests --- .../documentation/src/routes/docs/-$.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts index 95705e9c..03caed32 100644 --- a/packages/documentation/src/routes/docs/-$.test.ts +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -1,12 +1,42 @@ /** * Integration tests for AI/LLM features. * - * Run with: bun test src/routes/docs/-llm.test.ts - * Requires dev server running: bun run dev + * Run with: bun test src/routes/docs/-$.test.ts */ -import { describe, expect, test } from "bun:test"; +import { type Subprocess, spawn } from "bun"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -const BASE_URL = process.env["TEST_URL"] || "http://localhost:1440"; +const TEST_PORT = 14401; +const BASE_URL = process.env["TEST_URL"] || `http://localhost:${TEST_PORT}`; + +let devServer: Subprocess | null = null; + +async function waitForServer(maxAttempts = 30): Promise { + for (let i = 0; i < maxAttempts; i++) { + try { + const res = await fetch(BASE_URL, { signal: AbortSignal.timeout(1000) }); + if (res.ok || res.status < 500) return; + } catch {} + await new Promise((r) => setTimeout(r, 1000)); + } + throw new Error(`Server not ready after ${maxAttempts}s`); +} + +beforeAll(async () => { + if (process.env["TEST_URL"]) return; // user-provided server + + devServer = spawn(["bun", "--bun", "vite", "dev", "--port", String(TEST_PORT)], { + cwd: import.meta.dir + "/../../..", + stdout: "ignore", + stderr: "ignore", + }); + + await waitForServer(); +}, 60_000); + +afterAll(() => { + devServer?.kill(); +}); describe("llms.txt", () => { test("returns index of docs", async () => { From 83cbfad0f4ba43886b02772a81c6ab964c6d56b8 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 01:04:27 +0100 Subject: [PATCH 13/24] Log a message --- .../documentation/src/routes/docs/-$.test.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts index 03caed32..cb0ef242 100644 --- a/packages/documentation/src/routes/docs/-$.test.ts +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -3,10 +3,10 @@ * * Run with: bun test src/routes/docs/-$.test.ts */ -import { type Subprocess, spawn } from "bun"; +import { spawn, type Subprocess } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -const TEST_PORT = 14401; +const TEST_PORT = 14_401; const BASE_URL = process.env["TEST_URL"] || `http://localhost:${TEST_PORT}`; let devServer: Subprocess | null = null; @@ -16,7 +16,10 @@ async function waitForServer(maxAttempts = 30): Promise { try { const res = await fetch(BASE_URL, { signal: AbortSignal.timeout(1000) }); if (res.ok || res.status < 500) return; - } catch {} + } catch { + // eslint-disable-next-line no-console + console.log(`Server not ready after ${i + 1}s, retrying...`); + } await new Promise((r) => setTimeout(r, 1000)); } throw new Error(`Server not ready after ${maxAttempts}s`); @@ -25,11 +28,14 @@ async function waitForServer(maxAttempts = 30): Promise { beforeAll(async () => { if (process.env["TEST_URL"]) return; // user-provided server - devServer = spawn(["bun", "--bun", "vite", "dev", "--port", String(TEST_PORT)], { - cwd: import.meta.dir + "/../../..", - stdout: "ignore", - stderr: "ignore", - }); + devServer = spawn( + ["bun", "--bun", "vite", "dev", "--port", String(TEST_PORT)], + { + cwd: import.meta.dir + "/../../..", + stderr: "ignore", + stdout: "ignore", + }, + ); await waitForServer(); }, 60_000); @@ -70,7 +76,9 @@ describe(".mdx extension", () => { }); test("/docs/test.mdx returns markdown for nested page", async () => { - const res = await fetch(`${BASE_URL}/docs/test.mdx`, { redirect: "follow" }); + const res = await fetch(`${BASE_URL}/docs/test.mdx`, { + redirect: "follow", + }); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toBe("text/markdown"); const text = await res.text(); From c231781698ab42c98c9afa0887885da20f56e673 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 01:08:29 +0100 Subject: [PATCH 14/24] Add install step in test.yml --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 663b9899..fca4cc17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,11 @@ jobs: with: fetch-depth: 2 - - name: setup environment - uses: ./.github/actions/setup - with: - actor: test + - name: setup bun + uses: oven-sh/setup-bun@v2 + + - name: install dependencies + run: bun install --frozen-lockfile - name: run tests run: pnpm test From fe0d188acac2367311dd87362ce5f60be4fe92aa Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 01:12:51 +0100 Subject: [PATCH 15/24] Fix test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fca4cc17..a3779e0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,4 +17,4 @@ jobs: run: bun install --frozen-lockfile - name: run tests - run: pnpm test + run: bun run test From ef4068239eb366bbd7ab762e9aebd3c234bd82d9 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Wed, 21 Jan 2026 01:26:37 +0100 Subject: [PATCH 16/24] Use processed text, not raw --- .../documentation/src/routes/llms[.]mdx.docs.$.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/documentation/src/routes/llms[.]mdx.docs.$.ts b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts index 14afbf7d..e5e6ba78 100644 --- a/packages/documentation/src/routes/llms[.]mdx.docs.$.ts +++ b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts @@ -8,7 +8,19 @@ export const Route = createFileRoute("/llms.mdx/docs/$")({ const slugs = params._splat?.split("/") ?? []; const page = source.getPage(slugs); if (!page) throw notFound(); - return new Response(await page.data.getText("raw"), { + + const processed = await page.data.getText("processed"); + // We're adding frontmatter back, but only the parts relevant to LLMs. + const frontmatter = [ + "---", + `title: ${page.data.title}`, + page.data.description && `description: ${page.data.description}`, + "---", + ] + .filter(Boolean) + .join("\n"); + + return new Response(`${frontmatter}\n${processed}`, { headers: { "Content-Type": "text/markdown", }, From 835f74cf8da2eaf50fc745e8045ad56446056897 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 00:23:55 +0100 Subject: [PATCH 17/24] Use rewrites instead of redirects for .txt/md/mdx paths --- packages/design-system/eslint.config.ts | 3 +++ packages/design-system/tsconfig.json | 2 +- packages/documentation/src/router.tsx | 20 ++++++++++++++++++-- packages/documentation/src/start.ts | 10 ---------- packages/documentation/tsconfig.json | 2 +- packages/documentation/vite.config.ts | 2 +- packages/documentation/todo.md => todo.md | 1 - 7 files changed, 24 insertions(+), 16 deletions(-) rename packages/documentation/todo.md => todo.md (98%) diff --git a/packages/design-system/eslint.config.ts b/packages/design-system/eslint.config.ts index 95d65083..bb662da1 100644 --- a/packages/design-system/eslint.config.ts +++ b/packages/design-system/eslint.config.ts @@ -12,6 +12,9 @@ export default [ languageOptions: { parserOptions: { project: "./tsconfig.json", + projectService: { + allowDefaultProject: ["*.config.ts"], + }, }, }, }, diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json index 4389207a..9aeb1da2 100644 --- a/packages/design-system/tsconfig.json +++ b/packages/design-system/tsconfig.json @@ -6,6 +6,6 @@ "noEmit": false, "outDir": "dist" }, - "include": ["src",], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/packages/documentation/src/router.tsx b/packages/documentation/src/router.tsx index 8bade5db..f4641992 100644 --- a/packages/documentation/src/router.tsx +++ b/packages/documentation/src/router.tsx @@ -1,12 +1,28 @@ import { NotFound } from "@/components/not-found"; -import { createRouter as createTanStackRouter } from "@tanstack/react-router"; +import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; +const TEXT_FILE_REGEX = /\.(txt|md|mdx)$/; + export function getRouter() { - return createTanStackRouter({ + return createRouter({ defaultNotFoundComponent: NotFound, defaultPreload: "intent", + rewrite: { + /** + * Rewrite .txt, .md, and .mdx paths + * so simple `curl http://localhost:1440/docs/article.txt` works. + * Content negotiation is handled by the middleware in `src/start.ts`, + * but we handle paths here to make sure accessing the raw text is foolproof. + */ + input: ({ url }) => { + if (TEXT_FILE_REGEX.test(url.pathname)) { + url.pathname = `/llms.mdx${url.pathname.slice(0, url.pathname.lastIndexOf("."))}`; + } + return url; + }, + }, routeTree, scrollRestoration: true, }); diff --git a/packages/documentation/src/start.ts b/packages/documentation/src/start.ts index 4a97b697..a5d0b6de 100644 --- a/packages/documentation/src/start.ts +++ b/packages/documentation/src/start.ts @@ -2,11 +2,6 @@ import { redirect } from "@tanstack/react-router"; import { createMiddleware, createStart } from "@tanstack/react-start"; import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; -const extensionRewrites = [ - rewritePath("/docs.:ext", "/llms.mdx/docs"), - rewritePath("/docs/*path.:ext", "/llms.mdx/docs/*path"), -]; - const acceptRewrites = [ rewritePath("/docs", "/llms.mdx/docs"), rewritePath("/docs{/*path}", "/llms.mdx/docs{/*path}"), @@ -26,11 +21,6 @@ function tryRewrite( const llmMiddleware = createMiddleware().server(({ next, request }) => { const url = new URL(request.url); - const extensionPath = tryRewrite(url.pathname, extensionRewrites); - if (extensionPath) { - throw redirect({ href: new URL(extensionPath, url).href }); - } - if (isMarkdownPreferred(request)) { const acceptPath = tryRewrite(url.pathname, acceptRewrites); if (acceptPath) { diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index 06589865..509774b1 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*.ts", "src/**/*.tsx", ".source/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", ".source/**/*.ts", "*.ts"], "compilerOptions": { "types": ["vite/client", "bun"], "rootDir": ".", diff --git a/packages/documentation/vite.config.ts b/packages/documentation/vite.config.ts index 994ac2c3..211f09cf 100644 --- a/packages/documentation/vite.config.ts +++ b/packages/documentation/vite.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ }), tanstackStart({ prerender: { - enabled: false, + enabled: false, // todo: enable this }, }), react(), diff --git a/packages/documentation/todo.md b/todo.md similarity index 98% rename from packages/documentation/todo.md rename to todo.md index 01d6352d..dbe026b0 100644 --- a/packages/documentation/todo.md +++ b/todo.md @@ -1,3 +1,2 @@ -- - SEO https://tanstack.com/start/latest/docs/framework/react/guide/seo - LLMO https://tanstack.com/start/latest/docs/framework/react/guide/llmo From b9b33a3dae7e732529922f88abef3bf0341d69c8 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 00:44:36 +0100 Subject: [PATCH 18/24] Do not rewrite llms.txt --- packages/documentation/src/router.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/documentation/src/router.tsx b/packages/documentation/src/router.tsx index f4641992..b91f97bc 100644 --- a/packages/documentation/src/router.tsx +++ b/packages/documentation/src/router.tsx @@ -4,6 +4,7 @@ import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; const TEXT_FILE_REGEX = /\.(txt|md|mdx)$/; +const TEXT_FILE_ROUTES = new Set(['/llms-full.txt', '/llms.txt']); export function getRouter() { return createRouter({ @@ -17,7 +18,7 @@ export function getRouter() { * but we handle paths here to make sure accessing the raw text is foolproof. */ input: ({ url }) => { - if (TEXT_FILE_REGEX.test(url.pathname)) { + if (TEXT_FILE_REGEX.test(url.pathname) && !TEXT_FILE_ROUTES.has(url.pathname)) { url.pathname = `/llms.mdx${url.pathname.slice(0, url.pathname.lastIndexOf("."))}`; } return url; From eed63db3ca7cfd0e60af2c1b07463ec37068495c Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 00:49:23 +0100 Subject: [PATCH 19/24] Add tests for the newest rewrite shenanigans --- .../documentation/src/routes/docs/-$.test.ts | 43 ++++++++++++++++++- .../src/routes/llms-full[.]txt.ts | 4 +- .../documentation/src/routes/llms[.]txt.ts | 2 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts index cb0ef242..8376aa54 100644 --- a/packages/documentation/src/routes/docs/-$.test.ts +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -48,21 +48,57 @@ describe("llms.txt", () => { test("returns index of docs", async () => { const res = await fetch(`${BASE_URL}/llms.txt`); expect(res.status).toBe(200); - expect(res.headers.get("content-type")).toContain("text/plain"); + expect(res.headers.get("content-type")).toContain("text/markdown"); const text = await res.text(); expect(text).toContain("# GraphQL Hive"); expect(text).toContain(".md)"); }); + + test("is not rewritten to a doc page", async () => { + const res = await fetch(`${BASE_URL}/llms.txt`); + const text = await res.text(); + // llms.txt should NOT start with frontmatter (doc pages do) + expect(text.startsWith("---")).toBe(false); + }); }); describe("llms-full.txt", () => { test("returns all docs as markdown", async () => { const res = await fetch(`${BASE_URL}/llms-full.txt`); expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/markdown"); const text = await res.text(); expect(text).toContain("# "); expect(text).toContain("(/docs"); }); + + test("is not rewritten to a doc page", async () => { + const res = await fetch(`${BASE_URL}/llms-full.txt`); + expect(res.status).toBe(200); + // llms-full.txt contains multiple docs concatenated, not a single doc + const text = await res.text(); + expect(text).toContain("(/docs/"); + }); +}); + +describe(".txt extension", () => { + test("/docs.txt returns markdown for root docs page", async () => { + const res = await fetch(`${BASE_URL}/docs.txt`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + expect(text).toContain("title:"); + }); + + test("/docs/test.txt returns markdown for nested page", async () => { + const res = await fetch(`${BASE_URL}/docs/test.txt`, { redirect: "follow" }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("text/markdown"); + const text = await res.text(); + expect(text).toContain("---"); + expect(text).toContain("title:"); + }); }); describe(".mdx extension", () => { @@ -161,4 +197,9 @@ describe("404 handling", () => { const json = await res.json(); expect(json).toEqual({ isNotFound: true }); }); + + test("non-text extensions like .png are not rewritten", async () => { + const res = await fetch(`${BASE_URL}/docs.png`, { redirect: "follow" }); + expect(res.status).toBe(404); + }); }); diff --git a/packages/documentation/src/routes/llms-full[.]txt.ts b/packages/documentation/src/routes/llms-full[.]txt.ts index 1b4cf88d..813e8e92 100644 --- a/packages/documentation/src/routes/llms-full[.]txt.ts +++ b/packages/documentation/src/routes/llms-full[.]txt.ts @@ -8,7 +8,9 @@ export const Route = createFileRoute("/llms-full.txt")({ GET: async () => { const scan = source.getPages().map(getLLMText); const scanned = await Promise.all(scan); - return new Response(scanned.join("\n\n")); + return new Response(scanned.join("\n\n"), { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); }, }, }, diff --git a/packages/documentation/src/routes/llms[.]txt.ts b/packages/documentation/src/routes/llms[.]txt.ts index f8fcf95d..943d8b17 100644 --- a/packages/documentation/src/routes/llms[.]txt.ts +++ b/packages/documentation/src/routes/llms[.]txt.ts @@ -24,7 +24,7 @@ export const Route = createFileRoute("/llms.txt")({ ]; return new Response(lines.join("\n"), { - headers: { "Content-Type": "text/plain; charset=utf-8" }, + headers: { "Content-Type": "text/markdown; charset=utf-8" }, }); }, }, From d3df65f3d8145aad037e814a9b4a740a6e3ad1de Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 00:51:14 +0100 Subject: [PATCH 20/24] Commit lockfile --- bun.lock | 4 ++-- packages/documentation/src/router.tsx | 7 +++++-- packages/documentation/src/routes/docs/-$.test.ts | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 54fbe0b0..af6f93d9 100644 --- a/bun.lock +++ b/bun.lock @@ -1751,9 +1751,9 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "nf3": ["nf3@0.3.5", "", {}, "sha512-1VozaVz0lVfGL3c2wZ4c6bmQCm340gDiIYUU3lcg8vVGL/WeuTdrd6OhJiUHZWofc7fFdquhS8Gm+13c3Tumcw=="], + "nf3": ["nf3@0.3.6", "", {}, "sha512-/XRUUILTAyuy1XunyVQuqGp8aEmZ2TfRTn8Rji+FA4xqv20qzL4jV7Reqbuey2XucKgPeRVcEYGScmJM0UnB6Q=="], - "nitro": ["nitro-nightly@3.0.1-20260120-140218-d2383f00", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-5SWkL/b2r8x5ZyfkwhhZA7Ra3pM3lS6K8DIxCt5OIzQYFbfOjdzg9ANMKtSz1+Bfjb6u8BtSzQBLdgFJLgXrxQ=="], + "nitro": ["nitro-nightly@3.0.1-20260122-201913-dfdff9e9", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.6", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-s20CTdwzRl63TehtkAd5ip2hLCfjrhnYahFWNQpA2CdjWKKWRtvYXuNAHZxwouLi81VDvO6ckd+XY1pliM1plg=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], diff --git a/packages/documentation/src/router.tsx b/packages/documentation/src/router.tsx index b91f97bc..a508f9b3 100644 --- a/packages/documentation/src/router.tsx +++ b/packages/documentation/src/router.tsx @@ -4,7 +4,7 @@ import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; const TEXT_FILE_REGEX = /\.(txt|md|mdx)$/; -const TEXT_FILE_ROUTES = new Set(['/llms-full.txt', '/llms.txt']); +const TEXT_FILE_ROUTES = new Set(["/llms-full.txt", "/llms.txt"]); export function getRouter() { return createRouter({ @@ -18,7 +18,10 @@ export function getRouter() { * but we handle paths here to make sure accessing the raw text is foolproof. */ input: ({ url }) => { - if (TEXT_FILE_REGEX.test(url.pathname) && !TEXT_FILE_ROUTES.has(url.pathname)) { + if ( + TEXT_FILE_REGEX.test(url.pathname) && + !TEXT_FILE_ROUTES.has(url.pathname) + ) { url.pathname = `/llms.mdx${url.pathname.slice(0, url.pathname.lastIndexOf("."))}`; } return url; diff --git a/packages/documentation/src/routes/docs/-$.test.ts b/packages/documentation/src/routes/docs/-$.test.ts index 8376aa54..83149862 100644 --- a/packages/documentation/src/routes/docs/-$.test.ts +++ b/packages/documentation/src/routes/docs/-$.test.ts @@ -92,7 +92,9 @@ describe(".txt extension", () => { }); test("/docs/test.txt returns markdown for nested page", async () => { - const res = await fetch(`${BASE_URL}/docs/test.txt`, { redirect: "follow" }); + const res = await fetch(`${BASE_URL}/docs/test.txt`, { + redirect: "follow", + }); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toBe("text/markdown"); const text = await res.text(); From c1d64288d1bc5eda8902cf86115ef3cc42352980 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 00:54:13 +0100 Subject: [PATCH 21/24] Add a type to ESLint configs to ensure we don't have typos (don't ask) --- packages/design-system/eslint.config.ts | 10 +++++++--- packages/documentation/eslint.config.ts | 10 +++++++--- packages/documentation/tsconfig.json | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/design-system/eslint.config.ts b/packages/design-system/eslint.config.ts index bb662da1..78e041bf 100644 --- a/packages/design-system/eslint.config.ts +++ b/packages/design-system/eslint.config.ts @@ -1,7 +1,9 @@ -import config from "@hasparus/eslint-config/the-guild"; +import type { Linter } from "eslint"; -export default [ - ...config, +import baseConfig from "@hasparus/eslint-config/the-guild"; + +const config: Linter.Config[] = [ + ...baseConfig, { files: ["src/routes/**/*.tsx"], rules: { @@ -19,3 +21,5 @@ export default [ }, }, ]; + +export default config; diff --git a/packages/documentation/eslint.config.ts b/packages/documentation/eslint.config.ts index 2cbfb585..c2057a86 100644 --- a/packages/documentation/eslint.config.ts +++ b/packages/documentation/eslint.config.ts @@ -1,7 +1,9 @@ -import config from "@hasparus/eslint-config/the-guild"; +import type { Linter } from "eslint"; -export default [ - ...config, +import baseConfig from "@hasparus/eslint-config/the-guild"; + +const config: Linter.Config[] = [ + ...baseConfig, { files: ["./src/routes/**/*.ts", "./src/routes/**/*.tsx"], rules: { @@ -16,3 +18,5 @@ export default [ }, }, ]; + +export default config; diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index 509774b1..06589865 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*.ts", "src/**/*.tsx", ".source/**/*.ts", "*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", ".source/**/*.ts"], "compilerOptions": { "types": ["vite/client", "bun"], "rootDir": ".", From f3f591955562e2972e817c8b1ce22c601d05ee92 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 14:42:55 +0100 Subject: [PATCH 22/24] Throw if markdownUrl 500s --- packages/documentation/src/components/page-actions.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/documentation/src/components/page-actions.tsx b/packages/documentation/src/components/page-actions.tsx index 66b53376..2f9bf6d1 100644 --- a/packages/documentation/src/components/page-actions.tsx +++ b/packages/documentation/src/components/page-actions.tsx @@ -21,8 +21,15 @@ export function LLMCopyButton({ markdownUrl }: { markdownUrl: string }) { new ClipboardItem({ "text/plain": fetch(markdownUrl).then(async (res) => { const content = await res.text(); - cache.set(markdownUrl, content); + if (!res.ok) { + // If we're rendering this page, we should definitely have Markdown for it. + // eslint-disable-next-line no-console + console.error(`Failed to fetch ${markdownUrl}`, res); + throw new Error(`${markdownUrl} is unexpectedly missing, try again later`); + } + + cache.set(markdownUrl, content); return content; }), }), From 016fa5485cb9a5d680625f1022a491c71c7f0355 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 14:44:32 +0100 Subject: [PATCH 23/24] Edge case: Filter out empty strings in GET handler --- packages/documentation/src/routes/llms[.]mdx.docs.$.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/routes/llms[.]mdx.docs.$.ts b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts index e5e6ba78..a74db2ae 100644 --- a/packages/documentation/src/routes/llms[.]mdx.docs.$.ts +++ b/packages/documentation/src/routes/llms[.]mdx.docs.$.ts @@ -5,7 +5,7 @@ export const Route = createFileRoute("/llms.mdx/docs/$")({ server: { handlers: { GET: async ({ params }) => { - const slugs = params._splat?.split("/") ?? []; + const slugs = params._splat?.split("/").filter(Boolean) ?? []; const page = source.getPage(slugs); if (!page) throw notFound(); From efa30b047afcc46e0024742745ecf1e07c3b03f1 Mon Sep 17 00:00:00 2001 From: Piotr Monwid-Olechnowicz Date: Fri, 23 Jan 2026 14:49:06 +0100 Subject: [PATCH 24/24] Format --- packages/documentation/src/components/page-actions.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/documentation/src/components/page-actions.tsx b/packages/documentation/src/components/page-actions.tsx index 2f9bf6d1..242193f6 100644 --- a/packages/documentation/src/components/page-actions.tsx +++ b/packages/documentation/src/components/page-actions.tsx @@ -26,7 +26,9 @@ export function LLMCopyButton({ markdownUrl }: { markdownUrl: string }) { // If we're rendering this page, we should definitely have Markdown for it. // eslint-disable-next-line no-console console.error(`Failed to fetch ${markdownUrl}`, res); - throw new Error(`${markdownUrl} is unexpectedly missing, try again later`); + throw new Error( + `${markdownUrl} is unexpectedly missing, try again later`, + ); } cache.set(markdownUrl, content);