Migrating from MkDocs
Every MkDocs Material feature in use across the HELIA product docs has a counterpart here. This page is the mapping, one section per feature, so a conversion is mechanical rather than a judgment call each time.
Start from templates/product-docs in this package: it is a working Starlight
site with the plugin, the Tailwind entry, the product accent and a Pages
workflow already wired. Copy it in as the product repository’s docs/, then
move content into it.
Converter
Section titled “Converter”Most of this page is mechanical, so the package ships it as a command. It reads
a MkDocs docs_dir and writes a Starlight content collection:
npx helia-ui-mkdocs-convert --docs docs --out src/content/docs \ --base /PRODUCT-REPO-NAME --public public \ --sidebar src/generated/docs-sidebar.json| Option | What it does |
|---|---|
| `--docs <dir>` | MkDocs `docs_dir` to read. Required. |
| `--out <dir>` | Content collection directory to write. Required. |
| `--base <path>` | The site's base path, so a root-relative link keeps working under it. |
| `--config <file>` | `mkdocs.yml`. Defaults to the one beside `--docs`. |
| `--icons <file>` | JSON map of extra `:material-*:` names, merged over the built-in table. |
| `--public <dir>` | Where linked images and `docs/assets/**` are copied. |
| `--sidebar <file>` | Writes the MkDocs nav as a Starlight sidebar fragment. |
| `--skip <path>` | A page to leave alone; repeatable. Defaults to `index.md`. |
| `--keep <name>` | An entry in `--out` not to delete; repeatable. Defaults to `index.mdx` and `api`. |
| `--dry-run` | Convert and report, write nothing. |
| `--report` | Print per-rewrite counts, unmapped icons and the hand-pass list. |
It rewrites what the rest of this page maps: admonitions and collapsible
admonitions to asides, content tabs to Tabs and TabItem, :material-*:
shortcodes to Icon, Termynal blocks to AsciiTerminal, relative links to
route form, HTML comments to MDX expression comments, and the MDX escaping that
Markdown did not need. Front matter comes from the page’s H1 and lead
paragraph. Attribute lists and markdown="1" attributes are dropped, void tags
are closed, and fenced code is left alone.
The MkDocs tree stays the source of truth until the deploy switch, so the
command is idempotent: it rewrites --out from scratch on every run, keeping
only what --keep names. Anything that has had a hand pass lives in files the
converter owns, so re-run it against a clean tree and re-apply the hand pass
from the diff.
What it will not do by hand-waving, it prints:
- Raw HTML blocks and Chart.js canvases, kept verbatim and listed per page. See Raw HTML and Charts.
{{ read_csv(...) }}macros, left as a marker for aDataTableisland.- Unmapped icon names, emitted as a visible
[unmapped icon: name]marker rather than guessed at, and counted in the report. - Pages with no H1, whose title is derived from the filename.
- Nav entries with no page behind them, which would be a 404 after the switch.
A --8<-- snippet has no equivalent, so the run fails on one rather than
dropping the content it pulled in.
Admonitions
Section titled “Admonitions”!!! note "Title" becomes Starlight’s Aside. Material has twelve admonition
types and Aside has four, so several Material types land on the same tone.
Where the distinction matters, the package’s Callout has nine and takes a
free-text title.
This table is the whole Material set, so a converter has one mapping to read and never has to decide.
| Material | Aside type | Package Callout tone |
|---|---|---|
| note | note | note |
| abstract, summary, tldr | note | note |
| info | note | note |
| tip, hint | tip | tip |
| important | tip | important |
| success, check, done | tip | success |
| question, help, faq | note | note |
| warning, caution, attention | caution | warning |
| failure, fail, missing | caution | warning |
| danger, error | danger | critical |
| bug | danger | critical |
| example | note | note |
| quote, cite | none — use a blockquote | none |
Aside is the default because it is the built-in: it is styled by the theme,
it is translated, and it needs no import in Markdown. Reach for Callout when
the page needs success, compatibility, experimental or deprecated,
which have no Aside equivalent, or when the title is not one of the four
fixed words.
Why success maps to tip
Section titled “Why success maps to tip”Starlight 0.41 does not take a custom Aside type. Aside.astro validates the
prop against a fixed ['note', 'tip', 'caution', 'danger'] and throws an
AstroError on anything else, and the remark directive that turns :::note
into an Aside drops a name it does not recognize, so :::success renders as
nothing at all. A success skin in starlight.css would therefore have no
element to attach to.
So success, check and done convert to Aside type="tip", which is the
closest built-in and already reads as a positive tone. Where a page genuinely
needs to distinguish a completed step from a suggestion, use the package’s
Callout instead, which gained a success tone in 0.1.0-alpha.3:
<Callout title="Calibration complete" tone="success"> The sensor is ready to record.</Callout><!-- MkDocs -->
!!! warning "Check the input rate"The converter assumes 16 kHz.{/* Starlight */}:::caution[Check the input rate]The converter assumes 16 kHz.:::The directive form needs no import. The component form (<Aside type="caution" title="...">) is equivalent and is what MDX pages already importing components
tend to use.
Content tabs
Section titled “Content tabs”=== "Label" becomes Tabs and TabItem. Material’s linked tabs — the ones
that switch together across a page — are syncKey on Tabs.
<!-- MkDocs -->
=== "pip"`sh pip install product-name `
=== "uv"`sh uv add product-name `{/* Starlight */}
<Tabs syncKey="installer"> <TabItem label="pip">```sh pip install product-name ```</TabItem> <TabItem label="uv">```sh uv add product-name ```</TabItem></Tabs>Blank lines matter: leave one after the opening <TabItem> and one before the
closing tag, or the fenced block is parsed as JSX children rather than
Markdown.
The package’s CodeTabs is a different thing and not the migration target. It
is driven by a data array rather than by markup, and it has a select variant
for long tab lists. Use it when the tab set comes from data; use Tabs when it
is written in the page.
Material’s :material-gauge: shortcodes have no direct equivalent: Starlight’s
own Icon carries a small set chosen for its UI, not a general icon library.
This package therefore ships Icon, a build-time wrapper over Font Awesome’s
free solid set — the same dependency Callout and AccordionGroup already
use, so it adds nothing to install and nothing to the client.
import Icon from '@ambiqai/helia-ui/astro/Icon';
<Icon name="gauge-high" /> Latency<Icon name="triangle-exclamation" label="Warning" />gauge-high microchip chart-line triangle-exclamation
Names are Font Awesome’s, in kebab case, and an unknown name fails the build
rather than rendering nothing. The Material and Font Awesome vocabularies
overlap heavily but not exactly, so the icons in use need a pass by hand;
:material-memory: is memory, :material-speedometer: is gauge-high,
:material-alert: is triangle-exclamation.
Omit label for an icon that sits beside text saying the same thing, which is
most of them. Supply it when the icon is alone in a table cell or a heading.
Material rendered math with KaTeX, and so does this. Expressions move across
unchanged. remark-math parses $…$ and $$…$$, rehype-katex renders it, and
katex/dist/katex.min.css goes in customCss. All three go on Astro’s
markdown config, not Starlight’s:
markdown: { remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex],},Starlight’s own markdown option covers heading links and extra processed
directories only; it takes no plugins. The template has this wired already.
Footnotes and task lists
Section titled “Footnotes and task lists”Both are supported with no configuration. [^1] references, [^1]:
definitions, and - [ ] / - [x] items all render as written.
Snippets
Section titled “Snippets”--8<-- "file.md" becomes an import. There is no snippet plugin; MDX already
does this, and the result is typed and traceable rather than a string
substitution.
For shared prose, put the fragment in src/components/ as an .astro or
.mdx file and import it. For shared code, import the real source file as raw
text so the sample cannot drift from what it documents:
import example from '../../../examples/measure.py?raw';
<CodeBlock filename="measure.py" language="python" code={example} />?raw is Vite’s, so it works anywhere in the site. It is the better form for
code: the file stays runnable and testable.
Data tables
Section titled “Data tables”Almost every table is plain Markdown and stays plain Markdown. Two cases are not.
A table that needs a caption, a scroll frame on narrow screens, or paging is
the DataTable part. It is static markup with an optional pager and costs no
JavaScript until pageSize is set:
import DataTable from '@ambiqai/helia-ui/astro/DataTable';
<DataTable caption="Fields in result.json" columns={['Field', 'Unit', 'Meaning']} rows={[['mean', 'ms', 'Arithmetic mean across runs']]} pageSize={10}/>A table that needs sorting, filtering or column controls — what
simple-datatables does — is the React data table, hydrated as an island:
import { DataTable, type ColumnDef } from '@ambiqai/helia-ui/react/data-table';<ResultsTable client:visible />Prefer the Astro part. A sortable table is worth an island when a reader is expected to search it, and not otherwise.
Charts
Section titled “Charts”Plotly and Chart.js become Recharts islands. The package ships the shadcn chart
wrapper over Recharts as @ambiqai/helia-ui/react/chart, and the existing data
moves over unchanged: what changes is the renderer, not the numbers.
There is no chart component to import beyond that wrapper, because a chart is too specific to a page to be a part. The conversion is a recipe instead.
What the MkDocs side looks like
Section titled “What the MkDocs side looks like”Material sites drive charts from data attributes on an empty element, read at
load by a chart-init.js that hands them to Plotly or Chart.js:
<!-- MkDocs --><div class="chart" data-chart-type="line" data-chart-x="0,1,2,3,4,5" data-chart-series='[{"name":"heliaRT","y":"5.4,5.2,5.6,5.1,5.3,5.2"}, {"name":"heliaAOT","y":"4.9,4.8,4.7,4.9,4.6,4.8"}]' data-chart-colors="#1769e0,#00a7b5"></div>Everything there is a string the build cannot check: a typo in a series name or a missing value only shows up as an empty chart in a browser.
The conversion, attribute by attribute
Section titled “The conversion, attribute by attribute”| MkDocs attribute | Becomes |
|---|---|
| data-chart-type | The Recharts element: LineChart, BarChart, AreaChart |
| data-chart-x | The shared key on each row, named by dataKey on XAxis |
| data-chart-series | One key per series on each row of the data array |
| series name | The label on that series’ ChartConfig entry |
| data-chart-colors | A --chart-N token on that entry’s color |
| chart-init.js | Nothing. The island is the initializer. |
Three things are worth keeping straight. The parallel arrays become one array
of rows, because Recharts takes a row per x value rather than a series per
line. The per-trace hex colors become --chart-1 through --chart-5 from the
token set, so a chart follows the theme instead of carrying a palette of its
own. And chart-init.js itself is deleted rather than ported.
The island
Section titled “The island”import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts';import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig,} from '@ambiqai/helia-ui/react/chart';
const windows = [ { window: 0, rt: 5.4, aot: 4.9 }, { window: 1, rt: 5.2, aot: 4.8 }, // one row per x value, one key per series];
const config = { rt: { label: 'heliaRT', color: 'var(--chart-1)' }, aot: { label: 'heliaAOT', color: 'var(--chart-2)' },} satisfies ChartConfig;
export default function LatencyOverWindows() { return ( <ChartContainer config={config}> <LineChart data={windows}> <CartesianGrid vertical={false} /> <XAxis dataKey="window" tickLine={false} axisLine={false} /> <YAxis tickLine={false} axisLine={false} unit="ms" /> <ChartTooltip content={<ChartTooltipContent />} /> <Line dataKey="rt" stroke="var(--color-rt)" dot={false} /> <Line dataKey="aot" stroke="var(--color-aot)" dot={false} /> </LineChart> </ChartContainer> );}ChartContainer turns each config key into a --color-<key> custom property,
which is why the series reads stroke="var(--color-rt)" rather than repeating
the token.
Drop it on the page with client:visible, so a chart below the fold costs
nothing until it is scrolled to:
import LatencyOverWindows from '../../islands/LatencyOverWindows';
<LatencyOverWindows client:visible />This exact example is rendered on Data display, where the site’s own React islands live. This page stays static: only the React-components section hydrates, which is what the docs build asserts.
Terminals
Section titled “Terminals”Termynal becomes AsciiTerminal. A static transcript is the default and ships
no JavaScript; animated types the command lines and reveals output by line,
and honors prefers-reduced-motion by showing everything at once. See
Code for the forms.
Raw HTML
Section titled “Raw HTML”This is the part that does not map mechanically. MDX is stricter than Markdown: attributes are JSX, so class
is className in components, style takes an object, and every tag must
close. Most of the raw HTML in these pages exists because Material had no
component for something — a two-column layout, a card, a badge — and those have
parts here. Read them page by page and replace rather than convert.
Navigation
Section titled “Navigation”mkdocs.yml’s nav becomes Starlight’s sidebar. A directory of pages is a
group whose items hold an autogenerate object — { label: 'How-to', items: [{ autogenerate: { directory: 'how-to' } }] } — which orders by the
sidebar.order frontmatter key; an explicit list is an items array of
entries. Routes that change need a
redirect, since the product sites are already linked to from the hub.
--sidebar writes that array for you, as a JSON fragment the Astro config
spreads into sidebar. The template’s route test reads the same fragment and
asserts every entry serves 200, so a nav entry that gained no page fails as a
test rather than as a reader’s 404.