Skip to content
Draft
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
30 changes: 28 additions & 2 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,43 @@ useHead({
]
})

const route = useRoute()
const site = useSiteConfig()
const canonicalUrl = computed(() => `${site.url}${route.path}`)

if (import.meta.server) {
useHead({
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{ rel: 'icon', type: 'image/png', href: '/icon.png' }
{ rel: 'icon', type: 'image/png', href: '/icon.png' },
{ rel: 'canonical', href: canonicalUrl }
],
htmlAttrs: {
lang: 'en'
}
},
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
'name': 'Nuxt',
'url': site.url,
'description': 'The Intuitive Vue Framework. Nuxt is a free and open-source framework to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.',
'publisher': {
'@type': 'Organization',
'name': 'Nuxt',
'url': site.url,
'logo': {
'@type': 'ImageObject',
'url': `${site.url}/icon.png`
}
}
})
}
]
})
useSeoMeta({
ogSiteName: 'Nuxt',
Expand Down
50 changes: 26 additions & 24 deletions modules/md-rewrite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { defineNuxtModule } from 'nuxt/kit'
import { AI_AGENT_UA_PATTERNS } from '@vercel/agent-readability'

function buildAgentUARegex(): string {
return `(?i).*(${AI_AGENT_UA_PATTERNS.join('|')}).*`
}

function mdRewrite(nitro) {
if (nitro.options.dev || !nitro.options.preset.includes('vercel')) {
Expand All @@ -10,36 +15,33 @@ function mdRewrite(nitro) {
= process.getBuiltinModule('node:fs/promises')
const vcJSON = resolve(nitro.options.output.dir, 'config.json')
const vcConfig = JSON.parse(await readFile(vcJSON, 'utf8'))

const agentUA = buildAgentUARegex()
const agentHas = [{ type: 'header', key: 'user-agent', value: agentUA }]
const acceptMd = [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }]

const skipPattern = '^(?!/api/|/_nuxt/|/__nuxt|/raw/|/agent-md/)(.*)$'

// --- Catch-all Agent UA detection → rewrite to /agent-md/ ---
vcConfig.routes.unshift({
src: '^/docs/(.*)$',
dest: '/raw/docs/$1.md',
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
check: true
})
vcConfig.routes.unshift({
src: '^/deploy/(.*)$',
dest: '/raw/deploy/$1.md',
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
check: true
})
vcConfig.routes.unshift({
src: '^/blog/(.*)$',
dest: '/raw/blog/$1.md',
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
check: true
src: skipPattern,
dest: '/agent-md/$1',
has: agentHas
})

// --- Accept: text/markdown header → rewrite to /agent-md/ ---
vcConfig.routes.unshift({
src: '^/modules/?$',
dest: '/modules.md',
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
check: true
src: skipPattern,
dest: '/agent-md/$1',
has: acceptMd
})

// --- Explicit .md extension requests → rewrite to /agent-md/ ---
vcConfig.routes.unshift({
src: '^/changelog/?$',
dest: '/changelog.md',
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
check: true
src: '^/(.*)\\.md$',
dest: '/agent-md/$1'
})

await writeFile(vcJSON, JSON.stringify(vcConfig, null, 2), 'utf8')
})
}
Expand Down
9 changes: 8 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const { resolve } = createResolver(import.meta.url)

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({

Check failure on line 7 in nuxt.config.ts

View workflow job for this annotation

GitHub Actions / lint

Expected config key "modules" to come before "$development"
modules: [
'@nuxt/ui',
'nuxt-content-twoslash',
Expand All @@ -27,6 +27,9 @@
'@vercel/analytics',
'@vercel/speed-insights'
],
site: {
url: 'https://nuxt.com'
},
$development: {
site: {
url: 'http://localhost:3000'
Expand Down Expand Up @@ -106,13 +109,17 @@
resend: {
apiKey: '',
audienceId: ''
},
mdTracking: {
url: '',
apiKey: ''
}
},
routeRules: {
// Pre-render
'/': { prerender: true },
'/blog/rss.xml': { prerender: true },
'/sitemap.xml': { prerender: true },
'/sitemap.md': { prerender: true },
'/404.html': { prerender: true },
'/docs/3.x/getting-started/introduction': { prerender: true },
'/docs/4.x/getting-started/introduction': { prerender: true },
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@nuxtjs/mcp-toolkit": "^0.7.0",
"@nuxtjs/mdc": "^0.20.2",
"@nuxtjs/plausible": "^2.0.1",
"@vercel/agent-readability": "^0.2.1",
"@vercel/analytics": "^2.0.0",
"@vercel/functions": "^3.4.3",
"@vercel/speed-insights": "^2.0.0",
Expand All @@ -52,6 +53,7 @@
"drizzle-orm": "^0.45.1",
"feed": "^5.2.0",
"h3": "^1.15.6",
"html2canvas-pro": "^2.0.2",
"little-date": "^1.2.1",
"motion-v": "^1.10.3",
"nuxt": "^4.4.2",
Expand Down
54 changes: 54 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
User-agent: *
Disallow: /docs/5.x/

User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: CCBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Amazonbot
Allow: /

Sitemap: https://nuxt.com/sitemap.xml
Sitemap: https://nuxt.com/sitemap.md
54 changes: 54 additions & 0 deletions server/middleware/agent-readability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { shouldServeMarkdown, generateNotFoundMarkdown } from '@vercel/agent-readability'
import { trackMdRequest, extractTrackingContext, resolveSource } from '~~/server/utils/md-tracking'

const SKIP_PREFIXES = ['/api/', '/_nuxt/', '/__nuxt', '/raw/', '/agent-md/']

const NOT_FOUND_OPTIONS = {
baseUrl: 'https://nuxt.com',
sitemapUrl: '/sitemap.md',
indexUrl: '/llms.txt',
fullContentUrl: '/llms-full.txt',
exampleUrl: '/docs/4.x/getting-started/introduction.md'
}

function respondMarkdown(event: Parameters<typeof setResponseHeader>[0], body: string, reason: string) {
setResponseHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
setResponseHeader(event, 'Vary', 'Accept, User-Agent')
setResponseHeader(event, 'X-Agent-Readability', reason)
return body
}

export default defineEventHandler(async (event) => {
const url = getRequestURL(event)
let pathname = url.pathname

if (SKIP_PREFIXES.some(p => pathname.startsWith(p))) return
if (/\.(?:js|css|ico|png|jpg|svg|woff2|json|xml|txt)$/.test(pathname)) return

if (pathname.endsWith('.md')) {
pathname = pathname.slice(0, -3)
}

const request = toWebRequest(event)
const { serve, reason, detection } = shouldServeMarkdown(request)

if (!serve && !url.pathname.endsWith('.md')) return

const ctx = extractTrackingContext(event)
const requestType = url.pathname.endsWith('.md')
? 'md-url' as const
: reason === 'accept-header'
? 'header-negotiated' as const
: 'agent-rewrite' as const

const effectiveReason = reason || 'md-url'

try {
const md = await $fetch<string>(`/agent-md${pathname}`)
trackMdRequest(event, { ...ctx, path: pathname, source: resolveSource(pathname), requestType, detectionMethod: detection.method })
return respondMarkdown(event, md, effectiveReason)
} catch {
trackMdRequest(event, { ...ctx, path: pathname, source: 'agent-404', requestType, detectionMethod: detection.method })
return respondMarkdown(event, generateNotFoundMarkdown(pathname, NOT_FOUND_OPTIONS), effectiveReason)
}
})
Loading
Loading