From 29d9b02b2974b57aebeedaa66d4f20155612a1c5 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Tue, 15 Sep 2026 15:21:56 +0200 Subject: [PATCH 1/5] docs: redirect /docs and /api to the documentation --- docs/nuxt.config.ts | 3 ++ test/docs-redirects.test.ts | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 test/docs-redirects.test.ts diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts index b2495240..72c08a49 100644 --- a/docs/nuxt.config.ts +++ b/docs/nuxt.config.ts @@ -45,6 +45,9 @@ export default defineNuxtConfig({ }, routeRules: { + // Discoverable entry points for agents and crawlers that probe the conventional URLs + '/docs': { redirect: '/getting-started/introduction' }, + '/api': { redirect: '/reference/reference' }, '/play/booking': { redirect: '/play?example=booking' }, '/play/recipe': { redirect: '/play?example=recipe' }, '/play/nuxt-ui': { redirect: '/play?example=nuxt-ui' }, diff --git a/test/docs-redirects.test.ts b/test/docs-redirects.test.ts new file mode 100644 index 00000000..de947246 --- /dev/null +++ b/test/docs-redirects.test.ts @@ -0,0 +1,69 @@ +import { existsSync, readdirSync, statSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { join, relative } from 'node:path' +import { describe, expect, it } from 'vitest' + +// Every `routeRules` redirect in the docs app must land on a page that exists: a +// content document, an app page, or another redirect. A stale target is a 404 +// behind a URL we advertise. +const docsDir = fileURLToPath(new URL('../docs', import.meta.url)) +const contentDir = join(docsDir, 'content') +const pagesDir = join(docsDir, 'app/pages') + +function walk(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + const full = join(dir, name) + return statSync(full).isDirectory() ? walk(full) : [full] + }) +} + +/** `1.getting-started/0.introduction.md` -> `/getting-started/introduction` */ +function contentPath(file: string): string { + const segments = relative(contentDir, file) + .replace(/\.md$/, '') + .split('/') + .map((segment) => segment.replace(/^\d+\./, '')) + if (segments[segments.length - 1] === 'index') segments.pop() + return '/' + segments.join('/') +} + +/** `play/index.vue` -> `/play`, `compare.vue` -> `/compare` */ +function pagePath(file: string): string { + const segments = relative(pagesDir, file) + .replace(/\.vue$/, '') + .split('/') + if (segments[segments.length - 1] === 'index') segments.pop() + return '/' + segments.join('/') +} + +const pages = new Set([ + ...walk(contentDir) + .filter((file) => file.endsWith('.md')) + .map(contentPath), + ...walk(pagesDir) + .filter((file) => file.endsWith('.vue')) + .map(pagePath), +]) + +// The config relies on Nuxt's global; the identity is all the shape needs here. +;(globalThis as Record).defineNuxtConfig = (config: unknown) => config +const config = (await import('../docs/nuxt.config.ts')).default as { + routeRules?: Record +} +const redirects = Object.entries(config.routeRules ?? {}).flatMap(([from, rule]) => + rule.redirect ? [[from, rule.redirect] as const] : [] +) + +describe('docs redirects', () => { + it('declares the conventional entry points agents probe', () => { + const rules = Object.fromEntries(redirects) + expect(rules['/docs']).toBe('/getting-started/introduction') + expect(rules['/api']).toBe('/reference/reference') + }) + + it.each(redirects)('%s -> %s exists', (from, to) => { + const target = to.split('?')[0]! + const resolved = redirects.find(([source]) => source === target)?.[1].split('?')[0] ?? target + expect(pages.has(resolved) || existsSync(join(docsDir, 'public', resolved))).toBe(true) + }) +}) From 16784d35a0f0e68699fd6ef09a0d540252cc1f9f Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Tue, 15 Sep 2026 15:21:57 +0200 Subject: [PATCH 2/5] docs: link the Documentation tab to /docs --- docs/app/app.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/app/app.config.ts b/docs/app/app.config.ts index 800cf823..06f81347 100644 --- a/docs/app/app.config.ts +++ b/docs/app/app.config.ts @@ -13,6 +13,7 @@ export default defineAppConfig({ nav: [ { label: 'Documentation', + to: '/docs', sections: ['getting-started', 'syntax', 'rendering', 'reference', 'compare', 'kb'], }, { label: 'Plugins', sections: ['plugins'], link: 'section' as const }, From 45bc8c6dc3ce070446930a165b7e909ab858ab9a Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Wed, 16 Sep 2026 10:32:37 +0200 Subject: [PATCH 3/5] chore(deps): bump comark-docs for the linked Documentation tab --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4211e1a9..6ea3f087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,7 +418,7 @@ importers: version: link:../packages/comark comark-docs: specifier: github:comarkdown/comark-docs - version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/05a64d327e27ac37bf1ca014e67003fd5b39bfd3(8dbfe6d44f6e94ec7ed1b2f7dff141e8) + version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/2d97ac17ede7e84c3a338429135952391bce8483(8dbfe6d44f6e94ec7ed1b2f7dff141e8) katex: specifier: 'catalog:' version: 0.17.0 @@ -7075,8 +7075,8 @@ packages: resolution: {integrity: sha512-z0nuo3PM53FwBYbxP4rNrUo1QW7jv8YO44HDYaGm2Wx4pmHsLqKdsA6CiHcSOaURze6m7my0Z0YFaLL77LtDUA==} hasBin: true - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/05a64d327e27ac37bf1ca014e67003fd5b39bfd3: - resolution: {gitHosted: true, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/05a64d327e27ac37bf1ca014e67003fd5b39bfd3} + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/2d97ac17ede7e84c3a338429135952391bce8483: + resolution: {gitHosted: true, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/2d97ac17ede7e84c3a338429135952391bce8483} version: 0.0.1 engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} peerDependencies: @@ -18531,7 +18531,7 @@ snapshots: - shiki - uploadthing - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/05a64d327e27ac37bf1ca014e67003fd5b39bfd3(8dbfe6d44f6e94ec7ed1b2f7dff141e8): + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/2d97ac17ede7e84c3a338429135952391bce8483(8dbfe6d44f6e94ec7ed1b2f7dff141e8): dependencies: '@ai-sdk/gateway': 4.0.64(zod@4.4.3) '@ai-sdk/vue': 4.0.79(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) From bd838ab9a602797e5b00026e38cc99783bb208a9 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Wed, 16 Sep 2026 10:36:44 +0200 Subject: [PATCH 4/5] test(docs): split relative paths on the platform separator --- test/docs-redirects.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/docs-redirects.test.ts b/test/docs-redirects.test.ts index de947246..9540bab2 100644 --- a/test/docs-redirects.test.ts +++ b/test/docs-redirects.test.ts @@ -1,6 +1,6 @@ import { existsSync, readdirSync, statSync } from 'node:fs' import { fileURLToPath } from 'node:url' -import { join, relative } from 'node:path' +import { join, relative, sep } from 'node:path' import { describe, expect, it } from 'vitest' // Every `routeRules` redirect in the docs app must land on a page that exists: a @@ -21,7 +21,7 @@ function walk(dir: string): string[] { function contentPath(file: string): string { const segments = relative(contentDir, file) .replace(/\.md$/, '') - .split('/') + .split(sep) .map((segment) => segment.replace(/^\d+\./, '')) if (segments[segments.length - 1] === 'index') segments.pop() return '/' + segments.join('/') @@ -31,7 +31,7 @@ function contentPath(file: string): string { function pagePath(file: string): string { const segments = relative(pagesDir, file) .replace(/\.vue$/, '') - .split('/') + .split(sep) if (segments[segments.length - 1] === 'index') segments.pop() return '/' + segments.join('/') } From 30f0420724361f6c12bacf9c8a05a894683f72cb Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Wed, 16 Sep 2026 10:45:15 +0200 Subject: [PATCH 5/5] test(docs): drop the redirect test --- test/docs-redirects.test.ts | 69 ------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 test/docs-redirects.test.ts diff --git a/test/docs-redirects.test.ts b/test/docs-redirects.test.ts deleted file mode 100644 index 9540bab2..00000000 --- a/test/docs-redirects.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { existsSync, readdirSync, statSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import { join, relative, sep } from 'node:path' -import { describe, expect, it } from 'vitest' - -// Every `routeRules` redirect in the docs app must land on a page that exists: a -// content document, an app page, or another redirect. A stale target is a 404 -// behind a URL we advertise. -const docsDir = fileURLToPath(new URL('../docs', import.meta.url)) -const contentDir = join(docsDir, 'content') -const pagesDir = join(docsDir, 'app/pages') - -function walk(dir: string): string[] { - return readdirSync(dir).flatMap((name) => { - const full = join(dir, name) - return statSync(full).isDirectory() ? walk(full) : [full] - }) -} - -/** `1.getting-started/0.introduction.md` -> `/getting-started/introduction` */ -function contentPath(file: string): string { - const segments = relative(contentDir, file) - .replace(/\.md$/, '') - .split(sep) - .map((segment) => segment.replace(/^\d+\./, '')) - if (segments[segments.length - 1] === 'index') segments.pop() - return '/' + segments.join('/') -} - -/** `play/index.vue` -> `/play`, `compare.vue` -> `/compare` */ -function pagePath(file: string): string { - const segments = relative(pagesDir, file) - .replace(/\.vue$/, '') - .split(sep) - if (segments[segments.length - 1] === 'index') segments.pop() - return '/' + segments.join('/') -} - -const pages = new Set([ - ...walk(contentDir) - .filter((file) => file.endsWith('.md')) - .map(contentPath), - ...walk(pagesDir) - .filter((file) => file.endsWith('.vue')) - .map(pagePath), -]) - -// The config relies on Nuxt's global; the identity is all the shape needs here. -;(globalThis as Record).defineNuxtConfig = (config: unknown) => config -const config = (await import('../docs/nuxt.config.ts')).default as { - routeRules?: Record -} -const redirects = Object.entries(config.routeRules ?? {}).flatMap(([from, rule]) => - rule.redirect ? [[from, rule.redirect] as const] : [] -) - -describe('docs redirects', () => { - it('declares the conventional entry points agents probe', () => { - const rules = Object.fromEntries(redirects) - expect(rules['/docs']).toBe('/getting-started/introduction') - expect(rules['/api']).toBe('/reference/reference') - }) - - it.each(redirects)('%s -> %s exists', (from, to) => { - const target = to.split('?')[0]! - const resolved = redirects.find(([source]) => source === target)?.[1].split('?')[0] ?? target - expect(pages.has(resolved) || existsSync(join(docsDir, 'public', resolved))).toBe(true) - }) -})