From bde6b6a8a7a5cc3dc8b3ae11aed9bc42c28c8016 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Mon, 1 Sep 2025 14:50:23 +0200 Subject: [PATCH 1/9] initial --- app/routes.ts | 4 +- app/routes/llms[.]txt.ts | 10 +++++ app/utils/llms-utils.ts | 85 ++++++++++++++++++++++++++++++++++++++ app/utils/version-links.ts | 21 ++++------ content-collections.ts | 21 ++++++++++ 5 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 app/routes/llms[.]txt.ts create mode 100644 app/utils/llms-utils.ts diff --git a/app/routes.ts b/app/routes.ts index 193d09d..5c84d00 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,7 +1,6 @@ -import { type RouteConfig, index, layout, route } from "@react-router/dev/routes" +import { type RouteConfig, layout, route } from "@react-router/dev/routes" export default [ - index("routes/index.tsx"), layout("routes/documentation-layout.tsx", [ route(":version?/home", "routes/documentation-homepage.tsx"), route(":version/:section/:subsection?/:filename", "routes/documentation-page.tsx"), @@ -11,4 +10,5 @@ export default [ route("resource/*", "routes/resource.locales.ts"), route("$", "routes/$.tsx"), route("sitemap/:lang.xml", "routes/sitemap.$lang[.]xml.ts"), + route("llms.txt", "routes/llms[.]txt.ts"), ] satisfies RouteConfig diff --git a/app/routes/llms[.]txt.ts b/app/routes/llms[.]txt.ts new file mode 100644 index 0000000..a730e5e --- /dev/null +++ b/app/routes/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { renderLlmsTxt } from "~/utils/llms-utils" +import type { Route } from "./+types/llms[.]txt" + +export async function loader({ request }: Route.LoaderArgs) { + const body = await renderLlmsTxt(request, { + title: "Documentation Template", + tagline: "Official documentation and guides.", + }) + return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } }) +} diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts new file mode 100644 index 0000000..52eca1b --- /dev/null +++ b/app/utils/llms-utils.ts @@ -0,0 +1,85 @@ +import { createDomain } from "~/utils/http" +import { loadContentCollections } from "~/utils/load-content-collections" +import type { PageRec, SectionRec } from "../../content-collections" +import { pageUrl } from "./version-links" +import { versions } from "./versions" +import type { Version } from "./versions-utils" + +async function loadVersionData(version: Version) { + try { + const { allPages, allSections } = await loadContentCollections(version) + return { pages: allPages, sections: allSections } + } catch { + return null + } +} + +async function loadAllVersions() { + const acc: Record = {} + for (const v of versions) { + const loaded = await loadVersionData(v) + if (loaded) acc[v] = loaded + } + return acc +} + +function buildSectionTitles(sections: SectionRec[]) { + return new Map(sections.map((s) => [s.slug.split("/").pop() || "", s.title])) +} + +function groupPagesByFolder(pages: PageRec[]) { + const groups = new Map() + + for (const p of pages) { + const id = p.section ?? p._meta?.path?.split("/")[0] + if (!id) continue + + let list = groups.get(id) + if (!list) { + list = [] + groups.set(id, list) + } + list.push(p) + } + + return groups +} + +function renderVersionBlock(domain: string, version: string, pages: PageRec[], sections: SectionRec[]) { + if (!pages.length) return `## ${version}\n\n_No pages found._` + + const sectionTitles = buildSectionTitles(sections) + const groups = groupPagesByFolder(pages) + + const blocks = Array.from(groups.entries()) + .map(([id, list]) => { + const label = sectionTitles.get(id) ?? id + const lines = list + .map((p) => { + const url = pageUrl(domain, version, p.slug) + const note = p.summary || p.description || "" + return `- [${p.title}](${url})${note ? `: ${note}` : ""}` + }) + .join("\n") + return `### ${label}\n\n${lines}` + }) + .join("\n\n") + + return `\n## ${version}\n\n${blocks}` +} + +export async function renderLlmsTxt(request: Request, additionalData: { title: string; tagline: string }) { + const domain = createDomain(request) + const versionsData = await loadAllVersions() + const project = additionalData.title + const tagline = additionalData.tagline + + const content = Object.keys(versionsData) + .map((version) => { + const { pages, sections } = versionsData[version] + return renderVersionBlock(domain, version, pages, sections) + }) + .join("\n") + + return [`# ${project}`, `> ${tagline}`, content, ""].join("\n") +} diff --git a/app/utils/version-links.ts b/app/utils/version-links.ts index ec81943..6722864 100644 --- a/app/utils/version-links.ts +++ b/app/utils/version-links.ts @@ -1,5 +1,5 @@ import { redirect } from "react-router" -import { getLatestVersion, isKnownVersion } from "~/utils/versions-utils" +import { getLatestVersion, isKnownVersion } from "./versions-utils" function firstPathSegment(request: Request) { return new URL(request.url).pathname.split("/").filter(Boolean)[0] @@ -17,11 +17,6 @@ export function ensureVersion(v?: string) { return { version: isKnownVersion(v) ? v : getLatestVersion() } } -/** - * For (documentation-homepage): - * - If a version is present but it's the latest or unknown -> redirect to `/home` - * - Otherwise, use the provided version if known, or fallback to latest - */ export function resolveHomeVersionOrRedirect(versionParam?: string) { if (isUnknownVersion(versionParam) || isLatestVersion(versionParam)) { throw redirect("/home") @@ -29,14 +24,16 @@ export function resolveHomeVersionOrRedirect(versionParam?: string) { return ensureVersion(versionParam) } -/** - * For (documentation-layout): - * - If `params.version` is a known version -> use it - * - Else, peek the first path segment, if it's a known version -> use it - * - Else fall back to latest - */ export function resolveLayoutVersion(paramsVersion: string | undefined, request: Request) { if (isKnownVersion(paramsVersion)) return { version: paramsVersion } const first = firstPathSegment(request) return ensureVersion(isKnownVersion(first) ? first : undefined) } + +export function homepageUrl(base: string, version: string) { + return `${base}/${version}/home` +} + +export function pageUrl(base: string, version: string, slug: string) { + return slug === "_index" ? homepageUrl(base, version) : `${base}/${version}/${slug}` +} diff --git a/content-collections.ts b/content-collections.ts index f8782d9..337434d 100644 --- a/content-collections.ts +++ b/content-collections.ts @@ -78,3 +78,24 @@ const page = defineCollection({ export default defineConfig({ collections: [section, page], }) + +export const sectionOutputSchema = sectionSchema.extend({ + slug: z.string(), + version: z.string(), + _meta: z.object({ path: z.string() }).partial().optional(), +}) + +export const pageOutputSchema = pageSchema.extend({ + slug: z.string(), + section: z.string().optional(), + version: z.string(), + rawMdx: z.string(), + content: z.unknown(), + _meta: z.object({ path: z.string() }).partial().optional(), +}) + +export type SectionRec = z.infer +export type PageRec = z.infer + +export const SectionsArray = z.array(sectionOutputSchema) +export const PagesArray = z.array(pageOutputSchema) From efedf80d1a7f274a4e1f20b5861fa68a2bceed41 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Mon, 1 Sep 2025 16:12:15 +0200 Subject: [PATCH 2/9] refactoring --- app/utils/llms-utils.ts | 38 ++++++++++++------------ app/utils/version-links.ts | 2 +- content-collections.ts | 60 +++++++++++++++++--------------------- 3 files changed, 46 insertions(+), 54 deletions(-) diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts index 52eca1b..7628321 100644 --- a/app/utils/llms-utils.ts +++ b/app/utils/llms-utils.ts @@ -1,6 +1,6 @@ import { createDomain } from "~/utils/http" import { loadContentCollections } from "~/utils/load-content-collections" -import type { PageRec, SectionRec } from "../../content-collections" +import type { PageRecord, SectionRecord } from "../../content-collections" import { pageUrl } from "./version-links" import { versions } from "./versions" import type { Version } from "./versions-utils" @@ -15,7 +15,7 @@ async function loadVersionData(version: Version) { } async function loadAllVersions() { - const acc: Record = {} + const acc: Record = {} for (const v of versions) { const loaded = await loadVersionData(v) if (loaded) acc[v] = loaded @@ -23,12 +23,12 @@ async function loadAllVersions() { return acc } -function buildSectionTitles(sections: SectionRec[]) { +function buildSectionTitles(sections: SectionRecord[]) { return new Map(sections.map((s) => [s.slug.split("/").pop() || "", s.title])) } -function groupPagesByFolder(pages: PageRec[]) { - const groups = new Map() +function groupPagesByFolder(pages: PageRecord[]) { + const groups = new Map() for (const p of pages) { const id = p.section ?? p._meta?.path?.split("/")[0] @@ -45,25 +45,25 @@ function groupPagesByFolder(pages: PageRec[]) { return groups } -function renderVersionBlock(domain: string, version: string, pages: PageRec[], sections: SectionRec[]) { +function renderVersionBlock(domain: string, version: string, pages: PageRecord[], sections: SectionRecord[]) { if (!pages.length) return `## ${version}\n\n_No pages found._` const sectionTitles = buildSectionTitles(sections) const groups = groupPagesByFolder(pages) - const blocks = Array.from(groups.entries()) - .map(([id, list]) => { - const label = sectionTitles.get(id) ?? id - const lines = list - .map((p) => { - const url = pageUrl(domain, version, p.slug) - const note = p.summary || p.description || "" - return `- [${p.title}](${url})${note ? `: ${note}` : ""}` - }) - .join("\n") - return `### ${label}\n\n${lines}` - }) - .join("\n\n") + const renderPageLink = (p: PageRecord) => { + const url = pageUrl(domain, version, p.slug) + const note = p.summary || p.description || "" + return `- [${p.title}](${url})${note ? `: ${note}` : ""}` + } + + const renderSection = ([id, list]: [string, PageRecord[]]) => { + const label = sectionTitles.get(id) ?? id + const lines = list.map(renderPageLink).join("\n") + return `### ${label}\n\n${lines}` + } + + const blocks = Array.from(groups.entries()).map(renderSection).join("\n\n") return `\n## ${version}\n\n${blocks}` } diff --git a/app/utils/version-links.ts b/app/utils/version-links.ts index 6722864..19cda45 100644 --- a/app/utils/version-links.ts +++ b/app/utils/version-links.ts @@ -30,7 +30,7 @@ export function resolveLayoutVersion(paramsVersion: string | undefined, request: return ensureVersion(isKnownVersion(first) ? first : undefined) } -export function homepageUrl(base: string, version: string) { +function homepageUrl(base: string, version: string) { return `${base}/${version}/home` } diff --git a/content-collections.ts b/content-collections.ts index 337434d..d8ab2c9 100644 --- a/content-collections.ts +++ b/content-collections.ts @@ -7,6 +7,31 @@ const sectionSchema = z.object({ title: z.string(), }) +const pageSchema = z.object({ + title: z.string(), + summary: z.string(), + description: z.string(), +}) + +const metaSchema = z.object({ path: z.string() }).partial().optional() +const outputBaseSchema = z.object({ + slug: z.string(), + _meta: metaSchema, +}) + +const sectionOutputSchema = sectionSchema.extend(outputBaseSchema.shape) +const pageOutputSchema = pageSchema.extend({ + ...outputBaseSchema.shape, + section: z.string().optional(), + rawMdx: z.string(), + content: z.unknown(), +}) + +export type SectionRecord = z.infer +export type PageRecord = z.infer +export const SectionsArray = z.array(sectionOutputSchema) +export const PagesArray = z.array(pageOutputSchema) + /** * Removes leading number prefixes like "01-", "02-" from each path segment. */ @@ -30,20 +55,10 @@ const section = defineCollection({ transform: (document) => { const relativePath = document._meta.path.split("/").filter(Boolean).join("/") const slug = cleanSlug(relativePath) - - return { - ...document, - slug, - } + return { ...document, slug } }, }) -const pageSchema = z.object({ - title: z.string(), - summary: z.string(), - description: z.string(), -}) - /* * This collection defines an individual documentation page within the package documentation. * @@ -57,11 +72,9 @@ const page = defineCollection({ transform: async (document, context) => { const relativePath = document._meta.path.split("/").filter(Boolean).join("/") const slug = cleanSlug(relativePath) - const content = await compileMDX(context, document, { rehypePlugins: [rehypeSlug], }) - // rawMdx is the content without the frontmatter, used to read headings from the mdx file and create a content tree for the table of content component const rawMdx = document.content.replace(/^---\s*[\r\n](.*?|\r|\n)---/, "").trim() @@ -78,24 +91,3 @@ const page = defineCollection({ export default defineConfig({ collections: [section, page], }) - -export const sectionOutputSchema = sectionSchema.extend({ - slug: z.string(), - version: z.string(), - _meta: z.object({ path: z.string() }).partial().optional(), -}) - -export const pageOutputSchema = pageSchema.extend({ - slug: z.string(), - section: z.string().optional(), - version: z.string(), - rawMdx: z.string(), - content: z.unknown(), - _meta: z.object({ path: z.string() }).partial().optional(), -}) - -export type SectionRec = z.infer -export type PageRec = z.infer - -export const SectionsArray = z.array(sectionOutputSchema) -export const PagesArray = z.array(pageOutputSchema) From 1d76ae59b8912ee4936004ceed8b27df46f1d6a2 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Mon, 1 Sep 2025 16:33:44 +0200 Subject: [PATCH 3/9] refactoring --- app/utils/llms-utils.ts | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts index 7628321..5f3cae6 100644 --- a/app/utils/llms-utils.ts +++ b/app/utils/llms-utils.ts @@ -6,21 +6,8 @@ import { versions } from "./versions" import type { Version } from "./versions-utils" async function loadVersionData(version: Version) { - try { - const { allPages, allSections } = await loadContentCollections(version) - return { pages: allPages, sections: allSections } - } catch { - return null - } -} - -async function loadAllVersions() { - const acc: Record = {} - for (const v of versions) { - const loaded = await loadVersionData(v) - if (loaded) acc[v] = loaded - } - return acc + const { allPages, allSections } = await loadContentCollections(version) + return { version, pages: allPages, sections: allSections } } function buildSectionTitles(sections: SectionRecord[]) { @@ -59,27 +46,20 @@ function renderVersionBlock(domain: string, version: string, pages: PageRecord[] const renderSection = ([id, list]: [string, PageRecord[]]) => { const label = sectionTitles.get(id) ?? id - const lines = list.map(renderPageLink).join("\n") - return `### ${label}\n\n${lines}` + return `### ${label}\n\n${list.map(renderPageLink).join("\n")}` } - const blocks = Array.from(groups.entries()).map(renderSection).join("\n\n") - - return `\n## ${version}\n\n${blocks}` + return `\n## ${version}\n\n${Array.from(groups.entries()).map(renderSection).join("\n\n")}` } export async function renderLlmsTxt(request: Request, additionalData: { title: string; tagline: string }) { const domain = createDomain(request) - const versionsData = await loadAllVersions() - const project = additionalData.title - const tagline = additionalData.tagline - const content = Object.keys(versionsData) - .map((version) => { - const { pages, sections } = versionsData[version] - return renderVersionBlock(domain, version, pages, sections) - }) + const versionsData = await Promise.all(versions.map(loadVersionData)) + + const content = versionsData + .map(({ version, pages, sections }) => renderVersionBlock(domain, version, pages, sections)) .join("\n") - return [`# ${project}`, `> ${tagline}`, content, ""].join("\n") + return [`# ${additionalData.title}`, `> ${additionalData.tagline}`, content, ""].join("\n") } From d63f628c0b7d2af6d7bd458ad0e4bcf9b1243799 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Mon, 1 Sep 2025 16:37:45 +0200 Subject: [PATCH 4/9] small fix --- app/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routes.ts b/app/routes.ts index 5c84d00..f017bcd 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,6 +1,7 @@ -import { type RouteConfig, layout, route } from "@react-router/dev/routes" +import { type RouteConfig, index, layout, route } from "@react-router/dev/routes" export default [ + index("routes/index.tsx"), layout("routes/documentation-layout.tsx", [ route(":version?/home", "routes/documentation-homepage.tsx"), route(":version/:section/:subsection?/:filename", "routes/documentation-page.tsx"), From 5933ef36edff2851b9c63d1b822939ba672306be Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Mon, 1 Sep 2025 16:46:19 +0200 Subject: [PATCH 5/9] small fix --- app/utils/llms-utils.ts | 18 +++++++++--------- content-collections.ts | 6 ++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts index 5f3cae6..330b649 100644 --- a/app/utils/llms-utils.ts +++ b/app/utils/llms-utils.ts @@ -1,6 +1,6 @@ import { createDomain } from "~/utils/http" import { loadContentCollections } from "~/utils/load-content-collections" -import type { PageRecord, SectionRecord } from "../../content-collections" +import type { Page, Section } from "../../content-collections" import { pageUrl } from "./version-links" import { versions } from "./versions" import type { Version } from "./versions-utils" @@ -10,12 +10,12 @@ async function loadVersionData(version: Version) { return { version, pages: allPages, sections: allSections } } -function buildSectionTitles(sections: SectionRecord[]) { +function buildSectionTitles(sections: Section[]) { return new Map(sections.map((s) => [s.slug.split("/").pop() || "", s.title])) } -function groupPagesByFolder(pages: PageRecord[]) { - const groups = new Map() +function groupPagesByFolder(pages: Page[]) { + const groups = new Map() for (const p of pages) { const id = p.section ?? p._meta?.path?.split("/")[0] @@ -32,19 +32,19 @@ function groupPagesByFolder(pages: PageRecord[]) { return groups } -function renderVersionBlock(domain: string, version: string, pages: PageRecord[], sections: SectionRecord[]) { +function renderVersionBlock(domain: string, version: string, pages: Page[], sections: Section[]) { if (!pages.length) return `## ${version}\n\n_No pages found._` const sectionTitles = buildSectionTitles(sections) const groups = groupPagesByFolder(pages) - const renderPageLink = (p: PageRecord) => { + const renderPageLink = (p: Page) => { const url = pageUrl(domain, version, p.slug) - const note = p.summary || p.description || "" - return `- [${p.title}](${url})${note ? `: ${note}` : ""}` + const note = p.description + return `- [${p.title}](${url}): ${note}` } - const renderSection = ([id, list]: [string, PageRecord[]]) => { + const renderSection = ([id, list]: [string, Page[]]) => { const label = sectionTitles.get(id) ?? id return `### ${label}\n\n${list.map(renderPageLink).join("\n")}` } diff --git a/content-collections.ts b/content-collections.ts index d8ab2c9..15d3f1e 100644 --- a/content-collections.ts +++ b/content-collections.ts @@ -27,10 +27,8 @@ const pageOutputSchema = pageSchema.extend({ content: z.unknown(), }) -export type SectionRecord = z.infer -export type PageRecord = z.infer -export const SectionsArray = z.array(sectionOutputSchema) -export const PagesArray = z.array(pageOutputSchema) +export type Section = z.infer +export type Page = z.infer /** * Removes leading number prefixes like "01-", "02-" from each path segment. From fe42cc6c12796f53a76d8d48d1ce68453408fb01 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Tue, 2 Sep 2025 01:02:20 +0200 Subject: [PATCH 6/9] update so llms.txt is dedicated per version --- app/routes.ts | 2 +- app/routes/llms[.]txt.ts | 17 ++++++++++++++-- app/utils/llms-utils.ts | 39 ++++++++++++++++--------------------- app/utils/versions-utils.ts | 10 +++++++--- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/app/routes.ts b/app/routes.ts index f017bcd..2545c85 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -11,5 +11,5 @@ export default [ route("resource/*", "routes/resource.locales.ts"), route("$", "routes/$.tsx"), route("sitemap/:lang.xml", "routes/sitemap.$lang[.]xml.ts"), - route("llms.txt", "routes/llms[.]txt.ts"), + route(":version?/llms.txt", "routes/llms[.]txt.ts"), ] satisfies RouteConfig diff --git a/app/routes/llms[.]txt.ts b/app/routes/llms[.]txt.ts index a730e5e..16742ab 100644 --- a/app/routes/llms[.]txt.ts +++ b/app/routes/llms[.]txt.ts @@ -1,10 +1,23 @@ +import { href, redirect } from "react-router" import { renderLlmsTxt } from "~/utils/llms-utils" +import { getLatestVersion, resolveVersion } from "~/utils/versions-utils" import type { Route } from "./+types/llms[.]txt" -export async function loader({ request }: Route.LoaderArgs) { - const body = await renderLlmsTxt(request, { +export async function loader({ request, params }: Route.LoaderArgs) { + const { version: paramVersion } = params + + if (!paramVersion) { + const latest = getLatestVersion() + return redirect(href("/:version?/llms.txt", { version: latest })) + } + + const version = resolveVersion(paramVersion) + const body = await renderLlmsTxt({ + request, + version, title: "Documentation Template", tagline: "Official documentation and guides.", }) + return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } }) } diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts index 330b649..4a924d1 100644 --- a/app/utils/llms-utils.ts +++ b/app/utils/llms-utils.ts @@ -2,7 +2,6 @@ import { createDomain } from "~/utils/http" import { loadContentCollections } from "~/utils/load-content-collections" import type { Page, Section } from "../../content-collections" import { pageUrl } from "./version-links" -import { versions } from "./versions" import type { Version } from "./versions-utils" async function loadVersionData(version: Version) { @@ -15,21 +14,14 @@ function buildSectionTitles(sections: Section[]) { } function groupPagesByFolder(pages: Page[]) { - const groups = new Map() - - for (const p of pages) { + return pages.reduce((groups, p) => { const id = p.section ?? p._meta?.path?.split("/")[0] - if (!id) continue - - let list = groups.get(id) - if (!list) { - list = [] - groups.set(id, list) - } + if (!id) return groups + const list = groups.get(id) ?? [] + if (!groups.has(id)) groups.set(id, list) list.push(p) - } - - return groups + return groups + }, new Map()) } function renderVersionBlock(domain: string, version: string, pages: Page[], sections: Section[]) { @@ -41,7 +33,7 @@ function renderVersionBlock(domain: string, version: string, pages: Page[], sect const renderPageLink = (p: Page) => { const url = pageUrl(domain, version, p.slug) const note = p.description - return `- [${p.title}](${url}): ${note}` + return `- [${p.title}](${url})${note ? `: ${note}` : ""}` } const renderSection = ([id, list]: [string, Page[]]) => { @@ -52,14 +44,17 @@ function renderVersionBlock(domain: string, version: string, pages: Page[], sect return `\n## ${version}\n\n${Array.from(groups.entries()).map(renderSection).join("\n\n")}` } -export async function renderLlmsTxt(request: Request, additionalData: { title: string; tagline: string }) { +export async function renderLlmsTxt(opts: { + request: Request + version: Version + title: string + tagline: string +}) { + const { request, version, title, tagline } = opts const domain = createDomain(request) - const versionsData = await Promise.all(versions.map(loadVersionData)) - - const content = versionsData - .map(({ version, pages, sections }) => renderVersionBlock(domain, version, pages, sections)) - .join("\n") + const { pages, sections } = await loadVersionData(version) + const content = renderVersionBlock(domain, version, pages, sections) - return [`# ${additionalData.title}`, `> ${additionalData.tagline}`, content, ""].join("\n") + return [`# ${title}`, `> ${tagline}`, content, ""].join("\n") } diff --git a/app/utils/versions-utils.ts b/app/utils/versions-utils.ts index 70d23a7..ff29e62 100644 --- a/app/utils/versions-utils.ts +++ b/app/utils/versions-utils.ts @@ -5,13 +5,17 @@ export type Version = (typeof versions)[number] export const getLatestVersion = () => versions[0] -export function isKnownVersion(v: string | undefined): v is Version { - return typeof v === "string" && versions.some((ver) => ver === v) +export const isKnownVersion = (v: string | undefined): v is Version => + typeof v === "string" && versions.includes(v as Version) + +export const resolveVersion = (param?: string) => { + if (!param) return getLatestVersion() + return isKnownVersion(param) ? param : getLatestVersion() } export function useCurrentVersion() { const { version } = useParams<"version">() - return isKnownVersion(version) ? version : getLatestVersion() + return resolveVersion(version) ?? getLatestVersion() } export const hrefForHomepage = (v: string) => From 11d33af8781d7b5dde09ce321c2aa1c188148d72 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Tue, 2 Sep 2025 01:38:15 +0200 Subject: [PATCH 7/9] small update --- app/components/sidebar/tests/build-breadcrumbs.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/sidebar/tests/build-breadcrumbs.test.tsx b/app/components/sidebar/tests/build-breadcrumbs.test.tsx index 0e2b4b8..e1fcacd 100644 --- a/app/components/sidebar/tests/build-breadcrumbs.test.tsx +++ b/app/components/sidebar/tests/build-breadcrumbs.test.tsx @@ -24,7 +24,7 @@ type Doc = { slug: string; title: string } const makeDoc = (slug: string, title: string): Doc => ({ slug, title }) type MinimalSection = Pick -const makeSection = (overrides: Partial = {}): SidebarSection => ({ +const makeSection = (overrides: Partial = {}) => ({ title: "", slug: "", documentationPages: [], From 786fd982d76d080136570174586775b49d7b7519 Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Wed, 3 Sep 2025 16:04:44 +0200 Subject: [PATCH 8/9] fix for versions - fixed naming of the files and functions --- .../sidebar/tests/build-breadcrumbs.test.tsx | 51 +++++++++---------- app/components/versions-dropdown.tsx | 12 ++--- app/routes/documentation-homepage.tsx | 5 +- app/routes/documentation-layout.tsx | 4 +- app/routes/documentation-page.tsx | 4 +- app/routes/index.tsx | 3 +- app/routes/llms[.]txt.ts | 4 +- app/utils/create-sidebar-tree.ts | 2 +- app/utils/llms-utils.ts | 5 +- app/utils/load-content-collections.ts | 2 +- app/utils/split-slug-and-append-version.ts | 4 +- .../split-slug-and-append-version.test.ts | 38 ++++++-------- app/utils/version-links.ts | 39 -------------- app/utils/version-resolvers.ts | 50 ++++++++++++++++++ app/utils/versions-utils.ts | 22 -------- 15 files changed, 113 insertions(+), 132 deletions(-) delete mode 100644 app/utils/version-links.ts create mode 100644 app/utils/version-resolvers.ts delete mode 100644 app/utils/versions-utils.ts diff --git a/app/components/sidebar/tests/build-breadcrumbs.test.tsx b/app/components/sidebar/tests/build-breadcrumbs.test.tsx index e1fcacd..723b4e6 100644 --- a/app/components/sidebar/tests/build-breadcrumbs.test.tsx +++ b/app/components/sidebar/tests/build-breadcrumbs.test.tsx @@ -1,24 +1,25 @@ import { describe, expect, it, vi } from "vitest" -import { buildBreadcrumb } from "../build-breadcrumbs" import type { SidebarSection } from "../sidebar" -vi.mock("~/utils/versions-utils", async () => { - const actual = await vi.importActual("~/utils/versions-utils") - return { - ...actual, - useCurrentVersion: () => "v1.0.0", - getLatestVersion: () => "v1.0.0", - isKnownVersion: (v?: string) => v === "v1.0.0", - } -}) +vi.mock("~/utils/split-slug-and-append-version", () => ({ + splitSlugAndAppendVersion: (slug: string) => { + const parts = slug.split("/").filter(Boolean) + const version = "v1.0.0" -vi.mock("~/utils/versions", async () => { - const actual = await vi.importActual("~/utils/versions") - return { - ...actual, - versions: ["v1.0.0"] as const, - } -}) + if (parts.length === 2) { + const [section, filename] = parts + return { version, section, filename } + } + if (parts.length === 3) { + const [section, subsection, filename] = parts + return { version, section, subsection, filename } + } + + throw new Error(`Bad slug in test: ${slug}`) + }, +})) + +import { buildBreadcrumb } from "../build-breadcrumbs" type Doc = { slug: string; title: string } const makeDoc = (slug: string, title: string): Doc => ({ slug, title }) @@ -41,9 +42,7 @@ describe("buildBreadcrumb (versioned paths via splitSlugAndAppendVersion)", () = documentationPages: [makeDoc("getting-started/intro", "Intro")], }), ] - - const result = buildBreadcrumb(items, "/v1.0.0/getting-started/unknown") - expect(result).toEqual([]) + expect(buildBreadcrumb(items, "/v1.0.0/getting-started/unknown")).toEqual([]) }) it("returns [section, doc] for a top-level doc", () => { @@ -54,9 +53,7 @@ describe("buildBreadcrumb (versioned paths via splitSlugAndAppendVersion)", () = documentationPages: [makeDoc("getting-started/intro", "Intro")], }), ] - - const result = buildBreadcrumb(items, "/v1.0.0/getting-started/intro") - expect(result).toEqual(["Getting Started", "Intro"]) + expect(buildBreadcrumb(items, "/v1.0.0/getting-started/intro")).toEqual(["Getting Started", "Intro"]) }) it("returns full trail for a nested doc (root → sub → doc)", () => { @@ -74,8 +71,10 @@ describe("buildBreadcrumb (versioned paths via splitSlugAndAppendVersion)", () = documentationPages: [makeDoc("configuration/setup", "Setup")], }), ] - - const result = buildBreadcrumb(items, "/v1.0.0/configuration/advanced/tuning") - expect(result).toEqual(["Configuration", "Advanced", "Tuning"]) + expect(buildBreadcrumb(items, "/v1.0.0/configuration/advanced/tuning")).toEqual([ + "Configuration", + "Advanced", + "Tuning", + ]) }) }) diff --git a/app/components/versions-dropdown.tsx b/app/components/versions-dropdown.tsx index 1238b7b..bf64dae 100644 --- a/app/components/versions-dropdown.tsx +++ b/app/components/versions-dropdown.tsx @@ -1,21 +1,21 @@ import { useState } from "react" import { useNavigate } from "react-router" import { Icon } from "~/ui/icon/icon" +import { getCurrentVersion, homepageUrlWithVersion, isKnownVersion } from "~/utils/version-resolvers" import { versions } from "~/utils/versions" -import { hrefForHomepage, isKnownVersion, useCurrentVersion } from "~/utils/versions-utils" export function VersionDropdown() { const navigate = useNavigate() - const current = useCurrentVersion() - const [selectedVersion, setSelectedVersion] = useState(current) + const { version: currentVersion } = getCurrentVersion() + const [selectedVersion, setSelectedVersion] = useState(currentVersion) function onChange(e: React.ChangeEvent) { const next = e.target.value - if (next === current) return + if (next === currentVersion) return - setSelectedVersion(isKnownVersion(next) ? next : current) + setSelectedVersion(isKnownVersion(next) ? next : currentVersion) - const to = hrefForHomepage(next) + const to = homepageUrlWithVersion(next) const nav = () => { navigate(to) e.target.blur() diff --git a/app/routes/documentation-homepage.tsx b/app/routes/documentation-homepage.tsx index 7116ca2..28f2bdb 100644 --- a/app/routes/documentation-homepage.tsx +++ b/app/routes/documentation-homepage.tsx @@ -1,12 +1,11 @@ import GithubContributeLinks from "~/components/github-contribute-links" import PageMdxArticle from "~/components/page-mdx-article" import { loadContentCollections } from "~/utils/load-content-collections" -import { resolveHomeVersionOrRedirect } from "~/utils/version-links" +import { resolveVersionForHomepage } from "~/utils/version-resolvers" import type { Route } from "./+types/documentation-homepage" export async function loader({ params }: Route.LoaderArgs) { - const { version } = resolveHomeVersionOrRedirect(params.version) - + const { version } = resolveVersionForHomepage(params.version) const { allPages } = await loadContentCollections(version) const page = allPages.find((p) => p._meta.path === "_index") if (!page) throw new Response("Not Found", { status: 404 }) diff --git a/app/routes/documentation-layout.tsx b/app/routes/documentation-layout.tsx index fe386ba..e48bbb8 100644 --- a/app/routes/documentation-layout.tsx +++ b/app/routes/documentation-layout.tsx @@ -5,11 +5,11 @@ import { Sidebar } from "~/components/sidebar/sidebar" import { ThemeToggle } from "~/components/theme-toggle" import { VersionDropdown } from "~/components/versions-dropdown" import { createSidebarTree } from "~/utils/create-sidebar-tree" -import { resolveLayoutVersion } from "~/utils/version-links" +import { resolveVersionForLayout } from "~/utils/version-resolvers" import type { Route } from "./+types/documentation-layout" export async function loader({ params, request }: Route.LoaderArgs) { - const { version } = resolveLayoutVersion(params.version, request) + const { version } = resolveVersionForLayout(params.version, request) const sidebarTree = await createSidebarTree(version) return { sidebarTree, version } } diff --git a/app/routes/documentation-page.tsx b/app/routes/documentation-page.tsx index 27eece4..d80348e 100644 --- a/app/routes/documentation-page.tsx +++ b/app/routes/documentation-page.tsx @@ -6,14 +6,14 @@ import { useDocumentationLayoutLoaderData } from "~/hooks/use-documentation-layo import { usePreviousNextPages } from "~/hooks/use-previous-next-pages" import { extractHeadingTreeFromMarkdown } from "~/utils/extract-heading-tree-from-mdx" import { loadContentCollections } from "~/utils/load-content-collections" -import { ensureVersion } from "~/utils/version-links" +import { normalizeVersion } from "~/utils/version-resolvers" import type { Route } from "./+types/documentation-page" export async function loader({ params }: Route.LoaderArgs) { const { version: v, section, subsection, filename } = params if (!section || !filename) throw new Response("Not Found", { status: 404 }) - const { version } = ensureVersion(v) + const { version } = normalizeVersion(v) const slug = [section, subsection, filename].filter(Boolean).join("/") diff --git a/app/routes/index.tsx b/app/routes/index.tsx index cdef097..baced90 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -2,7 +2,8 @@ import { href, useNavigate } from "react-router" import { Header } from "~/components/header" import { Logo } from "~/components/logo" import { Icon } from "~/ui/icon/icon" -import { getLatestVersion } from "~/utils/versions-utils" +import { getLatestVersion } from "~/utils/version-resolvers" + export default function Index() { const navigate = useNavigate() // Customize index page diff --git a/app/routes/llms[.]txt.ts b/app/routes/llms[.]txt.ts index 16742ab..fe919a5 100644 --- a/app/routes/llms[.]txt.ts +++ b/app/routes/llms[.]txt.ts @@ -1,6 +1,6 @@ import { href, redirect } from "react-router" import { renderLlmsTxt } from "~/utils/llms-utils" -import { getLatestVersion, resolveVersion } from "~/utils/versions-utils" +import { getLatestVersion, normalizeVersion } from "~/utils/version-resolvers" import type { Route } from "./+types/llms[.]txt" export async function loader({ request, params }: Route.LoaderArgs) { @@ -11,7 +11,7 @@ export async function loader({ request, params }: Route.LoaderArgs) { return redirect(href("/:version?/llms.txt", { version: latest })) } - const version = resolveVersion(paramVersion) + const { version } = normalizeVersion(paramVersion) const body = await renderLlmsTxt({ request, version, diff --git a/app/utils/create-sidebar-tree.ts b/app/utils/create-sidebar-tree.ts index ec2015b..b1a7cb6 100644 --- a/app/utils/create-sidebar-tree.ts +++ b/app/utils/create-sidebar-tree.ts @@ -1,6 +1,6 @@ import type { SidebarSection } from "~/components/sidebar/sidebar" import { loadContentCollections } from "./load-content-collections" -import type { Version } from "./versions-utils" +import type { Version } from "./version-resolvers" const parentOf = (slug: string) => { const i = slug.lastIndexOf("/") diff --git a/app/utils/llms-utils.ts b/app/utils/llms-utils.ts index 4a924d1..0dbf1ae 100644 --- a/app/utils/llms-utils.ts +++ b/app/utils/llms-utils.ts @@ -1,8 +1,7 @@ import { createDomain } from "~/utils/http" import { loadContentCollections } from "~/utils/load-content-collections" import type { Page, Section } from "../../content-collections" -import { pageUrl } from "./version-links" -import type { Version } from "./versions-utils" +import { type Version, pageUrlWithVersion } from "./version-resolvers" async function loadVersionData(version: Version) { const { allPages, allSections } = await loadContentCollections(version) @@ -31,7 +30,7 @@ function renderVersionBlock(domain: string, version: string, pages: Page[], sect const groups = groupPagesByFolder(pages) const renderPageLink = (p: Page) => { - const url = pageUrl(domain, version, p.slug) + const url = pageUrlWithVersion(domain, version, p.slug) const note = p.description return `- [${p.title}](${url})${note ? `: ${note}` : ""}` } diff --git a/app/utils/load-content-collections.ts b/app/utils/load-content-collections.ts index 8dc099f..447d445 100644 --- a/app/utils/load-content-collections.ts +++ b/app/utils/load-content-collections.ts @@ -1,6 +1,6 @@ import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" -import type { Version } from "~/utils/versions-utils" +import type { Version } from "./version-resolvers" /** * Load content-collections outputs diff --git a/app/utils/split-slug-and-append-version.ts b/app/utils/split-slug-and-append-version.ts index 64674ed..640218a 100644 --- a/app/utils/split-slug-and-append-version.ts +++ b/app/utils/split-slug-and-append-version.ts @@ -1,8 +1,8 @@ -import { useCurrentVersion } from "./versions-utils" +import { getCurrentVersion } from "./version-resolvers" export function splitSlugAndAppendVersion(slug: string) { const parts = slug.split("/").filter(Boolean) - const version = useCurrentVersion() + const { version } = getCurrentVersion() if (parts.length === 2) { const [section, filename] = parts return { version, section, filename } diff --git a/app/utils/tests/split-slug-and-append-version.test.ts b/app/utils/tests/split-slug-and-append-version.test.ts index 7663e96..1caf2bb 100644 --- a/app/utils/tests/split-slug-and-append-version.test.ts +++ b/app/utils/tests/split-slug-and-append-version.test.ts @@ -1,22 +1,20 @@ -const versionsMock = ["v1.0.0"] as const +import { describe, expect, it, vi } from "vitest" -vi.mock("~/utils/versions", () => ({ - versions: versionsMock, +vi.mock("../version-resolvers", () => ({ + getLatestVersion: () => "v1.0.0", + isKnownVersion: (v?: string) => v === "v1.0.0", + getCurrentVersion: () => ({ version: "v1.0.0" }), +})) + +vi.mock("../versions", () => ({ + versions: ["v1.0.0"] as const, })) -vi.mock("../versions-utils", () => { - return { - getLatestVersion: vi.fn(() => versionsMock[0]), - isKnownVersion: vi.fn(() => versionsMock[0]), - useCurrentVersion: vi.fn(() => versionsMock[0]), - } -}) import { splitSlugAndAppendVersion } from "../split-slug-and-append-version" describe("splitSlug test suite", () => { it("splits a slug with 2 parts (no subsection)", () => { - const result = splitSlugAndAppendVersion("getting-started/intro") - expect(result).toEqual({ + expect(splitSlugAndAppendVersion("getting-started/intro")).toEqual({ version: "v1.0.0", section: "getting-started", filename: "intro", @@ -24,8 +22,7 @@ describe("splitSlug test suite", () => { }) it("splits a slug with 3 parts (with subsection)", () => { - const result = splitSlugAndAppendVersion("getting-started/basics/intro") - expect(result).toEqual({ + expect(splitSlugAndAppendVersion("getting-started/basics/intro")).toEqual({ version: "v1.0.0", section: "getting-started", subsection: "basics", @@ -34,20 +31,19 @@ describe("splitSlug test suite", () => { }) it("throws if slug has less than 2 parts", () => { - expect(() => splitSlugAndAppendVersion("getting-started")).toThrowError( + expect(() => splitSlugAndAppendVersion("getting-started")).toThrow( /expected "section\/page" or "section\/subsection\/page"/i ) }) it("throws if slug has more than 3 parts", () => { - expect(() => splitSlugAndAppendVersion("section/subsection/file/extra")).toThrowError( + expect(() => splitSlugAndAppendVersion("section/subsection/file/extra")).toThrow( /expected "section\/page" or "section\/subsection\/page"/i ) }) it("handles numeric or dashed parts", () => { - const result = splitSlugAndAppendVersion("01-intro/02-basics/file-name") - expect(result).toEqual({ + expect(splitSlugAndAppendVersion("01-intro/02-basics/file-name")).toEqual({ version: "v1.0.0", section: "01-intro", subsection: "02-basics", @@ -56,8 +52,7 @@ describe("splitSlug test suite", () => { }) it("handles special characters in parts", () => { - const result = splitSlugAndAppendVersion("@special$/#weird$/file-name") - expect(result).toEqual({ + expect(splitSlugAndAppendVersion("@special$/#weird$/file-name")).toEqual({ version: "v1.0.0", section: "@special$", subsection: "#weird$", @@ -66,8 +61,7 @@ describe("splitSlug test suite", () => { }) it("handles uppercase letters in slug", () => { - const result = splitSlugAndAppendVersion("GettingStarted/Intro") - expect(result).toEqual({ + expect(splitSlugAndAppendVersion("GettingStarted/Intro")).toEqual({ version: "v1.0.0", section: "GettingStarted", filename: "Intro", diff --git a/app/utils/version-links.ts b/app/utils/version-links.ts deleted file mode 100644 index 19cda45..0000000 --- a/app/utils/version-links.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { redirect } from "react-router" -import { getLatestVersion, isKnownVersion } from "./versions-utils" - -function firstPathSegment(request: Request) { - return new URL(request.url).pathname.split("/").filter(Boolean)[0] -} - -function isUnknownVersion(v?: string) { - return typeof v === "string" && !isKnownVersion(v) -} - -function isLatestVersion(v?: string) { - return typeof v === "string" && v === getLatestVersion() -} - -export function ensureVersion(v?: string) { - return { version: isKnownVersion(v) ? v : getLatestVersion() } -} - -export function resolveHomeVersionOrRedirect(versionParam?: string) { - if (isUnknownVersion(versionParam) || isLatestVersion(versionParam)) { - throw redirect("/home") - } - return ensureVersion(versionParam) -} - -export function resolveLayoutVersion(paramsVersion: string | undefined, request: Request) { - if (isKnownVersion(paramsVersion)) return { version: paramsVersion } - const first = firstPathSegment(request) - return ensureVersion(isKnownVersion(first) ? first : undefined) -} - -function homepageUrl(base: string, version: string) { - return `${base}/${version}/home` -} - -export function pageUrl(base: string, version: string, slug: string) { - return slug === "_index" ? homepageUrl(base, version) : `${base}/${version}/${slug}` -} diff --git a/app/utils/version-resolvers.ts b/app/utils/version-resolvers.ts new file mode 100644 index 0000000..18d9f4b --- /dev/null +++ b/app/utils/version-resolvers.ts @@ -0,0 +1,50 @@ +import { href, redirect, useParams } from "react-router" +import { versions } from "./versions" + +export type Version = (typeof versions)[number] + +export const getLatestVersion = () => versions[0] + +export const isKnownVersion = (v: string | undefined): v is Version => + typeof v === "string" && versions.includes(v as Version) + +function isUnknownVersion(v?: string) { + return typeof v === "string" && !isKnownVersion(v) +} + +export function normalizeVersion(v?: string) { + return { version: isKnownVersion(v) ? v : getLatestVersion() } +} + +export function getCurrentVersion() { + const { version } = useParams<"version">() + return normalizeVersion(version) +} + +export function resolveVersionForHomepage(version?: string) { + if (isUnknownVersion(version) || getLatestVersion() === version) { + throw redirect("/home") + } + return normalizeVersion(version) +} + +function homepageUrl(base: string, version: string) { + return `${base}/${version}/home` +} + +export function pageUrlWithVersion(base: string, version: string, slug: string) { + return slug === "_index" ? homepageUrl(base, version) : `${base}/${version}/${slug}` +} + +function firstPathSegment(request: Request) { + return new URL(request.url).pathname.split("/").filter(Boolean)[0] +} + +export function resolveVersionForLayout(version: string | undefined, request: Request) { + if (isKnownVersion(version)) return { version } + const first = firstPathSegment(request) + return normalizeVersion(first) +} + +export const homepageUrlWithVersion = (v: string) => + v === getLatestVersion() ? href("/:version?/home") : href("/:version?/home", { version: v }) diff --git a/app/utils/versions-utils.ts b/app/utils/versions-utils.ts deleted file mode 100644 index ff29e62..0000000 --- a/app/utils/versions-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { href, useParams } from "react-router" -import { versions } from "./versions" - -export type Version = (typeof versions)[number] - -export const getLatestVersion = () => versions[0] - -export const isKnownVersion = (v: string | undefined): v is Version => - typeof v === "string" && versions.includes(v as Version) - -export const resolveVersion = (param?: string) => { - if (!param) return getLatestVersion() - return isKnownVersion(param) ? param : getLatestVersion() -} - -export function useCurrentVersion() { - const { version } = useParams<"version">() - return resolveVersion(version) ?? getLatestVersion() -} - -export const hrefForHomepage = (v: string) => - v === getLatestVersion() ? href("/:version?/home") : href("/:version?/home", { version: v }) From fd58db300912138b9ae9a31c6e033837ee7634ae Mon Sep 17 00:00:00 2001 From: abrulic1 Date: Wed, 3 Sep 2025 16:15:46 +0200 Subject: [PATCH 9/9] small fix in naming --- app/routes/llms[.]txt.ts | 3 +-- app/utils/{llms-utils.ts => llms-txt-builder.ts} | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename app/utils/{llms-utils.ts => llms-txt-builder.ts} (100%) diff --git a/app/routes/llms[.]txt.ts b/app/routes/llms[.]txt.ts index fe919a5..3de7e58 100644 --- a/app/routes/llms[.]txt.ts +++ b/app/routes/llms[.]txt.ts @@ -1,11 +1,10 @@ import { href, redirect } from "react-router" -import { renderLlmsTxt } from "~/utils/llms-utils" +import { renderLlmsTxt } from "~/utils/llms-txt-builder" import { getLatestVersion, normalizeVersion } from "~/utils/version-resolvers" import type { Route } from "./+types/llms[.]txt" export async function loader({ request, params }: Route.LoaderArgs) { const { version: paramVersion } = params - if (!paramVersion) { const latest = getLatestVersion() return redirect(href("/:version?/llms.txt", { version: latest })) diff --git a/app/utils/llms-utils.ts b/app/utils/llms-txt-builder.ts similarity index 100% rename from app/utils/llms-utils.ts rename to app/utils/llms-txt-builder.ts