Skip to content
helia-ui
Starlight plugin
HELIA HUB

Discoverability

Every HELIA site is a public front door, and two kinds of reader arrive at it: a search crawler that wants a title, a description, and a share card, and an agent that wants the page as plain text with links it can follow. The plugin serves both, once, so no site authors its own metadata and no two sites answer the same question differently.

Nothing here is opt-in. Adopting heliaStarlight() adopts the contract below.

Starlight’s own head is left alone. With site set in astro.config.mjs, it emits the page <title>, <link rel="canonical">, og:title, og:type, og:url, og:locale, og:description, og:site_name, twitter:card, <meta name="description">, and <link rel="sitemap">. Restating any of those would put two answers to one question in the same document, so the plugin adds only what Starlight leaves to the site.

TagValue
og:image, og:image:*The generated card for this route, absolute
twitter:title, twitter:descriptionThe page title and description
twitter:imageThe same card
<link rel="alternate" type="text/markdown">The markdown rendition of this route
<script type="application/ld+json">WebSite and Organization on the home page, TechArticle elsewhere, plus a BreadcrumbList built from the sidebar path
<meta name="robots" content="noindex, nofollow">Only on a page with draft: true

The head tags come from the Head override. A site that sets shell.head: false, or names its own Head in Starlight’s components map, takes them on itself.

A page with no description has no search result worth reading and no line in llms.txt. The build fails and lists every page that is missing one, rather than shipping the site description in its place on a dozen routes.

An index page whose title is the whole of it opts out in frontmatter:

---
title: Reference
descriptionOptional: true
---

Use it sparingly. It is for a page that exists to hold a section together, not for a page nobody has written a description for yet.

Each route gets og.svg beside its index.html: a 1200 by 630 card carrying the page title, the site title, and the Ambiq mark, drawn on the token surface in Roboto.

It is an SVG rather than a PNG. Rasterizing text needs a font rasterizer, and none of the ways to get one — satori with resvg, or a headless browser — is a dependency the package takes on for a social preview. An SVG card is served and indexed like any other image, but some social scrapers will not rasterize one. A site that needs a PNG sets discoverability: { ogImage: false } and supplies its own through Starlight’s head config.

Four artifacts land at the site root, under the base path.

URLWhat it is
<route>/index.mdThe page’s own markdown, frontmatter stripped, links resolved to absolute site URLs, component tags reduced to their text or to what the part stated, code fences untouched
llms.txtThe site title, its one-line description, and every route with its title and description, grouped by sidebar section and linking to the renditions
llms-full.txtEvery rendition concatenated in sidebar order
content-index.jsonRoute, title, description, section, headings, source path, and the git commit date per route

The renditions are a pure function of the source tree, the sidebar and the pages the build just wrote, so two builds of one commit produce identical bytes and a deploy diff shows only what actually changed.

robots.txt is written with a Sitemap: line if the site does not already ship one; a site with its own keeps it. An RSS feed at rss.xml is written when the site has a blog content collection, and skipped when it has none.

A rendition is reduced from the authored source, which cannot see a value a component was handed: a transcript whose lines are imported from a module, a card whose title and line come from a record, a button whose label is an expression. Those parts — AsciiTerminal, LinkCard, a Card whose CardHeader carries the link, and a linked Button — write their own markdown into a hidden block in the built page, and the pass splices it in where the component sits. A site needs no code of its own for this, and the block renders nothing, is not indexed by search, and is not in the page’s JSON-LD.

A kind is spliced only where the page rendered as many of them as the source has occurrences. A grid built by mapping over a model is one tag in the source and a card per record on the page, so the sequence that anchors one to the other is not there and the source form stands. The build says so, naming the route and the kind, and a site that composes its own rendition for a generated table or catalog should keep doing so.

A tag the page imported from somewhere other than this package is left alone: LinkCard is Starlight’s name as well as this one’s, and a page writing Starlight’s card, or a component of its own under one of these names, is not writing a part that states anything.

check:discoverability reads the contract back off the built site: every HTML route carries a title, a description, a canonical link, og:title, og:description, and og:image; every route has a markdown rendition beside it; and llms.txt lists all of them. It runs in this site’s assert step and in the hub’s validate.

Terminal window
npm run build
node node_modules/@ambiqai/helia-ui/scripts/check-discoverability.mjs --root .

Astro’s 404 page and its redirect stubs are skipped: neither has a source page, and a redirect stub already carries noindex and a canonical link to the route it forwards to.