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', }, }), ],});Options
Section titled “Options”| Option | Type | Default | Effect |
|---|---|---|---|
styles | boolean | true | Splices the four package stylesheets into customCss. false leaves customCss untouched, and the site loads them itself. |
accent | product id | undefined | The product identity color, as data-helia-accent on the document element. See Product accent. |
shell.themeSelect | boolean | true | Installs the ThemeSelect override: the theme menu with its three choices. |
shell.mobileMenuToggle | boolean | true | Installs the MobileMenuToggle override. |
shell.footer | boolean | true | Installs 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.tagline | string | Part of the Ambiq HELIA AI platform | One line printed under the logo. |
footer.logo | 'ambiq' | false | 'ambiq' | false drops the brand lockup and renders the tagline on its own. |
header | object | unset | Installs 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.title | string | site title | The name at the top-left. It links to the site root. |
header.titleRegularPrefix | string | unset | Initial 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 } | unset | A 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.search | boolean | true | Starlight’s search trigger. |
header.themeToggle | boolean | true | The 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[].sidebar | entries | false | required | The 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.head | boolean | true | Installs the Head override: the brand font preload and the per-page discoverability tags. |
shell.pageTitle | boolean | true | Installs the PageTitle override, which reads helia.pageTitle. See A page that opens with a hero. |
code | boolean | true | Themes Expressive Code onto the package surface. false leaves Starlight’s code frame alone. |
markdownCallouts | boolean | true | Renders 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. |
discoverability | object | false | {} | Search-engine and agent metadata. false switches the lot off; see Discoverability. |
discoverability.ogImage | boolean | true | Per-page Open Graph card at <route>/og.svg, with the og:image and twitter:image tags that point at it. |
discoverability.jsonLd | boolean | true | WebSite and Organization on the home page, TechArticle elsewhere, and a BreadcrumbList from the sidebar path. |
discoverability.markdown | boolean | true | <route>/index.md renditions and the alternate link that points at them. |
discoverability.llms | boolean | true | llms.txt, llms-full.txt, and content-index.json at the site root. |
The product top bar
Section titled “The product top bar”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.
Sections
Section titled “Sections”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.linksis 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, writesheader.linksand 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 Homesidebar: falseand it is read at the full width of the frame. - A section with no pane.
sidebar: falsein place of the entries is a section with no pages of its own: no pane on its routes and no column reserved for one, whateversidebar: '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 withsidebar: falsehave 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 reference pages
Section titled “Generated reference pages”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.
Keeping the sidebar on a landing page
Section titled “Keeping the sidebar on a landing page”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.
A page that opens with a hero
Section titled “A page that opens with a hero”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: heliaCOREhelia: 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.
Product accent
Section titled “Product accent”--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.
How much of it to spend
Section titled “How much of it to spend”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.
| Moment | What carries it | How often |
|---|---|---|
| The hub link | The link out to the HELIA Dev Hub in the top bar | Once, in the chrome |
| The eyebrow dot | Hero’s eyebrow on a contrast ground | Once, in the hero |
| The accent word | One or two words of the Hero headline | Once, in the hero |
| The primary action | Button variant="primary", its outline only | Once, in the hero |
| The page’s own layer | Stack with accent on one layer | Once, where a diagram sits |
| The leading bar | Bars with accent on one row | Once per comparison |
| The unit glyph | BigNumber accent, the × or the % | Once per row of figures |
| The section link | The link beside a section heading | Once per section |
| Link hover | Any link in prose, under the pointer | Not 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.
Stylesheet order is the contract
Section titled “Stylesheet order is the contract”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 yoursTwo 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.
Overrides you already set win
Section titled “Overrides you already set win”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.
How options reach the shell
Section titled “How options reach the shell”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.
Discoverability
Section titled “Discoverability”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.