Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b746ef3
feat: add agent discovery surfaces
benjamincanac Apr 29, 2026
8e9fc59
feat: add canonical, JSON-LD and markdown alternate per page
benjamincanac Apr 29, 2026
64dcdd2
feat: add sitemap.md, lastmod and trim llms.txt
benjamincanac Apr 29, 2026
f512c86
chore: bump @nuxt/test-utils setup version
benjamincanac Apr 29, 2026
991c02c
refactor: derive URLs from useSiteConfig and prerender sitemap.md
benjamincanac Apr 29, 2026
c7384d1
fix: align agent surfaces with robots.txt and clean up payloads
benjamincanac Apr 29, 2026
c59a7ee
fix(sitemap-md): mirror sitemap.xml scope (drop v3 docs)
benjamincanac Apr 29, 2026
7d59b25
chore(sitemap): rename sitemap.xml.ts to .get.ts for consistency
benjamincanac Apr 29, 2026
d122dc8
Merge branch 'main' into feat/agent-readability
benjamincanac Apr 29, 2026
3709708
fix(agent-discovery): hardcode canonical domain & strip trailing slash
benjamincanac Apr 29, 2026
c3c5f00
fix(agent-readability): address self-review feedback
benjamincanac Apr 29, 2026
22bd0b3
fix(agent-readability): add Vary header on /raw/** rewrite destinations
benjamincanac Apr 29, 2026
6cf4872
refactor(agent-readability): migrate to nuxt-schema-org v6 + dedupe i…
benjamincanac Apr 30, 2026
eb9f7bc
refactor: drop queryCollectionWithEvent type cast
benjamincanac Apr 30, 2026
c213d38
refactor: centralize docs version constants in shared/utils/docs
benjamincanac Apr 30, 2026
abc2ae0
chore: remove @takumi-rs/wasm dev dependency
benjamincanac Apr 30, 2026
d85b6cb
fix: read WebSite name from siteConfig
benjamincanac Apr 30, 2026
1f0fd62
Merge branch 'main' into feat/agent-readability
benjamincanac Apr 30, 2026
e1f35d5
refactor: split server utils into site.ts and agent.ts
benjamincanac Apr 30, 2026
acc76d0
fix: set Content-Type on sitemap.xml + type-safe CURRENT_DOCS_VERSION
benjamincanac Apr 30, 2026
0a9de43
fix(nuxt.config): configure evlog exclude
benjamincanac Apr 30, 2026
7e43af5
Merge branch 'main' into feat/agent-readability
HugoRCD Apr 30, 2026
1039061
fix(nuxt.config): update og image timeout
benjamincanac Apr 30, 2026
93c24ff
sample every info logs
HugoRCD Apr 30, 2026
58327eb
fix(nuxt.config): disable evlog in ci
benjamincanac Apr 30, 2026
568ef88
fix(nuxt.config): disable og image cache
benjamincanac Apr 30, 2026
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
10 changes: 10 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ if (import.meta.server) {
twitterCard: 'summary_large_image',
twitterSite: 'nuxt_js'
})
// Organization identity is provided via `schemaOrg.identity` in
// nuxt.config.ts so the module can emit a single `#identity` node
// (instead of a duplicated `#organization` graph entry). The WebSite
// resolver inherits `url` from siteConfig but not `name`, so we wire
// it up here against the same source of truth.
useSchemaOrg([
defineWebSite({
name: useSiteConfig().name
})
])
}

const versionNavigation = computed(() => navigation.value?.filter(item => item.path === version.value.path || item.path === '/blog') ?? [])
Expand Down
4 changes: 2 additions & 2 deletions app/components/content/IndexExample.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
size: 'xl'
}, {
label: 'Use this template',
to: 'https://github.com/nuxt-ui-pro/starter',
to: 'https://github.com/nuxt-ui-templates/starter',
target: '_blank',
icon: 'i-simple-icons-github',
size: 'xl',
Expand Down Expand Up @@ -147,7 +147,7 @@
color: 'neutral'
}, {
label: 'GitHub',
to: 'https://github.com/nuxt-ui-pro/starter',
to: 'https://github.com/nuxt-ui-templates/starter',
target: '_blank',
trailingIcon: 'i-simple-icons-github',
color: 'neutral',
Expand Down
30 changes: 30 additions & 0 deletions app/composables/useCanonical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { withoutTrailingSlash } from 'ufo'
import { toValue, type MaybeRefOrGetter } from 'vue'

// Adds a canonical URL for the current route plus, optionally, a
// `rel="alternate"; type="text/markdown"` link pointing at the agent-friendly
// markdown counterpart. Vercel rewrites the public `.md` URLs to the
// underlying `/raw/**` handlers (see `modules/md-rewrite.ts`).
export function useCanonical(markdownAlternate?: MaybeRefOrGetter<string | null | undefined>) {
const route = useRoute()
const site = useSiteConfig()

useHead({
link: computed(() => {
const url = withoutTrailingSlash(site.url)
const path = route.path === '/' ? '' : route.path.replace(/\/$/, '')

const links: Array<{ rel: string, href: string, type?: string }> = [
{ rel: 'canonical', href: `${url}${path}` }
]

const md = toValue(markdownAlternate)
if (md) {
const href = md.startsWith('http') ? md : `${url}${md.startsWith('/') ? md : `/${md}`}`
links.push({ rel: 'alternate', type: 'text/markdown', href })
}

return links
})
})
}
18 changes: 15 additions & 3 deletions app/middleware/docs-version.global.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { SUPPORTED_DOC_VERSIONS, EXCLUDED_DOC_VERSIONS, CURRENT_DOCS_VERSION } from '#shared/utils/docs'

// Versions kept here as a fast prefix scan — the canonical list lives in
// `shared/utils/docs.ts` (SUPPORTED_DOC_VERSIONS + EXCLUDED_DOC_VERSIONS).
const KNOWN_DOC_VERSIONS = [...SUPPORTED_DOC_VERSIONS, ...EXCLUDED_DOC_VERSIONS]

export default defineNuxtRouteMiddleware((to) => {
if (to.path.startsWith('/docs/') && !to.path.startsWith('/docs/5.x') && !to.path.startsWith('/docs/4.x') && !to.path.startsWith('/docs/3.x')) {
return to.fullPath.replace('/docs', '/docs/4.x')
}
if (!to.path.startsWith('/docs/')) return
if (KNOWN_DOC_VERSIONS.some(v => to.path.startsWith(`/docs/${v}`))) return
Comment thread
benjamincanac marked this conversation as resolved.

// 302 (not 301): the target version flips when a new stable release
// ships and we don't want browsers/CDNs caching stale 301s pointing at
// /docs/4.x/*. Note: prerendered pages can't emit a real HTTP redirect,
// so the static output falls back to a meta-refresh stub — modern
// crawlers and clients still follow it.
return navigateTo(to.fullPath.replace('/docs', `/docs/${CURRENT_DOCS_VERSION}`), { redirectCode: 302 })
})
10 changes: 5 additions & 5 deletions app/pages/blog/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ useSeoMeta({
title,
description,
ogDescription: description,
ogTitle: `${title} · Nuxt Blog`
ogTitle: `${title} · Nuxt Blog`,
...(article.value.image ? { ogImage: article.value.image } : {})
})
useCanonical(`${route.path.replace(/\/$/, '')}.md`)

if (article.value.image) {
defineOgImage({ url: article.value.image })
} else {
defineOgImageComponent('Docs', {
if (!article.value.image) {
defineOgImage('Docs.takumi', {
headline: 'Blog',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/blog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ useSeoMeta({
ogDescription: page.value.description,
ogTitle: page.value.title
})
defineOgImageComponent('Docs', {
useCanonical()
defineOgImage('Docs.takumi', {
headline: 'Blog',
title: page.value.title,
description: page.value.description
Expand Down
7 changes: 6 additions & 1 deletion app/pages/changelog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ useSeoMeta({
ogDescription: description,
ogTitle: title
})
defineOgImageComponent('Docs', {
useCanonical('/raw/changelog.md')
defineOgImage('Docs.takumi', {
headline: 'Changelog',
title,
description
})
if (import.meta.server) {
prerenderRoutes(['/raw/changelog.md'])
}
const { data: releases } = await useFetch('/api/releases')
const openStates = reactive<Record<string, boolean>>({})
Expand Down
1 change: 1 addition & 0 deletions app/pages/chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ useSeoMeta({
twitterDescription: 'The Nuxt Agent helps you explore the documentation — ask about Nuxt, modules, deployment, and more.',
twitterImage: joinURL(site.url, '/nuxt-agent.jpg')
})
useCanonical()
</script>

<template>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/deploy/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: `Deploy Nuxt to ${title}`
})
useCanonical(`${route.path.replace(/\/$/, '')}.md`)

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: 'Deploy To',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/deploy/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: title
})
useCanonical()

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
title: 'Deploy Nuxt',
description
})
Expand Down
3 changes: 2 additions & 1 deletion app/pages/design-kit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ useSeoMeta({
ogDescription: description,
ogTitle: title
})
defineOgImageComponent('Docs', {
useCanonical()
defineOgImage('Docs.takumi', {
title,
description
})
Expand Down
40 changes: 26 additions & 14 deletions app/pages/docs/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { joinURL } from 'ufo'
import type { ContentNavigationItem } from '@nuxt/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'
import { mapContentNavigation } from '@nuxt/ui/utils/content'
import { SUPPORTED_DOCS_PATH_REGEX } from '#shared/utils/docs'

definePageMeta({
heroBackground: 'opacity-30',
Expand All @@ -19,7 +20,6 @@ const nuxtApp = useNuxtApp()
const { version } = useDocsVersion()
const { headerLinks } = useHeaderLinks()
const { isAgentDocked } = useNuxtAgent()
const site = useSiteConfig()
const path = computed(() => route.path.replace(/\/$/, ''))

const ignoredPaths = ['.nuxt', '.output', '.env', 'node_modules']
Expand Down Expand Up @@ -132,28 +132,40 @@ useSeoMeta({
titleTemplate,
title
})

// Pre-render the markdown path + add it to alternate links
prerenderRoutes([joinURL('/raw', `${path.value}.md`)])
useHead({
link: [
{
rel: 'alternate',
href: joinURL(site.url, 'raw', `${path.value}.md`),
type: 'text/markdown'
}
]
})
// Only emit canonical/markdown alternate on versioned paths (e.g.
// `/docs/4.x/*`). Unversioned `/docs/*` URLs are meta-refresh stubs that
// the docs-version middleware redirects to the active version, so agents
// should not treat the stub URL as authoritative. The supported version
// list lives in `shared/utils/docs.ts` (kept in sync with `md-rewrite.ts`).
if (SUPPORTED_DOCS_PATH_REGEX.test(path.value)) {
useCanonical(() => `${path.value}.md`)
}

if (import.meta.server) {
prerenderRoutes([joinURL('/raw', `${path.value}.md`)])

useSchemaOrg([
defineArticle({
'@type': 'TechArticle',
'headline': page.value?.title,
'description': page.value?.seo?.description || page.value?.description
}),
defineBreadcrumb({
itemListElement: breadcrumb.value.map(item => ({
name: item.label,
item: item.to
}))
})
])

const description = page.value?.seo?.description || page.value?.description
useSeoMeta({
description,
ogDescription: description,
ogTitle: titleTemplate.value?.includes('%s') ? titleTemplate.value.replace('%s', title.value) : title.value
})

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: breadcrumb.value.length ? breadcrumb.value.map(link => link.label).join(' > ') : '',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/enterprise/agencies/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: `${title} · Nuxt Agencies`
})
useCanonical()

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: 'Nuxt Agencies',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/enterprise/agencies/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: `${title} · Enterprise`
})
useCanonical()

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: 'Enterprise',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/enterprise/jobs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: `${title} · Enterprise`
})
useCanonical()
defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: 'Enterprise',
title,
description
Expand Down
3 changes: 2 additions & 1 deletion app/pages/enterprise/sponsors.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ useSeoMeta({
ogDescription: description,
ogTitle: `${title} · Community`
})
useCanonical()

defineOgImageComponent('Docs', {
defineOgImage('Docs.takumi', {
headline: 'Community',
title,
description
Expand Down
8 changes: 3 additions & 5 deletions app/pages/enterprise/support.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ useSeoMeta({
title,
description,
ogDescription: description,
ogTitle: `${title} · Enterprise`
})
defineOgImage({
url: '/assets/enterprise/support/social-card.png'
ogTitle: `${title} · Enterprise`,
ogImage: '/assets/enterprise/support/social-card.png'
})
useCanonical()
</script>

<template>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/evals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ useSeoMeta({
ogDescription: description,
ogTitle: title
})
defineOgImageComponent('Docs', { title, description })
useCanonical()
defineOgImage('Docs.takumi', { title, description })

// Build experiment map by name
const experimentMap = computed(() => {
Expand Down
20 changes: 17 additions & 3 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,34 @@ function openVideoModal() {

const site = useSiteConfig()
const title = 'Nuxt: The Full-Stack Vue Framework'
const description = 'Build fast, production-ready web apps with Vue. File-based routing, auto-imports, and server-side rendering — all configured out of the box.'

useSeoMeta({
titleTemplate: '%s',
title,
titleTemplate: '%s'
description
})
useCanonical('/raw/index.md')

if (import.meta.server) {
const description = 'Build fast, production-ready web apps with Vue. File-based routing, auto-imports, and server-side rendering — all configured out of the box.'
prerenderRoutes(['/raw/index.md'])

useSeoMeta({
ogTitle: title,
description: description,
ogDescription: description,
ogImage: joinURL(site.url, '/new-social.jpg'),
twitterImage: joinURL(site.url, '/new-social.jpg')
})

useSchemaOrg([
defineSoftwareApp({
name: 'Nuxt',
description,
operatingSystem: 'Cross-platform',
applicationCategory: 'DeveloperApplication',
offers: { '@type': 'Offer', 'price': '0', 'priceCurrency': 'USD' }
})
])
}

const tabs = computed(() => page.value?.hero.tabs.map(tab => ({
Expand Down
24 changes: 15 additions & 9 deletions app/pages/modules/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,23 @@ const createdAgo = useTimeAgo(module.value.stats.createdAt)
useSeoMeta({
titleTemplate: '%s · Nuxt Modules',
title,
description,
ogDescription: description,
ogTitle: `${title} · Nuxt Modules`
})

defineOgImageComponent('Module', {
module: module.value,
headline: 'Nuxt Modules',
title,
description
})
useCanonical()

if (import.meta.server) {
useSeoMeta({
ogDescription: description,
ogTitle: `${title} · Nuxt Modules`
})

defineOgImage('Module.takumi', {
module: module.value,
headline: 'Nuxt Modules',
title,
description
})
}
</script>

<template>
Expand Down
Loading