# 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.

The `mkdocstrings`-generated API pages are a separate problem with no
Starlight equivalent. They are their own piece of work, not part of this
mapping.

## 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:

```sh
npx helia-ui-mkdocs-convert --docs docs --out src/content/docs \
  --base /PRODUCT-REPO-NAME --public public \
  --sidebar src/generated/docs-sidebar.json
```

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](https://ambiqai.github.io/helia-ui/migrating-from-mkdocs/#raw-html) and [Charts](https://ambiqai.github.io/helia-ui/migrating-from-mkdocs/#charts).
- **`{{ read_csv(...) }}` macros**, left as a marker for a `DataTable` island.
- **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

`!!! 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.

`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`

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:

```mdx
<Callout title="Calibration complete" tone="success">
  The sensor is ready to record.
</Callout>
```

```md
<!-- MkDocs -->

!!! warning "Check the input rate"
The converter assumes 16 kHz.
```

```mdx
{/* 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

`=== "Label"` becomes `Tabs` and `TabItem`. Material's linked tabs — the ones
that switch together across a page — are `syncKey` on `Tabs`.

```md
<!-- MkDocs -->

=== "pip"
`sh
    pip install product-name
    `

=== "uv"
`sh
    uv add product-name
    `
```

````mdx
{/* 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.

## Icons

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.

```mdx
import Icon from '@ambiqai/helia-ui/astro/Icon';

<Icon name="gauge-high" /> Latency
<Icon name="triangle-exclamation" label="Warning" />
```

gauge-high &nbsp;
microchip &nbsp;
chart-line &nbsp;
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.

An icon inside a heading or a table cell is usually decoration. If deleting it
would lose information, it needs a `label`; if not, it does not.

## Math

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:

```js
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

Both are supported with no configuration. `[^1]` references, `[^1]:`
definitions, and `- [ ]` / `- [x]` items all render as written.

## 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:

```mdx
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

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:

```mdx
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:

```tsx
// src/islands/ResultsTable.tsx
import { DataTable, type ColumnDef } from '@ambiqai/helia-ui/react/data-table';
```

```mdx
<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

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

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:

```html
<!-- 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

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

```tsx
// src/islands/LatencyOverWindows.tsx
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:

```mdx
import LatencyOverWindows from '../../islands/LatencyOverWindows';

<LatencyOverWindows client:visible />
```

This exact example is rendered on
[Data display](https://ambiqai.github.io/helia-ui/react/data-display/#a-converted-mkdocs-chart), 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.

A chart states something about a part. Move the data across unchanged and keep
whatever source of record the MkDocs page cited; do not round, re-scale or
re-label a series during the conversion.

## 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](https://ambiqai.github.io/helia-ui/code/) for the forms.

## 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

`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.
