From 8e96c467d8d80bcdc8508104dbb3c0b4c855f384 Mon Sep 17 00:00:00 2001 From: Mohith Gajjela <109003762+Mohith26@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:25:02 -0500 Subject: [PATCH 1/3] Load docs backed by index files at their canonical path The docs path manifest canonicalizes `/index.md` files to `` (getCanonicalDocsPath strips the trailing `/index`), so the route resolver answers 'render ' for pages like pacer's Core API Reference (docs/reference/index.md). Loading then fetched `.md`, which does not exist, so every root-level `reference/index` page 404ed and the framework-scoped ones bounced to the framework overview instead of rendering. Retry `/index.md` when `.md` is missing, before falling back to frontmatter redirects and not-found. Fetchers are injectable so the route pipeline is testable without GitHub network/cache, mirroring collectRedirectEntriesForFile. Reported in TanStack/pacer#237. --- src/utils/docs.ts | 219 +++++++++++++++++++++++++++++++++------------- 1 file changed, 159 insertions(+), 60 deletions(-) diff --git a/src/utils/docs.ts b/src/utils/docs.ts index 45f82cbd7..562bcaae8 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -12,20 +12,50 @@ import { buildDocsRedirectHref, resolveDocsPathRedirect, type DocsPathResolution, + type DocsRedirectManifest, } from './docs-redirects' import { removeLeadingSlash } from './utils' -export const loadDocs = async ({ - repo, - branch, - docsRoot, - docsPath, -}: { - repo: string - branch: string - docsRoot: string - docsPath: string -}) => { +// Narrow structural fetcher types so tests can inject fakes without hitting +// real GitHub network/cache (mirrors the injectable fetchFile in +// collectRedirectEntriesForFile). +export type LoadDocsRouteFetchers = { + fetchDocs: (opts: { + data: { repo: string; branch: string; filePath: string } + }) => Promise>> + fetchDocsPathManifest: (opts: { + data: { repo: string; branch: string; docsRoot: string } + }) => Promise + fetchDocsRedirect: (opts: { + data: { + repo: string + branch: string + docsRoot: string + docsPaths: Array + } + }) => Promise +} + +const defaultLoadDocsRouteFetchers: LoadDocsRouteFetchers = { + fetchDocs, + fetchDocsPathManifest, + fetchDocsRedirect, +} + +export const loadDocs = async ( + { + repo, + branch, + docsRoot, + docsPath, + }: { + repo: string + branch: string + docsRoot: string + docsPath: string + }, + fetchDocsFn: LoadDocsRouteFetchers['fetchDocs'] = fetchDocs, +) => { if (!branch || !docsRoot || !docsPath) { throw notFound({ data: { @@ -34,7 +64,7 @@ export const loadDocs = async ({ }) } - const doc = await fetchDocs({ + const doc = await fetchDocsFn({ data: { repo, branch, @@ -61,21 +91,24 @@ export async function getDocsManifest(opts: { return fetchDocsManifest({ data: opts }) } -export async function resolveDocsRoutePath(opts: { - branch: string - defaultDocs: string - docsPath: string - docsRoot: string - frameworks: Array - repo: string -}): Promise { +export async function resolveDocsRoutePath( + opts: { + branch: string + defaultDocs: string + docsPath: string + docsRoot: string + frameworks: Array + repo: string + }, + fetchers: LoadDocsRouteFetchers = defaultLoadDocsRouteFetchers, +): Promise { const defaultDocsResolution = getDefaultDocsResolution(opts) if (defaultDocsResolution) { return defaultDocsResolution } - const manifest = await fetchDocsPathManifest({ + const manifest = await fetchers.fetchDocsPathManifest({ data: { repo: opts.repo, branch: opts.branch, @@ -153,16 +186,19 @@ export type LoadDocsRouteResult = type: 'not-found' } -export async function loadDocsRoute(opts: { - branch: string - defaultDocs: string - docsPath: string - docsRoot: string - frameworks: Array - redirectFromPaths: Array - repo: string -}): Promise { - const resolution = await resolveDocsRoutePathWithRedirects(opts) +export async function loadDocsRoute( + opts: { + branch: string + defaultDocs: string + docsPath: string + docsRoot: string + frameworks: Array + redirectFromPaths: Array + repo: string + }, + fetchers: LoadDocsRouteFetchers = defaultLoadDocsRouteFetchers, +): Promise { + const resolution = await resolveDocsRoutePathWithRedirects(opts, fetchers) if (resolution.type !== 'render') { return resolution @@ -172,19 +208,42 @@ export async function loadDocsRoute(opts: { return { type: 'loaded', docsPath: resolution.docsPath, - doc: await loadDocs({ - repo: opts.repo, - branch: opts.branch, - docsRoot: opts.docsRoot, - docsPath: resolution.docsPath, - }), + doc: await loadDocs( + { + repo: opts.repo, + branch: opts.branch, + docsRoot: opts.docsRoot, + docsPath: resolution.docsPath, + }, + fetchers.fetchDocs, + ), } } catch (error) { if (!isDocsNotFoundError(error)) { throw error } - const redirectPath = await resolveDocsRedirectFromPaths(opts) + // The docs path manifest canonicalizes `/index.md` files to + // `` (see getCanonicalDocsPath in docs.functions.ts), so a + // resolvable docs path may be backed by an index file instead of + // `.md`. Retry the index file before falling back to redirects, + // otherwise committed pages like `reference/index.md` 404 at their + // canonical URL. + const indexDoc = await loadDocsIndexFile( + opts, + resolution.docsPath, + fetchers, + ) + + if (indexDoc !== null) { + return { + type: 'loaded', + docsPath: resolution.docsPath, + doc: indexDoc, + } + } + + const redirectPath = await resolveDocsRedirectFromPaths(opts, fetchers) if (redirectPath !== null) { return { @@ -197,22 +256,57 @@ export async function loadDocsRoute(opts: { } } -async function resolveDocsRoutePathWithRedirects(opts: { - branch: string - defaultDocs: string - docsPath: string - docsRoot: string - frameworks: Array - redirectFromPaths: Array - repo: string -}): Promise { - const resolution = await resolveDocsRoutePath(opts) +async function loadDocsIndexFile( + opts: { + branch: string + docsRoot: string + repo: string + }, + docsPath: string, + fetchers: LoadDocsRouteFetchers, +) { + if (!docsPath || docsPath === 'index' || docsPath.endsWith('/index')) { + return null + } + + try { + return await loadDocs( + { + repo: opts.repo, + branch: opts.branch, + docsRoot: opts.docsRoot, + docsPath: `${docsPath}/index`, + }, + fetchers.fetchDocs, + ) + } catch (error) { + if (!isDocsNotFoundError(error)) { + throw error + } + + return null + } +} + +async function resolveDocsRoutePathWithRedirects( + opts: { + branch: string + defaultDocs: string + docsPath: string + docsRoot: string + frameworks: Array + redirectFromPaths: Array + repo: string + }, + fetchers: LoadDocsRouteFetchers, +): Promise { + const resolution = await resolveDocsRoutePath(opts, fetchers) if (resolution.type !== 'not-found') { return resolution } - const redirectPath = await resolveDocsRedirectFromPaths(opts) + const redirectPath = await resolveDocsRedirectFromPaths(opts, fetchers) if (redirectPath !== null) { return { @@ -224,23 +318,28 @@ async function resolveDocsRoutePathWithRedirects(opts: { return resolution } -async function resolveDocsRedirectFromPaths(opts: { - branch: string - docsRoot: string - redirectFromPaths: Array - repo: string -}) { +async function resolveDocsRedirectFromPaths( + opts: { + branch: string + docsRoot: string + redirectFromPaths: Array + repo: string + }, + fetchers: LoadDocsRouteFetchers, +) { const docsPaths = opts.redirectFromPaths.filter(Boolean) if (docsPaths.length === 0) { return null } - return resolveDocsRedirect({ - repo: opts.repo, - branch: opts.branch, - docsRoot: opts.docsRoot, - docsPaths, + return fetchers.fetchDocsRedirect({ + data: { + repo: opts.repo, + branch: opts.branch, + docsRoot: opts.docsRoot, + docsPaths, + }, }) } From 7775a404abbdfe1e25c8d76a0f6ca8a74972a0c7 Mon Sep 17 00:00:00 2001 From: Mohith Gajjela <109003762+Mohith26@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:25:02 -0500 Subject: [PATCH 2/3] Test index-file fallback in the docs route pipeline Covers, with pacer's real docs layout (TanStack/pacer#237): root-level and framework-scoped reference/index pages loading at their canonical paths, plain docs loading without an extra fallback fetch, genuinely missing paths still resolving to not-found, and the existing section-index redirect staying intact. --- tests/docs-route-index-fallback.test.ts | 150 ++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/docs-route-index-fallback.test.ts diff --git a/tests/docs-route-index-fallback.test.ts b/tests/docs-route-index-fallback.test.ts new file mode 100644 index 000000000..6c844256d --- /dev/null +++ b/tests/docs-route-index-fallback.test.ts @@ -0,0 +1,150 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { loadDocsRoute, type LoadDocsRouteFetchers } from '../src/utils/docs' + +// Mirrors TanStack/pacer's real docs layout (TanStack/pacer#237): the root +// docs/config.json links "Core API Reference" to `reference/index`, backed by +// `docs/reference/index.md`, and each framework has its own +// `docs/framework//reference/index.md`. Neither `docs/reference.md` +// nor `docs/framework/react/reference.md` exists, while the docs path +// manifest canonicalizes index files to their parent path. +const repoFiles: Record = { + 'docs/overview.md': 'Overview', + 'docs/reference/index.md': 'Core API Reference', + 'docs/reference/classes/Debouncer.md': 'Debouncer', + 'docs/framework/react/adapter.md': 'React Adapter', + 'docs/framework/react/reference/index.md': 'React API Reference', +} + +const manifestPaths = [ + 'overview', + 'reference', + 'reference/classes/Debouncer', + 'framework/react/adapter', + 'framework/react/reference', +] + +type FetchedDoc = Awaited> + +function createFetchers() { + const fetchedFilePaths: Array = [] + + const fetchers: LoadDocsRouteFetchers = { + fetchDocs: async ({ data }) => { + fetchedFilePaths.push(data.filePath) + const content = repoFiles[data.filePath] + + if (content === undefined) { + throw { isNotFound: true } + } + + const doc: FetchedDoc = { + content, + title: content, + description: '', + keywords: undefined, + frameworks: [], + filePath: data.filePath, + frontmatter: { description: '' }, + } + + return doc + }, + fetchDocsPathManifest: async () => ({ + paths: manifestPaths, + redirects: {}, + }), + fetchDocsRedirect: async () => null, + } + + return { fetchers, fetchedFilePaths } +} + +function loadRoute(docsPath: string, fetchers: LoadDocsRouteFetchers) { + return loadDocsRoute( + { + repo: 'TanStack/pacer', + branch: 'main', + docsRoot: 'docs', + docsPath, + defaultDocs: 'overview', + frameworks: ['react', 'solid'], + redirectFromPaths: [docsPath], + }, + fetchers, + ) +} + +test('root-level reference/index loads the index file behind its canonical path', async () => { + const { fetchers } = createFetchers() + + for (const docsPath of ['reference/index', 'reference']) { + const result = await loadRoute(docsPath, fetchers) + + assert.equal(result.type, 'loaded', `${docsPath} should load`) + assert.ok(result.type === 'loaded') + assert.equal(result.docsPath, 'reference') + assert.equal(result.doc.filePath, 'docs/reference/index.md') + } +}) + +test('framework-scoped reference/index loads the index file behind its canonical path', async () => { + const { fetchers } = createFetchers() + + for (const docsPath of [ + 'framework/react/reference/index', + 'framework/react/reference', + ]) { + const result = await loadRoute(docsPath, fetchers) + + assert.equal(result.type, 'loaded', `${docsPath} should load`) + assert.ok(result.type === 'loaded') + assert.equal(result.docsPath, 'framework/react/reference') + assert.equal(result.doc.filePath, 'docs/framework/react/reference/index.md') + } +}) + +test('plain docs files still load directly without an index fallback fetch', async () => { + const { fetchers, fetchedFilePaths } = createFetchers() + + const result = await loadRoute('framework/react/adapter', fetchers) + + assert.equal(result.type, 'loaded') + assert.ok(result.type === 'loaded') + assert.equal(result.doc.filePath, 'docs/framework/react/adapter.md') + assert.deepEqual(fetchedFilePaths, ['docs/framework/react/adapter.md']) +}) + +test('genuinely missing docs paths still resolve to not-found', async () => { + const { fetchers } = createFetchers() + + const result = await loadRoute('guides/does-not-exist', fetchers) + + assert.deepEqual(result, { type: 'not-found' }) +}) + +test('missing paths under an indexable section keep redirecting to the section index', async () => { + const { fetchers } = createFetchers() + + const result = await loadRoute('reference/does-not-exist', fetchers) + + assert.deepEqual(result, { type: 'redirect', docsPath: 'reference' }) +}) + +test('a manifest path with no backing file (even as an index) is not-found', async () => { + const { fetchers } = createFetchers() + + const ghostFetchers: LoadDocsRouteFetchers = { + ...fetchers, + fetchDocsPathManifest: async () => ({ + paths: [...manifestPaths, 'guides/ghost'], + redirects: {}, + }), + } + + const result = await loadRoute('guides/ghost', ghostFetchers) + + assert.deepEqual(result, { type: 'not-found' }) +}) + +console.log('docs route index fallback tests passed') From f4a82caf0c271c7930e64fcff5ec24fedec77b84 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 30 Jul 2026 15:40:13 -0600 Subject: [PATCH 3/3] Format docs fallback regression test --- tests/docs-route-index-fallback.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/docs-route-index-fallback.test.ts b/tests/docs-route-index-fallback.test.ts index 8c3e2eb56..b44e412f0 100644 --- a/tests/docs-route-index-fallback.test.ts +++ b/tests/docs-route-index-fallback.test.ts @@ -53,10 +53,7 @@ function load(docsPath: string, fetchDocs: FetchDocs) { test('canonical reference paths load their index files', async () => { for (const [docsPath, expectedFilePath] of [ ['reference', 'docs/reference/index.md'], - [ - 'framework/react/reference', - 'docs/framework/react/reference/index.md', - ], + ['framework/react/reference', 'docs/framework/react/reference/index.md'], ]) { const { fetchDocs, fetchedFilePaths } = createFetchDocs() const doc = await load(docsPath, fetchDocs)