Skip to content

Commit 0582da5

Browse files
committed
up
1 parent ae1ca2c commit 0582da5

File tree

12 files changed

+317
-101
lines changed

12 files changed

+317
-101
lines changed

app/app.vue

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,43 @@ useHead({
3131
]
3232
})
3333
34+
const route = useRoute()
35+
const site = useSiteConfig()
36+
const canonicalUrl = computed(() => `${site.url}${route.path}`)
37+
3438
if (import.meta.server) {
3539
useHead({
3640
meta: [
3741
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
3842
],
3943
link: [
40-
{ rel: 'icon', type: 'image/png', href: '/icon.png' }
44+
{ rel: 'icon', type: 'image/png', href: '/icon.png' },
45+
{ rel: 'canonical', href: canonicalUrl }
4146
],
4247
htmlAttrs: {
4348
lang: 'en'
44-
}
49+
},
50+
script: [
51+
{
52+
type: 'application/ld+json',
53+
innerHTML: JSON.stringify({
54+
'@context': 'https://schema.org',
55+
'@type': 'WebSite',
56+
'name': 'Nuxt',
57+
'url': site.url,
58+
'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.',
59+
'publisher': {
60+
'@type': 'Organization',
61+
'name': 'Nuxt',
62+
'url': site.url,
63+
'logo': {
64+
'@type': 'ImageObject',
65+
'url': `${site.url}/icon.png`
66+
}
67+
}
68+
})
69+
}
70+
]
4571
})
4672
useSeoMeta({
4773
ogSiteName: 'Nuxt',

modules/md-rewrite.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { defineNuxtModule } from 'nuxt/kit'
2+
import { AI_AGENT_UA_PATTERNS } from '@vercel/agent-readability'
3+
4+
function buildAgentUARegex(): string {
5+
return `(?i).*(${AI_AGENT_UA_PATTERNS.join('|')}).*`
6+
}
27

38
function mdRewrite(nitro) {
49
if (nitro.options.dev || !nitro.options.preset.includes('vercel')) {
@@ -10,41 +15,33 @@ function mdRewrite(nitro) {
1015
= process.getBuiltinModule('node:fs/promises')
1116
const vcJSON = resolve(nitro.options.output.dir, 'config.json')
1217
const vcConfig = JSON.parse(await readFile(vcJSON, 'utf8'))
18+
19+
const agentUA = buildAgentUARegex()
20+
const agentHas = [{ type: 'header', key: 'user-agent', value: agentUA }]
21+
const acceptMd = [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }]
22+
23+
const skipPattern = '^(?!/api/|/_nuxt/|/__nuxt|/raw/|/agent-md/)(.*)$'
24+
25+
// --- Catch-all Agent UA detection → rewrite to /agent-md/ ---
1326
vcConfig.routes.unshift({
14-
src: '^/docs/(.*)$',
15-
dest: '/raw/docs/$1.md',
16-
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
17-
check: true
18-
})
19-
vcConfig.routes.unshift({
20-
src: '^/deploy/(.*)$',
21-
dest: '/raw/deploy/$1.md',
22-
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
23-
check: true
24-
})
25-
vcConfig.routes.unshift({
26-
src: '^/blog/(.*)$',
27-
dest: '/raw/blog/$1.md',
28-
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
29-
check: true
30-
})
31-
vcConfig.routes.unshift({
32-
src: '^/modules/?$',
33-
dest: '/modules.md',
34-
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
35-
check: true
27+
src: skipPattern,
28+
dest: '/agent-md/$1',
29+
has: agentHas
3630
})
31+
32+
// --- Accept: text/markdown header → rewrite to /agent-md/ ---
3733
vcConfig.routes.unshift({
38-
src: '^/changelog/?$',
39-
dest: '/changelog.md',
40-
has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
41-
check: true
34+
src: skipPattern,
35+
dest: '/agent-md/$1',
36+
has: acceptMd
4237
})
38+
39+
// --- Explicit .md extension requests → rewrite to /agent-md/ ---
4340
vcConfig.routes.unshift({
44-
src: '^/(docs|deploy|blog)/(.*)\\.md$',
45-
dest: '/raw/$1/$2.md',
46-
check: true
41+
src: '^/(.*)\\.md$',
42+
dest: '/agent-md/$1'
4743
})
44+
4845
await writeFile(vcJSON, JSON.stringify(vcConfig, null, 2), 'utf8')
4946
})
5047
}

nuxt.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export default defineNuxtConfig({
2727
'@vercel/analytics',
2828
'@vercel/speed-insights'
2929
],
30+
site: {
31+
url: 'https://nuxt.com'
32+
},
3033
$development: {
3134
site: {
3235
url: 'http://localhost:3000'
@@ -106,13 +109,16 @@ export default defineNuxtConfig({
106109
resend: {
107110
apiKey: '',
108111
audienceId: ''
112+
},
113+
mdTracking: {
114+
url: '',
115+
apiKey: ''
109116
}
110117
},
111118
routeRules: {
112119
// Pre-render
113120
'/': { prerender: true },
114121
'/blog/rss.xml': { prerender: true },
115-
'/sitemap.xml': { prerender: true },
116122
'/sitemap.md': { prerender: true },
117123
'/404.html': { prerender: true },
118124
'/docs/3.x/getting-started/introduction': { prerender: true },

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"drizzle-orm": "^0.45.1",
5454
"feed": "^5.2.0",
5555
"h3": "^1.15.6",
56+
"html2canvas-pro": "^2.0.2",
5657
"little-date": "^1.2.1",
5758
"motion-v": "^1.10.3",
5859
"nuxt": "^4.4.2",

pnpm-lock.yaml

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,54 @@
11
import { shouldServeMarkdown, generateNotFoundMarkdown } from '@vercel/agent-readability'
2+
import { trackMdRequest, extractTrackingContext, resolveSource } from '~~/server/utils/md-tracking'
23

3-
const CONTENT_PREFIXES = ['/docs/', '/deploy/', '/blog/']
4+
const SKIP_PREFIXES = ['/api/', '/_nuxt/', '/__nuxt', '/raw/', '/agent-md/']
5+
6+
const NOT_FOUND_OPTIONS = {
7+
baseUrl: 'https://nuxt.com',
8+
sitemapUrl: '/sitemap.md',
9+
indexUrl: '/llms.txt',
10+
fullContentUrl: '/llms-full.txt',
11+
exampleUrl: '/docs/4.x/getting-started/introduction.md'
12+
}
13+
14+
function respondMarkdown(event: Parameters<typeof setResponseHeader>[0], body: string, reason: string) {
15+
setResponseHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
16+
setResponseHeader(event, 'Vary', 'Accept, User-Agent')
17+
setResponseHeader(event, 'X-Agent-Readability', reason)
18+
return body
19+
}
420

521
export default defineEventHandler(async (event) => {
622
const url = getRequestURL(event)
7-
const pathname = url.pathname
23+
let pathname = url.pathname
824

9-
if (pathname.endsWith('.md') || pathname.startsWith('/raw/')) return
10-
if (pathname.startsWith('/api/') || pathname.startsWith('/_nuxt/') || pathname.startsWith('/__nuxt')) return
25+
if (SKIP_PREFIXES.some(p => pathname.startsWith(p))) return
26+
if (/\.(?:js|css|ico|png|jpg|svg|woff2|json|xml|txt)$/.test(pathname)) return
1127

12-
const request = toWebRequest(event)
13-
const { serve, reason } = shouldServeMarkdown(request)
14-
if (!serve) return
28+
if (pathname.endsWith('.md')) {
29+
pathname = pathname.slice(0, -3)
30+
}
1531

16-
let mdRoute: string | null = null
32+
const request = toWebRequest(event)
33+
const { serve, reason, detection } = shouldServeMarkdown(request)
1734

18-
for (const prefix of CONTENT_PREFIXES) {
19-
if (pathname.startsWith(prefix)) {
20-
mdRoute = `/raw${pathname}.md`
21-
break
22-
}
23-
}
35+
if (!serve && !url.pathname.endsWith('.md')) return
2436

25-
if (!mdRoute) {
26-
if (pathname === '/modules' || pathname === '/modules/') mdRoute = '/modules.md'
27-
else if (pathname === '/changelog' || pathname === '/changelog/') mdRoute = '/changelog.md'
28-
}
37+
const ctx = extractTrackingContext(event)
38+
const requestType = url.pathname.endsWith('.md')
39+
? 'md-url' as const
40+
: reason === 'accept-header'
41+
? 'header-negotiated' as const
42+
: 'agent-rewrite' as const
2943

30-
if (!mdRoute) return
44+
const effectiveReason = reason || 'md-url'
3145

3246
try {
33-
const md = await $fetch<string>(mdRoute)
34-
setResponseHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
35-
setResponseHeader(event, 'Vary', 'Accept, User-Agent')
36-
setResponseHeader(event, 'X-Agent-Readability', reason)
37-
return md
38-
}
39-
catch {
40-
const notFound = generateNotFoundMarkdown(pathname, {
41-
baseUrl: 'https://nuxt.com',
42-
sitemapUrl: '/sitemap.md',
43-
indexUrl: '/llms.txt',
44-
fullContentUrl: '/llms-full.txt',
45-
exampleUrl: '/docs/4.x/getting-started/introduction.md'
46-
})
47-
setResponseHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
48-
setResponseHeader(event, 'Vary', 'Accept, User-Agent')
49-
setResponseHeader(event, 'X-Agent-Readability', `${reason}-not-found`)
50-
return notFound
47+
const md = await $fetch<string>(`/agent-md${pathname}`)
48+
trackMdRequest(event, { ...ctx, path: pathname, source: resolveSource(pathname), requestType, detectionMethod: detection.method })
49+
return respondMarkdown(event, md, effectiveReason)
50+
} catch {
51+
trackMdRequest(event, { ...ctx, path: pathname, source: 'agent-404', requestType, detectionMethod: detection.method })
52+
return respondMarkdown(event, generateNotFoundMarkdown(pathname, NOT_FOUND_OPTIONS), effectiveReason)
5153
}
5254
})

server/routes/[...slug].md.get.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)