Technical SEO for Static Sites
A static site hands search engines exactly what they want: complete HTML in the first response, a fast time to first byte from the edge, and URLs that never change unless you change them. That is why migrations from client-rendered apps to static generators so often show a rise in indexed pages. But the advantages stop at delivery. A static generator will happily publish two URLs for the same page, a sitemap that lists redirects, structured data that no longer validates, and a preview deploy that search engines index alongside production. None of those are visible in a browser.
This guide covers the technical SEO work that belongs in the build pipeline of a static site — the parts that can be generated from content and checked in CI — rather than keyword research or copywriting. It is written for developers and documentation teams running Astro, Eleventy, Hugo or Next.js static export, and it sits within Choosing the Right Static Site Generator for Production. Multilingual concerns such as hreflang are covered in Internationalization for Static Sites.
The measurements come from an Eleventy developer blog with 1,240 posts and a Hugo product documentation site with 3,900 pages, tracked in Google Search Console before and after each change.
Canonical URLs and One URL per Page
Most technical SEO problems on static sites are duplication problems. The same page is reachable at /guide, /guide/, /guide/index.html, with and without www, over HTTP and HTTPS, and — after a migration — at an old path that was copied rather than redirected. Search engines cope, but they split signals between variants and pick a canonical themselves, sometimes the wrong one.
The fix has two halves. Serve only one variant by redirecting the others at the host (a trailing-slash policy, www to apex, HTTP to HTTPS), and declare the chosen URL in every page with <link rel="canonical">, generated from the page's own path and the configured site origin. The details, including the traps specific to hosts that auto-append slashes, are in Canonical URLs and Duplicate Content on Static Sites.
{# Eleventy base layout #}
<link rel="canonical" href="{{ site.url }}{{ page.url }}">
---
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<link rel="canonical" href={canonical.href} />
On the Eleventy blog, a crawl with Screaming Frog found 312 URL pairs that served the same content with and without a trailing slash, because the host resolved both. After adding a host-level redirect and self-referencing canonicals, Search Console's "Duplicate, Google chose different canonical than user" report dropped from 287 URLs to 6 over five weeks.
Sitemaps That Match the Build
A sitemap is a list of URLs you want indexed. On a static site it should be generated from the same data that produces the pages, and it should contain exactly the indexable pages: no redirects, no 404s, no noindex pages, no preview hosts. That sounds obvious, but plugin defaults frequently include everything the generator emitted, including tag pages set to noindex and 404.html.
Large sites need a sitemap index, because a single sitemap is limited to 50,000 URLs and 50 MB uncompressed, and splitting by section makes Search Console coverage reports much easier to read. lastmod should come from the content's real modification date (frontmatter or Git), not the build time — a sitemap where every page changed today teaches crawlers to ignore lastmod. Generating Sitemaps for Large Static Sites covers generation per generator and a CI check that the sitemap and the build output agree.
On the Hugo documentation site, switching lastmod from build time to Git commit time (enableGitInfo = true) reduced the median time for an edited page to be recrawled from 9 days to 3, measured with the URL Inspection API on a sample of 60 edited pages.
Structured Data at Build Time
JSON-LD structured data tells search engines what a page is — an article with an author and dates, a how-to, a breadcrumb trail, a FAQ, a software application. Static generators can emit it from frontmatter with no runtime cost, and because it is generated rather than hand-written it stays consistent across thousands of pages.
The common failures are also consistent: dates in the wrong format, an image that 404s, a headline longer than the limit, HTML inside text fields, or a template change that breaks the JSON for every page at once. Adding JSON-LD Structured Data at Build Time shows how to build the objects in code rather than string templates, and Testing Structured Data and Meta Tags shows the CI side.
// Build JSON-LD as an object, then serialize once — never concatenate strings.
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: page.title.slice(0, 110),
datePublished: page.date.toISOString(),
dateModified: (page.updated ?? page.date).toISOString(),
author: { '@type': 'Organization', name: 'Example Docs' },
mainEntityOfPage: canonical
};
On the documentation site, moving from a Go template that concatenated JSON to a data structure passed through jsonify removed 214 "Unparsable structured data" errors in Search Console — every one caused by a title containing a double quote.
Feeds, Robots Rules and Preview Environments
Feeds. RSS and Atom are still how many developers follow blogs and changelogs, and feed readers are a meaningful traffic source for technical content. A good feed carries full content (or a real summary), absolute URLs, stable GUIDs and the right content type. See RSS and Atom Feeds for Static Blogs.
Robots rules. robots.txt controls crawling, not indexing; a disallowed page can still be indexed from links. Use noindex in a meta tag or X-Robots-Tag header to keep pages out of the index, and never disallow a page you want to noindex, because crawlers then cannot see the tag.
Previews. Every pull request preview and staging deploy is a full copy of the site at another URL. If one gets linked from a public issue tracker and crawled, it competes with production. Emit X-Robots-Tag: noindex on every non-production deploy, driven by an environment variable in the build or a header rule on the host. robots.txt and noindex for Preview and Staging has the configuration for Netlify, Vercel and Cloudflare.
Pagination, Tags and Thin Pages
Static blogs and documentation sites generate a lot of pages nobody writes: /page/2/ through /page/62/, a page per tag, a page per author, per year, per month. They help readers browse, but a tag with one post is a thin page that duplicates the post's summary, and very deep pagination spends crawl budget on pages that change every time a post is published.
The approach that worked on the Eleventy blog: keep pagination crawlable with plain <a href> links (not a "load more" button), give every paginated page a self-referencing canonical rather than pointing all of them at page one, and only generate tag pages for tags with at least five posts. Tags below the threshold are still displayed on posts but link to a search results page instead. Indexed-but-thin pages in Search Console fell from 418 to 37. SEO-Safe Pagination and Tag Pages has the Eleventy and Hugo configuration.
Validating SEO Output in CI
Everything above is deterministic, so it can be tested on every pull request against the built output rather than discovered weeks later in Search Console. A small suite covers most regressions:
- exactly one
<title>, one meta description and one canonical per page, and the canonical equals the page's own URL unless deliberately overridden; - titles and descriptions unique across the site, and within length limits;
- every JSON-LD block parses and has the required properties for its type;
- the sitemap lists exactly the pages that are indexable, and every listed URL exists in the build with a 200;
- no
noindexon production pages, andnoindexpresent on preview builds.
On the documentation site this suite runs in 11 seconds over 3,900 pages and caught 23 regressions in its first six months — most of them template edits that removed or duplicated a tag across a whole section. The test implementations are in Testing Structured Data and Meta Tags.
SEO Defaults by Generator
Generators differ a lot in how much of this they do out of the box, and that difference is worth weighing when choosing one — see the wider comparison in SSG Framework Selection Matrix.
Hugo ships an internal sitemap template, RSS templates for every section and taxonomy, .Permalink for canonicals, .Lastmod from Git with enableGitInfo, internal templates for Open Graph and schema.org meta, and robots.txt generation with enableRobotsTXT. Almost everything is present; the work is overriding the defaults that list too much, such as taxonomy feeds nobody subscribes to.
Astro provides official integrations for sitemaps (@astrojs/sitemap) and RSS (@astrojs/rss), Astro.site and Astro.url for canonicals, and nothing for structured data or robots rules — those are a few lines in a layout component. Starlight adds sensible head tags, canonical URLs and a sitemap for documentation sites.
Eleventy provides none of it by default and all of it through small templates and plugins: @11ty/eleventy-plugin-rss for feeds, a paginated template for sitemaps, a layout include for canonicals and JSON-LD. The explicitness makes it easy to see exactly what is emitted.
Next.js static export supports sitemap.ts and robots.ts metadata routes in the App Router, and the Metadata API sets canonicals via alternates.canonical. Feeds need a route handler that exports statically; details are in Generating Sitemaps and RSS in Next.js Static Export.
Whatever the generator, treat its defaults as a starting point to audit, not a finished job. On both measured sites, the default output included pages that should not have been indexed.
Open Graph and Social Cards
Open Graph and Twitter card tags are not ranking signals, but they decide how every shared link looks in chat apps, social networks and issue trackers — which is where a lot of developer content is discovered. The same build-time approach applies: og:title, og:description, og:url (equal to the canonical) and og:image from frontmatter, with an image generated per page at build time as in Generating Open Graph Images at Build Time. A CI check that every og:image URL exists in the build output prevents the broken-image previews that appear when an image path changes in a refactor.
Performance Is Part of Technical SEO
Core Web Vitals are a ranking signal, and static sites are well placed to pass them. The most common way static sites fail is not HTML delivery but what the HTML loads: an unoptimized hero image, render-blocking fonts, a heavy analytics tag. The performance side of the house is covered in Performance Optimization & Core Web Vitals for SSGs; from an SEO perspective, the useful habit is watching the Core Web Vitals report in Search Console for URL groups, because it groups pages by template, and template-level regressions are exactly what a static build produces.
Migrations Are Where Rankings Are Lost
The riskiest moment for a static site's search visibility is not a slow page or a missing tag; it is a migration between generators or hosts. URL shapes change subtly — a .html suffix dropped, a date removed from blog paths, a section renamed — and each changed URL without a redirect is a page whose accumulated links and rankings start again from zero. Both measured sites went through a migration before the work in this guide, and both had lost traffic to exactly this.
Treat a migration as an SEO project with three deliverables. First, a complete URL inventory from the old site — its sitemap, a crawl, and the top landing pages from analytics and Search Console — because the sitemap alone misses URLs that only external sites link to. Second, a redirect map from every old URL to its new equivalent, generated and tested in CI against the new build so that no redirect points at a 404. Third, a monitoring period of eight to twelve weeks watching Search Console's 404 report and the index coverage of each section sitemap. Keeping Redirects Working After an SSG Migration and Auditing Content Parity After a Migration cover the mechanics.
The canonical, sitemap and structured-data checks described above are what make the monitoring period quiet: with them in place, the only remaining source of problems is URLs the inventory missed.
Measured Impact
| Measure | Before | After |
|---|---|---|
| Duplicate-canonical URLs (Eleventy blog, Search Console) | 287 | 6 |
| Unparsable structured data (Hugo docs) | 214 | 0 |
| Median recrawl delay after edit (Hugo docs) | 9 days | 3 days |
| Thin indexed pages (Eleventy blog) | 418 | 37 |
| Preview URLs indexed (both sites) | 41 | 0 |
| SEO regressions caught in CI, first six months | — | 23 |
Common Pitfalls
- Canonical built from the request instead of config. Preview deploys then declare themselves canonical.
lastmodequal to build time. Every page looks changed on every deploy, so crawlers stop trusting it.- Disallowing a page you want deindexed. The crawler never sees the
noindex. - String-concatenated JSON-LD. One quote in a title breaks structured data for that page.
- Sitemaps listing redirects or 404s. Search Console flags them and trusts the sitemap less.
- Infinite scroll without links. Pages beyond the first are never discovered.
Key Takeaways
- Serve one URL per page and declare it with a self-referencing canonical from configuration.
- Generate sitemaps from the build's own page list, with real modification dates and only indexable URLs.
- Build JSON-LD as data and serialize it; validate it in CI.
- Keep previews out of the index with a host-wide
X-Robots-Tag: noindex. - Keep pagination crawlable and prune thin tag and archive pages.
- Test SEO output on every pull request; it is as deterministic as any other build artefact.
FAQ
Are static sites better for SEO than server-rendered ones?
Static sites start with advantages that matter to search engines, namely complete HTML in the first response, fast time to first byte from a CDN and stable URLs. They do not get sitemaps, canonical tags, structured data or crawl controls for free, and those still have to be generated correctly at build time.
Which technical SEO tasks can be fully automated in the build?
Sitemaps, canonical URLs, hreflang alternates, JSON-LD structured data, RSS and Atom feeds, robots meta tags per environment and Open Graph metadata can all be generated from frontmatter and site configuration. Validating them in CI is equally automatable.
Why do preview deploys need special SEO handling?
Preview and staging deploys contain the same content as production at different URLs. If they are crawled, they compete with production as duplicate content. They should send an X-Robots-Tag noindex header, and ideally be behind authentication.
How do I check technical SEO before deploying?
Run checks against the built output in CI, including one canonical per page that matches the URL, valid JSON-LD, a sitemap that lists exactly the indexable pages, unique titles and descriptions, and no noindex on production pages. Each is a short script over the HTML files.
Do tag and pagination pages hurt SEO?
Only when they are thin or duplicated. Tag pages with one or two entries and deep pagination with no unique value dilute crawl budget. Keep pagination crawlable with plain links, give each page a self-referencing canonical, and noindex or drop tag pages below a useful size.
Related
- Up: Choosing the Right Static Site Generator for Production — how SEO defaults factor into generator choice.
- Generating Sitemaps for Large Static Sites — sitemap indexes and real lastmod.
- Canonical URLs and Duplicate Content on Static Sites — one URL per page.
- Adding JSON-LD Structured Data at Build Time — structured data from frontmatter.
- RSS and Atom Feeds for Static Blogs — feeds that validate.
- robots.txt and noindex for Preview and Staging — keeping copies out of the index.
- SEO-Safe Pagination and Tag Pages — archives without thin pages.
- Internationalization for Static Sites — hreflang and multilingual URLs.
- Keeping Redirects Working After an SSG Migration — preserving rankings through a move.
- Fixing Pages Crawled but Not Indexed on Static Sites — work through Search Console's crawled but not indexed status on a static site: thin and duplicate pages, weak internal links and templates that hide content.
- Fixing Soft 404s on Static Hosts — find and fix soft 404s on static sites, from SPA fallback rewrites and 200-status error pages to empty list pages, on Cloudflare, Netlify, S3 and Nginx.
- Generating Breadcrumb Structured Data in Astro — generate visible breadcrumbs and matching BreadcrumbList JSON-LD in Astro from the URL or content collections, with readable names, and test both in CI.