
Seo Optimizer
- 13 installs
- 28 repo stars
- Updated December 5, 2025
- chongdashu/cc-skills
Analyze a codebase and implement SEO improvements including meta tags, structured data, sitemaps, and Core Web Vitals for Next.js, Astro, or React sites.
About
Audits a web app and adds meta tags, JSON-LD structured data, sitemaps, and performance fixes using framework-native patterns. Used when a developer wants to improve search and social discoverability.
- Meta tags, structured data, and sitemaps
- Framework-native for Next.js, Astro, and React
Seo Optimizer by the numbers
- 13 all-time installs (skills.sh)
- Ranked #1,503 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chongdashu/cc-skills --skill seo-optimizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 28 |
| Last updated | December 5, 2025 |
| Repository | chongdashu/cc-skills ↗ |
What it does
Analyze a codebase and implement SEO improvements including meta tags, structured data, sitemaps, and Core Web Vitals for Next.js, Astro, or React sites.
Files
SEO Optimizer
Transform your web application from invisible to discoverable. This skill analyzes your codebase and implements comprehensive SEO optimizations that help search engines and social platforms understand, index, and surface your content.
Philosophy: SEO as Semantic Communication
SEO is not about gaming algorithms—it's about clearly communicating what your content IS to machines (search engines, social platforms, AI crawlers) so they can properly understand and surface it.
Before optimizing, ask:
- What is this page actually about? (not what keywords we want to rank for)
- Who is the intended audience and what are they searching for?
- What unique value does this content provide?
- How should machines categorize and understand this content?
Core Principles:
1. Accuracy Over Optimization: Describe what IS, not what you wish would rank 2. User Intent First: Match content to what searchers actually want 3. Semantic Clarity: Use structured data to make meaning machine-readable 4. Progressive Enhancement: Basic SEO for all pages, rich optimization for key pages 5. Framework-Native: Use each framework's idioms, not generic hacks
The SEO Hierarchy (prioritize in order):
1. Content Quality ← Foundation: Valuable, accurate, unique content
2. Technical Access ← Can crawlers find and index your pages?
3. Semantic Structure ← Do machines understand your content's meaning?
4. Meta Optimization ← Are your titles/descriptions compelling?
5. Structured Data ← JSON-LD for rich search results
6. Performance ← Core Web Vitals affect rankings---
Codebase Analysis Workflow
ALWAYS analyze before implementing. Different codebases need different approaches.
Step 1: Discover Framework and Structure
Identify the framework and routing pattern:
- Next.js: Look for
next.config.js,app/orpages/directory - Astro: Look for
astro.config.mjs,src/pages/ - React Router: Look for route configuration,
react-router-dom - Gatsby: Look for
gatsby-config.js,gatsby-node.js - Static HTML: Look for
.htmlfiles in root orpublic/
Step 2: Audit Current SEO State
Check for existing implementations:
- [ ] Meta tags in
<head>(title, description, viewport) - [ ] Open Graph tags (
og:title,og:image, etc.) - [ ] Twitter Card tags (
twitter:card,twitter:image) - [ ] Structured data (
<script type="application/ld+json">) - [ ] Sitemap (
sitemap.xmlor generation config) - [ ] Robots.txt file
- [ ] Canonical URLs
- [ ] Alt text on images
Step 3: Identify Page Types
Different pages need different SEO approaches:
| Page Type | Priority | Key Optimizations |
|---|---|---|
| Landing/Home | Critical | Brand keywords, comprehensive structured data |
| Product/Service | High | Product schema, reviews, pricing |
| Blog/Article | High | Article schema, author, publish date |
| Documentation | Medium | HowTo/FAQ schema, breadcrumbs |
| About/Contact | Medium | Organization schema, local business |
| Legal/Privacy | Low | Basic meta only, often noindex |
Step 4: Generate Implementation Plan
Based on analysis, prioritize: 1. Quick wins: Missing meta tags, viewport, basic structure 2. High impact: Structured data for key pages, sitemap 3. Refinement: Performance, advanced schema, social optimization
See references/analysis-checklist.md for detailed audit procedures.
---
Meta Tags Implementation
Essential Meta Tags (Every Page)
<!-- Required -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{Page Title} | {Site Name}</title>
<meta name="description" content="{150-160 char description}">
<!-- Recommended -->
<link rel="canonical" href="{full canonical URL}">
<meta name="robots" content="index, follow">Title Tag Best Practices
Format: {Primary Content} | {Brand} or {Primary Content} - {Brand}
Guidelines:
- 50-60 characters (Google truncates at ~60)
- Front-load important keywords
- Unique for every page
- Accurately describe page content
- Include brand for recognition (usually at end)
Title Patterns by Page Type:
Homepage: {Brand} - {Value Proposition}
Product: {Product Name} - {Key Benefit} | {Brand}
Article: {Article Title} | {Brand}
Category: {Category} Products | {Brand}
Search: Search Results for "{Query}" | {Brand}Meta Description Best Practices
Guidelines:
- 150-160 characters (Google may truncate at ~155)
- Include a call to action when appropriate
- Accurately summarize page content
- Unique for every page
- Include primary keyword naturally
DO NOT:
- Stuff keywords unnaturally
- Use the same description across pages
- Write descriptions that don't match content
- Start with "Welcome to..." or similar filler
Open Graph Tags (Social Sharing)
<meta property="og:type" content="website">
<meta property="og:url" content="{canonical URL}">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{description}">
<meta property="og:image" content="{1200x630 image URL}">
<meta property="og:site_name" content="{Site Name}">Twitter Card Tags
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@{handle}">
<meta name="twitter:title" content="{title}">
<meta name="twitter:description" content="{description}">
<meta name="twitter:image" content="{image URL}">See references/meta-tags-complete.md for comprehensive tag reference.
---
Structured Data (JSON-LD)
Structured data enables rich search results (star ratings, prices, FAQs, etc.).
When to Use Which Schema
| Content Type | Schema | Rich Result |
|---|---|---|
| Organization info | Organization | Knowledge panel |
| Products | Product | Price, availability, reviews |
| Articles/Blog | Article | Headline, image, date |
| How-to guides | HowTo | Step-by-step in search |
| FAQs | FAQPage | Expandable Q&A |
| Events | Event | Date, location, tickets |
| Recipes | Recipe | Image, time, ratings |
| Local business | LocalBusiness | Maps, hours, contact |
| Breadcrumbs | BreadcrumbList | Navigation path |
Implementation Pattern
<script type="application/ld+json">
{
"@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"
]
}
</script>Multiple Schemas Per Page
Use @graph to combine schemas:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", ... },
{ "@type": "WebSite", ... },
{ "@type": "BreadcrumbList", ... }
]
}See references/structured-data-schemas.md for complete schema examples.
---
Technical SEO
Sitemap Generation
XML Sitemap Requirements:
- Include all indexable pages
- Exclude noindex pages, redirects, error pages
- Update
<lastmod>when content changes - Submit to Google Search Console
Framework implementations: See references/framework-implementations.md
Robots.txt
Standard Template:
User-agent: *
Allow: /
# Block admin/private areas
Disallow: /admin/
Disallow: /api/
Disallow: /private/
# Point to sitemap
Sitemap: https://yourdomain.com/sitemap.xmlCanonical URLs
Always set canonical URLs to:
- Prevent duplicate content issues
- Consolidate link equity
- Specify preferred URL version
Handle:
- www vs non-www
- http vs https
- Trailing slashes
- Query parameters
Performance (Core Web Vitals)
Core Web Vitals affect rankings. Monitor:
| Metric | Target | What It Measures |
|---|---|---|
| LCP | < 2.5s | Largest Contentful Paint (loading) |
| INP | < 200ms | Interaction to Next Paint (interactivity) |
| CLS | < 0.1 | Cumulative Layout Shift (visual stability) |
Quick wins:
- Optimize images (WebP, lazy loading, proper sizing)
- Minimize JavaScript bundles
- Use efficient fonts (display: swap)
- Implement proper caching
---
Anti-Patterns to Avoid
❌ Keyword Stuffing
<!-- BAD -->
<title>Best Shoes | Buy Shoes | Cheap Shoes | Shoes Online | Shoe Store</title>
<!-- GOOD -->
<title>Running Shoes for Marathon Training | SportShop</title>Why bad: Search engines penalize unnatural keyword repetition. Users don't click spammy titles.
❌ Duplicate Descriptions Using the same meta description across multiple pages. Why bad: Misses opportunity for page-specific relevance. Google may ignore and auto-generate.
❌ Description/Content Mismatch Writing descriptions for keywords rather than actual content. Why bad: High bounce rates signal low quality. Users feel deceived.
❌ Missing Alt Text
<!-- BAD -->
<img src="product.jpg">
<!-- GOOD -->
<img src="product.jpg" alt="Blue Nike Air Max running shoe, side view">Why bad: Accessibility violation. Missed image search opportunity.
❌ Blocking Crawlers Unintentionally
# Accidentally blocking everything
User-agent: *
Disallow: /Why bad: Complete deindexing. Check robots.txt carefully.
❌ Ignoring Mobile Not having responsive design or mobile-specific considerations. Why bad: Google uses mobile-first indexing. Most traffic is mobile.
❌ Over-Optimization Adding structured data for content that doesn't exist. Why bad: Schema violations can result in penalties. Trust erosion.
❌ Generic Auto-Generated Content
<!-- BAD: Template without customization -->
<meta name="description" content="Welcome to our website. We offer great products and services.">Why bad: Provides no value. Won't rank. Won't get clicks.
---
Variation Guidance
IMPORTANT: SEO implementation should vary based on context.
Vary based on:
- Industry: E-commerce needs Product schema; SaaS needs Software schema
- Content type: Blog posts vs landing pages vs documentation
- Audience: B2B vs B2C affects tone and keywords
- Competition: Highly competitive niches need more sophisticated optimization
- Framework: Use native patterns (Next.js metadata API vs manual tags)
Avoid converging on:
- Same title format for all page types
- Generic descriptions that could apply to any site
- Identical structured data without page-specific content
- One-size-fits-all sitemap configuration
---
Framework Quick Reference
Next.js (App Router)
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Page Title | Brand',
description: 'Page description',
openGraph: {
title: 'Page Title',
description: 'Page description',
images: ['/og-image.png'],
},
}Next.js (Pages Router)
// pages/index.tsx
import Head from 'next/head'
export default function Page() {
return (
<Head>
<title>Page Title | Brand</title>
<meta name="description" content="Page description" />
</Head>
)
}Astro
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
---
<Layout
title="Page Title | Brand"
description="Page description"
ogImage="/og-image.png"
/>React (react-helmet)
import { Helmet } from 'react-helmet';
function Page() {
return (
<Helmet>
<title>Page Title | Brand</title>
<meta name="description" content="Page description" />
</Helmet>
);
}See references/framework-implementations.md for complete guides.
---
Scripts
analyze_seo.py
Analyzes a codebase for SEO issues and opportunities:
python scripts/analyze_seo.py <path-to-project>Output:
- Current SEO state (what's implemented)
- Missing elements by priority
- Page-by-page recommendations
- Structured data opportunities
generate_sitemap.py
Generates sitemap.xml from project routes:
python scripts/generate_sitemap.py <path-to-project> --domain https://example.com---
Remember
SEO is semantic communication, not algorithm manipulation.
The best SEO:
- Accurately describes what content IS
- Helps machines understand meaning through structured data
- Prioritizes user value over keyword optimization
- Uses framework-native patterns
- Implements progressively based on page importance
Focus on making your content findable and understandable. The rankings follow from genuine value clearly communicated.
Claude is capable of comprehensive SEO analysis and implementation. These guidelines illuminate the path—they don't fence it.
SEO Analysis Checklist
Comprehensive checklist for auditing web application SEO. Use this to systematically evaluate and improve SEO.
Quick Assessment (5 minutes)
Homepage Check
- [ ] Title tag present and under 60 characters
- [ ] Meta description present and 150-160 characters
- [ ] Viewport meta tag present
- [ ] H1 tag present (only one per page)
- [ ] Open Graph image visible when sharing
Technical Basics
- [ ] Site loads over HTTPS
- [ ] robots.txt exists and accessible
- [ ] sitemap.xml exists and accessible
- [ ] No console errors on load
- [ ] Mobile responsive
---
Complete Audit
1. Crawlability & Indexing
robots.txt Analysis
Check: /robots.txt- [ ] File exists and is accessible
- [ ] Not blocking important pages accidentally
- [ ] Sitemap URL is specified
- [ ] No syntax errors
- [ ] Development/staging pages blocked
Common issues:
- Blocking CSS/JS files (hurts rendering)
- Blocking entire site (
Disallow: /) - Missing sitemap reference
XML Sitemap
Check: /sitemap.xml or /sitemap-index.xml- [ ] Sitemap exists and parses correctly
- [ ] All important pages included
- [ ] No noindex pages in sitemap
- [ ] No 404 or redirect URLs
- [ ] lastmod dates are accurate
- [ ] Sitemap size < 50MB, < 50,000 URLs
Indexing Status
- [ ] Check Google Search Console coverage
- [ ] Review "Excluded" pages
- [ ] Identify crawl errors
- [ ] Check for duplicate content issues
---
2. Page-Level Meta Tags
For Each Important Page
Title Tag
- [ ] Present in
<head> - [ ] 50-60 characters
- [ ] Contains primary keyword
- [ ] Unique across site
- [ ] Includes brand name
- [ ] No truncation in search results
Meta Description
- [ ] Present in
<head> - [ ] 150-160 characters
- [ ] Compelling call-to-action
- [ ] Contains primary keyword
- [ ] Unique across site
- [ ] Accurately describes page content
Canonical URL
- [ ] Present on every page
- [ ] Absolute URL (not relative)
- [ ] Self-referencing on unique pages
- [ ] Consistent trailing slash usage
- [ ] HTTPS version specified
Robots Meta
- [ ] Present if needed (index/noindex)
- [ ] No conflicting directives
- [ ] noindex on thin/duplicate content
---
3. Open Graph & Social
Open Graph Tags
- [ ]
og:typepresent - [ ]
og:urlmatches canonical - [ ]
og:titlepresent (can differ from title) - [ ]
og:descriptionpresent - [ ]
og:imagepresent - [ ]
og:imageis 1200x630 pixels - [ ]
og:site_namepresent
Twitter Cards
- [ ]
twitter:cardpresent - [ ]
twitter:titlepresent - [ ]
twitter:descriptionpresent - [ ]
twitter:imagepresent - [ ] Image meets minimum requirements
Testing
- [ ] Facebook Sharing Debugger passes
- [ ] Twitter Card Validator passes
- [ ] LinkedIn Post Inspector passes
---
4. Structured Data
Organization/WebSite Schema
- [ ] Present on homepage
- [ ] Valid JSON-LD format
- [ ] Accurate business information
- [ ] Logo URL accessible
- [ ] Social profiles linked (sameAs)
Page-Specific Schema
- [ ] Article schema on blog posts
- [ ] Product schema on product pages
- [ ] FAQPage schema where applicable
- [ ] BreadcrumbList on hierarchical pages
- [ ] LocalBusiness for physical locations
Validation
- [ ] Google Rich Results Test passes
- [ ] Schema.org validator passes
- [ ] No warnings or errors
- [ ] Required properties present
---
5. Content & Semantic Structure
Heading Hierarchy
- [ ] Single H1 per page
- [ ] H1 contains primary keyword
- [ ] Logical heading order (H1 → H2 → H3)
- [ ] No skipped heading levels
- [ ] Headings describe content sections
Images
- [ ] All images have alt text
- [ ] Alt text is descriptive (not keyword-stuffed)
- [ ] Decorative images have empty alt=""
- [ ] Image filenames are descriptive
- [ ] Images are optimized (WebP, compressed)
- [ ] Lazy loading on below-fold images
Links
- [ ] Internal links use descriptive anchor text
- [ ] No broken internal links
- [ ] External links to authoritative sources
- [ ] Outbound links use rel="noopener" when needed
- [ ] Sponsored/affiliate links use rel="sponsored"
---
6. Technical Performance
Core Web Vitals
- [ ] LCP < 2.5 seconds
- [ ] INP < 200 milliseconds
- [ ] CLS < 0.1
Page Speed
- [ ] Time to First Byte < 600ms
- [ ] First Contentful Paint < 1.8s
- [ ] Total page weight < 3MB
- [ ] JavaScript bundle size reasonable
Mobile
- [ ] Mobile-responsive design
- [ ] Text readable without zooming
- [ ] Tap targets adequately sized (48x48 CSS pixels)
- [ ] No horizontal scrolling
- [ ] Content same on mobile and desktop
---
7. URL Structure
URL Best Practices
- [ ] URLs are descriptive and readable
- [ ] Lowercase URLs only
- [ ] Hyphens for word separation (not underscores)
- [ ] No special characters or encoding
- [ ] Reasonable length (< 100 characters)
- [ ] Consistent trailing slash policy
Redirects
- [ ] www/non-www redirects properly
- [ ] HTTP → HTTPS redirects work
- [ ] No redirect chains (max 1 hop)
- [ ] Old URLs redirect to new locations
- [ ] 301 for permanent, 302 for temporary
---
8. International SEO (if applicable)
- [ ] hreflang tags for language variants
- [ ] x-default for default language
- [ ] Bidirectional hreflang links
- [ ] Consistent language/region targeting
- [ ] Content genuinely localized
---
Framework-Specific Checks
Next.js
- [ ] Using Metadata API (App Router) or next/head (Pages)
- [ ] generateMetadata for dynamic pages
- [ ] sitemap.ts or sitemap.xml present
- [ ] robots.ts or robots.txt present
- [ ] next-sitemap or built-in sitemap configured
Astro
- [ ] SEO props passed to Layout
- [ ] @astrojs/sitemap integration
- [ ] Static build optimizations enabled
- [ ] Proper use of client directives
React SPA
- [ ] Server-side rendering or pre-rendering
- [ ] react-helmet-async configured
- [ ] Pre-rendering for SEO-critical pages
- [ ] Dynamic routes pre-rendered
---
Priority Matrix
Critical (Fix Immediately)
- Missing title tags
- Blocking crawlers accidentally
- Broken canonical tags
- noindex on important pages
- Site-wide HTTPS issues
High (Fix This Week)
- Missing meta descriptions
- Missing Open Graph tags
- No structured data on key pages
- Missing sitemap
- Core Web Vitals failing
Medium (Plan for Next Sprint)
- Image alt text gaps
- Heading structure issues
- Internal linking improvements
- URL structure cleanup
- Schema expansion
Low (Ongoing Optimization)
- Title/description refinement
- Additional schema types
- A/B testing meta tags
- Content expansion
- Link building
---
Audit Report Template
# SEO Audit Report: [Site Name]
Date: [Date]
Auditor: Claude
## Executive Summary
[2-3 sentences on overall SEO health]
## Score: [X]/100
### Category Breakdown
- Crawlability: [X]/20
- Meta Tags: [X]/20
- Structured Data: [X]/20
- Content Quality: [X]/20
- Technical Performance: [X]/20
## Critical Issues
1. [Issue]: [Impact and recommendation]
2. [Issue]: [Impact and recommendation]
## High Priority Recommendations
1. [Recommendation]
2. [Recommendation]
## Quick Wins
1. [Easy fix with high impact]
2. [Easy fix with high impact]
## Page-by-Page Analysis
[Detailed findings per page]
## Next Steps
1. [Action item with priority]
2. [Action item with priority]---
Tools for Testing
Free Tools
- Google Search Console: Index coverage, performance
- Google PageSpeed Insights: Core Web Vitals
- Google Rich Results Test: Structured data
- Facebook Sharing Debugger: OG tags
- Twitter Card Validator: Twitter cards
Browser Extensions
- Lighthouse: Chrome DevTools
- SEO Meta in 1 Click: Quick meta overview
- Detailed SEO Extension: Comprehensive analysis
Command Line
# Check robots.txt
curl -I https://example.com/robots.txt
# Check sitemap
curl -s https://example.com/sitemap.xml | head -50
# Check headers
curl -I https://example.comFramework-Specific SEO Implementations
Complete implementation guides for popular frameworks.
Next.js (App Router - v13+)
Metadata API
The recommended approach for Next.js App Router.
Static Metadata
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Page Title | Brand',
description: 'Page description for search engines and social.',
keywords: ['keyword1', 'keyword2', 'keyword3'],
authors: [{ name: 'Author Name' }],
creator: 'Creator Name',
publisher: 'Publisher Name',
// Canonical URL
alternates: {
canonical: 'https://example.com/page',
languages: {
'en-US': 'https://example.com/en-US/page',
'es-ES': 'https://example.com/es-ES/page',
},
},
// Open Graph
openGraph: {
title: 'Page Title',
description: 'Description for social sharing',
url: 'https://example.com/page',
siteName: 'Site Name',
images: [
{
url: 'https://example.com/og-image.png',
width: 1200,
height: 630,
alt: 'Image description',
},
],
locale: 'en_US',
type: 'website',
},
// Twitter
twitter: {
card: 'summary_large_image',
title: 'Page Title',
description: 'Description for Twitter',
site: '@sitehandle',
creator: '@creatorhandle',
images: ['https://example.com/twitter-image.png'],
},
// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
// Verification
verification: {
google: 'google-site-verification-code',
yandex: 'yandex-verification-code',
},
}Dynamic Metadata
// app/blog/[slug]/page.tsx
import { 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} | Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
images: [post.featuredImage],
},
}
}Layout Metadata with Template
// app/layout.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
title: {
default: 'Site Name',
template: '%s | Site Name', // Applied to child pages
},
description: 'Default site description',
openGraph: {
title: {
default: 'Site Name',
template: '%s | Site Name',
},
siteName: 'Site Name',
},
}Structured Data in Next.js
// app/page.tsx
export default function Page() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Company Name',
url: 'https://example.com',
logo: 'https://example.com/logo.png',
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* Page content */}
</>
)
}Sitemap Generation
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://example.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}
// Dynamic sitemap with database content
export default async function sitemap(): MetadataRoute.Sitemap {
const posts = await getPosts()
const postUrls = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly' as const,
priority: 0.6,
}))
return [
{ url: 'https://example.com', lastModified: new Date(), priority: 1 },
...postUrls,
]
}Robots.txt
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/private/'],
},
sitemap: 'https://example.com/sitemap.xml',
}
}---
Next.js (Pages Router)
Using next/head
// pages/index.tsx
import Head from 'next/head'
export default function HomePage() {
return (
<>
<Head>
<title>Page Title | Brand</title>
<meta name="description" content="Page description" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="canonical" href="https://example.com/" />
{/* Open Graph */}
<meta property="og:type" content="website" />
<meta property="og:url" content="https://example.com/" />
<meta property="og:title" content="Page Title" />
<meta property="og:description" content="Page description" />
<meta property="og:image" content="https://example.com/og-image.png" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Page Title" />
<meta name="twitter:description" content="Page description" />
<meta name="twitter:image" content="https://example.com/twitter-image.png" />
</Head>
{/* Page content */}
</>
)
}Reusable SEO Component
// components/SEO.tsx
import Head from 'next/head'
interface SEOProps {
title: string
description: string
canonical?: string
ogImage?: string
ogType?: string
noindex?: boolean
}
export function SEO({
title,
description,
canonical,
ogImage = '/default-og.png',
ogType = 'website',
noindex = false,
}: SEOProps) {
const fullTitle = `${title} | Brand`
const siteUrl = 'https://example.com'
const canonicalUrl = canonical ? `${siteUrl}${canonical}` : undefined
const imageUrl = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`
return (
<Head>
<title>{fullTitle}</title>
<meta name="description" content={description} />
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
{noindex && <meta name="robots" content="noindex, nofollow" />}
<meta property="og:type" content={ogType} />
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageUrl} />
<meta property="og:site_name" content="Brand" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
</Head>
)
}---
Astro
Layout with SEO Props
---
// src/layouts/Layout.astro
interface Props {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
article?: {
publishedTime: string;
modifiedTime?: string;
author?: string;
tags?: string[];
};
}
const {
title,
description,
canonical = Astro.url.href,
ogImage = '/og-default.png',
ogType = 'website',
article,
} = Astro.props;
const siteUrl = import.meta.env.SITE || 'https://example.com';
const fullImageUrl = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} | Brand</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<!-- Open Graph -->
<meta property="og:type" content={ogType} />
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullImageUrl} />
<meta property="og:site_name" content="Brand" />
{article && (
<>
<meta property="article:published_time" content={article.publishedTime} />
{article.modifiedTime && (
<meta property="article:modified_time" content={article.modifiedTime} />
)}
{article.author && (
<meta property="article:author" content={article.author} />
)}
{article.tags?.map((tag) => (
<meta property="article:tag" content={tag} />
))}
</>
)}
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImageUrl} />
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<slot name="head" />
</head>
<body>
<slot />
</body>
</html>Using the Layout
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
---
<Layout
title="Home"
description="Welcome to our site. We help you do amazing things."
ogImage="/og-home.png"
>
<main>
<h1>Welcome</h1>
</main>
</Layout>Astro Sitemap Integration
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [
sitemap({
filter: (page) => !page.includes('/admin/'),
changefreq: 'weekly',
priority: 0.7,
lastmod: new Date(),
}),
],
});Structured Data Component
---
// src/components/JsonLd.astro
interface Props {
data: Record<string, any>;
}
const { data } = Astro.props;
const jsonLd = {
'@context': 'https://schema.org',
...data,
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />---
// Usage in page
import JsonLd from '../components/JsonLd.astro';
---
<JsonLd data={{
'@type': 'Organization',
name: 'Company Name',
url: 'https://example.com',
}} />---
React (with react-helmet-async)
Setup
// src/main.tsx
import { HelmetProvider } from 'react-helmet-async';
ReactDOM.createRoot(document.getElementById('root')!).render(
<HelmetProvider>
<App />
</HelmetProvider>
);SEO Component
// src/components/SEO.tsx
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
noindex?: boolean;
}
export function SEO({
title,
description,
canonical,
ogImage = '/og-default.png',
ogType = 'website',
noindex = false,
}: SEOProps) {
const siteUrl = import.meta.env.VITE_SITE_URL || 'https://example.com';
const fullTitle = `${title} | Brand`;
const canonicalUrl = canonical ? `${siteUrl}${canonical}` : undefined;
const imageUrl = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`;
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
{noindex && <meta name="robots" content="noindex, nofollow" />}
<meta property="og:type" content={ogType} />
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageUrl} />
<meta property="og:site_name" content="Brand" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
</Helmet>
);
}Usage
// src/pages/Home.tsx
import { SEO } from '../components/SEO';
export function HomePage() {
return (
<>
<SEO
title="Home"
description="Welcome to our site."
canonical="/"
ogImage="/og-home.png"
/>
<main>
<h1>Welcome</h1>
</main>
</>
);
}---
Gatsby
gatsby-plugin-react-helmet
// gatsby-config.js
module.exports = {
siteMetadata: {
title: 'Site Name',
description: 'Site description',
siteUrl: 'https://example.com',
author: '@twitterhandle',
},
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-sitemap',
{
resolve: 'gatsby-plugin-robots-txt',
options: {
host: 'https://example.com',
sitemap: 'https://example.com/sitemap.xml',
policy: [{ userAgent: '*', allow: '/' }],
},
},
],
};SEO Component for Gatsby
// src/components/SEO.tsx
import { useStaticQuery, graphql } from 'gatsby';
import { Helmet } from 'react-helmet';
interface SEOProps {
title: string;
description?: string;
pathname?: string;
image?: string;
article?: boolean;
}
export function SEO({ title, description, pathname, image, article = false }: SEOProps) {
const { site } = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
description
siteUrl
author
}
}
}
`);
const { siteUrl, defaultDescription, author } = site.siteMetadata;
const seo = {
title,
description: description || defaultDescription,
url: `${siteUrl}${pathname || ''}`,
image: image ? `${siteUrl}${image}` : `${siteUrl}/og-default.png`,
};
return (
<Helmet>
<title>{`${seo.title} | ${site.siteMetadata.title}`}</title>
<meta name="description" content={seo.description} />
<link rel="canonical" href={seo.url} />
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:url" content={seo.url} />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={author} />
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
</Helmet>
);
}---
Static HTML
Template File
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- SEO Essentials -->
<title>Page Title | Brand</title>
<meta name="description" content="Page description here.">
<link rel="canonical" href="https://example.com/page/">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Description for social sharing.">
<meta property="og:image" content="https://example.com/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="Brand">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@brand">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Description for Twitter.">
<meta name="twitter:image" content="https://example.com/twitter-image.png">
<!-- Favicons -->
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<!-- Theme -->
<meta name="theme-color" content="#4285f4">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Page Title",
"description": "Page description",
"url": "https://example.com/page/"
}
</script>
</head>
<body>
<!-- Content -->
</body>
</html>sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://example.com/about/</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://example.com/contact/</loc>
<lastmod>2024-01-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
</urlset>robots.txt
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /private/
Sitemap: https://example.com/sitemap.xmlComplete Meta Tags Reference
Comprehensive guide to all meta tags for SEO, social sharing, and browser behavior.
Essential Meta Tags
Document Character Set
<meta charset="utf-8">Always first in <head>. UTF-8 supports all languages.
Viewport
<meta name="viewport" content="width=device-width, initial-scale=1">Required for mobile responsiveness. Never use user-scalable=no (accessibility issue).
Title
<title>Primary Keyword - Secondary | Brand Name</title>- 50-60 characters optimal
- Front-load keywords
- Include brand (usually at end)
- Unique per page
Description
<meta name="description" content="Compelling description with primary keyword. Explain value and include call-to-action. 150-160 characters.">- 150-160 characters
- Include target keyword naturally
- Compelling call-to-action
- Unique per page
Canonical URL
<link rel="canonical" href="https://example.com/page/">- Always absolute URL
- Choose one version (www vs non-www, trailing slash)
- Self-referencing is fine
- Prevents duplicate content issues
---
Robots Meta Tags
Basic Indexing Control
<!-- Default: index and follow all links -->
<meta name="robots" content="index, follow">
<!-- Don't index but follow links -->
<meta name="robots" content="noindex, follow">
<!-- Index but don't follow links -->
<meta name="robots" content="index, nofollow">
<!-- Don't index, don't follow -->
<meta name="robots" content="noindex, nofollow">Advanced Directives
<!-- Prevent snippet in search results -->
<meta name="robots" content="nosnippet">
<!-- Prevent image indexing -->
<meta name="robots" content="noimageindex">
<!-- Limit snippet length -->
<meta name="robots" content="max-snippet:150">
<!-- Prevent page caching -->
<meta name="robots" content="noarchive">
<!-- Prevent translation offering -->
<meta name="robots" content="notranslate">
<!-- Combined directives -->
<meta name="robots" content="index, follow, max-snippet:150, max-image-preview:large">Bot-Specific Directives
<!-- Google-specific -->
<meta name="googlebot" content="index, follow">
<!-- Bing-specific -->
<meta name="bingbot" content="index, follow">---
Open Graph Tags (Facebook, LinkedIn, Slack, Discord)
Required Tags
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Page description for social sharing.">
<meta property="og:image" content="https://example.com/og-image.png">Recommended Tags
<meta property="og:site_name" content="Site Name">
<meta property="og:locale" content="en_US">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Description of image">Article-Specific Tags
<meta property="og:type" content="article">
<meta property="article:published_time" content="2024-01-15T08:00:00Z">
<meta property="article:modified_time" content="2024-01-16T10:30:00Z">
<meta property="article:author" content="https://example.com/authors/name">
<meta property="article:section" content="Technology">
<meta property="article:tag" content="JavaScript">
<meta property="article:tag" content="React">Product Tags
<meta property="og:type" content="product">
<meta property="product:price:amount" content="29.99">
<meta property="product:price:currency" content="USD">
<meta property="product:availability" content="in stock">Image Requirements
- Dimensions: 1200 x 630 pixels (1.91:1 ratio)
- Minimum: 600 x 315 pixels
- File size: < 8MB
- Formats: PNG, JPEG, GIF
- URL: Absolute, HTTPS preferred
---
Twitter Card Tags
Summary Card (Small Image)
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@yourbrand">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Page description (200 chars max)">
<meta name="twitter:image" content="https://example.com/image.png">Summary Large Image (Recommended)
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@yourbrand">
<meta name="twitter:creator" content="@authorhandle">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Page description">
<meta name="twitter:image" content="https://example.com/large-image.png">
<meta name="twitter:image:alt" content="Description of image">Image Requirements
- summary: 120 x 120 to 4096 x 4096 (1:1 displayed)
- summary_large_image: 300 x 157 to 4096 x 4096 (2:1 displayed)
- File size: < 5MB
- Formats: PNG, JPEG, GIF, WEBP
---
Additional SEO Tags
Language and Region
<!-- Page language -->
<html lang="en">
<!-- Alternate language versions -->
<link rel="alternate" hreflang="en" href="https://example.com/page/">
<link rel="alternate" hreflang="es" href="https://example.com/es/page/">
<link rel="alternate" hreflang="x-default" href="https://example.com/page/">Pagination
<!-- For paginated content -->
<link rel="prev" href="https://example.com/page/2/">
<link rel="next" href="https://example.com/page/4/">Note: Google no longer uses these for indexing but they help with crawling.
Author and Publisher
<meta name="author" content="Author Name">
<link rel="author" href="https://example.com/about/author">
<link rel="publisher" href="https://plus.google.com/+YourPage">---
Browser and PWA Tags
Favicon Links
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.json">Theme Colors
<!-- Browser theme color -->
<meta name="theme-color" content="#4285f4">
<!-- Dark mode support -->
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<!-- Safari/Apple specific -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">App-Like Behavior
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="App Name">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="App Name">---
Security Tags
Content Security Policy
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https:; script-src 'self' 'unsafe-inline'">Referrer Policy
<meta name="referrer" content="strict-origin-when-cross-origin">Options:
no-referrer: Never send referrerorigin: Send origin onlystrict-origin-when-cross-origin: Full URL for same-origin, origin for cross-origin HTTPS, none for HTTP
---
Complete Template
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Character encoding -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- SEO essentials -->
<title>Page Title - Keyword | Brand</title>
<meta name="description" content="Compelling 150-160 character description with primary keyword and call-to-action.">
<link rel="canonical" href="https://example.com/page/">
<meta name="robots" content="index, follow">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Description for social sharing.">
<meta property="og:image" content="https://example.com/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="Site Name">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@yourbrand">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Description for Twitter.">
<meta name="twitter:image" content="https://example.com/twitter-image.png">
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<!-- Theme -->
<meta name="theme-color" content="#4285f4">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Page Title",
"description": "Page description"
}
</script>
</head>
<body>
<!-- Content -->
</body>
</html>---
Validation Tools
- Google Rich Results Test: https://search.google.com/test/rich-results
- Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
- Twitter Card Validator: https://cards-dev.twitter.com/validator
- LinkedIn Post Inspector: https://www.linkedin.com/post-inspector/
- Schema Validator: https://validator.schema.org/
Structured Data Schemas Reference
Complete JSON-LD examples for common Schema.org types that enable rich search results.
Organization Schema
For company/brand information. Appears in knowledge panels.
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Company Name",
"legalName": "Company Name Inc.",
"url": "https://example.com",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png",
"width": 600,
"height": 60
},
"description": "Brief company description",
"foundingDate": "2020-01-01",
"founders": [
{
"@type": "Person",
"name": "Founder Name"
}
],
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94105",
"addressCountry": "US"
},
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-555-5555",
"contactType": "customer service",
"availableLanguage": ["English"]
},
"sameAs": [
"https://twitter.com/company",
"https://linkedin.com/company/company",
"https://github.com/company"
]
}---
WebSite Schema
For site-wide search functionality.
{
"@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
For blog posts, news articles, and editorial content.
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Article Title (Max 110 characters)",
"description": "Brief article summary",
"image": [
"https://example.com/article-image-16x9.jpg",
"https://example.com/article-image-4x3.jpg",
"https://example.com/article-image-1x1.jpg"
],
"datePublished": "2024-01-15T08:00:00+00:00",
"dateModified": "2024-01-16T10:30:00+00:00",
"author": {
"@type": "Person",
"name": "Author Name",
"url": "https://example.com/authors/author-name"
},
"publisher": {
"@type": "Organization",
"name": "Publisher Name",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://example.com/article-url"
}
}BlogPosting (Alternative)
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Blog Post Title",
"description": "Blog post summary",
"articleBody": "Full article text can go here...",
"wordCount": 1500,
"keywords": ["keyword1", "keyword2", "keyword3"]
// ... plus all Article fields
}---
Product Schema
For e-commerce product pages. Enables rich results with price, availability.
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Product Name",
"description": "Product description",
"image": [
"https://example.com/product-1.jpg",
"https://example.com/product-2.jpg"
],
"sku": "SKU12345",
"mpn": "MPN12345",
"brand": {
"@type": "Brand",
"name": "Brand Name"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/product",
"priceCurrency": "USD",
"price": "29.99",
"priceValidUntil": "2024-12-31",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": {
"@type": "Organization",
"name": "Seller Name"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "89"
},
"review": [
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Reviewer Name"
},
"datePublished": "2024-01-10",
"reviewBody": "This product is excellent...",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5"
}
}
]
}Availability Options
https://schema.org/InStockhttps://schema.org/OutOfStockhttps://schema.org/PreOrderhttps://schema.org/BackOrderhttps://schema.org/Discontinued
---
SoftwareApplication Schema
For apps, SaaS products, software downloads.
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "App Name",
"description": "App description",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web, iOS, Android",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"ratingCount": "2500"
},
"screenshot": "https://example.com/screenshot.png",
"featureList": "Feature 1, Feature 2, Feature 3"
}Application Categories
BusinessApplicationDeveloperApplicationEducationalApplicationGameApplicationHealthApplicationFinanceApplicationSocialNetworkingApplication
---
FAQPage Schema
For FAQ sections. Enables expandable Q&A in search results.
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is your return policy?",
"acceptedAnswer": {
"@type": "Answer",
"text": "We offer a 30-day money-back guarantee. Simply contact our support team to initiate a return."
}
},
{
"@type": "Question",
"name": "How do I contact support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "You can reach our support team via email at support@example.com or through our live chat available 24/7."
}
},
{
"@type": "Question",
"name": "Do you offer free shipping?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, we offer free shipping on all orders over $50 within the United States."
}
}
]
}---
HowTo Schema
For step-by-step guides. Shows numbered steps in search results.
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Set Up Your Account",
"description": "Complete guide to setting up your account in 5 minutes",
"totalTime": "PT5M",
"estimatedCost": {
"@type": "MonetaryAmount",
"currency": "USD",
"value": "0"
},
"supply": [],
"tool": [],
"step": [
{
"@type": "HowToStep",
"name": "Create an account",
"text": "Visit our signup page and enter your email address.",
"url": "https://example.com/signup",
"image": "https://example.com/step1.png"
},
{
"@type": "HowToStep",
"name": "Verify your email",
"text": "Check your inbox and click the verification link.",
"image": "https://example.com/step2.png"
},
{
"@type": "HowToStep",
"name": "Complete your profile",
"text": "Add your name and profile photo.",
"image": "https://example.com/step3.png"
}
]
}---
BreadcrumbList Schema
For navigation breadcrumbs. Shows path in search results.
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Products",
"item": "https://example.com/products"
},
{
"@type": "ListItem",
"position": 3,
"name": "Category",
"item": "https://example.com/products/category"
},
{
"@type": "ListItem",
"position": 4,
"name": "Product Name"
}
]
}---
LocalBusiness Schema
For businesses with physical locations.
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Business Name",
"description": "Business description",
"image": "https://example.com/storefront.jpg",
"url": "https://example.com",
"telephone": "+1-555-555-5555",
"email": "contact@example.com",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94105",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 37.7749,
"longitude": -122.4194
},
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday",
"opens": "10:00",
"closes": "16:00"
}
],
"priceRange": "$$",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "156"
}
}---
Event Schema
For events, conferences, webinars.
{
"@context": "https://schema.org",
"@type": "Event",
"name": "Annual Developer Conference",
"description": "Join us for our annual developer conference...",
"image": "https://example.com/event-image.jpg",
"startDate": "2024-06-15T09:00:00-07:00",
"endDate": "2024-06-17T17:00:00-07:00",
"eventStatus": "https://schema.org/EventScheduled",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"location": {
"@type": "Place",
"name": "Convention Center",
"address": {
"@type": "PostalAddress",
"streetAddress": "456 Event Drive",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94102",
"addressCountry": "US"
}
},
"organizer": {
"@type": "Organization",
"name": "Company Name",
"url": "https://example.com"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/tickets",
"price": "299",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"validFrom": "2024-01-01T00:00:00-08:00"
},
"performer": {
"@type": "Person",
"name": "Keynote Speaker"
}
}Virtual Event
{
"@type": "Event",
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
"location": {
"@type": "VirtualLocation",
"url": "https://example.com/webinar"
}
}---
Combining Multiple Schemas
Use @graph to include multiple related schemas:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Company Name",
"url": "https://example.com",
"logo": "https://example.com/logo.png"
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"url": "https://example.com",
"name": "Site Name",
"publisher": {
"@id": "https://example.com/#organization"
}
},
{
"@type": "WebPage",
"@id": "https://example.com/page/#webpage",
"url": "https://example.com/page/",
"name": "Page Title",
"isPartOf": {
"@id": "https://example.com/#website"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Page",
"item": "https://example.com/page/"
}
]
}
]
}---
Validation
Always validate structured data before deploying:
1. Google Rich Results Test: https://search.google.com/test/rich-results 2. Schema.org Validator: https://validator.schema.org/ 3. Google Search Console: Monitor structured data in the Enhancements section
Common Errors
- Missing required properties
- Invalid date formats (use ISO 8601)
- Invalid URLs (must be absolute)
- Missing
@context - Incorrect
@typespelling
#!/usr/bin/env python3
"""
SEO Analyzer - Analyzes a web project codebase for SEO issues and opportunities.
Usage:
python analyze_seo.py <project-path>
Outputs:
- Framework detection
- Current SEO implementation status
- Missing elements by priority
- Page-by-page recommendations
- Structured data opportunities
"""
import os
import sys
import re
import json
from pathlib import Path
from collections import defaultdict
from typing import Optional, Dict, List, Any
class SEOAnalyzer:
def __init__(self, project_path: str):
self.project_path = Path(project_path).resolve()
self.framework: Optional[str] = None
self.pages: List[Dict[str, Any]] = []
self.issues: Dict[str, List[str]] = defaultdict(list)
self.findings: Dict[str, Any] = {}
def analyze(self) -> Dict[str, Any]:
"""Run complete SEO analysis."""
print(f"Analyzing: {self.project_path}\n")
self._detect_framework()
self._find_pages()
self._check_robots_txt()
self._check_sitemap()
self._analyze_meta_tags()
self._check_structured_data()
self._generate_report()
return self.findings
def _detect_framework(self):
"""Detect the web framework being used."""
indicators = {
"next.js": [
"next.config.js", "next.config.mjs", "next.config.ts",
".next", "app/layout.tsx", "pages/_app.tsx"
],
"astro": [
"astro.config.mjs", "astro.config.ts", ".astro"
],
"gatsby": [
"gatsby-config.js", "gatsby-config.ts", "gatsby-node.js"
],
"react": [
"src/App.tsx", "src/App.jsx", "src/index.tsx"
],
"vue": [
"vue.config.js", "nuxt.config.js", "nuxt.config.ts"
],
"static": [
"index.html", "public/index.html"
]
}
for framework, files in indicators.items():
for file in files:
if (self.project_path / file).exists():
self.framework = framework
print(f"Framework detected: {framework.upper()}")
return
self.framework = "unknown"
print("Framework: Unknown (will check for static HTML)")
def _find_pages(self):
"""Find all page files based on framework."""
page_patterns = {
"next.js": [
"app/**/page.tsx", "app/**/page.jsx", "app/**/page.js",
"pages/**/*.tsx", "pages/**/*.jsx", "pages/**/*.js"
],
"astro": [
"src/pages/**/*.astro", "src/pages/**/*.md", "src/pages/**/*.mdx"
],
"gatsby": [
"src/pages/**/*.tsx", "src/pages/**/*.jsx", "src/pages/**/*.js"
],
"react": [
"src/pages/**/*.tsx", "src/pages/**/*.jsx",
"src/routes/**/*.tsx", "src/routes/**/*.jsx"
],
"static": [
"*.html", "**/*.html"
]
}
patterns = page_patterns.get(self.framework, page_patterns["static"])
for pattern in patterns:
for path in self.project_path.glob(pattern):
if self._should_include_page(path):
self.pages.append({
"path": str(path.relative_to(self.project_path)),
"full_path": str(path),
"type": self._determine_page_type(path)
})
print(f"Pages found: {len(self.pages)}")
for page in self.pages[:10]: # Show first 10
print(f" - {page['path']} ({page['type']})")
if len(self.pages) > 10:
print(f" ... and {len(self.pages) - 10} more")
print()
def _should_include_page(self, path: Path) -> bool:
"""Filter out non-page files."""
exclude_patterns = [
"_app", "_document", "_error", "layout", "loading",
"error", "not-found", "template", "default",
"api/", "_middleware", "middleware"
]
path_str = str(path)
return not any(p in path_str for p in exclude_patterns)
def _determine_page_type(self, path: Path) -> str:
"""Categorize page type based on path and content."""
path_str = str(path).lower()
if "blog" in path_str or "post" in path_str or "article" in path_str:
return "article"
if "product" in path_str or "shop" in path_str or "store" in path_str:
return "product"
if "about" in path_str:
return "about"
if "contact" in path_str:
return "contact"
if "pricing" in path_str:
return "pricing"
if "doc" in path_str or "guide" in path_str or "help" in path_str:
return "documentation"
if "faq" in path_str:
return "faq"
if path.name in ["index.html", "page.tsx", "page.jsx", "index.astro"]:
parent = path.parent.name
if parent in ["app", "pages", "src", ""]:
return "landing"
return "general"
def _check_robots_txt(self):
"""Check for robots.txt file."""
robots_locations = ["robots.txt", "public/robots.txt", "static/robots.txt"]
for location in robots_locations:
robots_path = self.project_path / location
if robots_path.exists():
content = robots_path.read_text()
self.findings["robots_txt"] = {
"exists": True,
"path": location,
"has_sitemap": "sitemap" in content.lower(),
"allows_all": "Disallow:" not in content or "Disallow: \n" in content
}
print(f"robots.txt: Found at {location}")
return
self.findings["robots_txt"] = {"exists": False}
self.issues["critical"].append("Missing robots.txt file")
print("robots.txt: NOT FOUND")
def _check_sitemap(self):
"""Check for sitemap configuration."""
sitemap_locations = [
"sitemap.xml", "public/sitemap.xml", "static/sitemap.xml"
]
# Check for static sitemap
for location in sitemap_locations:
sitemap_path = self.project_path / location
if sitemap_path.exists():
self.findings["sitemap"] = {
"exists": True,
"type": "static",
"path": location
}
print(f"Sitemap: Found static at {location}")
return
# Check for dynamic sitemap configuration
if self.framework == "next.js":
for sitemap_file in ["app/sitemap.ts", "app/sitemap.js"]:
if (self.project_path / sitemap_file).exists():
self.findings["sitemap"] = {
"exists": True,
"type": "dynamic",
"path": sitemap_file
}
print(f"Sitemap: Found dynamic at {sitemap_file}")
return
if self.framework == "astro":
config_path = self.project_path / "astro.config.mjs"
if config_path.exists():
content = config_path.read_text()
if "sitemap" in content.lower():
self.findings["sitemap"] = {
"exists": True,
"type": "integration",
"path": "astro.config.mjs"
}
print("Sitemap: Found @astrojs/sitemap integration")
return
self.findings["sitemap"] = {"exists": False}
self.issues["high"].append("Missing sitemap.xml or sitemap generation")
print("Sitemap: NOT FOUND")
def _analyze_meta_tags(self):
"""Analyze meta tag implementation across pages."""
print("\nAnalyzing meta tags...")
meta_findings = {
"pages_with_title": 0,
"pages_with_description": 0,
"pages_with_og": 0,
"pages_with_twitter": 0,
"pages_with_canonical": 0,
"issues": []
}
for page in self.pages[:20]: # Analyze first 20 pages
content = Path(page["full_path"]).read_text()
has_title = self._check_meta_tag(content, "title")
has_desc = self._check_meta_tag(content, "description")
has_og = self._check_meta_tag(content, "og:")
has_twitter = self._check_meta_tag(content, "twitter:")
has_canonical = self._check_meta_tag(content, "canonical")
if has_title:
meta_findings["pages_with_title"] += 1
if has_desc:
meta_findings["pages_with_description"] += 1
if has_og:
meta_findings["pages_with_og"] += 1
if has_twitter:
meta_findings["pages_with_twitter"] += 1
if has_canonical:
meta_findings["pages_with_canonical"] += 1
if not has_title:
meta_findings["issues"].append(f"Missing title: {page['path']}")
if not has_desc:
meta_findings["issues"].append(f"Missing description: {page['path']}")
self.findings["meta_tags"] = meta_findings
pages_analyzed = min(len(self.pages), 20)
print(f" Pages with title: {meta_findings['pages_with_title']}/{pages_analyzed}")
print(f" Pages with description: {meta_findings['pages_with_description']}/{pages_analyzed}")
print(f" Pages with Open Graph: {meta_findings['pages_with_og']}/{pages_analyzed}")
print(f" Pages with Twitter Cards: {meta_findings['pages_with_twitter']}/{pages_analyzed}")
print(f" Pages with canonical: {meta_findings['pages_with_canonical']}/{pages_analyzed}")
def _check_meta_tag(self, content: str, tag_type: str) -> bool:
"""Check if a meta tag type exists in content."""
patterns = {
"title": [
r"<title>", r"title:", r"title=", r'"title":'
],
"description": [
r'name="description"', r'name=\'description\'',
r"description:", r'"description":'
],
"og:": [
r'property="og:', r"property='og:", r"openGraph"
],
"twitter:": [
r'name="twitter:', r"name='twitter:", r"twitter:"
],
"canonical": [
r'rel="canonical"', r"rel='canonical'", r"canonical:"
]
}
for pattern in patterns.get(tag_type, []):
if re.search(pattern, content, re.IGNORECASE):
return True
return False
def _check_structured_data(self):
"""Check for structured data implementation."""
print("\nChecking structured data...")
schema_found = {
"organization": False,
"website": False,
"article": False,
"product": False,
"faq": False,
"breadcrumb": False
}
# Check for JSON-LD in pages
for page in self.pages[:20]:
content = Path(page["full_path"]).read_text()
if "application/ld+json" in content or "@context" in content:
if '"Organization"' in content or "'Organization'" in content:
schema_found["organization"] = True
if '"WebSite"' in content or "'WebSite'" in content:
schema_found["website"] = True
if '"Article"' in content or "'Article'" in content:
schema_found["article"] = True
if '"Product"' in content or "'Product'" in content:
schema_found["product"] = True
if '"FAQPage"' in content or "'FAQPage'" in content:
schema_found["faq"] = True
if '"BreadcrumbList"' in content or "'BreadcrumbList'" in content:
schema_found["breadcrumb"] = True
self.findings["structured_data"] = schema_found
found_schemas = [k for k, v in schema_found.items() if v]
if found_schemas:
print(f" Found schemas: {', '.join(found_schemas)}")
else:
print(" No structured data found")
self.issues["high"].append("No structured data (JSON-LD) implemented")
def _generate_report(self):
"""Generate final analysis report."""
self.findings["framework"] = self.framework
self.findings["total_pages"] = len(self.pages)
self.findings["page_types"] = defaultdict(int)
for page in self.pages:
self.findings["page_types"][page["type"]] += 1
self.findings["page_types"] = dict(self.findings["page_types"])
# Calculate overall score
score = 100
if not self.findings.get("robots_txt", {}).get("exists"):
score -= 10
if not self.findings.get("sitemap", {}).get("exists"):
score -= 15
meta = self.findings.get("meta_tags", {})
pages_analyzed = min(len(self.pages), 20) or 1
title_ratio = meta.get("pages_with_title", 0) / pages_analyzed
desc_ratio = meta.get("pages_with_description", 0) / pages_analyzed
og_ratio = meta.get("pages_with_og", 0) / pages_analyzed
score -= int((1 - title_ratio) * 20)
score -= int((1 - desc_ratio) * 15)
score -= int((1 - og_ratio) * 10)
structured = self.findings.get("structured_data", {})
if not any(structured.values()):
score -= 15
self.findings["score"] = max(0, score)
self.findings["issues"] = dict(self.issues)
def print_report(self):
"""Print formatted analysis report."""
print("\n" + "=" * 60)
print("SEO ANALYSIS REPORT")
print("=" * 60)
print(f"\nOverall Score: {self.findings['score']}/100")
print(f"Framework: {self.findings['framework'].upper()}")
print(f"Total Pages: {self.findings['total_pages']}")
print("\nPage Types:")
for ptype, count in self.findings["page_types"].items():
print(f" {ptype}: {count}")
if self.findings["issues"]:
print("\n" + "-" * 40)
print("ISSUES FOUND")
print("-" * 40)
for priority, issues in self.findings["issues"].items():
if issues:
print(f"\n{priority.upper()} Priority:")
for issue in issues[:5]: # Show first 5
print(f" • {issue}")
print("\n" + "-" * 40)
print("RECOMMENDATIONS")
print("-" * 40)
recommendations = self._get_recommendations()
for i, rec in enumerate(recommendations, 1):
print(f"\n{i}. {rec['title']}")
print(f" {rec['description']}")
print(f" Priority: {rec['priority']}")
def _get_recommendations(self) -> List[Dict[str, str]]:
"""Generate prioritized recommendations."""
recommendations = []
if not self.findings.get("robots_txt", {}).get("exists"):
recommendations.append({
"title": "Add robots.txt",
"description": "Create a robots.txt file to control crawler access and link to sitemap.",
"priority": "CRITICAL"
})
if not self.findings.get("sitemap", {}).get("exists"):
recommendations.append({
"title": "Add XML Sitemap",
"description": f"Implement sitemap generation for {self.framework} to help search engines discover all pages.",
"priority": "HIGH"
})
meta = self.findings.get("meta_tags", {})
if meta.get("pages_with_description", 0) < len(self.pages):
recommendations.append({
"title": "Add Meta Descriptions",
"description": "Ensure all pages have unique, compelling meta descriptions (150-160 characters).",
"priority": "HIGH"
})
if meta.get("pages_with_og", 0) < len(self.pages):
recommendations.append({
"title": "Add Open Graph Tags",
"description": "Implement Open Graph tags for better social media sharing previews.",
"priority": "MEDIUM"
})
structured = self.findings.get("structured_data", {})
if not structured.get("organization"):
recommendations.append({
"title": "Add Organization Schema",
"description": "Add Organization JSON-LD to homepage for knowledge panel eligibility.",
"priority": "MEDIUM"
})
# Page-type specific recommendations
if self.findings["page_types"].get("article") and not structured.get("article"):
recommendations.append({
"title": "Add Article Schema",
"description": "Implement Article structured data on blog/article pages for rich results.",
"priority": "HIGH"
})
if self.findings["page_types"].get("product") and not structured.get("product"):
recommendations.append({
"title": "Add Product Schema",
"description": "Implement Product structured data on product pages for price/availability in search.",
"priority": "HIGH"
})
if self.findings["page_types"].get("faq") and not structured.get("faq"):
recommendations.append({
"title": "Add FAQ Schema",
"description": "Implement FAQPage structured data for expandable FAQ results in search.",
"priority": "MEDIUM"
})
return recommendations
def main():
if len(sys.argv) < 2:
print("Usage: python analyze_seo.py <project-path>")
print("\nAnalyzes a web project for SEO issues and opportunities.")
sys.exit(1)
project_path = sys.argv[1]
if not os.path.isdir(project_path):
print(f"Error: '{project_path}' is not a valid directory")
sys.exit(1)
analyzer = SEOAnalyzer(project_path)
analyzer.analyze()
analyzer.print_report()
# Optionally output JSON
if "--json" in sys.argv:
print("\n" + "-" * 40)
print("JSON OUTPUT")
print("-" * 40)
print(json.dumps(analyzer.findings, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Sitemap Generator - Generates sitemap.xml from project routes.
Usage:
python generate_sitemap.py <project-path> --domain https://example.com
Options:
--domain Required. The domain for URLs in the sitemap.
--output Output path (default: sitemap.xml in project root)
--priority Default priority (default: 0.7)
Outputs:
- sitemap.xml with all discovered routes
"""
import os
import sys
import re
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Optional
import xml.etree.ElementTree as ET
from xml.dom import minidom
class SitemapGenerator:
def __init__(self, project_path: str, domain: str):
self.project_path = Path(project_path).resolve()
self.domain = domain.rstrip('/')
self.routes: List[Dict] = []
self.framework: Optional[str] = None
def generate(self) -> str:
"""Generate sitemap XML content."""
self._detect_framework()
self._discover_routes()
return self._build_xml()
def _detect_framework(self):
"""Detect the web framework being used."""
indicators = {
"next.js": ["next.config.js", "next.config.mjs", "app/layout.tsx", "pages/_app.tsx"],
"astro": ["astro.config.mjs", "astro.config.ts"],
"gatsby": ["gatsby-config.js", "gatsby-config.ts"],
"static": ["index.html", "public/index.html"]
}
for framework, files in indicators.items():
for file in files:
if (self.project_path / file).exists():
self.framework = framework
print(f"Framework detected: {framework}")
return
self.framework = "static"
print("Framework: Static HTML assumed")
def _discover_routes(self):
"""Discover all routes based on framework."""
if self.framework == "next.js":
self._discover_nextjs_routes()
elif self.framework == "astro":
self._discover_astro_routes()
elif self.framework == "gatsby":
self._discover_gatsby_routes()
else:
self._discover_static_routes()
print(f"Routes discovered: {len(self.routes)}")
def _discover_nextjs_routes(self):
"""Discover routes from Next.js app or pages directory."""
# App Router
app_dir = self.project_path / "app"
if app_dir.exists():
for page_file in app_dir.glob("**/page.tsx"):
route = self._nextjs_path_to_route(page_file, app_dir)
if route:
self.routes.append(route)
for page_file in app_dir.glob("**/page.jsx"):
route = self._nextjs_path_to_route(page_file, app_dir)
if route:
self.routes.append(route)
# Pages Router
pages_dir = self.project_path / "pages"
if pages_dir.exists():
for page_file in pages_dir.glob("**/*.tsx"):
route = self._nextjs_pages_to_route(page_file, pages_dir)
if route:
self.routes.append(route)
for page_file in pages_dir.glob("**/*.jsx"):
route = self._nextjs_pages_to_route(page_file, pages_dir)
if route:
self.routes.append(route)
def _nextjs_path_to_route(self, page_file: Path, app_dir: Path) -> Optional[Dict]:
"""Convert Next.js App Router path to route."""
relative = page_file.parent.relative_to(app_dir)
path_str = str(relative)
# Skip special routes
if any(x in path_str for x in ["api", "_", "(", "@", "."]):
return None
# Convert path
if path_str == ".":
url_path = "/"
else:
url_path = "/" + path_str.replace("\\", "/")
return self._create_route_entry(url_path, page_file)
def _nextjs_pages_to_route(self, page_file: Path, pages_dir: Path) -> Optional[Dict]:
"""Convert Next.js Pages Router path to route."""
relative = page_file.relative_to(pages_dir)
path_str = str(relative)
# Skip special files
if any(x in path_str for x in ["_app", "_document", "_error", "api/", "404", "500"]):
return None
# Convert to URL path
url_path = "/" + path_str.replace("\\", "/")
url_path = re.sub(r"\.tsx?$", "", url_path)
url_path = re.sub(r"/index$", "/", url_path)
return self._create_route_entry(url_path, page_file)
def _discover_astro_routes(self):
"""Discover routes from Astro pages directory."""
pages_dir = self.project_path / "src" / "pages"
if not pages_dir.exists():
return
for page_file in pages_dir.glob("**/*.astro"):
route = self._astro_path_to_route(page_file, pages_dir)
if route:
self.routes.append(route)
for page_file in pages_dir.glob("**/*.md"):
route = self._astro_path_to_route(page_file, pages_dir)
if route:
self.routes.append(route)
for page_file in pages_dir.glob("**/*.mdx"):
route = self._astro_path_to_route(page_file, pages_dir)
if route:
self.routes.append(route)
def _astro_path_to_route(self, page_file: Path, pages_dir: Path) -> Optional[Dict]:
"""Convert Astro path to route."""
relative = page_file.relative_to(pages_dir)
path_str = str(relative)
# Skip dynamic routes and special files
if "[" in path_str or path_str.startswith("_"):
return None
# Convert to URL path
url_path = "/" + path_str.replace("\\", "/")
url_path = re.sub(r"\.(astro|md|mdx)$", "", url_path)
url_path = re.sub(r"/index$", "/", url_path)
return self._create_route_entry(url_path, page_file)
def _discover_gatsby_routes(self):
"""Discover routes from Gatsby pages directory."""
pages_dir = self.project_path / "src" / "pages"
if not pages_dir.exists():
return
for ext in ["tsx", "jsx", "js"]:
for page_file in pages_dir.glob(f"**/*.{ext}"):
relative = page_file.relative_to(pages_dir)
path_str = str(relative)
# Skip special files
if path_str.startswith("_") or "404" in path_str:
continue
url_path = "/" + path_str.replace("\\", "/")
url_path = re.sub(r"\.(tsx|jsx|js)$", "", url_path)
url_path = re.sub(r"/index$", "/", url_path)
route = self._create_route_entry(url_path, page_file)
if route:
self.routes.append(route)
def _discover_static_routes(self):
"""Discover routes from static HTML files."""
for html_file in self.project_path.glob("**/*.html"):
# Skip common non-page files
if any(x in str(html_file) for x in ["node_modules", "dist", ".next", "build"]):
continue
relative = html_file.relative_to(self.project_path)
url_path = "/" + str(relative).replace("\\", "/")
# Convert index.html to /
url_path = re.sub(r"/index\.html$", "/", url_path)
url_path = re.sub(r"\.html$", "/", url_path)
route = self._create_route_entry(url_path, html_file)
if route:
self.routes.append(route)
def _create_route_entry(self, url_path: str, file_path: Path) -> Dict:
"""Create a route entry with metadata."""
# Determine priority based on path depth and type
depth = url_path.count("/") - 1
if url_path == "/":
priority = 1.0
elif depth == 0:
priority = 0.8
elif "blog" in url_path or "article" in url_path:
priority = 0.6
else:
priority = max(0.5, 0.8 - (depth * 0.1))
# Determine change frequency
if url_path == "/":
changefreq = "weekly"
elif "blog" in url_path or "news" in url_path:
changefreq = "weekly"
elif "product" in url_path:
changefreq = "daily"
else:
changefreq = "monthly"
# Get last modified from file
try:
mtime = file_path.stat().st_mtime
lastmod = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d")
except:
lastmod = datetime.now().strftime("%Y-%m-%d")
return {
"url": url_path,
"lastmod": lastmod,
"changefreq": changefreq,
"priority": round(priority, 1)
}
def _build_xml(self) -> str:
"""Build the sitemap XML."""
urlset = ET.Element("urlset")
urlset.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
# Sort routes by priority (descending) then URL
sorted_routes = sorted(self.routes, key=lambda x: (-x["priority"], x["url"]))
for route in sorted_routes:
url_elem = ET.SubElement(urlset, "url")
loc = ET.SubElement(url_elem, "loc")
loc.text = self.domain + route["url"]
lastmod = ET.SubElement(url_elem, "lastmod")
lastmod.text = route["lastmod"]
changefreq = ET.SubElement(url_elem, "changefreq")
changefreq.text = route["changefreq"]
priority = ET.SubElement(url_elem, "priority")
priority.text = str(route["priority"])
# Pretty print
xml_str = ET.tostring(urlset, encoding="unicode")
dom = minidom.parseString(xml_str)
return '<?xml version="1.0" encoding="UTF-8"?>\n' + dom.toprettyxml(indent=" ").split("\n", 1)[1]
def main():
if len(sys.argv) < 2:
print("Usage: python generate_sitemap.py <project-path> --domain https://example.com")
print("\nGenerates sitemap.xml from project routes.")
print("\nOptions:")
print(" --domain Required. The domain for URLs in the sitemap.")
print(" --output Output path (default: sitemap.xml in project)")
sys.exit(1)
project_path = sys.argv[1]
if not os.path.isdir(project_path):
print(f"Error: '{project_path}' is not a valid directory")
sys.exit(1)
# Parse domain
domain = None
output_path = None
args = sys.argv[2:]
i = 0
while i < len(args):
if args[i] == "--domain" and i + 1 < len(args):
domain = args[i + 1]
i += 2
elif args[i] == "--output" and i + 1 < len(args):
output_path = args[i + 1]
i += 2
else:
i += 1
if not domain:
print("Error: --domain is required")
print("Example: python generate_sitemap.py ./project --domain https://example.com")
sys.exit(1)
generator = SitemapGenerator(project_path, domain)
xml_content = generator.generate()
# Output
if output_path:
output_file = Path(output_path)
else:
output_file = Path(project_path) / "sitemap.xml"
output_file.write_text(xml_content)
print(f"\nSitemap written to: {output_file}")
print(f"Total URLs: {len(generator.routes)}")
if __name__ == "__main__":
main()