seo-technical
Technical SEO Implementation (Next.js 2025)
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
What This Skill Covers
- Sitemaps →
app/sitemap.tsfor dynamic sitemap generation - Robots.txt →
app/robots.tsfor crawler directives - 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
Part 1: Sitemap Implementation
Next.js App Router Sitemap (app/sitemap.ts)
Next.js automatically serves /sitemap.xml when you create app/sitemap.ts:
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
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)
Use generateSitemaps() for sitemap index:
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 lastModified accurate |
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 |
changeFrequency is ignored by Google |
Include for other search engines |
| Max 50,000 URLs per sitemap | Use sitemap index for more |
Part 2: Robots.txt Implementation
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 Rules
| Directive | Usage |
|---|---|
User-agent: * |
Applies to all crawlers |
Allow: / |
Allow crawling of path |
Disallow: /private/ |
Block crawling of path |
Sitemap: |
Advertise sitemap location |
Crawl-delay: |
Slow down crawling (not respected by Google) |
Common AI Bots to Block/Allow
// 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
What 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
Part 3: Metadata Implementation
Root Layout Metadata (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",
},
};
Page-Level Metadata
// 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)
// 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 (proxy.ts or 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).*)",
],
};
Part 5: Environment Variables
Required environment variables for SEO:
# .env.local (development)
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# .env.production (production)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
Quick Reference: File Locations
| File | Location | Purpose |
|---|---|---|
| Sitemap | app/sitemap.ts |
Generates /sitemap.xml |
| Robots | app/robots.ts |
Generates /robots.txt |
| Root Metadata | app/layout.tsx |
Default meta tags |
| Page Metadata | app/[route]/page.tsx |
Page-specific meta |
| OG Image | public/og-image.png |
Social sharing image (1200x630) |
| Favicon | app/icon.tsx or public/favicon.ico |
Browser tab icon |
| Apple Icon | app/apple-icon.tsx or public/apple-icon.png |
iOS icon |
Implementation Checklist
Before implementing, verify:
-
NEXT_PUBLIC_SITE_URLenvironment variable is set - Auth middleware allows
/robots.txtand/sitemap.xml - OG image exists at
public/og-image.png(1200x630px) - All public pages have unique titles and descriptions
- Canonical URLs point to preferred versions
After implementing, verify:
- Visit
/robots.txt- should show rules - Visit
/sitemap.xml- should show URLs - Test with Google Rich Results Test
- Test with Facebook Sharing Debugger
- Submit sitemap to Google Search Console
More from andrehfp/tinyplate
abacatepay
Help with AbacatePay payment integration in Next.js projects. Use when implementing PIX payments, managing subscriptions, handling webhooks, or debugging payment flows. Covers SDK usage, webhook verification, and billing management for Brazilian SaaS applications.
42marketing-copy
Generate compelling marketing copy using the Elevated Direct Response framework. Use this skill when creating landing pages, headlines, email campaigns, ad copy, CTAs, value propositions, or any persuasive marketing content. Applies contrarian educator tone and direct response principles.
37posthog
Implement PostHog analytics, feature flags, and session replay for Next.js apps. Use this skill for event tracking, user identification, A/B testing, experiments, and session recording setup. Also handles analytics reporting (funnel analysis, retention, SEO) with Google Search Console integration.
27ux-design
Design intuitive user experiences following Jobs-era Apple principles. Use this skill when designing onboarding flows, empty states, dashboards, user journeys, CTAs, forms, or any UI that needs to anticipate user needs and reduce friction. Applies progressive disclosure, anticipatory design, and conversion optimization principles.
18stripe
Help with Stripe payment integration in Next.js projects. Use when implementing checkout flows, subscriptions, webhooks, customer portal, or debugging payment issues. Covers Stripe SDK usage, webhook verification, and subscription management.
13favicon
Generate favicons and app icons for Next.js projects. Creates all required sizes, formats, and configures metadata.
12