Skip to content
helia-ui
Starlight plugin
HELIA HUB

Starlight plugin

heliaStarlight() is how a Starlight site adopts the theme. It does two things: it splices the package stylesheets into customCss in the order the cascade depends on, and it installs the shell components that carry no site vocabulary, which is the theme menu, the mobile menu toggle, and the footer.

import starlight from '@astrojs/starlight';
import { heliaStarlight } from '@ambiqai/helia-ui/starlight';
starlight({
customCss: ['./src/styles/tailwind.css'],
plugins: [
heliaStarlight({
footer: {
links: [{ label: 'GitHub', href: 'https://github.com/AmbiqAI' }],
tagline: 'One line under the logo.',
logo: 'ambiq',
},
}),
],
});
OptionTypeDefaultEffect
stylesbooleantrueSplices the four package stylesheets into customCss. false leaves customCss untouched, and the site loads them itself.
accentproduct idundefinedThe product identity color, as data-helia-accent on the document element. See Product accent.
shell.themeSelectbooleantrueInstalls the ThemeSelect override: the theme menu with its three choices.
shell.mobileMenuTogglebooleantrueInstalls the MobileMenuToggle override.
shell.footerbooleantrueInstalls the Footer override, which renders the brand row and the pagination.
footer.links{ label, href }[][]Navigation shown on the right of the footer’s brand row.
footer.taglinestringPart of the Ambiq HELIA AI platformOne line printed under the logo.
footer.logo'ambiq' | false'ambiq'false drops the brand lockup and renders the tagline on its own.
headerobjectunsetInstalls the Header override: the site name as text at the top-left, the links, search, and the theme menu. Unset, Starlight’s own header stands.
header.titlestringsite titleThe name at the top-left. It links to the site root.
header.titleRegularPrefixstringunsetInitial title text to render at regular weight; the remaining title stays bold. The helia prefix is automatic for names such as heliaCORE.
header.links{ label, href, match? }[][]The sections beside the name, or the sections list when there is one and this is unset. A link is current when the path starts with its href, longest prefix first; match is the prefix to use instead.
header.hub{ label?, href }unsetA link back to the HELIA Dev Hub at the end of the bar, in the menu below 42rem. label defaults to HELIA HUB; custom destination labels are supported.
header.searchbooleantrueStarlight’s search trigger.
header.themeTogglebooleantrueThe package theme menu. Off anyway when shell.themeSelect is false.
sections{ label, href, match?, sidebar }[][]The site’s sections, as one definition the top bar and the left sidebar both read. See Sections.
sections[].sidebarentries | falserequiredThe section’s pages, in Starlight’s own sidebar vocabulary. false is a section with no pane and no column on its routes. See Sections.
sidebar'docs' | 'always''docs''always' keeps the left sidebar on every page at desktop widths, a template: splash landing page included. A section that declared sidebar: false is not one of them.
shell.headbooleantrueInstalls the Head override: the brand font preload and the per-page discoverability tags.
shell.pageTitlebooleantrueInstalls the PageTitle override, which reads helia.pageTitle. See A page that opens with a hero.
codebooleantrueThemes Expressive Code onto the package surface. false leaves Starlight’s code frame alone.
markdownCalloutsbooleantrueRenders Starlight’s :::note, :::tip, :::caution and :::danger asides as the package Callout, in .md and .mdx alike. A directive label becomes the title. false leaves Starlight’s asides alone. Registered on whichever markdown processor the site configured. See Callouts.
discoverabilityobject | false{}Search-engine and agent metadata. false switches the lot off; see Discoverability.
discoverability.ogImagebooleantruePer-page Open Graph card at <route>/og.svg, with the og:image and twitter:image tags that point at it.
discoverability.jsonLdbooleantrueWebSite and Organization on the home page, TechArticle elsewhere, and a BreadcrumbList from the sidebar path.
discoverability.markdownbooleantrue<route>/index.md renditions and the alternate link that points at them.
discoverability.llmsbooleantruellms.txt, llms-full.txt, and content-index.json at the site root.

A product site puts its own name at the top-left, as text, with its sections beside it. That is the header option, and it installs a Header override in place of Starlight’s:

heliaStarlight({
header: {
title: 'heliaCORE',
links: [
{ label: 'Getting started', href: '/heliaCORE/getting-started/' },
{ label: 'Guides', href: '/heliaCORE/guides/' },
{ label: 'API', href: '/heliaCORE/api/' },
],
},
});

The helia prefix in a title such as heliaCORE uses regular weight while the product suffix stays bold. Other product names can opt into the same treatment with header.titleRegularPrefix. For example, set title to 'physioKIT' and titleRegularPrefix to 'physio' to render physio at regular weight and KIT bold. The prefix must match the start of the title and leave a nonempty suffix.

An href is a path on the site, written the way a footer link is: with the site’s base on the front, since the plugin does not prefix it for you. A link is current when the page’s path starts with its href, so a section stays lit on the pages under it; match names a different prefix for a section whose pages do not live under its link. A link to the site root is current on the root alone, and where two links both match, the longer prefix is the one that lights, so only ever one of them does.

header.hub puts a link back to the HELIA Dev Hub at the end of the bar, so a product site reads as one of a family rather than as a site on its own:

heliaStarlight({
header: {
title: 'heliaCORE',
hub: { href: 'https://ambiqai.github.io/helia-developer-hub/' },
},
});

The outlined link uses the navigation foreground in either theme. label replaces the wording, which defaults to HELIA HUB. The link stays in the bar when the section links become a dropdown below 62rem. Below 42rem, the section dropdown includes a HELIA AI DEV Hub destination and the duplicate header link is hidden. A header with no section links keeps the Hub link visible. Documentation sidebars also carry the Hub link on narrow screens.

Search and the theme menu sit at the end of the bar, and either can be dropped with header.search: false or header.themeToggle: false. What Starlight draws there by default and this bar does not is the social icon row: a site that wants those links keeps them by naming them in links.

Below 62rem, the section links move into a dropdown in the shared header. Documentation pages also have a separate button for the current section’s sidebar pages. Landing sections declaring sidebar: false keep the dropdown and omit that button. The sidebar button uses Starlight’s menu state so its overlay and scroll lock stay synchronized. This site uses the shared header, so the bar above this page demonstrates that behavior.

A product site is read one part at a time. sections is the option that says so: the top bar lists the site’s sections, and a page inside one gets a left sidebar of that section’s pages under the section’s name. Both come from the one list, so the bar and the pane cannot drift apart.

heliaStarlight({
header: { title: 'heliaCORE' },
sections: [
{ label: 'Home', href: '/heliaCORE/', sidebar: false },
{
label: 'Getting started',
href: '/heliaCORE/getting-started/',
sidebar: [
{ label: 'Choose your path', slug: 'getting-started' },
{ label: 'CMake', slug: 'getting-started/cmake' },
{ label: 'Zephyr', slug: 'getting-started/zephyr' },
],
},
{
label: 'Architecture',
href: '/heliaCORE/architecture/',
sidebar: [{ autogenerate: { directory: 'architecture' } }],
},
],
});

A section is a label, an href written with the site’s base on the front the way a bar link is, an optional match for a section whose pages do not live under its href, and a sidebar of Starlight’s own entries: links, slugs, groups, autogenerate directories, badges, or false for a section with no pages of its own. They are Starlight’s entries, not a copy of them, because the plugin hands them back to Starlight as sidebar config; nothing about them is resolved here.

  • The bar. header.links is derived from the sections when the site does not write its own. A site whose bar carries something that is not a section, a link out to GitHub for instance, writes header.links and keeps both.
  • The pane. The section’s entries become the whole sidebar, with the section’s name above them as a heading rather than as an entry. The group the entries were declared under is not drawn: it is the heading.
  • The landing page. Give the Home section the site’s base as its href. A base prefix would otherwise match every page on the site, so a section at the base is current on the landing page alone. A landing page has the bar above it and the page itself to navigate from, so give Home sidebar: false and it is read at the full width of the frame.
  • A section with no pane. sidebar: false in place of the entries is a section with no pages of its own: no pane on its routes and no column reserved for one, whatever sidebar: 'always' says, since that option is about a template rather than about a section. The section still lists in the bar and in the narrow-width menu, as a link rather than an expandable item.
  • Narrow screens. With a configured header, the section dropdown replaces the bar’s links below 62rem. Documentation pages retain a menu button for the current section’s sidebar. Sections with sidebar: false have no menu button or sidebar. Without a configured header, section switching remains available through Starlight’s mobile sidebar.
  • Which section a page is in. The longest matching prefix wins, so a section nested under another one takes its own pages. A page under no section keeps the sidebar the site declared in sidebar, or the whole navigation if it declared none.

The sections demo is three sections wired up this way. It is a fixture rather than this site’s own navigation: this site’s bar links out to GitHub and leaves several groups off the bar, and sections only work as navigation when every part of the site is one.

Generated pages sit at most one level under their group. The trap is the shape a generator’s own fragment usually arrives in, a group wrapping an autogenerate:

/* Two levels: Reference > Generated C API > Convolution > conv page. */
{
label: 'Reference',
href: '/heliaCORE/reference/',
sidebar: [
{ label: 'Generated C API', items: [{ autogenerate: { directory: 'reference/api' } }] },
],
}

Inside a section there is no need for the wrapper, because the section itself is the heading the entries sit under. Declare one entry per group instead and the groups are the top level of the pane, with module pages one level under them:

/* The generator's directory names, one group each. */
const GROUPS = [
['Activation', 'activation'],
['Basic math', 'basicmath'],
/* ...one row per group the generator emits. */
];
{
label: 'Reference',
href: '/heliaCORE/reference/',
sidebar: [
{ label: 'C API index', slug: 'reference' },
...GROUPS.map(([label, directory]) => ({
label,
collapsed: true,
items: [{ autogenerate: { directory: `reference/api/${directory}` } }],
})),
{ label: 'API generation notes', slug: 'reference/generation' },
],
}

A generator that emits a sidebar fragment of its own splices in the same way: the fragment has to be the list of groups, not a single group containing them. If it hands you the wrapped shape, spread its items rather than the entry itself.

Starlight gives a template: splash page no sidebar, which is right for a hub landing page and wrong for a product site: a reader who arrives on a product’s landing page should be able to see the rest of the product from it. Set sidebar: 'always' and every page keeps the left sidebar at desktop widths, the landing page included. Narrow screens are untouched: the sidebar stays off-canvas there, as it is on a documentation page.

heliaStarlight({ sidebar: 'always' });

A site with sections says the same thing per section instead: a section that declared sidebar: false has no pane on its routes, and this option does not put one back there.

The plugin does this with a route middleware rather than a component override. Starlight decides a page has no sidebar from its template, and one flag on the route carries that decision to the page shell, to the frame, and to the layout widths; the middleware sets the flag back, so the sidebar is Starlight’s own and the widths follow it. This site sets the option, and Landing page example is what it looks like.

Starlight draws the page title above the content of every page whose frontmatter carries no hero, whatever the template. A page whose content opens with the Hero part therefore shows its name twice, with a band above the card, and ships two h1s. helia: { pageTitle: false } drops Starlight’s heading and leaves the hero’s as the page’s only one.

The field is not Starlight’s, and a key its schema does not declare is stripped before a component can read it. Starlight has no hook for a plugin to extend that schema either, so the site names the fragment once in content.config.ts:

import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
import { heliaFrontmatterSchema } from '@ambiqai/helia-ui/starlight';
export const collections = {
docs: defineCollection({
loader: docsLoader(),
schema: docsSchema({ extend: heliaFrontmatterSchema }),
}),
};

The page then asks for it:

---
title: heliaCORE
helia:
pageTitle: false
---
<Hero variant="contrast" eyebrow="Kernel library">
<Fragment>
Kernel <em>acceleration</em> for Ambiq AI.
</Fragment>
</Hero>

The heading goes; a bare _top anchor stays in its place, because that is what the skip link and the overview entry in the table of contents point at, and the panel that held the title collapses so the hero is the first thing under the header. Hero page example is the page this site builds from that frontmatter.

--helia-product-accent is the one color a site gets to call its own: the accent word in a Hero, an accent Badge, the outline on a primary action. Without an accent it resolves to slate, which is a site with no identity color rather than a site with a neutral one.

accent names a product the package already holds a color for. The plugin puts that id on the document element as data-helia-accent, and semantic.css derives the accent from it, so the palette stays in the package and the site writes no CSS.

heliaStarlight({ accent: 'helia-core' });

The ids are helia-aot, helia-core, helia-dsp, helia-edge, helia-ml, helia-profiler, helia-rt, kit-compression, kit-heart, kit-physio, kit-sleep, kit-sound, and kit-vision. Anything else fails the build rather than falling back quietly.

Setting the accent is not the same as using it. The budget below is what a product site spends on a full landing page, and it is the same budget on every page of that site: enough that the color means “this one”, little enough that it still can.

MomentWhat carries itHow often
The hub linkThe link out to the HELIA Dev Hub in the top barOnce, in the chrome
The eyebrow dotHero’s eyebrow on a contrast groundOnce, in the hero
The accent wordOne or two words of the Hero headlineOnce, in the hero
The primary actionButton variant="primary", its outline onlyOnce, in the hero
The page’s own layerStack with accent on one layerOnce, where a diagram sits
The leading barBars with accent on one rowOnce per comparison
The unit glyphBigNumber accent, the × or the %Once per row of figures
The section linkThe link beside a section headingOnce per section
Link hoverAny link in prose, under the pointerNot counted: it is transient

The ink is scarcer: one inked field above the fold, which is usually Hero variant="contrast", and at most one Card or LinkCard with tone="ink" per set below it. Two inked tiles in one mosaic read as a dark row with a hole in it.

Off the budget entirely: card edges and brims, a colored fill behind body copy, persistent colored marks, and a second accent color on the same page. A page that really does carry two products scopes each subtree with data-helia-accent rather than introducing a color the plugin does not know about.

Every part named above draws from --helia-product-accent, so all of it follows from the one accent option and the site still writes no CSS. The longer argument, and the mockup the budget was drawn from, are in the hub’s design system document.

The four sheets the plugin injects are tokens.css, semantic.css, recipes.css, and starlight.css, in that order. They go after the leading Tailwind roots in your customCss and before everything else:

./src/styles/tailwind.css your Tailwind entry, which imports
@ambiqai/helia-ui/starlight-tailwind.css
@ambiqai/helia-ui/tokens.css injected
@ambiqai/helia-ui/semantic.css injected
@ambiqai/helia-ui/recipes.css injected
@ambiqai/helia-ui/starlight.css injected
./src/styles/site.css yours

Two properties depend on that position. The package sheets are unlayered, so they outrank every cascade layer the Tailwind entry declares, which is what keeps the tokens authoritative over the Starlight Tailwind compat package. And the site’s own sheet lands last, so a page-specific override needs no extra specificity to win.

The plugin finds the insertion point by treating any leading customCss entry whose filename matches tailwind*.css as a Tailwind root. If your entry is named something else, put it first and name it accordingly, or set styles: false and order the sheets yourself.

A Starlight plugin’s updateConfig shallow-merges over the site’s config, so an array or a map written by a plugin would replace the site’s rather than add to it. Both values this plugin touches are read back from the config and merged by hand instead. customCss is spliced, not replaced, and a shell override is installed only under a key the site left alone.

Starlight hands nothing to a component override, so the resolved options reach the Footer through the module graph. The plugin adds a small Astro integration that serves them from the virtual module virtual:helia-ui/starlight-config, which is how Starlight itself does this and keeps the values build-time constants rather than runtime lookups. Nothing outside the package should import that module: it is an implementation detail of the shell components, and its shape is not part of the export map.

The plugin also emits the search-engine and agent metadata every HELIA site needs, so no site authors its own. Discoverability states the contract: what lands in the head, which artifacts land at the site root, and how a page opts out of the description rule.