关于 seo-technical
面向 Next.js App Router 项目的技术 SEO 实施指南,适用于开发者。涵盖站点地图、爬虫规则、元数据、结构化数据与上线检查。
主要功能
- 01生成动态站点地图
- 02配置 robots.txt 爬虫规则
- 03设置页面元数据与社交卡片
- 04添加 JSON-LD 结构化数据
- 05管理规范网址与 SEO 检查清单
适用场景
- 01为动态页面生成站点地图
- 02按环境配置爬虫访问规则
- 03完善页面搜索与社交分享信息
- 04上线前检查技术 SEO 配置
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ andrehfp/ tinyplate --skill seo-technical安装后在 Claude Code 中输入/seo-technical确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/checklist.mdnextjs-implementation.mdstructured-data.md详细介绍
技术 SEO 实现(Next.js 2025)
技能文件
此技能包含多个参考文件:
- SKILL.md(此文件):技术 SEO 核心实现指南
- nextjs-implementation.md:Next.js 专用代码模板和模式
- checklist.md:上线前技术 SEO 检查清单
- structured-data.md:JSON-LD 结构化数据标记模板
此技能涵盖的内容
- 站点地图 →
app/sitemap.ts,用于动态生成站点地图 - Robots.txt →
app/robots.ts,用于爬虫指令 - 元标签 → OpenGraph、Twitter Cards、关键词、描述
- 结构化数据 → 用于丰富摘要的 JSON-LD
- 规范 URL → 防止重复内容问题
- 性能 SEO → Core Web Vitals 注意事项
第 1 部分:站点地图实现
Next.js App Router 站点地图(app/sitemap.ts)
创建 app/sitemap.ts 后,Next.js 会自动提供 /sitemap.xml:
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;
}
使用数据库内容生成动态站点地图
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];
}
大型站点地图(50,000+ 个 URL)
使用 generateSitemaps() 生成站点地图索引:
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,
}));
}
站点地图最佳实践
| 实践 | 原因 |
|---|---|
确保 lastModified 准确 | 持续准确时,Google 会使用该值 |
| 仅包含规范 URL | 重复内容会浪费抓取预算 |
| 优先级:主页为 1.0,关键页面为 0.8-0.9,其他页面为 0.6-0.7 | 指示爬虫页面的重要性 |
Google 会忽略 changeFrequency | 可为其他搜索引擎提供此项 |
| 每个站点地图最多包含 50,000 个 URL | 超出时使用站点地图索引 |
第 2 部分:Robots.txt 实现
Next.js App Router Robots(app/robots.ts)
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 规则
| 指令 | 用途 |
|---|---|
User-agent: * | 适用于所有爬虫 |
Allow: / | 允许抓取此路径 |
Disallow: /private/ | 禁止抓取此路径 |
Sitemap: | 指明站点地图位置 |
Crawl-delay: | 放慢抓取速度(Google 不遵守此项) |
可禁止/允许的常见 AI 机器人
// 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 indexing
不要禁止抓取的内容
- 不要禁止抓取
/sitemap.xml - 不要禁止抓取 CSS/JS 文件(
/_next/static/) - 不要禁止抓取希望被编入索引的图片
- 不要禁止抓取主页
第 3 部分:元数据实现
根布局元数据(app/layout.tsx)
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",
},
};
页面级元数据
// 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() {
// ...
}
动态元数据(generateMetadata)
// 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}`,
},
};
}
第 4 部分:身份验证中间件集成
使用身份验证(Clerk、NextAuth 等)时,将 SEO 路由添加到公开匹配器:
Clerk(proxy.ts 或 middleware.ts)
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
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).*)",
],
};
第 5 部分:环境变量
SEO 所需的环境变量:
# .env.local (development)
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# .env.production (production)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
快速参考:文件位置
| 文件 | 位置 | 用途 |
|---|---|---|
| 站点地图 | app/sitemap.ts | 生成 /sitemap.xml |
| Robots | app/robots.ts | 生成 /robots.txt |
| 根元数据 | app/layout.tsx | 默认元标签 |
| 页面元数据 | app/[route]/page.tsx | 页面专属元数据 |
| OG 图片 | public/og-image.png | 社交分享图片(1200x630) |
| 网站图标 | app/icon.tsx 或 public/favicon.ico | 浏览器标签页图标 |
| Apple 图标 | app/apple-icon.tsx 或 public/apple-icon.png | iOS 图标 |
实现检查清单
实现前,请确认:
- 已设置
NEXT_PUBLIC_SITE_URL环境变量 - 身份验证中间件允许访问
/robots.txt和/sitemap.xml -
public/og-image.png中存在 OG 图片(1200x630px) - 所有公开页面都有唯一的标题和描述
- 规范 URL 指向首选版本
实现后,请确认:
- 访问
/robots.txt——应显示规则 - 访问
/sitemap.xml——应显示 URL - 使用 Google Rich Results Test 进行测试
- 使用 Facebook Sharing Debugger 进行测试
- 将站点地图提交至 Google Search Console
评论
支持 Markdown