Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 26 additions & 27 deletions app/components/sidebar/tests/build-breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
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<typeof import("~/utils/versions-utils")>("~/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<typeof import("~/utils/versions")>("~/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 })

type MinimalSection = Pick<SidebarSection, "title" | "slug" | "documentationPages" | "subsections">
const makeSection = (overrides: Partial<MinimalSection> = {}): SidebarSection => ({
const makeSection = (overrides: Partial<MinimalSection> = {}) => ({
title: "",
slug: "",
documentationPages: [],
Expand All @@ -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", () => {
Expand All @@ -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)", () => {
Expand All @@ -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",
])
})
})
12 changes: 6 additions & 6 deletions app/components/versions-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>) {
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()
Expand Down
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export default [
route("resource/*", "routes/resource.locales.ts"),
route("$", "routes/$.tsx"),
route("sitemap/:lang.xml", "routes/sitemap.$lang[.]xml.ts"),
route(":version?/llms.txt", "routes/llms[.]txt.ts"),
] satisfies RouteConfig
5 changes: 2 additions & 3 deletions app/routes/documentation-homepage.tsx
Original file line number Diff line number Diff line change
@@ -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 })
Expand Down
4 changes: 2 additions & 2 deletions app/routes/documentation-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
4 changes: 2 additions & 2 deletions app/routes/documentation-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")

Expand Down
3 changes: 2 additions & 1 deletion app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions app/routes/llms[.]txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { href, redirect } from "react-router"
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 }))
}

const { version } = normalizeVersion(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" } })
}
2 changes: 1 addition & 1 deletion app/utils/create-sidebar-tree.ts
Original file line number Diff line number Diff line change
@@ -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("/")
Expand Down
59 changes: 59 additions & 0 deletions app/utils/llms-txt-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createDomain } from "~/utils/http"
import { loadContentCollections } from "~/utils/load-content-collections"
import type { Page, Section } from "../../content-collections"
import { type Version, pageUrlWithVersion } from "./version-resolvers"

async function loadVersionData(version: Version) {
const { allPages, allSections } = await loadContentCollections(version)
return { version, pages: allPages, sections: allSections }
}

function buildSectionTitles(sections: Section[]) {
return new Map(sections.map((s) => [s.slug.split("/").pop() || "", s.title]))
}

function groupPagesByFolder(pages: Page[]) {
return pages.reduce((groups, p) => {
const id = p.section ?? p._meta?.path?.split("/")[0]
if (!id) return groups
const list = groups.get(id) ?? []
if (!groups.has(id)) groups.set(id, list)
list.push(p)
return groups
}, new Map<string, Page[]>())
}

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: Page) => {
const url = pageUrlWithVersion(domain, version, p.slug)
const note = p.description
return `- [${p.title}](${url})${note ? `: ${note}` : ""}`
}

const renderSection = ([id, list]: [string, Page[]]) => {
const label = sectionTitles.get(id) ?? id
return `### ${label}\n\n${list.map(renderPageLink).join("\n")}`
}

return `\n## ${version}\n\n${Array.from(groups.entries()).map(renderSection).join("\n\n")}`
}

export async function renderLlmsTxt(opts: {
request: Request
version: Version
title: string
tagline: string
}) {
const { request, version, title, tagline } = opts
const domain = createDomain(request)

const { pages, sections } = await loadVersionData(version)
const content = renderVersionBlock(domain, version, pages, sections)

return [`# ${title}`, `> ${tagline}`, content, ""].join("\n")
}
2 changes: 1 addition & 1 deletion app/utils/load-content-collections.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/utils/split-slug-and-append-version.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down
38 changes: 16 additions & 22 deletions app/utils/tests/split-slug-and-append-version.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
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",
})
})

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",
Expand All @@ -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",
Expand All @@ -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$",
Expand All @@ -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",
Expand Down
Loading