Skip to content

Commit 6bb0c9b

Browse files
authored
feat: insert docs version segment for all internal link shapes (#2419)
1 parent 52854d7 commit 6bb0c9b

3 files changed

Lines changed: 67 additions & 23 deletions

File tree

nuxt.config.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createResolver } from 'nuxt/kit'
22
import { parseMdc } from './helpers/mdc-parser.mjs'
33
import { agentHowToCall, agentWhenToUse } from './shared/utils/agents'
44
import { CLI_DOCS_PREFIX, CLI_DOCS_REFS, CLI_DOCS_REPO } from './shared/utils/cli-docs'
5-
import { CURRENT_DOCS_VERSION, EXCLUDED_DOC_VERSIONS } from './shared/utils/docs'
5+
import { CURRENT_DOCS_VERSION, DOCS_COLLECTION_VERSIONS, EXCLUDED_DOC_VERSIONS, insertDocsVersion } from './shared/utils/docs'
66

77
const { resolve } = createResolver(import.meta.url)
88

@@ -441,7 +441,6 @@ export default defineNuxtConfig({
441441
'/docs/5.x/getting-started/directory-structure': { redirect: '/docs/4.x/directory-structure', prerender: false },
442442
'/docs/4.x/guide/going-further/modules': { redirect: '/docs/4.x/guide/modules', prerender: false },
443443
'/docs/5.x/guide/going-further/modules': { redirect: '/docs/4.x/guide/modules', prerender: false },
444-
'/docs/4.x/guide/modules/module-dependencies': { redirect: '/docs/5.x/guide/modules/module-dependencies', prerender: false },
445444
'/docs/4.x/guide/concepts/rendering-modes': { redirect: '/docs/4.x/guide/concepts/rendering', prerender: false },
446445
'/docs/5.x/guide/concepts/rendering-modes': { redirect: '/docs/4.x/guide/concepts/rendering', prerender: false },
447446
'/docs/4.x/guide/directory-structure/nuxt.config': { redirect: '/docs/4.x/directory-structure/nuxt-config', prerender: false },
@@ -539,27 +538,9 @@ export default defineNuxtConfig({
539538
const base = `https://raw.githubusercontent.com/${CLI_DOCS_REPO}/${CLI_DOCS_REFS[collection]}`
540539
file.body = file.body.replaceAll(/(!\[[^\]]*\]\()\/(?!\/)/g, `$1${base}/`)
541540
}
542-
if (file.id.startsWith('docsv5/')) {
543-
file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/5.x/')
544-
// Pages that only exist on main (5.x) but are linked as /docs/4.x/* from
545-
// the 5.x docs. Left unrewritten they 404, which fails the prerender now
546-
// that the crawler is on. Only paths whose 5.x counterpart exists belong
547-
// here — a blanket 4.x→5.x rewrite would break the ~13 links that point
548-
// at pages 5.x dropped (guide/concepts/esm, going-further/internals, …).
549-
for (const path of [
550-
'guide/modules/module-dependencies',
551-
'guide/best-practices/accessibility',
552-
'guide/concepts/server-components',
553-
'guide/recipes/mostly-static-sites'
554-
]) {
555-
file.body = file.body.replaceAll(`/docs/4.x/${path}`, `/docs/5.x/${path}`)
556-
}
557-
}
558-
if (file.id.startsWith('docsv4/')) {
559-
file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/4.x/')
560-
}
561-
if (file.id.startsWith('docsv3/')) {
562-
file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/3.x/')
541+
const docsVersion = DOCS_COLLECTION_VERSIONS[collection]
542+
if (docsVersion) {
543+
file.body = insertDocsVersion(file.body, docsVersion)
563544
}
564545
},
565546
'content:file:afterParse': async ({ file, content }) => {

shared/utils/docs.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,27 @@
77
// - nuxt.config.ts (agentDiscovery) → excluded versions never negotiate markdown, MCP server card docs link
88
// - server/plugins/agent-discovery.ts → generated /raw/index.md links to versioned docs
99
// - app/middleware/docs-version.global.ts → unversioned `/docs/*` redirect target
10+
// - nuxt.config.ts (content:file:beforeParse) → version segment inserted into internal docs links
11+
// via DOCS_COLLECTION_VERSIONS, whose keys must match the collection names in content.config.ts
1012
//
1113
// When Nuxt 5 ships: move `'5.x'` from EXCLUDED_DOC_VERSIONS into
1214
// SUPPORTED_DOC_VERSIONS and bump CURRENT_DOCS_VERSION.
1315
export const SUPPORTED_DOC_VERSIONS = ['3.x', '4.x'] as const
1416
export const EXCLUDED_DOC_VERSIONS = ['5.x'] as const
1517
export const CURRENT_DOCS_VERSION: (typeof SUPPORTED_DOC_VERSIONS)[number] = '4.x'
1618

19+
// Content collection id (the first segment of `file.id`) → docs version.
20+
export const DOCS_COLLECTION_VERSIONS: Record<string, string | undefined> = Object.fromEntries(
21+
[...SUPPORTED_DOC_VERSIONS, ...EXCLUDED_DOC_VERSIONS].map(version => [`docsv${version.split('.')[0]}`, version])
22+
)
23+
24+
// Docs sources write internal links unversioned so a docs PR can be
25+
// cherry-picked between release branches unchanged. The URL-opening delimiter
26+
// is what keeps `/assets/docs/…` and external `…/docs/…` URLs out of scope.
27+
export function insertDocsVersion(body: string, version: string) {
28+
return body.replaceAll(/(["'(=])\/docs\/(?!\d\.x)/g, `$1/docs/${version}/`)
29+
}
30+
1731
const escape = (v: string) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1832

1933
// `^/docs/(?:3\.x|4\.x)(?:/|$)` — matches versioned doc paths only.

test/unit/docs.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { readFileSync } from 'node:fs'
2+
import { fileURLToPath } from 'node:url'
3+
import { describe, expect, it } from 'vitest'
4+
import { DOCS_COLLECTION_VERSIONS, insertDocsVersion } from '../../shared/utils/docs'
5+
6+
describe('DOCS_COLLECTION_VERSIONS', () => {
7+
it('should key on collections declared in content.config.ts', () => {
8+
const config = readFileSync(fileURLToPath(new URL('../../content.config.ts', import.meta.url)), 'utf8')
9+
const collections = [...config.matchAll(/^ {4}(\w+): defineCollection\(/gm)].map(match => match[1])
10+
expect(collections).toContain('docsv4')
11+
for (const collection of Object.keys(DOCS_COLLECTION_VERSIONS)) {
12+
expect(collections).toContain(collection)
13+
}
14+
})
15+
})
16+
17+
describe('insertDocsVersion', () => {
18+
it('should version markdown links', () => {
19+
expect(insertDocsVersion('see [intro](/docs/getting-started/introduction)', '5.x'))
20+
.toBe('see [intro](/docs/5.x/getting-started/introduction)')
21+
})
22+
23+
it('should version quoted and unquoted MDC props', () => {
24+
expect(insertDocsVersion('::read-more{to="/docs/api/composables/use-fetch"}\n::', '3.x'))
25+
.toBe('::read-more{to="/docs/3.x/api/composables/use-fetch"}\n::')
26+
expect(insertDocsVersion(':read-more{to=\'/docs/api\'}', '4.x'))
27+
.toBe(':read-more{to=\'/docs/4.x/api\'}')
28+
expect(insertDocsVersion('::card{link="/docs/guide"}\n::', '4.x'))
29+
.toBe('::card{link="/docs/4.x/guide"}\n::')
30+
expect(insertDocsVersion(':read-more{to=/docs/getting-started/data-fetching}', '4.x'))
31+
.toBe(':read-more{to=/docs/4.x/getting-started/data-fetching}')
32+
})
33+
34+
it('should leave already-versioned links alone', () => {
35+
const body = '[a](/docs/3.x/guide) and ::read-more{to="/docs/4.x/api"}'
36+
expect(insertDocsVersion(body, '5.x')).toBe(body)
37+
})
38+
39+
it('should not rewrite asset paths, external docs URLs or prose', () => {
40+
const body = [
41+
'![diagram](/assets/docs/guide/rendering.svg)',
42+
'<img src="/assets/docs/getting-started/nuxt.png">',
43+
'[MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch)',
44+
'see https://chrome.com/docs/lighthouse and /assets/docs/foo.png',
45+
'use relative paths without the domain: `/docs/getting-started/installation`'
46+
].join('\n')
47+
expect(insertDocsVersion(body, '5.x')).toBe(body)
48+
})
49+
})

0 commit comments

Comments
 (0)