All posts
Stack / how-to·10 min read

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 seo-for-nextjs-saas-app-router-checklist covers the same layer for App Router. For what to publish inside these pages, see solo-saas-seo-five-page-framework.

1. Astro's SSG-by-default advantage

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. First: Googlebot's rendering pipeline is faster because there's nothing to render — the HTML is complete. Second: AI engines (Perplexity, ChatGPT with browsing, Claude) that skip JavaScript execution can still fully parse your content. In our tests as of July 2026, JS-heavy React SPAs get cited by Perplexity at roughly one-third the rate of static HTML sites with identical content. Third: LCP 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. The tradeoff: interactive components (search boxes, comment forms, product configurators) need explicit client:* hydration. Do this per-component, not per-page. As of July 2026 Astro's Islands architecture (partial hydration) is the model — hydrate 3 small islands, not the full page. For a content-first SaaS blog + marketing site, that's usually zero islands. The site is HTML.

2. @astrojs/sitemap + robots.txt

Install @astrojs/sitemap and add it to astro.config.mjs's integrations array. Set site: 'https://yourdomain.com' in the config (required for sitemap URL generation). Rebuild. You get /sitemap-index.xml and /sitemap-0.xml automatically, listing every static route. For per-page control (priority, changefreq, exclusions), pass options to the integration: filter to exclude draft paths, set default changefreq to 'weekly', default priority to 0.7. 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: /, Sitemap: https://yourdomain.com/sitemap-index.xml, plus any Disallow lines for admin or draft paths. Two lines above your sitemap URL are enough for 95% of Astro sites. The trap: robots.txt is served from public/ verbatim. It doesn't get processed at build time. That means you can't template it. If your sitemap URL differs across environments (preview vs production), you'll need a small build script to substitute the correct URL — or accept that preview URLs advertise the production sitemap. As of July 2026 the @astrojs/sitemap integration is on version 3.x. Config is stable, no known SEO-relevant bugs.

3. Content Collections + MDX for the blog

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 — title, description, publishedAt, tags, author, image, canonical, robots. Every MDX file in src/content/blog/ must satisfy the schema at build time. Miss a field, build fails. Type-safe by construction. 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. Deploy to Vercel or Netlify and every post is served from the edge. The SEO wins here are all defaults. Typed frontmatter means every post has a title, description, and publish date — no missing meta tags. MDX means you can embed a `<TLDR>`, `<FAQ>`, or `<CodeSandbox>` component without leaving markdown. Every post's HTML is complete on first byte. 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

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. BaseLayout accepts props via Astro's frontmatter script section. Pattern: destructure title, description, canonical, robots (default 'index,follow'), and ogImage from Astro.props. The layout then renders `<title>`, `<meta name='description'>`, `<link rel='canonical'>`, `<meta name='robots'>`, and OG/Twitter tags. Each page overrides these by passing props. For nested layouts (a BlogPostLayout that wraps BaseLayout and adds article-specific structure), Astro supports slot names: `<slot name='header' />` and `<slot name='footer' />` 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. BaseLayout is typically 60-100 lines. It handles ~70% of the SEO surface area for the entire site. Everything else is per-page overrides. As of July 2026 tourkit.us's BaseLayout is 84 lines and covers title/meta/OG/Twitter/canonical/robots/JSON-LD/favicon/font preload. One file, one place to change SEO defaults.

5. Canonical URLs + meta robots per page

Astro exposes Astro.site (the site URL from config) and Astro.url (the current URL). Build canonical URLs by combining: 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' to BaseLayout. The layout renders the meta tag correctly. For the whole site's default policy, set robots='index,follow' as the BaseLayout default. 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. Trailing slash policy: Astro respects your config's trailingSlash setting ('ignore', 'always', or 'never'). Pick one, set it in astro.config.mjs, and match your deploy platform's config. Mixed trailing slashes fragment ranking signal across two URLs for the same content. As of July 2026 the Astro-recommended default is trailingSlash: 'ignore' for maximum compatibility across static hosts.

6. JSON-LD in the layout

Same pattern as Next.js. Inside BaseLayout.astro's `<head>`, render `<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. For blog posts, the Content Collections pattern makes Article schema trivial. Pull the frontmatter, spread it into the schema object: @type 'Article', headline from post.data.title, datePublished from post.data.publishedAt, author from post.data.author, image from post.data.image. Astro renders it at build time; the JSON-LD is static in the response. 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. Validate every schema in Google's Rich Results Test before shipping.

7. Astro vs Next.js — the honest comparison

Astro wins for content sites. Next.js wins for app shells with streamed content and per-request personalization. Astro's advantages: SSG-by-default means faster crawler indexing, higher AI-engine citation rates (per our July 2026 test set), and near-zero client JavaScript. Content Collections make typed frontmatter enforceable. The default output is HTML — no client-side rendering to worry about. If your site is 90% marketing + blog + docs + a small login link out to an external app, Astro is the correct choice. Next.js's advantages: App Router's streaming and Suspense are powerful for authenticated dashboards, real-time data views, and personalized pages. React Server Components let you mix static and dynamic within one page. If your marketing site and product live in one repo and share components, Next.js keeps that unified. The honest split: if you can afford two repos (Astro for marketing, Next.js for app), do it. Marketing SEO benefits from Astro's static-first model. Product velocity benefits from Next.js's mature app ecosystem. Deploy both to Vercel, or split across Netlify + Vercel. If you must pick one for a solo SaaS starting today: content-first (blog is the acquisition channel, product is secondary or later) → Astro. Product-first (app is the acquisition surface, blog exists but isn't the moat) → Next.js. As of July 2026 tourkit.us is Astro; citeclip.com is Next.js. Both work; the tradeoff is real.

8. What to do this week

If you're on Astro: install @astrojs/sitemap, build BaseLayout.astro with the head-tag pattern above, move your blog to Content Collections with a Zod-typed schema, and deploy 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 that framework — the mechanical layer here doesn't help if the content isn't right. If you're not on Astro yet and you're building content-first, this is the moment to switch — greenfield migration to Astro takes a day for a 20-page site. 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.


Draft the next post about your competitors — automatically

CiteClip monitors your competitors' blogs, runs gap analysis, and drafts SEO + GEO-ready articles with TL;DR + FAQ + JSON-LD schema baked in. Publish to WordPress with one click.