seo-technical
Implement technical SEO including schema markup, sitemaps, robots.txt, Core Web Vitals optimization, and metadata. Use when setting up SEO foundations or optimizing website performance for search engines.
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for implementing technical SEO specifically tailored for Next.js applications. It automates the creation of search-engine-friendly foundations by handling metadata, structured data (JSON-LD), and performance optimizations like Core Web Vitals, ensuring websites are fully optimized for crawling, indexing, and ranking.
Use Cases
- case 1: Setting up dynamic metadata and Open Graph tags for e-commerce or blog platforms to improve social media visibility and click-through rates.
- case 2: Implementing various JSON-LD schema types (Organization, Product, FAQ, Breadcrumbs) to enable rich snippets and enhanced search result displays.
- case 3: Optimizing Core Web Vitals (LCP, CLS, and INP) through advanced image handling and script loading strategies to meet Google's page experience requirements.
- case 4: Automating the generation of dynamic sitemaps and robots.txt files to ensure search engines efficiently discover and prioritize high-value content.
- case 5: Managing canonical URLs and internal linking structures to prevent duplicate content issues and strengthen site authority.
| name | seo-technical |
|---|---|
| description | Implement technical SEO including schema markup, sitemaps, robots.txt, Core Web Vitals optimization, and metadata. Use when setting up SEO foundations or optimizing website performance for search engines. |
Technical SEO
This skill covers the technical aspects of SEO implementation for Next.js websites.
When to Use This Skill
- Setting up metadata and Open Graph tags
- Implementing schema markup (JSON-LD)
- Creating sitemaps and robots.txt
- Optimizing Core Web Vitals
- Implementing canonical URLs
Core Implementation
1. Next.js Metadata API
Static Metadata (layout.tsx or page.tsx):
import type { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "Site Name",
template: "%s | Site Name"
},
description: "Site description for search results",
keywords: ["keyword1", "keyword2"],
authors: [{ name: "Author Name" }],
creator: "Creator Name",
metadataBase: new URL("https://example.com"),
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
locale: "en_US",
url: "https://example.com",
siteName: "Site Name",
title: "Site Name",
description: "Site description",
images: [
{
url: "/og-image.jpg",
width: 1200,
height: 630,
alt: "Site preview image"
}
]
},
twitter: {
card: "summary_large_image",
title: "Site Name",
description: "Site description",
images: ["/og-image.jpg"],
creator: "@twitterhandle"
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1
}
},
verification: {
google: "google-verification-code",
yandex: "yandex-verification-code"
}
}
Dynamic Metadata (for pages with dynamic content):
import type { Metadata } from "next"
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
authors: [post.author.name],
images: [
{
url: post.image,
width: 1200,
height: 630,
alt: post.title
}
]
}
}
}
2. Schema Markup (JSON-LD)
Organization Schema (layout.tsx):
export default function RootLayout({ children }) {
const organizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
name: "Company Name",
url: "https://example.com",
logo: "https://example.com/logo.png",
sameAs: [
"https://twitter.com/company",
"https://linkedin.com/company/company",
"https://facebook.com/company"
],
contactPoint: {
"@type": "ContactPoint",
telephone: "+1-800-555-1234",
contactType: "customer service"
}
}
return (
<html>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
</head>
<body>{children}</body>
</html>
)
}
Website Schema with Search:
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Site Name",
url: "https://example.com",
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: "https://example.com/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
Article Schema (blog posts):
const articleSchema = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.image,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: post.author.name,
url: post.author.url
},
publisher: {
"@type": "Organization",
name: "Site Name",
logo: {
"@type": "ImageObject",
url: "https://example.com/logo.png"
}
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://example.com/blog/${post.slug}`
}
}
Product Schema:
const productSchema = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
image: product.images,
description: product.description,
sku: product.sku,
brand: {
"@type": "Brand",
name: product.brand
},
offers: {
"@type": "Offer",
url: `https://example.com/products/${product.slug}`,
priceCurrency: "USD",
price: product.price,
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: "Store Name"
}
},
aggregateRating: product.reviews?.length ? {
"@type": "AggregateRating",
ratingValue: product.averageRating,
reviewCount: product.reviews.length
} : undefined
}
FAQ Schema:
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer
}
}))
}
Breadcrumb Schema:
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://example.com"
},
{
"@type": "ListItem",
position: 2,
name: "Blog",
item: "https://example.com/blog"
},
{
"@type": "ListItem",
position: 3,
name: post.title,
item: `https://example.com/blog/${post.slug}`
}
]
}
Local Business Schema:
const localBusinessSchema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: "Business Name",
image: "https://example.com/image.jpg",
"@id": "https://example.com",
url: "https://example.com",
telephone: "+1-800-555-1234",
address: {
"@type": "PostalAddress",
streetAddress: "123 Main St",
addressLocality: "City",
addressRegion: "State",
postalCode: "12345",
addressCountry: "US"
},
geo: {
"@type": "GeoCoordinates",
latitude: 40.7128,
longitude: -74.0060
},
openingHoursSpecification: [
{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
opens: "09:00",
closes: "17:00"
}
]
}
3. Sitemap
app/sitemap.ts:
import { MetadataRoute } from "next"
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://example.com"
// Static pages
const staticPages = [
"",
"/about",
"/services",
"/contact",
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: route === "" ? 1 : 0.8,
}))
// Dynamic pages (e.g., blog posts)
const posts = await getAllPosts()
const blogPages = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "weekly" as const,
priority: 0.6,
}))
return [...staticPages, ...blogPages]
}
4. Robots.txt
app/robots.ts:
import { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
const baseUrl = "https://example.com"
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: [
"/api/",
"/admin/",
"/private/",
"/_next/",
],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
}
}
5. Core Web Vitals Optimization
LCP (Largest Contentful Paint) - Target: < 2.5s
// Preload critical images
<link
rel="preload"
href="/hero-image.jpg"
as="image"
fetchPriority="high"
/>
// Priority loading for hero images
<Image
src="/hero.jpg"
alt="Hero"
priority
fetchPriority="high"
/>
// Preconnect to external origins
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
CLS (Cumulative Layout Shift) - Target: < 0.1
// Always set dimensions on images
<Image
src="/image.jpg"
alt="Description"
width={800}
height={600}
/>
// Or use aspect ratio
<div className="aspect-video relative">
<Image src="/image.jpg" alt="Description" fill />
</div>
// Reserve space for dynamic content
<div className="min-h-[400px]">
{/* Dynamic content */}
</div>
FID/INP (Input Delay) - Target: < 100ms
// Defer non-critical JavaScript
<Script src="/analytics.js" strategy="lazyOnload" />
// Use dynamic imports for heavy components
const HeavyComponent = dynamic(() => import("./HeavyComponent"), {
loading: () => <Skeleton />,
ssr: false
})
6. Image Optimization
import Image from "next/image"
// Responsive image with proper sizes
<Image
src="/image.jpg"
alt="Descriptive alt text for SEO"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
// next.config.js - enable WebP/AVIF
module.exports = {
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
7. Canonical URLs
// In metadata
export const metadata: Metadata = {
alternates: {
canonical: "https://example.com/page",
},
}
// For paginated content
export async function generateMetadata({ searchParams }) {
const page = searchParams.page || 1
return {
alternates: {
canonical: "https://example.com/blog", // Always point to page 1
},
}
}
8. Internal Linking
// Use Next.js Link for internal navigation
import Link from "next/link"
<Link href="/about">About Us</Link>
// Descriptive anchor text (good for SEO)
<Link href="/services/web-design">
Professional Web Design Services
</Link>
// Avoid generic text
// Bad: <Link href="/services">Click here</Link>
// Good: <Link href="/services">Explore our services</Link>
SEO Audit Checklist
- Every page has unique title and description
- All images have alt text
- Schema markup validates (use Google's Rich Results Test)
- Sitemap includes all important pages
- Robots.txt doesn't block important content
- Canonical URLs prevent duplicate content
- Core Web Vitals pass (use PageSpeed Insights)
- Mobile-friendly (use Mobile-Friendly Test)
- No broken links (use Screaming Frog or similar)
- HTTPS enabled
- No mixed content warnings