A practical guide for your team
Astro SEO checklist: verify the built site, not just the source.
Most Astro SEO problems are visible only in the built output: a canonical with an unexpected trailing slash, a sitemap date that changes on every deploy, a second Organization definition added by a component. This checklist covers each setting and ends with a free script that checks your dist folder before you deploy.
What to keep in mind
Set site, generate canonicals from it, keep sitemap URLs and canonicals identical, give lastmod only to pages whose content changed, render JSON-LD once per entity, use host redirects for real status codes, and run a build check that fails on regressions. On 13 October 2026 our script found no issues across the 68 sitemap URLs of this site and caught all 12 faults in a deliberately broken test site.
Run the build check first
Download check-astro-seo.mjs, put it in your project root and run it after a production build:
npm run build
node check-astro-seo.mjs https://www.your-domain.com distIt reads the sitemap written by @astrojs/sitemap and every page it lists. For each URL it checks that a built file exists, the entry has lastmod, there is exactly one canonical and it equals the sitemap URL, the page is not noindex, a title and meta description exist, og:url matches, there is exactly one <h1>, JSON-LD parses without any @id being fully defined twice, images have alt and internal links resolve. It also lists duplicate titles and descriptions. It has no dependencies and exits with code 1 when it finds a problem, so it can fail a CI build.
We tested it two ways on 13 October 2026. Against this site’s production build (Astro 7.3.2, @astrojs/sitemap 3.7.4) it reported 68 sitemap URLs and no issues. Against a small test site built to fail, created by make-fixture.mjs, it detected all 12 injected faults and passed the clean control page. The dated test results and README record both runs.
It does not fetch the live site, follow host redirects, read robots.txt, render JavaScript, validate Schema.org vocabulary or predict rankings. Use it alongside Search Console and a live crawl, not instead of them.
Set site and build canonicals from it
Set site in astro.config.mjs to your final production URL. The configuration reference says Astro uses it to generate the sitemap and canonical URLs, and Astro.site is undefined without it. In a shared layout, build the canonical from the path and the site rather than from the request host:
const canonical = new URL(Astro.url.pathname, Astro.site);
<link rel="canonical" href={canonical} />Decide on trailing slashes once. trailingSlash accepts 'always', 'never' or 'ignore', and build.format controls whether pages are written as page/index.html or page.html. The docs warn that for prerendered pages the hosting platform handles trailing slashes and may not follow your setting. Check the deployed URL, not only the config: request both forms and confirm one redirects to the other. This site uses trailingSlash: 'never', and a request for a guide URL ending in a slash returns a 308 redirect to the version without it.
Google treats redirects and rel=canonical as strong canonical signals and sitemap inclusion as a weaker one, and asks you not to send conflicting signals (consolidating duplicate URLs). In practice that means the sitemap URL, the canonical, og:url and your internal links should all be the same string. The build check compares the first three.
Give lastmod only to pages that changed
The sitemap integration accepts a site-wide lastmod, but that stamps every URL with the same date. Google says it uses lastmod only when it is consistently and verifiably accurate, and ignores changefreq and priority (build a sitemap). Setting lastmod to the build time on every deploy teaches Google to ignore it.
The integration cannot read a page’s source, so set per-page dates in serialize. This site keeps a short list of routes for each content release:
sitemap({
filter: (page) => !['/thank-you/workflow'].includes(new URL(page).pathname),
serialize(item) {
const path = new URL(item.url).pathname;
if (['/guides/astro-seo-checklist'].includes(path)) {
item.lastmod = '2026-10-13T00:00:00.000Z';
}
return item;
},
})Update a route’s date when its visible content changes, not when only a title or description changes. Use filter to keep thank-you pages, redirects and anything marked noindex out of the sitemap; the build check reports a noindex page that is still listed.
Write unique titles and descriptions
Google’s documentation says there is no limit on the length of a <title> and that title links are truncated as needed to fit the device (title links). It also says there is no limit on meta description length (snippets). Character targets are editorial choices, not Google rules. We aim for titles of about 60 characters and descriptions of 150–158 so the main point survives truncation.
What matters more is that each page has its own title and description that describe that page. Templates that repeat the same description across a section are easy to miss in source and obvious in the build. The check lists every duplicate title and description across the sitemap.
Keep one <h1> per page that says what the page is about. Google can also use the main heading, og:title and anchor text when it generates a title link, so keep them consistent with the title.
Render JSON-LD once per entity
Astro’s set:html directive is the usual way to output a JSON-LD script. The docs warn that the value is not escaped, so serialize the object and escape < before inserting it:
<script type="application/ld+json"
set:html={JSON.stringify(data).replace(/</g, '\u003c')} />Let the layout own site-wide entities such as Organization and WebSite, give them stable @id values and reference those IDs from page-level entities. A second full definition of the same @id, often added by a component or an SEO package, is what the build check reports as a duplicate. A reference that contains only @id is fine. Our Astro JSON-LD implementation guide walks through layout ownership, a Service-to-Organization graph and validator results.
Google’s structured data guidelines require markup to represent visible page content and do not guarantee rich results. Parseable JSON-LD is necessary, not sufficient: test eligible types with the Rich Results Test after deployment.
Use host redirects, a real 404 and robots.txt carefully
Astro’s redirects config behaves differently by output. Without an adapter, a static build writes an HTML file with a meta refresh for each redirect, and the docs note that the status code is not used by the server (routing). This site’s build still writes those fallback files, but our host configuration answers the same paths first with a 308 permanent redirect. Check yours with a request that does not follow redirects:
curl -sI https://www.your-domain.com/old-pathA 200 with a meta refresh is not a server redirect. Add the rules to your host’s redirect configuration or use an adapter that writes it.
Create src/pages/404.astro; Astro builds it to 404.html, which most hosts serve for unknown URLs (Astro pages). Confirm that a random URL returns status 404 rather than 200.
Do not use robots.txt to hide a page you have marked noindex. Google can only see the noindex rule if it is allowed to crawl the page (block indexing).
Check images and social tags
Astro’s <Image /> component requires alt; use alt="" for decorative images. It needs width and height for images in public/ and infers them for imported images in src/ (astro:assets). Plain <img> tags in components and Markdown are where missing alt usually slips through, which is why the build check scans the output rather than the components.
The Open Graph protocol lists og:title, og:type, og:image and og:url as required, and describes og:url as the canonical URL of the object (ogp.me). Generate it from the same canonical value as the link tag so they cannot drift apart.
Fail the build when a check regresses
Run the check in CI or as a postbuild script so a broken canonical or duplicate entity stops the deploy instead of reaching Search Console weeks later:
"scripts": {
"build": "astro check && astro build",
"postbuild": "node check-astro-seo.mjs https://www.your-domain.com dist"
}After each deploy, repeat the checks the script cannot do: request the live URLs, confirm redirects return 301 or 308, compare the live sitemap with your expectations and inspect key URLs in Search Console. Marsen Search runs these checks as part of technical SEO work, and our SEO service page explains how we scope a review of your site.
Does Astro generate canonical URLs automatically?
No. Astro uses the site setting to build URLs, but you add the canonical link yourself, usually in a shared layout. Build it from Astro.url.pathname and Astro.site so it matches the sitemap.
Should every sitemap entry have the same lastmod date?
No. Google uses lastmod only when it is consistently accurate. Set a date per route when its visible content changes, using the sitemap integration’s serialize option.
Do Astro redirects return a 301 on a static host?
Not by themselves. Without an adapter, a static build writes HTML files with a meta refresh. Configure redirects on your host, or use an adapter that writes the host configuration, and confirm the status code with a request.
What does the check script not cover?
It reads the built files only. It does not fetch the live site, follow host redirects, read robots.txt, render JavaScript, validate Schema.org vocabulary, measure speed or predict rankings.
Built around your business
Put these ideas to work in your business.
Book a demoShow us how your team works today. We’ll help you choose a useful first step.