-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathmiddleware.ts
More file actions
140 lines (124 loc) · 5.26 KB
/
Copy pathmiddleware.ts
File metadata and controls
140 lines (124 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { NextRequest, NextResponse } from "next/server";
import { resolveDocsPath } from "@/redirects.mjs";
/**
* Middleware for AI agent content negotiation and SEO canonicalization.
*
* Responsibilities:
*
* 1. SEO canonicalization (all pages)
* Emits a `Link: <canonical>; rel="canonical"` HTTP response header (clean
* pathname, no query params) as a belt-and-suspenders signal to crawlers.
* The HTML `<link rel="canonical">` tag itself comes from the root layout's
* `metadata.alternates.canonical` (app router) and `pages/_app.tsx` (pages
* router) — not from middleware.
*
* 2. Markdown serving for AI agents / AEO (blog + docs only)
*
* Strategy A — .md extension routes (no header sniffing required):
* GET /blog/my-post.md
* → internal rewrite → /blog-markdown/my-post (returns text/markdown)
*
* GET /docs/getting-started/nextjs-quick-start.md
* → internal rewrite → /docs-markdown/getting-started/nextjs-quick-start
*
* Strategy B — Accept: text/markdown content negotiation:
* GET /blog/my-post Accept: text/markdown
* → internal rewrite → /blog-markdown/my-post (returns text/markdown)
*
* GET /docs/getting-started Accept: text/markdown
* → internal rewrite → /docs-markdown/getting-started
*
* For normal browser requests the middleware adds a `Vary: Accept` header
* so CDN/edge caches keep the two representations separate.
*/
const SITE_ORIGIN = "https://www.inngest.com";
function acceptsMarkdown(req: NextRequest): boolean {
const accept = req.headers.get("accept") ?? "";
return accept.includes("text/markdown");
}
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// ── Strategy A: .md extension routes ──────────────────────────────────────
if (pathname.endsWith(".md")) {
// /blog/[slug].md → /blog-markdown/[slug]
if (/^\/blog\/[^/]+\.md$/.test(pathname)) {
const slug = pathname.slice("/blog/".length, -".md".length);
const url = req.nextUrl.clone();
url.pathname = `/blog-markdown/${slug}`;
return NextResponse.rewrite(url);
}
// /docs/[...path].md → /docs-markdown/[...path]
if (pathname.startsWith("/docs/")) {
const docPath = pathname.slice("/docs/".length, -".md".length);
const url = req.nextUrl.clone();
url.pathname = `/docs-markdown/${docPath}`;
return NextResponse.rewrite(url);
}
}
// ── Strategy B: Accept: text/markdown content negotiation ─────────────────
if (acceptsMarkdown(req)) {
// /blog/[slug] → /blog-markdown/[slug]
if (/^\/blog\/[^/]+$/.test(pathname)) {
const slug = pathname.replace(/^\/blog\//, "");
const url = req.nextUrl.clone();
url.pathname = `/blog-markdown/${slug}`;
return NextResponse.rewrite(url);
}
// /docs/[...path] → /docs-markdown/[...path]
if (pathname.startsWith("/docs/") || pathname === "/docs") {
const docPath = pathname.replace(/^\/docs\/?/, "");
const url = req.nextUrl.clone();
url.pathname = `/docs-markdown/${docPath}`;
return NextResponse.rewrite(url);
}
}
// ── Pass-through: add Vary, canonical Link, and markdown alternate headers ─
const res = NextResponse.next();
res.headers.set("Vary", "Accept");
// Canonical: always the clean pathname with no query params. For the
// markdown mirrors (/docs-markdown, /blog-markdown), canonicalize to the
// corresponding HTML page so crawlers don't index the raw markdown as a
// duplicate.
// For /docs-markdown the canonical must name where the doc actually lives:
// the route serves moved docs in place (see resolveDocsPath), so canonicalising
// to the requested slug would point crawlers at a URL that only 308s.
const docsMarkdownSlug = pathname.match(/^\/docs-markdown(?:\/(.*))?$/);
const canonicalPathname = docsMarkdownSlug
? `/docs${
docsMarkdownSlug[1] ? `/${resolveDocsPath(docsMarkdownSlug[1])}` : ""
}`
: pathname.replace(/^\/blog-markdown(\/|$)/, "/blog$1");
res.headers.set(
"Link",
`<${SITE_ORIGIN}${canonicalPathname}>; rel="canonical"`
);
// Markdown alternate link for blog/docs so agents can discover raw content.
if (/^\/blog\/[^/]+$/.test(pathname)) {
const slug = pathname.replace(/^\/blog\//, "");
res.headers.append(
"Link",
`<${SITE_ORIGIN}/blog/${slug}.md>; rel="alternate"; type="text/markdown"`
);
} else if (pathname.startsWith("/docs/") || pathname === "/docs") {
const docPath = pathname.replace(/^\/docs\/?/, "");
const mdPath = docPath ? `/docs/${docPath}.md` : `/docs.md`;
res.headers.append(
"Link",
`<${SITE_ORIGIN}${mdPath}>; rel="alternate"; type="text/markdown"`
);
}
return res;
}
export const config = {
matcher: [
/*
* Match all routes except:
* - _next/static (static assets)
* - _next/image (image optimisation)
* - favicon.* (favicon files)
* - public files with common static extensions
* - api routes (handled separately)
*/
"/((?!_next/static|_next/image|favicon|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|css|js|map)$).*)",
],
};