Astro doesn’t ship SEO defaults. Every page tag — title, meta, canonical, hreflang — is your responsibility. This is the short list you need to get right.
Background
SEO basics are not a bonus; they are table stakes. Astro gives you full control of the <head>, which means you have to put the right tags in yourself. The good news: the right setup fits in one <BaseHead> component used across the whole site.
How to tell
- You’re shipping any Astro site that depends on Google traffic.
- You have or plan to have multiple language versions.
- You publish or update content regularly.
- You want to avoid the long tail of indexing problems that come from missing tags.
Quick verdict
Build one <BaseHead> component, use it on every page, and never inline meta tags in individual pages.
Step by step
- Configure
astro.config.mjssoAstro.siteis set — every absolute URL in<head>depends on this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://yourdomain.com',
trailingSlash: 'always',
build: { format: 'directory' },
});
- Create
src/components/BaseHead.astro. This is the only place meta tags are emitted:
---
export interface Props {
title: string;
description: string;
lang?: 'en' | 'zh';
translationKey?: string;
ogImage?: string;
}
const { title, description, lang = 'en', translationKey, ogImage } = Astro.props;
if (!title || !description) {
throw new Error(`BaseHead requires title + description. Got: ${Astro.url.pathname}`);
}
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const ogImg = new URL(ogImage ?? '/og-default.png', Astro.site).toString();
---
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImg} />
<meta property="og:locale" content={lang === 'zh' ? 'zh_CN' : 'en_US'} />
<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={ogImg} />
{translationKey && (
<>
<link rel="alternate" hreflang="en"
href={new URL(`/en/articles/${translationKey}/`, Astro.site).toString()} />
<link rel="alternate" hreflang="zh"
href={new URL(`/zh/articles/${translationKey}/`, Astro.site).toString()} />
<link rel="alternate" hreflang="x-default"
href={new URL(`/en/articles/${translationKey}/`, Astro.site).toString()} />
</>
)}
- Use it once at the top of your
BaseLayout.astro:
---
import BaseHead from '../components/BaseHead.astro';
const { title, description, lang, translationKey } = Astro.props;
---
<html lang={lang}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<BaseHead {title} {description} {lang} {translationKey} />
</head>
<body><slot /></body>
</html>
- The rendered HTML you should see on
view-source:is exactly this shape:
<title>Astro SEO Basics: Title, Meta, Canonical, Hreflang</title>
<meta name="description" content="The minimum-viable SEO setup..." />
<link rel="canonical" href="https://yourdomain.com/en/articles/astro-seo-basics/" />
<meta property="og:title" content="Astro SEO Basics..." />
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/articles/astro-seo-basics/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/articles/astro-seo-basics/" />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/articles/astro-seo-basics/" />
- Audit production with a one-liner — any URL missing canonical or hreflang prints below:
for u in $(cat sitemap-urls.txt); do
curl -s "$u" | grep -E 'rel="canonical"|hreflang=' \
| wc -l | awk -v u=$u '$1 < 3 { print u, "missing tags" }'
done
-
Plug the canonical URL of a representative page into Google’s Rich Results Test — it parses the
<head>and surfaces silent errors (broken canonical, malformed OG, etc.). -
After deploy, request indexing for the homepage and one article via Search Console’s URL Inspection tool — don’t wait for the next crawl.
Common pitfalls
- Letting individual pages override
<title>inline — eventually some pages will be missing it. - Setting canonical to a relative path; absolute is the only safe form.
- Forgetting hreflang
x-default— Google falls back to wrong language guesses. - Duplicating Open Graph and Twitter tags with inconsistent values.
- Using JavaScript to set meta tags client-side — search engines may not run it.
Who this is for
Any indie Astro site that wants reliable SEO without an SEO consultant.
When to skip this
Private internal tools where SEO is actively unwanted; in that case use <meta name="robots" content="noindex"> globally.
FAQ
- Do I need structured data on day one?: No. Title, description, canonical, and hreflang are enough to start. Add structured data (Article, FAQPage) once the basics are stable.
- Should canonical always point to the current URL?: Yes, with a few exceptions like paginated lists pointing to page 1. Self-canonical is the safe default.
- How long is a good title in 2026?: Aim for 50-60 characters. Long titles get truncated in SERP and lose impact.
- What if my page has no description?: Generate one from the first paragraph as a fallback. Empty descriptions get auto-generated by Google, usually worse than yours.
Related
- Setting up sitemap.xml properly in Astro
- Building category and tag pages in Astro
- Building a Markdown / MDX content site that scales
- How to Use AI to Audit an Astro Content Site (Without Reading Every File)
- SEO Title Prompts: 15 Templates for Click-Worthy Search Titles
- Deploying an Astro Site to Firebase Hosting
Tags: #Indie dev #Astro #SEO #Technical SEO #Canonical #hreflang