| name | nextjs-seo-optimization |
| description | Implement SEO best practices in Next.js applications. Includes metadata, Open Graph tags, canonical URLs, sitemap generation, schema markup, and performance optimization for search ranking. |
Next.js SEO Optimization Skill
When to Use
Use this skill when:
- Building new pages requiring SEO
- Optimizing existing pages for search ranking
- Implementing dynamic metadata (products, articles)
- Adding Open Graph tags for social sharing
- Generating sitemaps and robots.txt
- Implementing structured data (Schema.org)
- Improving Core Web Vitals for ranking
Core Concepts
1. Metadata (Next.js 13+)
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'OfertaChina - Melhores Cupons e Ofertas',
description: 'Encontre as melhores ofertas da AliExpress e plataformas chinesas. Cupons exclusivos, frete grátis e cashback.',
keywords: 'ofertas AliExpress, cupons, cashback, frete grátis',
metadataBase: new URL('https://ofertachina.com'),
openGraph: {
type: 'website',
locale: 'pt_BR',
url: 'https://ofertachina.com',
siteName: 'OfertaChina',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'OfertaChina - Melhores ofertas online'
}
]
},
twitter: {
card: 'summary_large_image',
title: 'OfertaChina',
description: 'Melhores cupons e ofertas da AliExpress',
images: ['/og-image.png']
},
robots: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
'max-video-preview': -1,
}
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pt-BR">
<head>
{/* Additional meta tags */}
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#FF6B35" />
{/* Preconnect to external domains */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body>{children}</body>
</html>
)
}
2. Dynamic Metadata (Products)
// app/products/[slug]/page.tsx
import type { Metadata } from 'next'
import { getProduct } from '@/lib/api'
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await getProduct(params.slug)
if (!product) {
return {
title: 'Produto não encontrado',
description: 'O produto solicitado não existe'
}
}
// Dynamic metadata based on product data
return {
title: `${product.title} | OfertaChina`,
description: product.description.substring(0, 160),
keywords: `${product.title}, cupom, oferta, ${product.category}`,
openGraph: {
type: 'product',
url: `https://ofertachina.com/products/${product.slug}`,
title: product.title,
description: product.description,
images: [
{
url: product.imageUrl,
width: 1200,
height: 630,
alt: product.title
}
]
},
twitter: {
card: 'summary_large_image',
title: product.title,
description: product.description,
images: [product.imageUrl]
}
}
}
export async function generateStaticParams() {
// Generate static pages for most popular products
const products = await getProduct('popular', { limit: 100 })
return products.map((product) => ({
slug: product.slug
}))
}
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.slug)
if (!product) {
return <div>Produto não encontrado</div>
}
return (
<article>
<h1>{product.title}</h1>
<img src={product.imageUrl} alt={product.title} />
<p>{product.description}</p>
</article>
)
}
3. Schema Markup (Structured Data)
// app/products/[slug]/page.tsx
import type { Product as SchemaProduct } from 'schema-dts'
import { JsonLd } from 'react-schemaorg'
export default function ProductPage({ product }) {
const schemaData: SchemaProduct = {
'@type': 'Product',
'@context': 'https://schema.org/',
name: product.title,
description: product.description,
image: product.imageUrl,
url: `https://ofertachina.com/products/${product.slug}`,
sku: product.sku,
brand: {
'@type': 'Brand',
name: product.brand
},
offers: {
'@type': 'Offer',
url: `https://ofertachina.com/products/${product.slug}`,
priceCurrency: 'BRL',
price: product.price.toString(),
priceValidUntil: product.expiresAt,
availability: product.inStock ? 'InStock' : 'OutOfStock',
seller: {
'@type': 'Organization',
name: 'OfertaChina'
}
},
aggregateRating: product.rating ? {
'@type': 'AggregateRating',
ratingValue: product.rating.score,
ratingCount: product.rating.count
} : undefined
}
return (
<>
<JsonLd<SchemaProduct> item={schemaData} />
<article>
<h1>{product.title}</h1>
{/* Product content */}
</article>
</>
)
}
4. Sitemap Generation
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllProducts } from '@/lib/api'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://ofertachina.com'
// Static routes
const staticRoutes: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1
},
{
url: `${baseUrl}/products`,
lastModified: new Date(),
changeFrequency: 'hourly',
priority: 0.9
},
{
url: `${baseUrl}/categories`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5
}
]
// Dynamic routes (products)
const products = await getAllProducts()
const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(product.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7
}))
return [...staticRoutes, ...productRoutes]
}
5. Robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin', '/api', '/*.json$'],
},
{
userAgent: 'GPTBot',
disallow: '/',
},
],
sitemap: 'https://ofertachina.com/sitemap.xml',
}
}
6. Canonical URLs
// components/CanonicalURL.tsx
interface CanonicalURLProps {
path: string
baseUrl?: string
}
export function CanonicalURL({ path, baseUrl = 'https://ofertachina.com' }: CanonicalURLProps) {
return (
<link
rel="canonical"
href={`${baseUrl}${path}`}
key="canonical"
/>
)
}
// Usage in page.tsx
export const metadata: Metadata = {
// ... other metadata
other: {
canonical: 'https://ofertachina.com/products/smartphone-xyz'
}
}
7. Image Optimization
// components/OptimizedImage.tsx
import Image from 'next/image'
interface OptimizedImageProps {
src: string
alt: string
title?: string
width?: number
height?: number
}
export function OptimizedImage({
src,
alt,
title,
width = 800,
height = 600
}: OptimizedImageProps) {
return (
<Image
src={src}
alt={alt}
title={title || alt}
width={width}
height={height}
quality={85}
priority={false}
placeholder="blur"
blurDataURL="data:image/webp;base64,..." // Placeholder
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}
8. Open Graph Dynamic Images
// app/products/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getProduct } from '@/lib/api'
export const runtime = 'nodejs'
export const alt = 'Product preview'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
interface Props {
params: { slug: string }
}
export default async function Image({ params }: Props) {
const product = await getProduct(params.slug)
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%',
height: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
padding: '40px',
}}
>
<div>
<h1>{product.title}</h1>
<p>💰 R$ {product.price}</p>
<p>⭐ {product.rating}/5</p>
</div>
</div>
),
{
...size,
},
)
}
9. SEO Checklist
// hooks/useSEOChecklist.ts
export function useSEOChecklist(page: string) {
const checks = {
homepage: [
'✅ Title tag (50-60 chars)',
'✅ Meta description (150-160 chars)',
'✅ H1 tag (only one)',
'✅ Open Graph image (1200x630)',
'✅ Mobile responsive',
'✅ Core Web Vitals score',
'✅ Schema markup (Organization)',
'✅ robots.txt configured',
'✅ sitemap.xml present',
'✅ Canonical URL set'
],
productPage: [
'✅ Product title in H1',
'✅ Product image optimized',
'✅ Price structured data',
'✅ Rating schema markup',
'✅ Dynamic meta description',
'✅ Open Graph tags dynamic',
'✅ Canonical URL set',
'✅ Related products linked',
'✅ Breadcrumb schema',
'✅ No duplicate content'
]
}
return checks[page] || []
}
10. Performance Optimization
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
},
{
protocol: 'https',
hostname: 'aliexpress.com',
},
],
minimumCacheSize: 50,
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// ISR configuration
onDemandEntries: {
maxInactiveAge: 60 * 10 * 1000, // 10 min
pagesBufferLength: 5,
},
// Headers for SEO
headers: async () => [
{
source: '/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
]
}
]
}
module.exports = nextConfig
SEO Best Practices
✅ Title: 50-60 characters, keyword at start
✅ Description: 150-160 characters, compelling CTA
✅ H1: One per page, keyword relevant
✅ Images: Optimized, descriptive alt text
✅ Links: Internal linking strategy, no orphaned pages
✅ Mobile: Fully responsive, Core Web Vitals ≥ 75
✅ Speed: Images optimized, code splitting
✅ Schema: Product, Organization, BreadcrumbList
✅ Canonicals: Set for paginated/filtered content
✅ Structured Data: Tested with Google Schema Validator
Testing Tools
Related Files
- seo-config.ts - SEO configuration templates
- metadata-generator.ts - Dynamic metadata helper
- schema-templates.ts - Schema markup templates
References
- Next.js SEO: https://nextjs.org/learn/seo/introduction-to-seo
- Google SEO Guide: https://developers.google.com/search/docs
- Schema.org: https://schema.org/
- Open Graph: https://ogp.me/