SEO for Astro sites: a lightweight rank plan for content-first SaaS (as of July 2026)
TL;DR
How to do SEO for an Astro site: SSG-by-default, Content Collections, and @astrojs/sitemap give you a ranking foundation Next.js has to work harder to match. Here's the 9-item plan for any content-first Astro SaaS as of July 2026.
Astro is the fastest way to ship an SEO-optimized content-first SaaS site as of July 2026. SSG-by-default, zero client JavaScript by default, Content Collections for typed frontmatter, and a first-party sitemap integration mean 80% of the SEO work is done by Astro's defaults. You don't opt out of streaming. You don't opt out of hydration. There's nothing to hydrate. Crawlers get complete HTML on the first byte.
That's the good news. The bad news: Astro's SSG-first model constrains what you can build inside the same site. If your marketing/blog and your app dashboard live in one Astro repo, you'll fight the framework. If they're split (Astro for content, something else for the app) this is the SEO plan we'd run on the Astro half.
Companion post: our Next.js App Router SEO checklist covers the same layer for App Router. For what to publish inside these pages, see the solo-SaaS SEO five-page framework.
1. Astro's SSG-by-default advantage
Every page ships as complete HTML on first byte. Crawlers and AI engines that skip JavaScript still see the full content.
Astro renders every page to static HTML at build time. No React runtime ships to the client unless you explicitly hydrate a component with client:load or a similar directive. The first-byte HTML is complete: every H1, paragraph, link, and meta tag is there before any JavaScript runs.
For SEO this matters at three levels:
- 1Googlebot's rendering pipeline is faster because there's nothing to render. The HTML is complete on first fetch.
- 2AI engines that skip JavaScript execution (Perplexity, ChatGPT with browsing, Claude) can still fully parse your content.
- 3LCP is trivially fast because the DOM is complete from the first paint.
Combine this with edge caching on Vercel, Netlify, or Cloudflare Pages and every page serves in under 50ms globally. That's the Core Web Vitals ceiling for a content site.
2. @astrojs/sitemap + robots.txt
Install one integration, set your site URL, rebuild. You get a valid sitemap with zero code. robots.txt stays manual.
Install @astrojs/sitemap and add it to astro.config.mjs's integrations array. Set site in the config (required for sitemap URL generation). Rebuild. You get /sitemap-index.xml and /sitemap-0.xml automatically, listing every static route.
import { defineConfig } from 'astro/config';import sitemap from '@astrojs/sitemap';export default defineConfig({ site: 'https://yourdomain.com', integrations: [ sitemap({ changefreq: 'weekly', priority: 0.7, filter: (page) => !page.includes('/draft/'), }), ],});For per-page control (priority, changefreq, exclusions), pass options to the integration. Drafts, admin, and preview routes are typically filtered out.
For robots.txt, create public/robots.txt manually. Astro doesn't ship a first-party robots integration as of July 2026. A minimal file:
User-agent: *Allow: /Disallow: /admin/Disallow: /draft/Sitemap: https://yourdomain.com/sitemap-index.xml3. Content Collections + MDX for the blog
Typed frontmatter means missing meta tags become build errors, not silent SEO holes.
Astro Content Collections (introduced in Astro 2, mature in 3+) give you typed frontmatter for a blog. Create src/content/config.ts and define a Zod schema for each collection. Every MDX file in src/content/blog/ must satisfy the schema at build time. Miss a field, build fails. Type-safe by construction.
import { defineCollection, z } from 'astro:content';const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), description: z.string().min(50).max(160), publishedAt: z.date(), updatedAt: z.date().optional(), tags: z.array(z.string()).min(1), author: z.string(), image: z.string().url().optional(), canonical: z.string().url().optional(), draft: z.boolean().default(false), }),});export const collections = { blog };The blog post file itself is MDX: frontmatter block up top, JSX components mixed with markdown in the body. Import BaseLayout, ArticleHeader, CTABlock, whatever your components are. Content Collections make the frontmatter typed inside every layout and page that consumes them.
For routing, create src/pages/blog/[slug].astro. Use getCollection('blog') to load all posts, filter to published only, generate a static page per slug. Astro pre-renders every URL at build time.
As of July 2026 tourkit.us uses this exact Astro pattern for its content side. TTFB is under 40ms globally, LCP under 1 second.
4. BaseLayout.astro with slots
One 60-100 line file owns ~70% of your site's SEO surface. Change it in one place, applies everywhere.
Astro layouts use the <slot /> element for content injection. Create src/layouts/BaseLayout.astro with all the head tags (meta, OG, JSON-LD), the site header, the footer, and a single <slot /> in the middle. Every page imports BaseLayout and wraps its content in it.
---interface Props { title: string; description: string; canonical?: string; robots?: string; ogImage?: string;}const { title, description, canonical = new URL(Astro.url.pathname, Astro.site).toString(), robots = 'index,follow', ogImage = '/og-default.png',} = Astro.props;---<html lang="en"> <head> <meta charset="UTF-8" /> <title>{title}</title> <meta name="description" content={description} /> <link rel="canonical" href={canonical} /> <meta name="robots" content={robots} /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:image" content={new URL(ogImage, Astro.site)} /> </head> <body> <slot /> </body></html>For nested layouts (a BlogPostLayout that wraps BaseLayout and adds article-specific structure), Astro supports named slots: <slot name='header' /> inside a layout, populated via slot='header' on child elements from consumers. Use this to inject per-post breadcrumbs, table of contents, or related-posts widgets without duplicating markup.
5. Canonical URLs + meta robots per page
Compute canonical from Astro.site + Astro.url.pathname in the layout. Pick a trailing-slash policy and enforce it.
Astro exposes Astro.site (the site URL from config) and Astro.url (the current URL). Build canonical URLs by combining them:
const canonical = new URL(Astro.url.pathname, Astro.site).toString();Pass the result to BaseLayout as a prop. For pages that shouldn't be indexed (thank-you pages, internal previews, thin utility pages), pass robots='noindex,follow'. The layout renders the meta tag correctly.
Cross-domain canonicals for syndication: if you publish the same post on Medium or Dev.to, pass the canonical prop as the Medium URL from the original Astro page. Both platforms honor the canonical link.
As of July 2026 the Astro-recommended default is trailingSlash: 'ignore' for maximum compatibility across static hosts.
6. JSON-LD in the layout
Site-wide schema goes in BaseLayout. Per-page schema (Article, FAQPage) goes in the page. Validate every one before shipping.
Same pattern as Next.js. Inside BaseLayout.astro's <head>, render the JSON-LD as a script tag with set:html:
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)}/>Astro's set:html directive is the equivalent of React's dangerouslySetInnerHTML. Inlines the string without escaping.
Site-wide schema (Organization, WebSite, SoftwareApplication) goes in BaseLayout, loads on every page. Per-page schema (Article, FAQPage, HowTo, Product) goes in the specific layout (BlogPostLayout, ProductLayout) or the page itself.
FAQPage schema goes on posts with an FAQ block. Wrap the FAQ questions/answers in an <FAQ> MDX component that both renders the visible HTML and emits the JSON-LD to a nearby script tag. This is the pattern that gets you cited by ChatGPT, see our generative engine optimization checklist for why FAQPage schema is signal #2 in the citation-rate correlation.
7. Astro vs Next.js: the honest comparison
Astro wins for content-first sites. Next.js wins for app shells with per-request personalization. If you can split, do.
This is the split most people get wrong. Both frameworks are excellent. The tradeoff is real and it's about what your site is, not about framework preference.
Option A
Astro
Content-first sites
- SSG-by-default = faster crawler indexing + higher AI-engine citation rates.
- Content Collections enforce typed frontmatter at build.
- Near-zero client JavaScript. Default output is complete HTML.
- Best for: 90% marketing + blog + docs, small login link out to an external app.
Option B
Next.js
Product-first sites
- App Router streaming + Suspense = powerful for authenticated dashboards + real-time views.
- React Server Components let you mix static and dynamic in one page.
- Mature app ecosystem: auth, ORM adapters, third-party integrations.
- Best for: marketing + product in one repo, shared components across both.
If you must pick one for a solo SaaS starting today: content-first (blog is the acquisition channel, product is secondary or later) points to Astro. Product-first (app is the acquisition surface, blog exists but isn't the moat) points to Next.js. As of July 2026 tourkit.us is Astro; citeclip.com is Next.js. Both work.
8. What to do this week
One day of setup gets you 90% of the technical SEO surface. Then the work is content, not framework.
If you're on Astro:
- 1Install
@astrojs/sitemapand add yoursiteURL toastro.config.mjs. - 2Build
BaseLayout.astrowith the head-tag pattern above (~80 lines). - 3Move your blog to Content Collections with a Zod-typed schema.
- 4Deploy to Vercel or Netlify with edge caching enabled.
That's 90% of the technical SEO surface for a content site. Then apply the solo-SaaS SEO five-page framework to decide what to publish inside it. The mechanical layer here doesn't help if the content isn't right.
CiteClip drafts SEO + GEO-ready articles ready to drop into a Content Collections folder (with typed frontmatter, TL;DR, FAQ, and JSON-LD baked in). Sign up at citeclip.com, the first 4 articles are free, no credit card required.
Frequently asked
Is Astro good for SEO?
How do I add a sitemap to an Astro site?
@astrojs/sitemap and add it to the integrations array in astro.config.mjs. Set site: 'https://yourdomain.com' in the config (required for URL generation). Rebuild and you get /sitemap-index.xml automatically listing every static route. Add filter options for drafts, admin, or preview paths.Should I use Astro or Next.js for a new SaaS?
How do I add JSON-LD structured data in Astro?
<script type="application/ld+json"> tag inside your BaseLayout.astro's <head> using Astro's set:html directive (equivalent to React's dangerouslySetInnerHTML). Site-wide schema (Organization, WebSite) goes in the layout; per-page schema (Article, FAQPage) goes in the specific layout or page.How do I set canonical URLs in Astro?
Astro.site and Astro.url.pathname in your layout: new URL(Astro.url.pathname, Astro.site).toString(). Pass the result to BaseLayout as a prop and render it as <link rel="canonical">. Pick a trailing-slash policy in astro.config.mjs and match your deploy platform to prevent duplicate-content splits.