SEO for Astro: Making the Fastest Framework Also the Smartest
Philosophy: Astro’s “content-first, JavaScript-last” approach gives you a massive SEO advantage. But to turn that raw performance into actual organic visibility, you need to wire structured data, routing, metadata, and caching correctly.
Why Astro Excels at SEO
Section titled “Why Astro Excels at SEO”Core Advantages
Section titled “Core Advantages”- Zero JavaScript by Default - Googlebot receives full, clean HTML with no hydration delay or broken metadata
- Performance Out of the Box - Fast page loads directly impact SEO rankings
- Clean HTML Output - Search engines reward clarity and proper semantic markup
- Edge-Ready - Deploy globally with minimal latency
The SEO Reality
Section titled “The SEO Reality”Performance alone doesn’t guarantee SEO success. Search engines reward:
- Clean HTML with proper semantic structure
- Accurate metadata that describes your content
- Structured relationships between pages
- Structured data that helps search engines understand your content
- Proper caching and canonical URLs
In 2025, reaching the top of search results no longer guarantees site visits, but laying the foundation for structured content benefits both search engines and LLMs.
Essential SEO Components
Section titled “Essential SEO Components”1. Meta Tags & Metadata
Section titled “1. Meta Tags & Metadata”Meta tags are the foundation of on-page SEO. Every page should have:
---const title = "Your Page Title - Brand Name";const description = "Compelling description that appears in search results (150-160 characters)";const canonical = new URL(Astro.url.pathname, Astro.site).href;---
<html lang="en"><head> <!-- Essential Meta Tags --> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags --> <title>{title}</title> <meta name="title" content={title} /> <meta name="description" content={description} />
<!-- Canonical URL (prevents duplicate content issues) --> <link rel="canonical" href={canonical} />
<!-- Open Graph / Facebook --> <meta property="og:type" content="website" /> <meta property="og:url" content={canonical} /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:image" content="/og-image.jpg" />
<!-- Twitter --> <meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:url" content={canonical} /> <meta property="twitter:title" content={title} /> <meta property="twitter:description" content={description} /> <meta property="twitter:image" content="/twitter-image.jpg" /></head><body> <!-- Your content --></body></html>Best Practices for Meta Tags
Section titled “Best Practices for Meta Tags”- Title: 50-60 characters, include primary keyword, add brand name
- Description: 150-160 characters, compelling, include call-to-action
- Canonical URL: Always use absolute URLs, be consistent across pages
- Images: Use dedicated social media images (OG: 1200×630px, Twitter: 1200×675px)
2. Sitemap Generation
Section titled “2. Sitemap Generation”A sitemap helps search engines discover and index your content efficiently.
Installation
Section titled “Installation”pnpm add @astrojs/sitemapConfiguration
Section titled “Configuration”import { defineConfig } from 'astro/config';import sitemap from '@astrojs/sitemap';
export default defineConfig({ site: 'https://yourdomain.com', // Required for sitemap integrations: [ sitemap({ // Optional: Customize URLs filter: (page) => !page.includes('/admin/'), // Optional: Change frequency hints changefreq: 'weekly', priority: 0.7, // Optional: Add last modification date lastmod: new Date(), }), ],});What You Get
Section titled “What You Get”After building your site, Astro automatically generates /sitemap-index.xml containing all your pages. This file should be referenced in your robots.txt.
Dynamic Sitemap (for large sites)
Section titled “Dynamic Sitemap (for large sites)”---import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ site }) => { const pages = [ { url: '/', lastmod: '2025-01-01', priority: 1.0 }, { url: '/about/', lastmod: '2025-01-01', priority: 0.8 }, // Fetch dynamic routes from CMS/database ];
const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${pages.map(({ url, lastmod, priority }) => ` <url> <loc>${site}${url}</loc> <lastmod>${lastmod}</lastmod> <priority>${priority}</priority> </url>`).join('')}</urlset>`;
return new Response(sitemap, { headers: { 'Content-Type': 'application/xml' }, });};3. Robots.txt
Section titled “3. Robots.txt”The robots.txt file guides search engine crawlers on which pages to crawl and which to ignore.
Using astro-robots-txt Plugin
Section titled “Using astro-robots-txt Plugin”pnpm add astro-robots-txtimport { defineConfig } from 'astro/config';import robotsTxt from 'astro-robots-txt';
export default defineConfig({ site: 'https://yourdomain.com', integrations: [ robotsTxt({ sitemap: true, // Automatically includes sitemap URL policy: [ { userAgent: '*', allow: '/', disallow: ['/admin/', '/api/private/'], }, ], }), ],});Manual Implementation
Section titled “Manual Implementation”---import type { APIRoute } from 'astro';
export const GET: APIRoute = ({ site }) => { const robotsTxt = `User-agent: *Allow: /
# Block admin areasDisallow: /admin/Disallow: /api/private/
# SitemapSitemap: ${site}sitemap-index.xml`.trim();
return new Response(robotsTxt, { headers: { 'Content-Type': 'text/plain' }, });};Best Practices
Section titled “Best Practices”- Always include your sitemap URL
- Block sensitive areas (
/admin/,/api/private/) - Use specific rules for different user agents if needed
- Test with Google Search Console
4. Structured Data (JSON-LD)
Section titled “4. Structured Data (JSON-LD)”Structured data helps search engines understand your content and can result in rich snippets in search results.
Organization Schema
Section titled “Organization Schema”---const schema = { "@context": "https://schema.org", "@type": "Organization", "name": "Your Company Name", "url": "https://yourdomain.com", "logo": "https://yourdomain.com/logo.png", "description": "Your company description", "sameAs": [ "https://twitter.com/yourhandle", "https://linkedin.com/company/yourcompany", "https://github.com/yourorg" ], "contactPoint": { "@type": "ContactPoint", "telephone": "+1-555-555-5555", "contactType": "customer service" }};---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />Article Schema (for blog posts)
Section titled “Article Schema (for blog posts)”---interface Props { title: string; description: string; publishDate: Date; author: string; image: string;}
const { title, description, publishDate, author, image } = Astro.props;const canonical = new URL(Astro.url.pathname, Astro.site).href;
const schema = { "@context": "https://schema.org", "@type": "Article", "headline": title, "description": description, "image": image, "datePublished": publishDate.toISOString(), "dateModified": publishDate.toISOString(), "author": { "@type": "Person", "name": author }, "publisher": { "@type": "Organization", "name": "Your Site Name", "logo": { "@type": "ImageObject", "url": "https://yourdomain.com/logo.png" } }, "mainEntityOfPage": { "@type": "WebPage", "@id": canonical }};---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />Product Schema (for e-commerce)
Section titled “Product Schema (for e-commerce)”---interface Props { product: { name: string; description: string; image: string; price: number; currency: string; availability: 'InStock' | 'OutOfStock'; rating?: number; reviewCount?: number; };}
const { product } = Astro.props;
const schema = { "@context": "https://schema.org", "@type": "Product", "name": product.name, "description": product.description, "image": product.image, "offers": { "@type": "Offer", "price": product.price, "priceCurrency": product.currency, "availability": `https://schema.org/${product.availability}` }};
if (product.rating && product.reviewCount) { schema["aggregateRating"] = { "@type": "AggregateRating", "ratingValue": product.rating, "reviewCount": product.reviewCount };}---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />Breadcrumb Schema
Section titled “Breadcrumb Schema”---interface Breadcrumb { name: string; url: string;}
interface Props { breadcrumbs: Breadcrumb[];}
const { breadcrumbs } = Astro.props;
const schema = { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": breadcrumbs.map((crumb, index) => ({ "@type": "ListItem", "position": index + 1, "name": crumb.name, "item": crumb.url }))};---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />Validation
Section titled “Validation”Always validate your structured data:
5. Creating an SEO Component
Section titled “5. Creating an SEO Component”Centralize your SEO metadata in a reusable component:
---export interface Props { title: string; description: string; image?: string; article?: { publishDate: Date; author: string; }; noindex?: boolean;}
const { title, description, image = '/default-og.jpg', article, noindex = false,} = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site).href;const imageUrl = new URL(image, Astro.site).href;const siteName = "Your Site Name";const fullTitle = `${title} | ${siteName}`;---
<!-- Primary Meta Tags --><title>{fullTitle}</title><meta name="title" content={fullTitle} /><meta name="description" content={description} />{noindex && <meta name="robots" content="noindex, nofollow" />}
<!-- Canonical URL --><link rel="canonical" href={canonical} />
<!-- Open Graph --><meta property="og:type" content={article ? "article" : "website"} /><meta property="og:url" content={canonical} /><meta property="og:title" content={fullTitle} /><meta property="og:description" content={description} /><meta property="og:image" content={imageUrl} /><meta property="og:site_name" content={siteName} />
<!-- Twitter --><meta property="twitter:card" content="summary_large_image" /><meta property="twitter:url" content={canonical} /><meta property="twitter:title" content={fullTitle} /><meta property="twitter:description" content={description} /><meta property="twitter:image" content={imageUrl} />
{article && ( <> <meta property="article:published_time" content={article.publishDate.toISOString()} /> <meta property="article:author" content={article.author} /> </>)}Usage:
---import SEO from '@/components/SEO.astro';---
<html><head> <SEO title="My Amazing Blog Post" description="Learn how to do something amazing" image="/blog/my-post-cover.jpg" article={{ publishDate: new Date('2025-01-15'), author: 'John Doe' }} /></head></html>Performance & Technical SEO
Section titled “Performance & Technical SEO”1. Cache Headers
Section titled “1. Cache Headers”Proper caching improves performance and SEO rankings:
export default defineConfig({ // For Cloudflare Pages adapter: cloudflare({ mode: 'directory', }), output: 'server', // or 'hybrid'});---import type { APIRoute } from 'astro';
export const GET: APIRoute = () => { return new Response('content', { headers: { 'Cache-Control': 'public, max-age=3600, s-maxage=3600', 'CDN-Cache-Control': 'public, max-age=86400', }, });};Cache Strategy Recommendations:
- Static pages:
max-age=3600, s-maxage=86400(1 hour browser, 1 day CDN) - Blog posts:
max-age=3600, s-maxage=604800(1 hour browser, 1 week CDN) - Dynamic content:
max-age=60, s-maxage=300(1 min browser, 5 min CDN) - API responses:
max-age=0, s-maxage=60(no browser cache, 1 min CDN)
2. Image Optimization
Section titled “2. Image Optimization”Optimize images for faster loading and better SEO:
---import { Image } from 'astro:assets';import myImage from '../assets/hero.jpg';---
<Image src={myImage} alt="Descriptive alt text for SEO" width={1200} height={630} format="webp" loading="lazy" decoding="async"/>Best Practices:
- Always provide descriptive
alttext - Use modern formats (WebP, AVIF)
- Specify
widthandheightto prevent layout shift - Use
loading="lazy"for below-the-fold images - Optimize file sizes (aim for <100KB for hero images)
3. Core Web Vitals
Section titled “3. Core Web Vitals”Monitor and optimize for Core Web Vitals:
-
LCP (Largest Contentful Paint): < 2.5s
- Optimize images and fonts
- Use server-side rendering
- Minimize JavaScript
-
FID (First Input Delay): < 100ms
- Minimize JavaScript execution
- Use code splitting
- Defer non-critical scripts
-
CLS (Cumulative Layout Shift): < 0.1
- Set image dimensions
- Reserve space for ads/embeds
- Avoid inserting content above existing content
Content Strategy
Section titled “Content Strategy”1. Semantic HTML
Section titled “1. Semantic HTML”Use proper HTML5 semantic elements:
<article> <header> <h1>Article Title</h1> <time datetime="2025-01-15">January 15, 2025</time> </header>
<section> <h2>Section Heading</h2> <p>Content...</p> </section>
<footer> <p>Author information</p> </footer></article>2. Heading Hierarchy
Section titled “2. Heading Hierarchy”Maintain proper heading structure:
<h1>Page Title (only one per page)</h1> <h2>Main Section</h2> <h3>Subsection</h3> <h3>Another Subsection</h3> <h2>Another Main Section</h2>3. Internal Linking
Section titled “3. Internal Linking”Build a strong internal linking structure:
<nav aria-label="Primary navigation"> <a href="/">Home</a> <a href="/blog/">Blog</a> <a href="/about/">About</a></nav>
<!-- Contextual links in content --><p> Learn more about <a href="/guides/seo/">SEO best practices</a> and how to implement them.</p>Monitoring & Analytics
Section titled “Monitoring & Analytics”1. Google Search Console
Section titled “1. Google Search Console”Set up Search Console to monitor:
- Index coverage
- Search performance
- Core Web Vitals
- Mobile usability
- Security issues
2. Performance Monitoring
Section titled “2. Performance Monitoring”<script> // Report Web Vitals import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics(metric) { const body = JSON.stringify(metric); // Send to your analytics endpoint fetch('/api/analytics', { method: 'POST', body }); }
getCLS(sendToAnalytics); getFID(sendToAnalytics); getLCP(sendToAnalytics);</script>SEO Checklist
Section titled “SEO Checklist”Pre-Launch
Section titled “Pre-Launch”- Set
siteURL inastro.config.mjs - Install and configure
@astrojs/sitemap - Create
robots.txtfile - Add meta tags to all pages
- Implement canonical URLs consistently
- Add structured data (JSON-LD) where appropriate
- Optimize all images (WebP, proper sizes, alt text)
- Test mobile responsiveness
- Validate structured data with Google Rich Results Test
- Check all internal links work
- Set up 404 page
- Configure proper cache headers
Post-Launch
Section titled “Post-Launch”- Submit sitemap to Google Search Console
- Submit sitemap to Bing Webmaster Tools
- Monitor Core Web Vitals
- Check index coverage in Search Console
- Set up performance monitoring
- Test page speed with PageSpeed Insights
- Verify social media previews (Twitter, LinkedIn, Facebook)
Ongoing
Section titled “Ongoing”- Monitor search rankings
- Update content regularly
- Add new content with proper SEO metadata
- Fix any crawl errors
- Monitor and improve Core Web Vitals
- Update structured data as needed
- Build quality backlinks
- Refresh old content
Tools & Resources
Section titled “Tools & Resources”Validation & Testing
Section titled “Validation & Testing”- Google Rich Results Test
- Schema.org Validator
- PageSpeed Insights
- Mobile-Friendly Test
- Twitter Card Validator
- Facebook Sharing Debugger
Monitoring
Section titled “Monitoring”SEO Packages
Section titled “SEO Packages”- @astrojs/sitemap
- astro-robots-txt
- astro-seo - Alternative SEO component library
Pro Tips
Section titled “Pro Tips”-
Use SSR/SSG Strategically
- Static (SSG) for content that rarely changes
- Server (SSR) for dynamic, personalized content
- Hybrid mode for mixing both approaches
-
Canonical Consistency is Critical
- Always use absolute URLs
- Ensure trailing slash consistency
- Test all pages for proper canonical tags
-
E-commerce Specific
- Use static builds for category pages
- Use SSR for product availability
- Implement product schema on all product pages
- Add review schema when available
-
Content-First Approach
- Write content for humans first
- Structure content with proper headings
- Use descriptive link text (avoid “click here”)
- Add alt text that describes the image, not just keywords
-
Mobile-First
- Design for mobile first
- Test on real devices
- Optimize for touch interactions
- Ensure text is readable without zooming
-
Page Speed Matters
- Minimize JavaScript
- Use Astro’s partial hydration strategically
- Optimize images aggressively
- Leverage Cloudflare’s CDN
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”- Missing Canonical URLs - Always set canonical URLs to avoid duplicate content penalties
- Inconsistent Trailing Slashes - Choose one convention and stick to it
- Missing Alt Text - Every image should have descriptive alt text
- Duplicate Meta Descriptions - Write unique descriptions for each page
- Ignoring Mobile - Mobile-first indexing means mobile experience matters most
- Slow Page Speed - Performance directly impacts SEO rankings
- Missing Structured Data - Don’t miss out on rich snippets
- No Sitemap - Make it easy for search engines to discover your content
- Broken Internal Links - Regularly audit and fix broken links
- Not Monitoring Search Console - Stay on top of crawl errors and issues
Next Steps
Section titled “Next Steps”- Audit Your Current Site: Run through the checklist above
- Implement Missing Components: Start with meta tags, sitemap, and robots.txt
- Add Structured Data: Implement relevant schemas for your content type
- Monitor Performance: Set up Search Console and analytics
- Iterate and Improve: SEO is ongoing - keep optimizing
Related Documentation:
- Fenod Stack README - Main stack documentation
- Development Strategy Guide - UI-first workflow
Last Updated: January 2025