Skip to content
AstroCraft Docs
On this theme

SEO & Structured Data

8-BitQuest’s SEO layer is owned, not vendored. Every meta tag is emitted natively by BaseHead, every artifact — sitemap, robots, RSS, llms — is generated, and there is no SEO, robots or schema package anywhere in the dependency list. The whole surface derives from one setting (site) and typed config, so it is small to keep correct. The <head> mechanics also cover the theme bootstrap and view transitions (Colors and Motion); this page is the SEO that rides on top.

What BaseHead emits

src/layouts/BaseHead.astro owns the entire <head> with native tags. On every page it emits:

  • Document basics — charset, viewport, generator, the two above-the-fold font preloads, the favicons, the sitemap link, and the RSS alternate link.
  • Title, description and canonical. The canonical URL and og:url both come from one new URL(Astro.url.pathname, Astro.site), so they can never disagree.
  • Open Graphog:type (website, or article when an article prop is passed), title, description, url, site_name, locale, and the image with its alt, width and height. The image resolves to an absolute URL with a siteData.defaultImage fallback; a bundled image contributes its real dimensions, otherwise the 1200×630 convention is emitted. og:locale is normalised from the BCP-47 siteLocale to the language_TERRITORY form OG wants (en-USen_US).
  • Twitter — a summary_large_image card, plus twitter:creator from siteData.author.twitter (omitted entirely if the handle is empty).
  • Article tags — when a page passes the article prop, article:published_time, and article:modified_time / article:author when present.
  • noindex — a page can set noindex, which flips on the robots noindex, nofollow tag. The 404 and the dev-only catalog are the two routes that do.

Structured data

The JSON-LD builders live in src/js/schema.ts — dependency-free functions that each return a plain node. getSiteSchema composes an Organization + WebSite graph, emitted on every page, the two linked by stable @id so they cross-reference rather than duplicate. BaseHead builds it from siteData and serialises it, along with any page-specific nodes, into a single inline <script type="application/ld+json">.

Page authors never hand-write JSON-LD — they call a builder and pass the result up through the schema prop:

<BaseLayout
  schema={[articleSchema, breadcrumbSchema]}
  article={{ published: pubDate, modified: updatedDate, author: name }}
>

serializeJsonLd escapes < to &lt; so a value containing </script> cannot break out of the inline tag, and the builder logic carries a runnable self-check (schema.test.ts, run by pnpm test). sameAs — the social/profile URLs in siteData — feeds the Organization node, which is why keeping that list current matters for how search engines connect your brand.

The blog post nodes

A blog article passes two extra nodes: a BlogPosting (with the hero as its image, the author’s authorLink as author.url, and a publisher reference to the same site Organization @id that BaseHead emits everywhere) and a BreadcrumbList. The breadcrumb schema is paired with a visible Home › Blog › <title> breadcrumb on the page — the rule the theme holds is that a breadcrumb schema is only ever emitted alongside a visible one, so markup and structured data always agree. dateModified appears only when a post carries an updatedDate; it is never invented.

Crawlability and indexation

  • Sitemap@astrojs/sitemap emits /sitemap-index.xml, linked in BaseHead. A filter drops the noindex routes (the dev catalog and 404) so the sitemap never contradicts a page, and because the on-demand /contact/ route emits no static file, it is added by hand through the sitemap’s customPages.
  • robots.txt — a dynamic endpoint (src/pages/robots.txt.ts), not a /public file, so its Sitemap: line resolves against site and never drifts from the real domain. It allows everything and prerenders to a static /robots.txt.
  • llms.txt — a dynamic endpoint emitting a small, curated markdown content map for AI crawlers (the core pages, the blog and project indexes, the feed and legal pages), with every URL absolute. It is a hand-maintained index, not an auto-sitemap, and it is not a ranking factor — add new top-level entry points here as the site grows.
  • Trailing slashes — one URL shape, enforced by trailingSlash: "always", agreeing with the directory build, the canonical link and OG.

The omissions that are rules

Two things this layer does not do are deliberate, not gaps:

  • No hreflang. 8-BitQuest is single-language, and hreflang is only meaningful with two or more locales. The block was removed with the i18n layer; the theme’s wiki/ records the shape to restore if a project reintroduces locales.
  • No breadcrumb schema without a visible breadcrumb. The blog post is the one place breadcrumb structured data is emitted, precisely because it is the one place a visible breadcrumb is rendered. If you add a breadcrumb schema elsewhere, add the visible navigation first.

The one setting that drives it all

The site value in astro.config.mjs — the SITE_URL env var, defaulting to the https://example.com placeholder — feeds canonical, OG, the sitemap, robots, llms and RSS. Setting it once before deploy fixes every absolute URL at the same time, and a production deploy throws if it is still the placeholder. See Deployment.

NEXT STEPContact Form