seo-technical
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTechnical SEO Implementation (Next.js 2025)
Next.js 2025 技术SEO实现方案
Skill Files
技能相关文件
This skill includes multiple reference files:
- SKILL.md (this file): Core technical SEO implementation guide
- nextjs-implementation.md: Next.js-specific code templates and patterns
- checklist.md: Pre-launch technical SEO checklist
- structured-data.md: JSON-LD schema markup templates
本技能包含多个参考文件:
- SKILL.md(本文档):核心技术SEO实施指南
- nextjs-implementation.md:Next.js专属代码模板与模式
- checklist.md:上线前技术SEO检查清单
- structured-data.md:JSON-LD schema标记模板
What This Skill Covers
本技能涵盖内容
- Sitemaps → for dynamic sitemap generation
app/sitemap.ts - Robots.txt → for crawler directives
app/robots.ts - Meta Tags → OpenGraph, Twitter Cards, keywords, descriptions
- Structured Data → JSON-LD for rich snippets
- Canonical URLs → Prevent duplicate content issues
- Performance SEO → Core Web Vitals considerations
- 站点地图 → 基于生成动态站点地图
app/sitemap.ts - Robots.txt → 基于配置爬虫指令
app/robots.ts - 元标签 → OpenGraph、Twitter卡片、关键词、描述信息
- 结构化数据 → 用于丰富搜索结果的JSON-LD
- 规范URL → 避免重复内容问题
- 性能SEO → Core Web Vitals相关考量
Part 1: Sitemap Implementation
第一部分:站点地图实现
Next.js App Router Sitemap (app/sitemap.ts)
Next.js App Router 站点地图(app/sitemap.ts)
Next.js automatically serves when you create :
/sitemap.xmlapp/sitemap.tstypescript
import type { MetadataRoute } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function sitemap(): MetadataRoute.Sitemap {
const currentDate = new Date().toISOString();
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: BASE_URL,
lastModified: currentDate,
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${BASE_URL}/pricing`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/about`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${BASE_URL}/privacy`,
lastModified: currentDate,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${BASE_URL}/terms`,
lastModified: currentDate,
changeFrequency: "yearly",
priority: 0.3,
},
];
return staticPages;
}当你创建文件后,Next.js会自动提供访问路径:
app/sitemap.ts/sitemap.xmltypescript
import type { MetadataRoute } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function sitemap(): MetadataRoute.Sitemap {
const currentDate = new Date().toISOString();
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: BASE_URL,
lastModified: currentDate,
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${BASE_URL}/pricing`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/about`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${BASE_URL}/privacy`,
lastModified: currentDate,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${BASE_URL}/terms`,
lastModified: currentDate,
changeFrequency: "yearly",
priority: 0.3,
},
];
return staticPages;
}Dynamic Sitemap with Database Content
基于数据库内容的动态站点地图
typescript
import type { MetadataRoute } from "next";
import { db } from "@/lib/db";
import { blogPosts, products } from "@/lib/db/schema";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Fetch dynamic content
const posts = await db.select().from(blogPosts).where(eq(blogPosts.published, true));
const allProducts = await db.select().from(products);
const staticPages: MetadataRoute.Sitemap = [
{ url: BASE_URL, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
];
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
lastModified: post.updatedAt || post.createdAt,
changeFrequency: "weekly" as const,
priority: 0.7,
}));
const productPages: MetadataRoute.Sitemap = allProducts.map((product) => ({
url: `${BASE_URL}/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: "daily" as const,
priority: 0.8,
}));
return [...staticPages, ...blogPages, ...productPages];
}typescript
import type { MetadataRoute } from "next";
import { db } from "@/lib/db";
import { blogPosts, products } from "@/lib/db/schema";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Fetch dynamic content
const posts = await db.select().from(blogPosts).where(eq(blogPosts.published, true));
const allProducts = await db.select().from(products);
const staticPages: MetadataRoute.Sitemap = [
{ url: BASE_URL, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
];
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
lastModified: post.updatedAt || post.createdAt,
changeFrequency: "weekly" as const,
priority: 0.7,
}));
const productPages: MetadataRoute.Sitemap = allProducts.map((product) => ({
url: `${BASE_URL}/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: "daily" as const,
priority: 0.8,
}));
return [...staticPages, ...blogPages, ...productPages];
}Large Sitemaps (50,000+ URLs)
大型站点地图(50,000+条URL)
Use for sitemap index:
generateSitemaps()typescript
import type { MetadataRoute } from "next";
const URLS_PER_SITEMAP = 50000;
export async function generateSitemaps() {
const totalProducts = await getProductCount();
const sitemapCount = Math.ceil(totalProducts / URLS_PER_SITEMAP);
return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const start = id * URLS_PER_SITEMAP;
const products = await getProducts({ start, limit: URLS_PER_SITEMAP });
return products.map((product) => ({
url: `${BASE_URL}/products/${product.slug}`,
lastModified: product.updatedAt,
}));
}使用生成站点地图索引:
generateSitemaps()typescript
import type { MetadataRoute } from "next";
const URLS_PER_SITEMAP = 50000;
export async function generateSitemaps() {
const totalProducts = await getProductCount();
const sitemapCount = Math.ceil(totalProducts / URLS_PER_SITEMAP);
return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const start = id * URLS_PER_SITEMAP;
const products = await getProducts({ start, limit: URLS_PER_SITEMAP });
return products.map((product) => ({
url: `${BASE_URL}/products/${product.slug}`,
lastModified: product.updatedAt,
}));
}Sitemap Best Practices
站点地图最佳实践
| Practice | Why |
|---|---|
Keep | Google uses it when consistently accurate |
| Only include canonical URLs | Duplicates waste crawl budget |
| Priority: 1.0 homepage, 0.8-0.9 key pages, 0.6-0.7 others | Guides crawler importance |
| Include for other search engines |
| Max 50,000 URLs per sitemap | Use sitemap index for more |
| 实践方案 | 原因 |
|---|---|
确保 | 当数据持续准确时,Google会参考该字段 |
| 仅包含规范URL | 重复URL会浪费爬虫预算 |
| 优先级设置:首页1.0,关键页面0.8-0.9,其他页面0.6-0.7 | 引导爬虫识别页面重要性 |
| 可包含以适配其他搜索引擎 |
| 每个站点地图最多包含50,000条URL | 超过时使用站点地图索引 |
Part 2: Robots.txt Implementation
第二部分:Robots.txt实现方案
Next.js App Router Robots (app/robots.ts)
Next.js App Router Robots配置(app/robots.ts)
typescript
import type { MetadataRoute } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function robots(): MetadataRoute.Robots {
const isProduction = process.env.NODE_ENV === "production";
// Block everything in non-production
if (!isProduction) {
return {
rules: { userAgent: "*", disallow: "/" },
};
}
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: [
"/api/",
"/dashboard/",
"/admin/",
"/private/",
"/_next/",
"/sign-in/",
"/sign-up/",
],
},
// Block AI training bots (optional)
{ userAgent: "GPTBot", disallow: "/" },
{ userAgent: "ChatGPT-User", disallow: "/" },
{ userAgent: "CCBot", disallow: "/" },
{ userAgent: "anthropic-ai", disallow: "/" },
{ userAgent: "Google-Extended", disallow: "/" },
],
sitemap: `${BASE_URL}/sitemap.xml`,
};
}typescript
import type { MetadataRoute } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function robots(): MetadataRoute.Robots {
const isProduction = process.env.NODE_ENV === "production";
// Block everything in non-production
if (!isProduction) {
return {
rules: { userAgent: "*", disallow: "/" },
};
}
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: [
"/api/",
"/dashboard/",
"/admin/",
"/private/",
"/_next/",
"/sign-in/",
"/sign-up/",
],
},
// Block AI training bots (optional)
{ userAgent: "GPTBot", disallow: "/" },
{ userAgent: "ChatGPT-User", disallow: "/" },
{ userAgent: "CCBot", disallow: "/" },
{ userAgent: "anthropic-ai", disallow: "/" },
{ userAgent: "Google-Extended", disallow: "/" },
],
sitemap: `${BASE_URL}/sitemap.xml`,
};
}Robots.txt Rules
Robots.txt规则说明
| Directive | Usage |
|---|---|
| Applies to all crawlers |
| Allow crawling of path |
| Block crawling of path |
| Advertise sitemap location |
| Slow down crawling (not respected by Google) |
| 指令 | 用途 |
|---|---|
| 适用于所有爬虫 |
| 允许爬虫访问该路径 |
| 禁止爬虫访问该路径 |
| 指定站点地图的位置 |
| 降低爬虫访问频率(Google不遵循该指令) |
Common AI Bots to Block/Allow
常见AI爬虫的屏蔽/允许配置
typescript
// Block AI training (keeps content out of training data)
{ userAgent: "GPTBot", disallow: "/" }, // OpenAI
{ userAgent: "ChatGPT-User", disallow: "/" }, // ChatGPT browsing
{ userAgent: "CCBot", disallow: "/" }, // Common Crawl
{ userAgent: "anthropic-ai", disallow: "/" }, // Anthropic
{ userAgent: "Google-Extended", disallow: "/" }, // Google AI training
{ userAgent: "Bytespider", disallow: "/" }, // ByteDance
// Allow AI search (keeps content in AI search results)
// Comment out the above to allow AI indexingtypescript
// Block AI training (keeps content out of training data)
{ userAgent: "GPTBot", disallow: "/" }, // OpenAI
{ userAgent: "ChatGPT-User", disallow: "/" }, // ChatGPT browsing
{ userAgent: "CCBot", disallow: "/" }, // Common Crawl
{ userAgent: "anthropic-ai", disallow: "/" }, // Anthropic
{ userAgent: "Google-Extended", disallow: "/" }, // Google AI training
{ userAgent: "Bytespider", disallow: "/" }, // ByteDance
// Allow AI search (keeps content in AI search results)
// Comment out the above to allow AI indexingWhat NOT to Block
不应屏蔽的内容
- Don't block
/sitemap.xml - Don't block CSS/JS files ()
/_next/static/ - Don't block images you want indexed
- Don't block your homepage
- 不要屏蔽
/sitemap.xml - 不要屏蔽CSS/JS文件()
/_next/static/ - 不要屏蔽你希望被索引的图片
- 不要屏蔽首页
Part 3: Metadata Implementation
第三部分:元数据实现方案
Root Layout Metadata (app/layout.tsx)
根布局元数据(app/layout.tsx)
typescript
import type { Metadata, Viewport } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#6366f1",
};
export const metadata: Metadata = {
metadataBase: new URL(BASE_URL),
// Title template for child pages
title: {
default: "Brand Name — Tagline",
template: "%s | Brand Name",
},
// Description (150-160 chars ideal)
description: "Your compelling meta description that includes primary keywords and encourages clicks.",
// Keywords (less important now, but include)
keywords: ["primary keyword", "secondary keyword", "brand name"],
// Author info
authors: [{ name: "Brand Name" }],
creator: "Brand Name",
publisher: "Brand Name",
// Robots directives
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
// OpenGraph (Facebook, LinkedIn, etc.)
openGraph: {
type: "website",
locale: "en_US",
url: BASE_URL,
siteName: "Brand Name",
title: "Brand Name — Tagline",
description: "Your compelling description for social sharing.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Brand Name - Description",
},
],
},
// Twitter Card
twitter: {
card: "summary_large_image",
title: "Brand Name — Tagline",
description: "Your compelling description for Twitter.",
images: ["/og-image.png"],
creator: "@twitterhandle",
site: "@twitterhandle",
},
// Canonical URL
alternates: {
canonical: BASE_URL,
},
// App categorization
category: "Technology",
// Verification codes
verification: {
google: "google-site-verification-code",
// yandex: "yandex-verification-code",
// bing: "bing-verification-code",
},
};typescript
import type { Metadata, Viewport } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#6366f1",
};
export const metadata: Metadata = {
metadataBase: new URL(BASE_URL),
// Title template for child pages
title: {
default: "Brand Name — Tagline",
template: "%s | Brand Name",
},
// Description (150-160 chars ideal)
description: "Your compelling meta description that includes primary keywords and encourages clicks.",
// Keywords (less important now, but include)
keywords: ["primary keyword", "secondary keyword", "brand name"],
// Author info
authors: [{ name: "Brand Name" }],
creator: "Brand Name",
publisher: "Brand Name",
// Robots directives
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
// OpenGraph (Facebook, LinkedIn, etc.)
openGraph: {
type: "website",
locale: "en_US",
url: BASE_URL,
siteName: "Brand Name",
title: "Brand Name — Tagline",
description: "Your compelling description for social sharing.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Brand Name - Description",
},
],
},
// Twitter Card
twitter: {
card: "summary_large_image",
title: "Brand Name — Tagline",
description: "Your compelling description for Twitter.",
images: ["/og-image.png"],
creator: "@twitterhandle",
site: "@twitterhandle",
},
// Canonical URL
alternates: {
canonical: BASE_URL,
},
// App categorization
category: "Technology",
// Verification codes
verification: {
google: "google-site-verification-code",
// yandex: "yandex-verification-code",
// bing: "bing-verification-code",
},
};Page-Level Metadata
页面级元数据
typescript
// app/pricing/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Pricing", // Becomes "Pricing | Brand Name" via template
description: "Simple, transparent pricing. Start free, upgrade when you need more.",
openGraph: {
title: "Pricing | Brand Name",
description: "Simple, transparent pricing. Start free, upgrade when you need more.",
},
};
export default function PricingPage() {
// ...
}typescript
// app/pricing/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Pricing", // Becomes "Pricing | Brand Name" via template
description: "Simple, transparent pricing. Start free, upgrade when you need more.",
openGraph: {
title: "Pricing | Brand Name",
description: "Simple, transparent pricing. Start free, upgrade when you need more.",
},
};
export default function PricingPage() {
// ...
}Dynamic Metadata (generateMetadata)
动态元数据(generateMetadata)
typescript
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
return { title: "Post Not Found" };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `${BASE_URL}/blog/${slug}`,
},
};
}typescript
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
return { title: "Post Not Found" };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `${BASE_URL}/blog/${slug}`,
},
};
}Part 4: Authentication Middleware Integration
第四部分:认证中间件集成
When using auth (Clerk, NextAuth, etc.), add SEO routes to public matchers:
当使用认证服务(Clerk、NextAuth等)时,需将SEO相关路由添加到公开匹配列表:
Clerk (proxy.ts or middleware.ts)
Clerk(proxy.ts或middleware.ts)
typescript
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/pricing",
"/about",
"/blog(.*)",
"/terms",
"/privacy",
// SEO files - IMPORTANT!
"/robots.txt",
"/sitemap.xml",
"/sitemap(.*).xml",
// Icons
"/icon(.*)",
"/apple-icon(.*)",
"/favicon.ico",
]);
export const proxy = clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};typescript
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/pricing",
"/about",
"/blog(.*)",
"/terms",
"/privacy",
// SEO files - IMPORTANT!
"/robots.txt",
"/sitemap.xml",
"/sitemap(.*).xml",
// Icons
"/icon(.*)",
"/apple-icon(.*)",
"/favicon.ico",
]);
export const proxy = clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};NextAuth
NextAuth
typescript
export { auth as middleware } from "@/auth";
export const config = {
matcher: [
// Exclude SEO files from auth
"/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|sitemap.*\\.xml).*)",
],
};typescript
export { auth as middleware } from "@/auth";
export const config = {
matcher: [
// Exclude SEO files from auth
"/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|sitemap.*\\.xml).*)",
],
};Part 5: Environment Variables
第五部分:环境变量
Required environment variables for SEO:
bash
undefinedSEO所需的环境变量:
bash
undefined.env.local (development)
.env.local (development)
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_SITE_URL=http://localhost:3000
.env.production (production)
.env.production (production)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
---NEXT_PUBLIC_SITE_URL=https://yourdomain.com
---Quick Reference: File Locations
快速参考:文件位置
| File | Location | Purpose |
|---|---|---|
| Sitemap | | Generates |
| Robots | | Generates |
| Root Metadata | | Default meta tags |
| Page Metadata | | Page-specific meta |
| OG Image | | Social sharing image (1200x630) |
| Favicon | | Browser tab icon |
| Apple Icon | | iOS icon |
| 文件 | 路径 | 用途 |
|---|---|---|
| 站点地图 | | 生成 |
| Robots配置 | | 生成 |
| 根元数据 | | 默认元标签配置 |
| 页面元数据 | | 页面专属元标签 |
| OpenGraph图片 | | 社交分享图片(1200x630) |
| 网站图标 | | 浏览器标签图标 |
| Apple图标 | | iOS设备图标 |
Implementation Checklist
实施检查清单
Before implementing, verify:
- environment variable is set
NEXT_PUBLIC_SITE_URL - Auth middleware allows and
/robots.txt/sitemap.xml - OG image exists at (1200x630px)
public/og-image.png - All public pages have unique titles and descriptions
- Canonical URLs point to preferred versions
After implementing, verify:
- Visit - should show rules
/robots.txt - Visit - should show URLs
/sitemap.xml - Test with Google Rich Results Test
- Test with Facebook Sharing Debugger
- Submit sitemap to Google Search Console
实施前需确认:
- 已设置环境变量
NEXT_PUBLIC_SITE_URL - 认证中间件已允许和
/robots.txt访问/sitemap.xml - 已存在(尺寸1200x630px)
public/og-image.png - 所有公开页面均有唯一的标题和描述
- 规范URL指向页面的首选版本
实施后需验证:
- 访问- 应显示配置规则
/robots.txt - 访问- 应显示页面URL列表
/sitemap.xml - 使用Google富媒体结果测试工具进行测试
- 使用Facebook分享调试工具进行测试
- 将站点地图提交至Google搜索控制台