Astro SEO: Building a Fast, Search-Friendly Static Site
This tutorial is part of the SEO series for web developers. It assumes you have Node.js installed. If not, start with Getting Started With Node Package Manager.
This site has been rebuilt more than once. It started in 2004 as a custom PHP content management system, and it is now a static site built with Astro: about 270 pages of plain HTML generated at build time, with almost no JavaScript sent to the browser. This post explains why that setup is good for SEO, and shows the handful of pieces every Astro site needs to be search-friendly.
Why a static site helps SEO
- The HTML is complete before any JavaScript runs. Crawlers get the full content, links and structured data on the first request. They don't have to render a JavaScript app to find out what the page says.
- It's fast by default. Pre-built HTML served from disk or a CDN is about as fast as the web gets, which shows up directly in Core Web Vitals.
- Less to break. No database, no plugins to update, nothing to hack. A static site that was correct when you built it stays correct.
Astro's particular strength is that it ships zero JavaScript by default. Components render to HTML at build time, and you opt in to client-side JavaScript only for the parts that need it, like a search box or a menu toggle.
Setting up
npm create astro@latest my-site -- --template minimal
cd my-site
npx astro add sitemap
npm run dev
The one setting you must not skip is site in astro.config.mjs. Canonical URLs, the sitemap and Open Graph tags all need your real domain to build absolute URLs:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://www.example.com',
integrations: [sitemap()],
});
An SEO-ready layout
Put the tags every page needs into one layout component, so no page can forget them:
---
// src/layouts/Base.astro
const { title, description } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
</head>
<body>
<slot />
</body>
</html>
Each page then passes its own title and description. Make them unique. A short script that loops over the built HTML in dist/ and flags duplicate titles is worth having. This site runs one after every build.
Keeping old URLs
Moving to a new framework is exactly when URLs tend to change. Astro's file-based routing makes it easy to keep them. A catch-all route with getStaticPaths() can generate any path, including old .html URLs:
// src/pages/[...path].astro (frontmatter)
export function getStaticPaths() {
return posts.map((post) => ({
params: { path: post.legacyPath.replace(/^\//, '') }, // e.g. "dev/a33/Passing-Bundles-Around-Activities.html"
props: { post },
}));
}
Anything that can't keep its URL gets a 301 in the server config. See 301 Redirects with .htaccess and the Website Migration SEO Checklist.
The rest of the checklist
- Structured data: output JSON-LD from your layout (see JSON-LD Schema Markup).
- Images: Astro's <Image> component generates width and height (no layout shift) and modern formats. Give every image real alt text.
- robots.txt: a static file in public/ that points to /sitemap-index.xml.
- 404 page: src/pages/404.astro. Make it helpful, with a search box and links to your main sections.
- Lighthouse: run it on mobile for each page template. A plain Astro site usually scores 95–100 without any tuning.
Astro is not the only good choice. A well-built WordPress site can rank just as well. But it is hard to beat for content sites that don't need a database. For a deeper comparison of Astro, WordPress and Next.js from an SEO agency's point of view, see Southern Whale's article Astro SEO: Why It Outranks WordPress and Next.js Builds.