大多数”Next.js SEO 不行”的帖子其实是”我忘了 export metadata”或”应该静态的 route 让它变动态了”。框架没问题,是默认值会咬人。本文是告诉 Google 索引你站之前要检查的清单,每一步都有 App Router 的具体代码。
问题背景
2026 年搜索引擎能渲染 JS 但不喜欢渲染。初始响应里就带 title、description、canonical、结构化数据和正文的静态 HTML,渲染、排序、缓存都比依赖 hydration 的 payload 快。Next.js 能产出这种,前提是你配对了。
判断标准
- GSC 提示一片”已索引但被 robots.txt 屏蔽”或”已发现 – 尚未编入索引”。
- 查看源代码发现正文不在,或者包在
<div id="__next">壳里。 - Lighthouse SEO 分数不到 100,提示”页面没有 meta description”或”链接无法被爬取”。
- 页面有重复
<title>或没 canonical 标签。 next build输出里文章 route 是 Dynamic(λ)而不是 Static(○)。
快速结论
部署后的文章 view-source: 里能看到完整正文和 metadata,就完成 90%。再加 sitemap、robots、JSON-LD 就齐了。
开始前准备
- 新站优先 App Router(不是 Pages Router)——下面示例都基于 App Router。
- 部署目标已知(Vercel / Cloudflare Pages / Netlify)。
- 测试用的样本文章 slug 选好。
实操步骤
- 确认 route 是静态。 跑
next build看输出:
Route (app) Size First Load JS
┌ ○ / 140 B 91.2 kB
├ ○ /articles/[slug] 1.34 kB 94.6 kB ← ○ 静态
├ λ /api/foo 0 B 0 B ← λ 动态(API 没问题)
└ ƒ /articles/dynamic-route 3.21 kB 97.1 kB ← ƒ 动态(内容页是红灯)
公开文章页是 ƒ Dynamic 就是红灯——通常是漏了 generateStaticParams。
- 每个页面都 export
metadata或generateMetadata。app/articles/[slug]/page.tsx:
import type { Metadata } from 'next';
import { getArticle, getAllSlugs } from '@/lib/content';
export async function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const article = await getArticle(params.slug);
return {
title: article.title,
description: article.description,
alternates: {
canonical: `https://yourdomain.com/articles/${article.urlSlug}/`,
languages: {
'en-US': `https://yourdomain.com/en/articles/${article.urlSlug}/`,
'zh-CN': `https://yourdomain.com/zh/articles/${article.urlSlug}/`,
'x-default': `https://yourdomain.com/en/articles/${article.urlSlug}/`,
},
},
openGraph: {
title: article.title,
description: article.description,
url: `https://yourdomain.com/articles/${article.urlSlug}/`,
type: 'article',
publishedTime: article.publishedAt.toISOString(),
},
twitter: { card: 'summary_large_image', title: article.title },
};
}
- 加
app/sitemap.ts列出所有已发布文章:
import { MetadataRoute } from 'next';
import { getAllArticles } from '@/lib/content';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const articles = await getAllArticles();
return [
{ url: 'https://yourdomain.com/', changeFrequency: 'weekly', priority: 1 },
...articles.flatMap((a) => [
{
url: `https://yourdomain.com/en/articles/${a.urlSlug}/`,
lastModified: a.updatedAt ?? a.publishedAt,
changeFrequency: 'monthly' as const,
priority: 0.6,
alternates: {
languages: {
'en-US': `https://yourdomain.com/en/articles/${a.urlSlug}/`,
'zh-CN': `https://yourdomain.com/zh/articles/${a.urlSlug}/`,
},
},
},
]),
];
}
- 加
app/robots.ts引用 sitemap:
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: '*', allow: '/' }],
sitemap: 'https://yourdomain.com/sitemap.xml',
host: 'https://yourdomain.com',
};
}
- 服务端发 JSON-LD 结构化数据:
export default async function ArticlePage({ params }) {
const a = await getArticle(params.slug);
const ld = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: a.title,
description: a.description,
datePublished: a.publishedAt,
dateModified: a.updatedAt ?? a.publishedAt,
author: { '@type': 'Organization', name: a.author },
mainEntityOfPage: `https://yourdomain.com/articles/${a.urlSlug}/`,
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
/>
<article>{/* … */}</article>
</>
);
}
next.config.js一次定下 trailing slash:
/** @type {import('next').NextConfig} */
module.exports = {
trailingSlash: true, // 选一个,永远别改
poweredByHeader: false,
async redirects() {
return [{ source: '/blog/:slug', destination: '/articles/:slug/', permanent: true }];
},
};
- 部署后看源代码。 这些要全部在原始 HTML 里,不能由客户端 JS 后加:
curl -sL https://yourdomain.com/articles/test-slug/ | grep -E '<title>|description|canonical|hreflang|application/ld\+json' | head
如果正文文本也能看到(curl -sL ... | grep "正文里某句话" 能命中),就是爬虫友好的 HTML。
- Search Console 提交 sitemap,并把 Lighthouse SEO 调到 100。
执行检查清单
next build输出文章 route 是○ Static。generateMetadata每个动态页都 export canonical、openGraph、alternates。app/sitemap.ts和app/robots.ts都有,生产 URL 能访问。- JSON-LD 服务端发,不靠客户端 JS 后加。
- 部署后文章 view-source 包含 title、description、canonical、JSON-LD、正文。
上线后验证
- 至少 3 篇样本文章 Lighthouse SEO = 100。
- Search Console URL Inspection 抽查样本 1-2 周内显示 “URL is on Google”。
curl -sL验证初始 HTML 含 hreflang 对。
容易踩的坑
- 在 client component 里设 metadata——不会生效。metadata 必须从 server component(page 或 layout)export。
- 在 layout 里调
redirect()或notFound(),结果回了 200 但内容空。 [slug]route 忘了generateStaticParams——动态段不会预渲染。next/image没给 LCP 图加priority,LCP 跌到掉排名。- sitemap、canonical、实际 URL 的结尾斜杠不一致——Google 把
/foo和/foo/当不同 URL。 - robots.txt 屏蔽了
_next/static——CSS 和 JS 也被屏,渲染和排名都跌。 - 服务端渲染内容包在没有 fallback HTML 的
Suspense里——Google 看到的只是 loading。
FAQ
- Google 真的会渲染 JS 吗?: 会但有延迟、有选择。静态 HTML 在 crawl budget、索引速度、排名上仍然占优,新站尤其明显。
- hreflang 要单独配吗?: 不用——metadata 里
alternates.languages自动生成对应<link rel="alternate" hreflang>。 - ISR 还是纯静态?: 内容真不变用纯静态。想不重新部署也能推改动用 ISR。SEO 上等价。
next/image对 SEO 友好吗?: 配对了就友好——自动响应式srcset、懒加载、正确尺寸,LCP 和 CLS 都受益。- tag 页和分页要 noindex 吗?: 标签页内容不实之前一般要;分页可以 index 只要每页列表不同。
相关阅读
- Next.js sitemap / robots 基础
- Next.js 图片优化基础
- 2026 年新站如何提交给 Google
- Next.js 不适合做内容站的情况
- Next.js App Router 概念
标签: #独立开发 #Next.js #SEO #Technical SEO #Canonical #Core Web Vitals