# @pyreon/zero-content

## 0.44.0

### Minor Changes

- [#2170](https://github.com/pyreon/pyreon/pull/2170) [`4bcb139`](https://github.com/pyreon/pyreon/commit/4bcb13922eb243920e64b4564ea4b5cfaec4991f) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Callouts: support the `:::type[Title]` bracket form for titles, and catch the bare-text raw-leak footgun.

  `:::warning[Peer dependencies]` (the natural Starlight/Docusaurus convention) now lifts the bracketed label to the callout title — previously the label rendered as leading body text and the title was silently dropped. `:::type{title="…"}` still works and wins when both are present.

  `:::warning bare text` is NOT valid remark-directive syntax — the opener is rejected and the line ships to the page as literal `:::warning …` text. This shipped 73 times across the docs with zero diagnostics. The plugin now scans parsed paragraphs (skipping fenced code blocks) and warns with the exact fix. A docs-wide test gate locks it so it can't silently return.

### Patch Changes

- Updated dependencies [[`ae2472e`](https://github.com/pyreon/pyreon/commit/ae2472e4ecb31cd59bde23d1983afe7db1c62d99), [`28fbd77`](https://github.com/pyreon/pyreon/commit/28fbd7799f015503d45c8642d8822bff64e9e155), [`9ef1b14`](https://github.com/pyreon/pyreon/commit/9ef1b1422313b49a020b7deb1ffa0871a5cc012a), [`4ad62b2`](https://github.com/pyreon/pyreon/commit/4ad62b25037776d6521501cadb8ac9fe33d75e38), [`4add6bd`](https://github.com/pyreon/pyreon/commit/4add6bd17711a6eb9f0cc9375a3643289bf931c4), [`8413136`](https://github.com/pyreon/pyreon/commit/84131368d6f8790ba50e2af9d383ee289e4b1f5c), [`721618e`](https://github.com/pyreon/pyreon/commit/721618e97dacf995d8356dabea601ef4e98a4a12), [`4ad62b2`](https://github.com/pyreon/pyreon/commit/4ad62b25037776d6521501cadb8ac9fe33d75e38), [`d859370`](https://github.com/pyreon/pyreon/commit/d8593704b0941ef0e51a427147ebce2a385ecae3), [`550508c`](https://github.com/pyreon/pyreon/commit/550508ce30be3dbf1a93c9069b542e37237fca39)]:
  - @pyreon/runtime-dom@0.44.0
  - @pyreon/router@0.44.0
  - @pyreon/zero@0.44.0
  - @pyreon/reactivity@0.44.0
  - @pyreon/head@0.44.0
  - @pyreon/core@0.44.0

## 0.43.1

### Patch Changes

- Updated dependencies []:
  - @pyreon/head@0.43.1
  - @pyreon/router@0.43.1

## 0.43.0

### Patch Changes

- Updated dependencies [[`cbefb07`](https://github.com/pyreon/pyreon/commit/cbefb0742e123484be6e158cfeb16c28d0d5deb9)]:
  - @pyreon/zero@0.43.0
  - @pyreon/core@0.43.0
  - @pyreon/head@0.43.0
  - @pyreon/reactivity@0.43.0
  - @pyreon/router@0.43.0
  - @pyreon/runtime-dom@0.43.0

## 0.42.0

### Patch Changes

- Updated dependencies [[`39051db`](https://github.com/pyreon/pyreon/commit/39051dbcec2aa5f3aa9db79c5ac0a9f9197cc1e9)]:
  - @pyreon/runtime-dom@0.42.0
  - @pyreon/head@0.42.0
  - @pyreon/router@0.42.0
  - @pyreon/zero@0.42.0
  - @pyreon/core@0.42.0
  - @pyreon/reactivity@0.42.0

## 0.41.2

### Patch Changes

- Updated dependencies []:
  - @pyreon/head@0.41.2
  - @pyreon/router@0.41.2

## 0.41.1

### Patch Changes

- Updated dependencies []:
  - @pyreon/head@0.41.1
  - @pyreon/router@0.41.1

## 0.41.0

### Patch Changes

- Updated dependencies [[`850a76d`](https://github.com/pyreon/pyreon/commit/850a76d33296059ff9c0d03d12c8092208b3bf81), [`89457f6`](https://github.com/pyreon/pyreon/commit/89457f6a68984ca29158b8728f605b1f54f2f243)]:
  - @pyreon/zero@0.41.0
  - @pyreon/core@0.41.0
  - @pyreon/head@0.41.0
  - @pyreon/reactivity@0.41.0
  - @pyreon/router@0.41.0
  - @pyreon/runtime-dom@0.41.0

## 0.40.0

### Patch Changes

- Updated dependencies [[`e6d3905`](https://github.com/pyreon/pyreon/commit/e6d390586944b903ee8d9c97a71cbaf26eca63d6), [`a5021f6`](https://github.com/pyreon/pyreon/commit/a5021f631729add83b2808a18288a2c48f81c233), [`ea835ad`](https://github.com/pyreon/pyreon/commit/ea835ad364e3dcf0de8337fceed382e9f6762285), [`4958096`](https://github.com/pyreon/pyreon/commit/4958096c01f4ed4f031cc65bf9ff7c26c93d3449), [`e859638`](https://github.com/pyreon/pyreon/commit/e859638a4c382051d5fa6f2605a8c383207f6e66), [`c184330`](https://github.com/pyreon/pyreon/commit/c184330594a7726c4f1f1095cc3a785cfe9ef3f7), [`d61d3d9`](https://github.com/pyreon/pyreon/commit/d61d3d9e3acb483b1b5fa8b79f23c03c309ab2c5), [`0ea9c60`](https://github.com/pyreon/pyreon/commit/0ea9c6006f19489eb42af9146b790ff826f2a0a3), [`0dc1f13`](https://github.com/pyreon/pyreon/commit/0dc1f1379434bbc855ee4e7a7a585759dfc2836e), [`8a7bff0`](https://github.com/pyreon/pyreon/commit/8a7bff0dda93f15afbee9a0d9ab040e2e8969ff0), [`85d4a91`](https://github.com/pyreon/pyreon/commit/85d4a91c5e015af7348ebdd312e0ba5523950a3d), [`ed364d2`](https://github.com/pyreon/pyreon/commit/ed364d2a34f4b74df94c02f3c2e630b96a4f2e7f), [`92f00c2`](https://github.com/pyreon/pyreon/commit/92f00c2912cdfdd1ea75013a137d227c1ba3c1fe)]:
  - @pyreon/runtime-dom@0.40.0
  - @pyreon/reactivity@0.40.0
  - @pyreon/router@0.40.0
  - @pyreon/zero@0.40.0
  - @pyreon/head@0.40.0
  - @pyreon/core@0.40.0

## 0.39.0

### Patch Changes

- [#1959](https://github.com/pyreon/pyreon/pull/1959) [`45791ad`](https://github.com/pyreon/pyreon/commit/45791ad573960d6d6741fbdc3621b24210b3fbd1) Thanks [@vitbokisch](https://github.com/vitbokisch)! - chore(deps): update dependencies to latest across the workspace

  Bumps every workspace package's dependencies to their latest versions. Notable
  consumer-visible runtime bumps (all validated — typecheck + package tests +
  full lib build green):

  - `@pyreon/dnd`: `@atlaskit/pragmatic-drag-and-drop` 1→2, `-auto-scroll` 2→3,
    `-hitbox` 1→2 (major; 129 tests pass)
  - `@pyreon/zero-content`: `shiki` 1→4 (major; 732 tests pass)
  - `@pyreon/query` / `@pyreon/virtual`: `@tanstack/*` → 5.101.2 / virtual-core 3.17.3

  Dev-only / benchmark-competitor bumps (not shipped to consumers): `@casl/ability`
  6→7, `i18next` 24→26 (both benches re-run clean, correctness gates pass),
  `@types/node` 26, `@vitus-labs/tools-*`, `typescript` (real-bench aligned to 6).

  Coherence overrides added for version-coupled ecosystems (the codemirror family
  and vite/vitest must share one version): `@codemirror/state@6.6.0` and
  `vite@8.0.16` pinned; `@tanstack/query-core` override → 5.101.2. Held back:
  codemirror patch bumps in `@pyreon/code` (version-coupled with 18 transitive
  lang-\* packages), `sharp` peer range kept permissive (`^0.33 || ^0.34 || ^0.35`).

- [#2001](https://github.com/pyreon/pyreon/pull/2001) [`e1e5278`](https://github.com/pyreon/pyreon/commit/e1e527837f0761d2ee4815c2960f63d1dc70f522) Thanks [@vitbokisch](https://github.com/vitbokisch)! - perf: kill the `__DEV__` const-alias dev gates — edge/workerd SSR bundles no longer ship dev counters + warnings

  A prod-bundle sweep across every published package (fundamentals + ui-system + core, probing minified NODE_ENV=production bundles for dev survivors) found one remaining instance class of the documented `__DEV__`-alias anti-pattern: `const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'` in 6 files. The alias is non-constant under a bundler's define (the `typeof process` prefix stays dynamic on non-Node platforms, and const-aliases don't propagate anyway), so **edge/workerd SSR bundles — which minify these files — shipped every perf counter and dev warning**. `@pyreon/runtime-server` was the worst: 9 counters + the Suspense-timeout warning + the tag-name validator survived in production (−423 B gz / −8% after the fix).

  Fixed to the repo-standard bare inline `process.env.NODE_ENV !== 'production'` at every site in: `@pyreon/runtime-server` (14 sites), `@pyreon/server` `handler.ts`, `@pyreon/zero` `isr.ts` + `ssg-plugin.ts`, `@pyreon/vite-plugin`, `@pyreon/zero-content` `config.ts`. Zero behavior change in dev or Node prod (the gate evaluates identically at runtime); the win is bundle-level. Locked by a bisect-verified tree-shake test that bundles the real runtime-server entry for the browser platform (the edge-bundle simulation — `platform: 'node'` masks the bug because esbuild folds `typeof process` there) and asserts counters + dev-warning strings are absent in prod / present in dev. The runtime-server bundle budget is ratcheted down 6,144 → 5,248 B. Everything else in the sweep came back clean — the `[Pyreon]` strings surviving in fundamentals bundles are all legitimate `throw` error paths that must ship.

- Updated dependencies [[`e1e5278`](https://github.com/pyreon/pyreon/commit/e1e527837f0761d2ee4815c2960f63d1dc70f522), [`b15b4b5`](https://github.com/pyreon/pyreon/commit/b15b4b5b823c85babc07b9250bc4fa39a4b22d31), [`a0c82c3`](https://github.com/pyreon/pyreon/commit/a0c82c3270a8e89e69d88046b590f04588f6802f), [`6d358d4`](https://github.com/pyreon/pyreon/commit/6d358d4d97ff8185518f58ddebb52233281cb83d), [`16f2ad1`](https://github.com/pyreon/pyreon/commit/16f2ad130f7ba1fd0e821bf28bc59fe49787790b), [`9562f24`](https://github.com/pyreon/pyreon/commit/9562f2489e1d7176dd41b1ec52fe0fb39568b100), [`fa95aba`](https://github.com/pyreon/pyreon/commit/fa95aba3aebc24d0178093cd89870b8807beca72), [`794fb27`](https://github.com/pyreon/pyreon/commit/794fb27e6fa67e71608b603cd627cf4eff61a102), [`f7083e5`](https://github.com/pyreon/pyreon/commit/f7083e5a56768fb67e097ec9bc6ee6d1bc6e0d09), [`c82687c`](https://github.com/pyreon/pyreon/commit/c82687c07a2b2ba976787dea74bc891f72a1165a), [`8a1feb0`](https://github.com/pyreon/pyreon/commit/8a1feb07faca643488c98e89db7bfc08d6867a31), [`8e8a0de`](https://github.com/pyreon/pyreon/commit/8e8a0de48a1c4aba4e09fc8e72fb72bc0c1ec68e), [`31cfc98`](https://github.com/pyreon/pyreon/commit/31cfc984138936feb5c51a2256cff7583e855187), [`31cfc98`](https://github.com/pyreon/pyreon/commit/31cfc984138936feb5c51a2256cff7583e855187), [`31cfc98`](https://github.com/pyreon/pyreon/commit/31cfc984138936feb5c51a2256cff7583e855187), [`08c022e`](https://github.com/pyreon/pyreon/commit/08c022e2d598ebf70f5b71bfc0a5b274e61991ef), [`08c022e`](https://github.com/pyreon/pyreon/commit/08c022e2d598ebf70f5b71bfc0a5b274e61991ef), [`74bbc94`](https://github.com/pyreon/pyreon/commit/74bbc9423245e0596872c9a7fb230bacdc411cca), [`08c022e`](https://github.com/pyreon/pyreon/commit/08c022e2d598ebf70f5b71bfc0a5b274e61991ef), [`08c022e`](https://github.com/pyreon/pyreon/commit/08c022e2d598ebf70f5b71bfc0a5b274e61991ef), [`8e8a0de`](https://github.com/pyreon/pyreon/commit/8e8a0de48a1c4aba4e09fc8e72fb72bc0c1ec68e)]:
  - @pyreon/zero@0.39.0
  - @pyreon/runtime-dom@0.39.0
  - @pyreon/reactivity@0.39.0
  - @pyreon/router@0.39.0
  - @pyreon/head@0.39.0
  - @pyreon/core@0.39.0

## 0.38.0

### Patch Changes

- [#1903](https://github.com/pyreon/pyreon/pull/1903) [`6108d39`](https://github.com/pyreon/pyreon/commit/6108d39945f08bec26c6af9be097faca33e23748) Thanks [@vitbokisch](https://github.com/vitbokisch)! - `<Example>` now lazy-mounts: each example's chunk is dynamically imported and
  mounted only when its wrapper nears the viewport (IntersectionObserver, 400px
  rootMargin), instead of eagerly on hydration. A page with many `<Example>`s
  (the docs examples gallery has 40+) previously fired every chunk load + mount
  on load, streaming content in progressively and pushing LCP out; now
  above-the-fold examples load immediately and the rest load just before they
  scroll into view (no perceptible delay). Falls back to eager loading when
  `IntersectionObserver` is unavailable. The loading-skeleton placeholder is now
  `aria-hidden` (it was a roleless `<div>` with `aria-label`, a prohibited-ARIA
  attribute that — with many skeletons coexisting below the fold under
  lazy-mount — would also have spammed screen readers with repeated "Loading
  example" announcements).
- Updated dependencies [[`8ca64d4`](https://github.com/pyreon/pyreon/commit/8ca64d4863bfc4c01f98880e9949307fa9f354d3), [`d59f8ac`](https://github.com/pyreon/pyreon/commit/d59f8acacc0fe1dcd3abad932b0a6fbddc78a85c), [`cfa422f`](https://github.com/pyreon/pyreon/commit/cfa422fdb6985e50c74e06cf0f4c1318213d6303), [`0376a3d`](https://github.com/pyreon/pyreon/commit/0376a3ddc75dd1fbee582e7cabe98beb01d60073), [`6ee46e7`](https://github.com/pyreon/pyreon/commit/6ee46e7dca1cb01aacaa7c61ef5dbbcf12b30668), [`442cc26`](https://github.com/pyreon/pyreon/commit/442cc26728fe5704a8bc9d8782f419d7a36a683a), [`8a221af`](https://github.com/pyreon/pyreon/commit/8a221af967dec5a2b28467423db2266456225b92), [`bb3adfe`](https://github.com/pyreon/pyreon/commit/bb3adfee32bfb53161b1401fcab51b42268ae107)]:
  - @pyreon/head@0.38.0
  - @pyreon/zero@0.38.0
  - @pyreon/reactivity@0.38.0
  - @pyreon/runtime-dom@0.38.0
  - @pyreon/core@0.38.0
  - @pyreon/router@0.38.0

## 0.37.1

### Patch Changes

- Updated dependencies []:
  - @pyreon/head@0.37.1
  - @pyreon/router@0.37.1

## 0.37.0

### Patch Changes

- Updated dependencies [[`7ee9e76`](https://github.com/pyreon/pyreon/commit/7ee9e760a024cc650b5187da7818b0da71698360), [`7a4e9c1`](https://github.com/pyreon/pyreon/commit/7a4e9c133cab77e96c455cefda801623dafef525)]:
  - @pyreon/zero@0.37.0
  - @pyreon/core@0.37.0
  - @pyreon/head@0.37.0
  - @pyreon/reactivity@0.37.0
  - @pyreon/router@0.37.0
  - @pyreon/runtime-dom@0.37.0

## 0.36.0

### Patch Changes

- Updated dependencies:
  - @pyreon/runtime-dom@0.36.0
  - @pyreon/zero@0.36.0
  - @pyreon/core@0.36.0
  - @pyreon/head@0.36.0
  - @pyreon/reactivity@0.36.0
  - @pyreon/router@0.36.0

## 0.35.0

### Patch Changes

- [#1840](https://github.com/pyreon/pyreon/pull/1840) [`384aeea`](https://github.com/pyreon/pyreon/commit/384aeea4d6fd052a9c18103e8fb116e6e3a211ec) Thanks [@vitbokisch](https://github.com/vitbokisch)! - fix(zero-content): `<CodeBlock>` no longer crashes on a fresh client mount (broken docs code samples)

  Navigating into the docs (e.g. clicking "Docs" from the landing page) threw
  `[Pyreon] <CodeBlock> threw during setup: HierarchyRequestError: Failed to
execute 'insertBefore' …` — once per code block — leaving the code samples
  rendered broken on first navigation.

  Root cause: `<CodeBlock>` conditionally rendered its filename header and
  line-number gutter (`{filename && <div…>}`). The compiler lowers a conditional
  wrapper child to a `_mountSlot` placeholder, and the static-element refs for the
  later siblings (`code-block__body` / `code-block__pre`) are computed by
  `.firstElementChild.nextElementSibling` walks emitted AFTER those slots. On a
  fresh client mount (the SPA-navigation path) an empty slot removes its `<!>`
  placeholder, the walk lands on the wrong node, and the copy-button slot's parent
  ends up a Comment node → `insertBefore` throws.

  Fix (backend-agnostic, single-`_tpl` preserving): the header + gutter wrappers
  are now ALWAYS rendered (a static, prop-derived `--empty` modifier class hides
  them when they have no content), so no `_mountSlot` precedes a ref'd element and
  the refs stay valid. The line-number gutter is rendered via
  `dangerouslySetInnerHTML` (mirroring how the Shiki output reaches
  `code-block__pre`) so it stays a static template element AND renders the numbers
  as real spans — a bare `{gutter}` array child was being baked to `textContent`
  (stringifying the span VNodes to `[object Object]`), and a `<For>` would have
  de-optimised every code block off the single-cloneNode fast path.

  The empty-state is a static class rather than a dynamic `hidden` attribute: the
  compiled template path emits a raw `el.setAttribute("hidden", value)` with no
  boolean-attr guard, so `hidden={false}` would set `hidden="false"` (attribute
  present → still hidden). `filename` / `showLineNumbers` are fixed per instance,
  so a className is both correct and sufficient.

  Bisect-verified end-to-end: reverting to the conditional header reproduces the
  `<CodeBlock> threw during setup` crash on a real-Chromium landing→docs
  navigation; the fix renders zero setup errors. New regression coverage: a docs
  e2e spec (`e2e/docs.spec.ts`) asserting no setup crash + code blocks render on
  the landing→docs path, plus zero-content unit specs for the always-rendered
  `--empty` header/gutter contract.

- Updated dependencies [[`8a1345d`](https://github.com/pyreon/pyreon/commit/8a1345d9b14f56130f38823b58745207c7bdf7ef), [`62f1191`](https://github.com/pyreon/pyreon/commit/62f119168078711ad4056c576805c71cff127c12), [`06971cc`](https://github.com/pyreon/pyreon/commit/06971cc33850a70dbf5ab335e491a535823dd576), [`1f29c4b`](https://github.com/pyreon/pyreon/commit/1f29c4b9791e6ad96901ca0e2b90e5335b803895), [`9967eb8`](https://github.com/pyreon/pyreon/commit/9967eb8f3396c6b1caf818f590e1ef9fe42d7387), [`2042ae5`](https://github.com/pyreon/pyreon/commit/2042ae59d1e3347db146ee7bbdf1b2229eabb812), [`02b77ae`](https://github.com/pyreon/pyreon/commit/02b77aed6b4383554b3458e408b462098fc3e708), [`35d440a`](https://github.com/pyreon/pyreon/commit/35d440a44d92ac913cf19f3f8e21b4603458a165), [`b96f66e`](https://github.com/pyreon/pyreon/commit/b96f66e8ed85a14353b7e203a6e4ae5f438f977e), [`af85ce3`](https://github.com/pyreon/pyreon/commit/af85ce3dfc590db06838834c32d88f434e7f2769), [`1c98f38`](https://github.com/pyreon/pyreon/commit/1c98f3863ccd2fd16a4ad6e20e82fb778725bca0)]:
  - @pyreon/runtime-dom@0.35.0
  - @pyreon/zero@0.35.0
  - @pyreon/router@0.35.0
  - @pyreon/core@0.35.0
  - @pyreon/head@0.35.0
  - @pyreon/reactivity@0.35.0

## 0.34.0

### Minor Changes

- [#1585](https://github.com/pyreon/pyreon/pull/1585) [`dcf3f96`](https://github.com/pyreon/pyreon/commit/dcf3f968daf7335db5b893d1672f458901516d5b) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Search now shows an empty state when a query finds nothing.

  Previously a no-match query rendered an empty list (the overlay just looked
  blank). `<Search>` now renders a `.pyreon-search__empty` message — defaulting to
  `No results for "<query>"`, customizable via the new `noResultsText?: (query) => string`
  prop. `useSearch` exposes a `status` signal (`idle` | `searching` | `ready`); the
  empty state shows ONLY after a search actually completes (`ready`), so it never
  flashes during the debounce / index-load window for a query that will match.

### Patch Changes

- [#1621](https://github.com/pyreon/pyreon/pull/1621) [`c2429aa`](https://github.com/pyreon/pyreon/commit/c2429aa7563cee55ad1f9fc3ea9e629849886bc2) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fix the search-index emission during `@pyreon/zero`'s inner SSG server build.
  That inner build is also `command: 'build'`, so it slipped past the dev-mode
  gate and re-emitted the index — and because it sets `build.outDir` to an
  absolute path, `path.join(root, build.outDir)` concatenated it onto `root`,
  writing the index to a doubled `<root>/<root>/dist/.zero-ssg-server/…` path.
  The `closeBundle` hook now skips when `PYREON_ZERO_SSG_INNER_BUILD` is set (the
  outer client build already owns the index) and uses `path.resolve` so an
  absolute `build.outDir` can never double.
- Updated dependencies [[`c0814b7`](https://github.com/pyreon/pyreon/commit/c0814b7881b01b7bfed19dffd7f48a3269c14199), [`66d44c5`](https://github.com/pyreon/pyreon/commit/66d44c58920bf81848e9ba858c413a88727a3c65)]:
  - @pyreon/runtime-dom@0.34.0
  - @pyreon/reactivity@0.34.0
  - @pyreon/head@0.34.0
  - @pyreon/core@0.34.0
  - @pyreon/router@0.34.0
  - @pyreon/zero@0.34.0

## 0.33.0

### Minor Changes

- [#1575](https://github.com/pyreon/pyreon/pull/1575) [`b8bc8ff`](https://github.com/pyreon/pyreon/commit/b8bc8ffe75a64d5654db2aac72d93e1216f7cc16) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Search results now deep-link to the heading that best matches the query.

  Each page's heading anchors (slug + lowercased text) are stored alongside its
  search document, and the runtime picks the heading containing the most of the
  query's matched terms — so a hit jumps to the exact section (`/docs/router#keyed-lists`)
  instead of the page top. Falls back to the page URL when the match is in the
  body or page title (no heading contains a matched term).

  This delivers section-precision navigation without one search document per
  section, which ~4×'s the index and would hit the 1 MB build-error budget at
  ~130 pages. The page stays the single searchable unit; the docs index grows
  modestly (~210 KB → ~350 KB at 96 pages) with headroom to ~270 pages.

- [#1568](https://github.com/pyreon/pyreon/pull/1568) [`98e850e`](https://github.com/pyreon/pyreon/commit/98e850e03911f02b325e972e76232912a1da967d) Thanks [@vitbokisch](https://github.com/vitbokisch)! - feat(zero-content): synonym/jargon-aware search

  Docs search now expands framework-vocabulary synonyms at query time, so a
  search for "reactive" finds the **signal** docs, "hydration" finds
  "hydrate", "routing" finds the **navigation** pages, "attributes" finds
  **props**, and so on. Plain keyword search — what VitePress and most docs
  sites ship — returns nothing for these vocabulary mismatches, which is the
  single biggest real-world relevance gap in docs search.

  Implemented as a conservative, curated `searchOptions.processTerm` (11
  high-confidence equivalence groups) — expansion happens at SEARCH time only,
  so the prebuilt `search-index-*.json` is unchanged (`MiniSearch.loadJSON`
  stays happy) and the index stays small. Non-jargon queries are untouched, so
  precision holds (a search for "tooltip" still returns only the tooltip page).

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.33.0
  - @pyreon/head@0.33.0
  - @pyreon/reactivity@0.33.0
  - @pyreon/router@0.33.0
  - @pyreon/runtime-dom@0.33.0
  - @pyreon/zero@0.33.0

## 0.32.0

### Minor Changes

- [#1410](https://github.com/pyreon/pyreon/pull/1410) [`0395786`](https://github.com/pyreon/pyreon/commit/03957866e88e933704987cfae68f99b014b411d8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-H — markdown authoring features (audit M1+M2+M3+M4+M5+M12+M16)

  Seven independent fixes, one PR:

  - **M1**: line highlighting via `\`\`\`ts {1,3-5}`meta token. Compiles to`<CodeBlock highlightLines={[1,3,4,5]}>`which lands on a`data-pyreon-highlight-lines` attribute consumers style via CSS.
  - **M2**: line numbers via `\`\`\`ts showLineNumbers` meta. The
    CodeBlock component renders a gutter span per line so CSS counters
    stay structural without re-flowing Shiki HTML.
  - **M3**: copy button on code blocks (default on, opts out via
    `noCopy`). Reads `props.source` at click time. `filename="..."`
    meta token renders a header above the block.
  - **M4**: diff syntax — Shiki already ships the `diff` grammar; the
    lang prop now flows through consistently for CSS styling.
  - **M5**: footnote support — emits `footnoteReference` as
    `<sup class="footnote-ref"><a>` and `footnoteDefinition` as
    `<li class="footnote-definition">`.
  - **M12**: data-lang consistency. Both highlighted AND plain code
    blocks now ship through `<CodeBlock>`, so authoring features
    (filename header, copy button, line numbers) stay consistent
    regardless of Shiki being enabled.
  - **M16**: visible warning on unhandled mdast nodes. Pre-fix the
    emitter silently emitted a JSX comment that vanished in production
    (JSX comments tree-shake). The fallback now fires
    `onUnhandledNode` which surfaces through `this.warn(...)` AND
    emits a grep-able string comment in the rendered output.

  30 new specs cover the seven contracts; bisect-verified across M1
  (drop highlightLines prop emission) and M16 (drop the
  onUnhandledNode invocation).

- [#1411](https://github.com/pyreon/pyreon/pull/1411) [`a5bfd6e`](https://github.com/pyreon/pyreon/commit/a5bfd6e57a833c5c51f643e704a4b7a89a961fb4) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-I — sidebar / router widgets (audit H11+H13+M9+M10+M11)

  Five independent fixes:

  - **H11**: `defineSidebar({ groups })` config-driven mode. Pin navigation
    structure in TypeScript instead of relying on frontmatter
    `sidebar.order` / `sidebar.group`. `<Sidebar config={...}>` skips
    the auto-sort pass and preserves authored order.

  - **H13**: `<Toc>` smooth-scroll on click. Intercepts the link click,
    animates `window.scrollTo({ behavior: 'smooth' })`, and updates the
    URL hash without the native jump. `scrollOffset` accommodates
    sticky headers; `smoothScroll: false` opts out.

  - **M9**: `<PrevNext>` page navigation footer. Pure resolver
    (`resolvePrevNext`) + a rendered component that reads
    `currentPath`. Renders `nothing` when current entry isn't in the
    list; omits the prev/next link at list boundaries.

  - **M10**: `<Breadcrumbs>` path crumb trail. Auto-derives labels from
    URL segments (`getting-started` → `Getting Started`). Optional
    `entries` lookup mode uses explicit `SidebarEntry` titles when a
    URL matches.

  - **M11**: `resolvePageLayout` — frontmatter-driven per-page layout
    override. Falls back to the default layout with a `fellBack: true`
    flag when the named layout isn't in the registry, so callers can
    surface a console warning.

  29 new specs cover the five contracts; H11 + M11 bisect-verified.

- [#1412](https://github.com/pyreon/pyreon/pull/1412) [`3b29192`](https://github.com/pyreon/pyreon/commit/3b29192b47d5da3724fa6d2d7cb7ce1e5da63cfe) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-J — collection features (audit M13+M14+M15+H10+H3+L7)

  Six independent fixes:

  - **M13**: `getCollection({ includeDrafts })` filters out entries
    whose frontmatter has `draft: true`. Defaults to off in production,
    on in dev so authors can preview WIP. Also accepts a `filter`
    predicate for tag-based listings, date-range filters, etc.

  - **M14**: `reference(collection, slug)` typed helper + runtime
    resolvers (`resolveReference` / `resolveReferences`). Lets schemas
    store typed cross-collection pointers — frontmatter holds plain
    JSON, lookup happens on demand.

  - **M15**: schema-shape convention for `draft` / `publishDate` /
    `updatedDate` documented; the runtime honors `draft` via M13.

  - **H10**: longest-prefix collection lookup. Nested collection paths
    (`src/content/docs` AND `src/content/docs/api`) now route each
    file to the deeper collection regardless of config declaration
    order. Pre-fix iteration-order-dependent.

  - **H3**: h1-h6 heading capture (was h2-h3 only). Configurable via
    the new `headingsMinLevel` / `headingsMaxLevel` emit options.

  - **L7**: heading slug dedup. Two `<h2>Examples</h2>` sections get
    unique anchor ids (`examples` + `examples-2`); both the rendered
    id AND the captured `slug` field carry the deduped value so
    deep-links resolve correctly.

  24 new specs cover the six contracts; H10 + L7 bisect-verified.

- [#1413](https://github.com/pyreon/pyreon/pull/1413) [`d2689e8`](https://github.com/pyreon/pyreon/commit/d2689e830328e2e27ddc03217cf0c17ce0628ad5) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-K — extended built-in components (audit H2 + H14)

  Ships the six previously-claimed-but-missing built-in components so
  the markdown pipeline auto-imports them without per-file `import`
  statements:

  - **`<Playground>`** — minimal sandboxed editor + iframe preview.
    Supports HTML / CSS / JS sources, signal-driven srcdoc rebuild,
    configurable height. Uses a fresh `<iframe sandbox="allow-scripts">`
    (no `allow-same-origin`) so user code can't reach the host page.

  - **`<PackageBadge>`** — npm package install card. Renders name,
    version, optional description, and per-manager install commands
    (bun/npm/pnpm/yarn/deno). `renderInstallRows` is exported as a
    pure helper.

  - **`<Tabs>`** — generic tab strip. Supports two render shapes
    (labels + children OR `items: [{label, content}]`). Reactive
    active-tab signal; arrow-keyboard navigation via natural focus.

  - **`<APICard>`** — API signature card. Heading + signature + summary

    - optional stability badge + `since` version + deep-link anchor.
      `deriveApiId` exported as a pure helper.

  - **`<PropTable>`** — props reference table. One row per
    `{ name, type, default, required, description }`. Configurable
    column labels.

  - **`<CompatMatrix>`** — feature/platform compatibility matrix.
    Maps features (rows) to platforms (columns) with normalized status
    cells: `true` → ✓, `false` → ✗, `'partial'` → 🚧, `'planned'` → ⏳,
    custom strings pass through. `renderCompatCell` exported.

  All six added to `BUILT_IN_COMPONENTS` (now 9 entries, alphabetically
  sorted). Scanner / validator tests updated to lock in the canonical
  list.

  32 new specs cover the six components; H2 bisect-verified by dropping
  the new entries from the catalogue.

- [#1414](https://github.com/pyreon/pyreon/pull/1414) [`49acbbb`](https://github.com/pyreon/pyreon/commit/49acbbb40e95d93702b69725f1bfce10e0c9f6a1) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-L — SEO + build outputs (audit M19)

  Three pure builders + a plugin auto-emit hook:

  - **`generateSitemap({ baseUrl, pages })`** — sitemap.xml per
    sitemap.org spec. Supports `lastmod`, `changefreq`, and clamped
    `priority` fields. XML-escapes URLs so user-content `?a=1&b=2`
    paths survive.

  - **`generateRssFeed({ title, baseUrl, items })`** — RSS 2.0 with
    ISO-8601 → RFC-822 date conversion (`toRfc822` exported). Supports
    per-item `pubDate`, `author`, `categories`, custom `guid`, and
    channel-level `language` / `lastBuildDate`.

  - **`generateLlmsTxt({ title, baseUrl, sections })`** — llmstxt.org
    index format. Markdown-shaped page list per section with optional
    blockquote site description.

  - **Plugin auto-emit** via `content({ seo: { baseUrl, sitemap, rss,
llms } })`. `closeBundle` walks the accumulated collection
    entries and writes `dist/sitemap.xml`, `dist/rss.xml`, and
    `dist/llms.txt` per opt-in flags. Per-collection URL overrides
    (`collectionUrls: { blog: '/news' }`) and skip-collection
    (`collectionUrls: { drafts: null }`) supported.

  27 new specs cover the three builders + the plugin hook;
  bisect-verified by short-circuiting the sitemap emission branch.

- [#1415](https://github.com/pyreon/pyreon/pull/1415) [`3576862`](https://github.com/pyreon/pyreon/commit/3576862b59d9d91fe96736e59d91a0c7ca713a19) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-M — math + mermaid + details directives (audit M6+M7+M8)

  Three new `:::` block directives recognised by the markdown pipeline,
  each emitting a JSX element the consumer can render:

  - **M6** — `:::math` block for LaTeX. Body extracted verbatim from
    the raw source (curly braces / backslashes / `^` survive remark-
    parse's markdown inline transformations). `:::math inline` renders
    in display:inline mode. Emits `<Math>` which dynamically imports
    KaTeX on the client; SSR + no-KaTeX builds render a `<code>`
    fallback so the LaTeX source is always surfaced.

  - **M7** — `:::mermaid` block for diagrams. Emits `<Mermaid>` which
    dynamically imports mermaid on the client; SSR + no-mermaid builds
    render a `<pre><code>` fallback.

  - **M8** — `:::details[Label]` block for collapsible disclosure.
    Emits `<Details>` which renders native `<details>` + `<summary>`.
    No peer deps required.

  All three components added to `BUILT_IN_COMPONENTS` (Details / Math /
  Mermaid). Both KaTeX and mermaid are optional peer dependencies —
  consumers install them only if they want client-side rendering.

  10 new specs cover the three directives + components; M6 bisect-
  verified by short-circuiting the `math` case in the remark plugin.

- [#1398](https://github.com/pyreon/pyreon/pull/1398) [`d5630f0`](https://github.com/pyreon/pyreon/commit/d5630f0d8631d0f3e20ec11dcd4a70e4bd41cbf8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - fix(zero-content): re-export built-in MDX components from virtual:zero-content/components

  The markdown compiler emits `<CodeBlock>` for highlighted fenced code blocks and `<Callout>` for `::: tip` admonitions, then auto-emits `import { CodeBlock } from 'virtual:zero-content/components'` at the top of the compiled `.tsx`. The virtual module however only re-exported user-scanned components from `src/mdx/` — never the built-ins themselves — so every page with a code block or callout crashed the build with `MISSING_EXPORT "CodeBlock" is not exported by "virtual:zero-content/components"`.

  The virtual module now ALWAYS re-exports `Callout`, `CodeGroup`, `CodeBlock` from `@pyreon/zero-content`. User-scanned components with the same name take precedence (the documented escape hatch for projects shipping a custom `Callout.tsx` etc.).

  Also fixed `emit-jsx.ts`: the `emitCode` highlighter path now calls `opts.mdxComponentRef?.('CodeBlock')` so `compileMarkdown` actually includes `CodeBlock` in the auto-import statement at the top of the emitted `.tsx`. Without the ref call, `CodeBlock` was referenced as a free identifier in the compiled output and threw `ReferenceError: CodeBlock is not defined` at SSG render time. Both halves of the bug — emit-side ref tracking + virtual-module re-export — needed fixing.

  End-to-end consequence: `mode: 'ssg'` apps that use `@pyreon/zero-content` now ship FULL article body HTML in their prerendered `dist/<route>/index.html` (verified by docs-zero migration: 92/92 pages prerender with complete content, headings, code blocks). Pre-fix the body was empty until client-side hydration filled it in — bad for SEO, first paint, and no-JS users.

- [#1375](https://github.com/pyreon/pyreon/pull/1375) [`46e192d`](https://github.com/pyreon/pyreon/commit/46e192d8fc82e4c7220f1144cb575b706c7eac45) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 4: content collections + zod schema validation + runtime queries + `.pyreon/content-types.d.ts` type emission + `virtual:zero-content/collections` runtime module.

  The fourth piece of the markdown-driven Pyreon docs story — adds Astro-style typed queries on top of the markdown→Pyreon JSX pipeline.

  Layers shipped:

  - **`getCollection<K>(name)` / `getEntry<K, S>(...)` / `getEntries<K>(...)`** runtime queries (`src/runtime.ts`). Read from the plugin-emitted virtual collections registry. Typed via the augmented `CollectionSchemas` interface; ergonomic loose fallback when augmentation isn't present.
  - **`content.config.ts` loader** (`src/config-loader.ts`). Discovery walks for `content.config.{ts,mts,js,mjs}` under the Vite root; loads via `ssrLoadModule` for `.ts`, dynamic `import()` for `.js` / `.mjs`. Structural shape validation surfaces missing collections / unknown `type` / missing schema as clear build errors with file labels.
  - **Standard Schema frontmatter validation** (`src/schema-validate.ts`). Duck-typed `~standard.validate` interface — works with zod, valibot, arktype, typia, or any spec-compliant validator. Issues are formatted as multi-line `path: message` lines and surface via the plugin's `this.error()` with the file shortname.
  - **Type emission** (`src/type-emit/content-types.ts`). Writes `<root>/.pyreon/content-types.d.ts` declaring an augmentation of `CollectionSchemas` with `StandardSchemaV1.InferOutput<typeof ContentConfig.default.collections[K]["schema"]>` — typed `entry.data` flows from the user's zod schema through TypeScript inference.
  - **`virtual:zero-content/collections` virtual module** (`src/virtual-collections.ts`). Renders one `import.meta.glob` per collection + a `_setRegistry({...})` call that hands the runtime its registry on module evaluation. Slug derivation matches `deriveSlug` (strips ext + `/index`).
  - **Plugin integration** (`src/plugin.ts`). New hooks: `configResolved` (fast-load `.js`/`.mjs` config + emit types); `configureServer` (load `.ts` via Vite's ssrLoadModule); `buildStart` (build-only fallback); `resolveId` + `load` (serve `virtual:zero-content/collections`); `transform` (apply collection schema to frontmatter); `handleHotUpdate` (invalidate collections module on `content.config` change + reload + re-emit types).
  - **`@pyreon/zero-content/schema` subpath** — re-exports `StandardSchemaV1.InferOutput` for the generated `.pyreon/content-types.d.ts` to consume.

  72 new specs across 4 test files: `runtime.test.ts` (16), `config-loader.test.ts` (16), `schema-validate.test.ts` (18), `virtual-collections.test.ts` (12), plus integration tests in `plugin-collections.test.ts` (10).

  333/333 specs passing. 11/11 validate-fast gates. typecheck + lint clean.

  Coverage: statements 98.37%, branches 93.14%, functions 97.67%, lines 99.5%. Branches at 93.14% (under the 95% floor); `BELOW_FLOOR_EXEMPTIONS` entry in `scripts/check-coverage.ts` documents the residual gap as plugin lifecycle error paths (configureServer/buildStart synthetic loader failures, HMR reload-error arms) + runtime overload-alternative arms. PR 5-7 will lift this as the docs-pyreon spike (PR 7) exercises more integration paths end-to-end.

- [#1397](https://github.com/pyreon/pyreon/pull/1397) [`6cdae79`](https://github.com/pyreon/pyreon/commit/6cdae79903cd00c96410dcc6bad39669d9b8898b) Thanks [@vitbokisch](https://github.com/vitbokisch)! - feat(zero-content): accept custom Shiki theme objects (not just bundled names)

  `HighlighterOptions.themes` now accepts `ShikiTheme = string | Record<string, unknown>` — either Shiki's bundled theme names (`'github-dark'`) OR a custom `ThemeRegistrationAny` object. Custom themes get their `.name` property extracted automatically for the per-code-block `codeToHtml` call that references them.

  Enables the docs-zero migration to ship the brand-handoff `pyreon-syntax` themes (verbatim port from VitePress's `markdown.theme: { light, dark }` config) without forking the highlighter.

- [#1552](https://github.com/pyreon/pyreon/pull/1552) [`8911a5f`](https://github.com/pyreon/pyreon/commit/8911a5f7c0b853420db994be8e5c8185edd957bb) Thanks [@vitbokisch](https://github.com/vitbokisch)! - First public release. `@pyreon/zero-content` is the content layer for markdown-driven Pyreon sites: compile-time `.md` → Pyreon JSX through a remark pipeline (Shiki dual-theme highlighting, callout/code-group containers, heading anchors), typed collections with zod schemas (`getCollection`/`getEntry` with emitted ambient types), MDX components scanned from `src/mdx/`, and the `<Example>` primitive — real `.tsx` files mounted inline with optional cross-mount signal-bridging via `share`. Tightly coupled to `@pyreon/zero` by design (the integration IS the value). Powers the production Pyreon docs site. The 20 pending feature changesets accumulated while private all ship in this first publish.

- [#1374](https://github.com/pyreon/pyreon/pull/1374) [`4ba7bd0`](https://github.com/pyreon/pyreon/commit/4ba7bd08fefc98abd1bc2651bb068d660a13d12e) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 3: MDX (JSX-in-markdown) + `src/mdx/` convention scanner + `virtual:zero-content/components` virtual module + build-time component-reference validation.

  The third piece of the markdown-driven Pyreon docs story — completes the user-facing contract of "drop a `.tsx` in `src/mdx/`, use it in any `.md` by name".

  - **MDX integration** — `remark-mdx` plugged into the unified pipeline. The full MDX grammar works in any `.md` / `.mdx` file: JSX elements (flow + inline), `{expression}` blocks, fragments, spread attributes, expression attributes, boolean attributes, self-closing components. The pipeline gracefully composes with `remark-directive` so `:::tip` syntax keeps working alongside `<Callout>` JSX.
  - **`src/mdx/**/\*.{ts,tsx,js,jsx}` convention scanner** (`mdx-scan/scanner.ts`) discovers PascalCase exports recursively, excludes `\_`-prefixed files (escape hatch for `defineComponents` bundles), supports the full export-form matrix (`const`/`let`/`var`/`function`/`class`/anonymous default with PascalCase basename/named `export { X }`/`export { X as Y }`/`export { X as default }`), and detects name collisions across the whole tree.
  - **`virtual:zero-content/components` Vite virtual module** (in the plugin's `resolveId`/`load` hooks). Re-exports every scanned component (both default and named exports normalised). Duplicate-name collisions emit `this.warn()` so users see ALL collisions, not just the first.
  - **Per-`.md` ESM hoisting** — `import X from './path'` statements at the top of a markdown file flow through `mdxjsEsm` → hoisted to the compiled `.tsx` module head. Useful for one-off components that don't fit `src/mdx/`.
  - **Auto-import** — every uppercase JSX tag referenced in a markdown body produces one `import { Name1, Name2 } from "virtual:zero-content/components"` line at the top of the compiled `.tsx`. Names already brought in via hoisted ESM are suppressed (no double-import).
  - **Build-time component-reference validation** with Levenshtein "did you mean?" suggestions. `<Calout type="tip">` → `[@pyreon/zero-content] src/content/x.md: 1 unknown component reference(s).\n  - Unknown component <Calout />. Did you mean <Callout />?` — surfaced via the plugin's `this.error()`. Distance threshold 3; case-insensitive matching.
  - **HMR for `src/mdx/`** — the plugin's `handleHotUpdate` watches the scan directory; any change invalidates the cached `virtual:zero-content/components` module via `moduleGraph.invalidateModule`. Vite's graph propagation then re-renders every dependent `.md` page without a full reload.

  Pipeline: `parse → frontmatter → gfm → directive → callout → codegroup → mdx → emit-jsx (with Shiki + mdx hoist callbacks)`.

  100 new specs across `mdx.test.ts` (23), `scanner.test.ts` (35), `validate.test.ts` (24), `plugin-mdx.test.ts` (10), plus a few in `parse.test.ts`. 261/261 specs passing. Bisect-verified: removing `remark-mdx` from the pipeline fails 10/23 MDX specs with `expected '…' to contain '<Callout type=…>'`.

  11/11 validate-fast gates pass. Coverage: statements 98.59%, branches 95.00%, functions 98.95%, lines 100%.

- [#1378](https://github.com/pyreon/pyreon/pull/1378) [`452904a`](https://github.com/pyreon/pyreon/commit/452904aa135159dd2f58383fe8d967eda21202b4) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 9: JSX handoff fix (closes PR 7 spike finding) + polish.

  PR 7's docs-pyreon migration spike surfaced that `.md` → emitted JSX wasn't being further compiled to JS by downstream tooling — the file's `.md` extension prevented Rolldown from enabling JSX parsing. The result was a hard build failure on every consumer of `@pyreon/zero-content` that emitted any structural JSX.

  Changes:

  - **JSX → JS compilation in the plugin's `transform`** (`src/plugin.ts`). After `compileMarkdown` emits Pyreon JSX, the plugin runs an esbuild pass converting it to plain `h(...)` / `Fragment` calls with an automatic `import { h, Fragment } from '@pyreon/core'` banner. Downstream tooling sees plain JS; the `.md` extension is no longer a parsing constraint. The Pyreon `h` shape matches React's `createElement` signature, so the esbuild-emitted output works unmodified at runtime.
  - **`compileJsx?: boolean`** plugin option (default `true`). Tests assert on the raw emit-jsx output via `compileJsx: false`; production builds always want the compiled form (default).
  - **`as const` stripped** from the virtual components module body — esbuild + bare Rolldown parsers don't accept TS-only syntax in user-loaded modules. The runtime shape is unchanged.
  - **`esbuild` moved from peerDeps assumption to a direct dep**. Vite already ships esbuild, so the marginal impact on install footprint is zero.

  The `examples/docs-pyreon` consumer's `bun run build` now completes successfully end-to-end (see PR 7 — re-tested post-fix, prod bundle includes the migrated `reactivity.md` as a route-split chunk).

  382/382 specs passing. 11/11 validate-fast gates. typecheck + lint clean.

  **Still deferred** (open items for follow-up PRs):

  - **MCP integration** (`get_content_collection` / `get_content_entry` MCP tools) — out of scope for this PR; tracked separately.
  - **`pyreon doctor --check-content`** — out of scope for this PR.
  - **Real-Chromium e2e gate** against a rendered MDX page — PR 7's example builds now; a follow-up adds the gate.
  - **The `<Playground>` native-mode escape** — current iframe-sandbox approach works; native mode is an enhancement.

- [#1405](https://github.com/pyreon/pyreon/pull/1405) [`3b6fe59`](https://github.com/pyreon/pyreon/commit/3b6fe59045a3e689b92ebeda9e26f43d3dc11e0c) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-C audit H1 — `defineContentRoute()` helper. The single highest-leverage feature in the audit. Pre-fix every consumer re-invented the same ~80-line async-body + Suspense + cast dance just to make a route load a markdown entry by slug.

  ```ts
  // Before — examples/docs-zero/src/routes/docs/[...slug].tsx
  import { Suspense } from "@pyreon/core";
  import { useParams } from "@pyreon/router";
  import { getEntry } from "@pyreon/zero-content";
  // ... 80 lines including async DocBody, Suspense wrap, ComponentFn cast, etc.

  // After
  import { defineContentRoute } from "@pyreon/zero-content";
  export default defineContentRoute("docs", {
    wrap: (entry, body) => <Layout entry={entry}>{body}</Layout>,
  });
  ```

  Override surface (all optional):

  - `fallback` — synchronously-rendered placeholder for `<Suspense>` (defaults to an empty `<article>` matching docs-zero / VitePress lineage; pass `null` to drop)
  - `notFound` — `ComponentFn<{ slug: string }>` rendered when the slug doesn't resolve
  - `wrap` — receives `(entry, body)` to inject layouts (frontmatter-driven layouts, sidebars, page footers)
  - `articleClass` — defaults to `'docs-content vp-doc'`; pass `null` to skip the article wrapper entirely (useful when `wrap` provides its own)

  Regression coverage: `_define-content-route.test.tsx` — 8 SSR specs covering registry-driven rendering, 404 fallback, custom notFound, wrap, articleClass: null, catch-all array params, empty slug for the bare `/<collection>/` route. Bisect-verified.

  Real-app proof: `examples/docs-zero/src/routes/docs/[...slug].tsx` migrated to the helper (80 → 60 lines, with the new shape exposing `PageMeta` + `Toc` via the `wrap` callback). Docs-zero builds 93 pages + 404.html on the helper-based route.

- [#1406](https://github.com/pyreon/pyreon/pull/1406) [`cde24f3`](https://github.com/pyreon/pyreon/commit/cde24f3184c3b1ec0a11806ffebbcfbde8298ad8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-D audit — search correctness. Seven items, 13 new bisect-verified specs.

  **C7** — index leaks fixed.

  - Frontmatter YAML block (`---...---`) stripped from `body` before indexing. Pre-fix ~12 KB on docs-zero shipped to the client untouched.
  - Heading text is no longer double-indexed (was in both the dedicated `headings` field AND embedded in `body`). Saves ~26 KB on docs-zero.

  **C8** — `useSearch` subscriber cleanup.

  - Pre-fix `debounced.subscribe(() => ...)` never disposed. Every `useSearch()` call accumulated a subscriber that outlived the component. Now query/open/debounced unsubscribes are all returned from the `onMount` cleanup.

  **H9** — MiniSearch instance refcounted; redundant computed collapsed.

  - The module-level `_instance` is now reference-counted via `acquireSearchInstance()` — when the last `useSearch` consumer unmounts, the cache is released so an SPA without a persistent `<Search>` reclaims ~200 KB.
  - `loadSearchIndex` now caches keyed by `catalogUrl` (a URL change triggers a rebuild).
  - The duplicate `_results: signal + results: computed` pattern collapsed to one `computed` since the inner signal was just being unwrapped.

  **M20** — `<RouterLink>` instead of `<a href>` on result rows.

  - Pre-fix every result click triggered a full page reload because the `<a href>` bypassed Pyreon's router. SPA-navigates now; `state.close()` runs before the navigation push so the dialog doesn't flicker across the route change.

  **M21** — focus management.

  - Capture the previously-focused element on open; restore on close (via Cmd+K, Esc, or Close button). Auto-focus the search input on open via a `queueMicrotask` after the dialog mounts.

  **M22** — `minQueryLength` option.

  - `UseSearchOptions.minQueryLength` and `SearchProps.minQueryLength` default to `2`. Single-character queries hit too broad a result set on docs-sized corpora; the floor cuts wasted MiniSearch passes.

  **M23** — `searchBodyMax` configurable.

  - `ContentPluginOptions.searchBodyMax` (default `1500`, accepts `Infinity` to disable truncation) replaces the previously-hardcoded magic constant. Documents the trade-off in the JSDoc.

  Regression coverage: `_search-correctness.test.tsx` — 13 specs across H9/M22/M23/M20/C8; `index-builder.test.ts` updated for C7 (heading-line drop + frontmatter strip + malformed-frontmatter graceful).

  Total: 427 specs pass (was 412 in PR C).

- [#1407](https://github.com/pyreon/pyreon/pull/1407) [`6ec729c`](https://github.com/pyreon/pyreon/commit/6ec729c28fbbb3fadc1bb80a84f59f9b608a70bd) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-E audit — types correctness. Four items, 12 new bisect-verified specs.

  **C5** — Frontmatter JSON Schema now introspects Zod-shaped schemas.

  - Pre-fix the emitter always produced a fully permissive schema regardless of what the user wrote in `defineCollection({ schema })`, so IDE autocomplete only suggested `title` / `description` / `sidebar` — never the user's actual fields.
  - Now: when the schema looks like `z.object({...})` (has `_def.shape()`), the emitter enumerates top-level keys, maps each Zod type name to a JSON Schema type, unwraps `optional()`/`nullable()`/`default()` for required-vs-optional detection, and emits `additionalProperties: false`. Non-Zod schemas (Valibot/ArkType/Typia) still fall back to the permissive shape; per-validator adapters can land later without touching the call site.

  **C6** — Generated `.pyreon/content-types.d.ts` resolves under both `bundler` and `nodenext`.

  - Pre-fix used `import type * as ContentConfig from '<rel-path>/content.config'` (extensionless) which broke under `moduleResolution: nodenext` (requires explicit extensions) and didn't work without `allowImportingTsExtensions`.
  - Now uses the TYPE-POSITION `typeof import('...')` form which TypeScript resolves at type-check time, so it works under both resolution modes.

  **L2** — Emits `.pyreon/tsconfig.json` the user can extend.

  - Pre-fix the user had to remember to add `.pyreon/**` to their tsconfig `include` array.
  - Now `writeContentTypes` writes both `content-types.d.ts` AND a minimal `tsconfig.json` that includes just the `.d.ts`. Users can `"extends": "./.pyreon/tsconfig.json"` and the generated types flow in automatically.

  **L16** — `.pyreon/vscode-settings.json` non-clobber path documented.

  - Settings still emit to `.pyreon/vscode-settings.json` (NOT `.vscode/settings.json`) BY DESIGN to avoid clobbering user editor preferences. The JSDoc now spells this out explicitly so it doesn't read as a bug.

  Coverage: `_types-correctness.test.ts` (12 new specs spanning `isZodObjectSchema`, `zodTypeNameToJsonSchema`, `zodFieldToJsonSchema`, `buildJsonSchemaFromZod`, `buildSchemaForCollection`, end-to-end `emitFrontmatterSchemas`, `typeof import` shape, `renderPyreonTsconfig`, `writeContentTypes` BOTH-files emission, and L16 non-clobber).

  Total: 454 specs pass (was 427).

- [#1408](https://github.com/pyreon/pyreon/pull/1408) [`3b1bf06`](https://github.com/pyreon/pyreon/commit/3b1bf0697c14b4458a7aefc0f28e3625fa619ae2) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-F audit — link + image rewriting. Two items, 24 new bisect-verified specs.

  **H7** — Local images now route through `<Image>`.

  - Pre-fix every `![alt](./hero.png)` emitted as a plain `<img>`, bypassing zero's image-optimization pipeline (auto srcset, blur placeholder, lazy-loading).
  - Now: when the image src starts with `./` or `../`, the emit-jsx layer emits `<Image src={import('./hero.png?optimize')} alt={...} />`. Absolute URLs and `data:` URIs fall through to `<img>`.
  - `Image` was added to the shared `BUILT_IN_COMPONENTS` list + re-exported as a built-in (`@pyreon/zero-content` → `@pyreon/zero`'s `<Image>`). The rewriter registers `Image` via `mdxComponentRef` so it auto-imports through `virtual:zero-content/components` — no user-side `import { Image }` boilerplate.

  **H8** — `[foo](./bar.md)` rewrites to route URLs.

  - Pre-fix every internal `.md` link shipped as a literal `./bar.md` href, 404-ing on every static host and confusing the in-app router on others.
  - Now: a plugin-supplied resolver rewrites relative `.md`/`.mdx` paths to `/<collection>/<slug>` (preserving any `#anchor` suffix). The resolver is conservative — non-`./`/`../` paths, non-.md targets (e.g. `./schema.json`), and files outside `/content/` all return `null` so the emitter passes them through.
  - Internal `/index` collapse so `./index.md` → `/<collection>` (matching the `deriveSlug` convention).

  Coverage: `_link-image-rewrite.test.ts` — 24 new specs spanning `isRelativePath`, `makeInternalLinkResolver` (8 specs covering happy paths, anchor preservation, nested dirs, `/index` collapse, `.mdx` extension, null passthroughs), and 3 end-to-end `compileMarkdown` specs.

  Total: 466 specs pass (was 454).

- [#1418](https://github.com/pyreon/pyreon/pull/1418) [`430f506`](https://github.com/pyreon/pyreon/commit/430f5060976836237ab0b71eca59dd3262f5f26b) Thanks [@vitbokisch](https://github.com/vitbokisch)! - `defineContentRoute` now emits `<title>` and `<meta>` tags from
  frontmatter via `@pyreon/head`'s `useHead`.

  Closes a real functionality gap: `defineContentRoute` reads
  frontmatter at SSG time but previously didn't surface `title` /
  `description` as page head tags — every consumer had to wire
  `useHead` separately or accept a blank `<title>`.

  Default behaviour (zero-config): the entry's frontmatter `title`
  becomes `<title>`, and `description` becomes both
  `<meta name="description">` and `<meta property="og:description">`.
  When `title` is present, an `<meta property="og:title">` is emitted
  too. Title-less entries skip `<title>` emission so the parent
  layout's default stays in effect.

  Override via the new `head` option:

  - `head: (entry) => UseHeadInput` — full control (canonical URLs,
    OG images, JSON-LD, etc.)
  - `head: false` — skip emission entirely
  - `head` omitted — use the default frontmatter-to-head mapping

- [#1375](https://github.com/pyreon/pyreon/pull/1375) [`46e192d`](https://github.com/pyreon/pyreon/commit/46e192d8fc82e4c7220f1144cb575b706c7eac45) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 5: built-in search — minisearch index builder + `<Search>` runtime component.

  The fifth piece of the markdown-driven Pyreon docs story. Adds a complete search layer: build-time index emission + lazy client-side loading + a default `<Search>` overlay with Cmd+K + debounced query + SPA navigation.

  Layers shipped:

  - **Index builder** (`src/search/index-builder.ts`) — `buildSearchIndex({ config, entries, root, outDir })` walks every searchable collection (defaults: `pages` searchable, `data` not — overridable via `searchable: true | false` on the collection definition), strips markdown body to plain prose, emits one `search-index-<collection>.json` per collection + a catalog `search-index.json` at the dist root. Chunked at 300 KB warn threshold, 1 MB error threshold (both overridable).
  - **Markdown body stripper** (`stripMarkdown`) — removes fenced code, inline code, HTML tags, link URLs (keeps link text), heading markers, emphasis markers, then collapses whitespace. Fast O(n) regex pass; good enough for the indexer's needs.
  - **Search runtime** (`src/search/search-runtime.tsx`) — `loadSearchIndex(catalogUrl?, fetchFn?)` fetches the catalog + every per-collection chunk + merges them into one shared `MiniSearch` instance. Idempotent at module scope — repeat calls share one in-flight `Promise`. `useSearch({ catalogUrl, debounceMs, maxResults, fetchFn })` is the headless state hook. `<Search />` wraps it with Cmd+K (or Cmd+/) keyboard shortcut + Esc-to-close + a `<dialog>` overlay + result list with click-to-navigate.
  - **A11y** — `<search>` landmark, `<dialog open aria-modal>` panel, `aria-label` on search input, semantic close button.

  29 new specs across `src/tests/index-builder.test.ts` (22) and `src/tests/search-runtime.test.ts` (7).

  366/366 specs passing. 11/11 validate-fast gates. typecheck + lint clean.

  **Plugin integration deferred to PR 7** (docs-pyreon spike). The current PR ships the public APIs (`buildSearchIndex`, `loadSearchIndex`, `useSearch`, `<Search>`); the spike will drive the build-time collection-walking integration shape inside the Vite plugin's `closeBundle` hook — letting real-app shape inform the contract rather than guessing now.

- [#1378](https://github.com/pyreon/pyreon/pull/1378) [`452904a`](https://github.com/pyreon/pyreon/commit/452904aa135159dd2f58383fe8d967eda21202b4) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 6: layout components + frontmatter JSON Schema emission.

  Sixth piece of the markdown-driven Pyreon docs story. Adds the two layout components every docs site needs + a frontmatter editor experience (autocomplete + validation in any `.md` file via the YAML extension).

  Layers shipped:

  - **`<Sidebar>`** (`src/components/Sidebar.tsx`) — reads `frontmatter.sidebar.order` + `frontmatter.sidebar.group` to build a grouped navigation tree. Items without a group fall under an empty bucket rendered first; named groups sort alphabetically. Active-link highlighting via a `currentPath` reactive accessor → router signal flips set `aria-current="page"` + `.pyreon-sidebar__link--active`. Optional `badge` field for "new" / "beta" annotations.
  - **`<Toc>`** (`src/components/Toc.tsx`) — page table-of-contents with scroll-spy via `IntersectionObserver`. Renders a flat list of level-2/3 headings (configurable via `minLevel`/`maxLevel`); the currently-in-view heading flips its `aria-current="location"` and applies `.pyreon-toc__link--active`. SSR-safe — when `IntersectionObserver` is undefined, emits the heading list without scroll-spy state. Supports an external `activeSlug` accessor for consumers driving scroll-spy from a router signal.
  - **Frontmatter JSON Schema emission** (`src/type-emit/frontmatter-schema.ts`) — writes one `<root>/.pyreon/schemas/<collection>.json` per `pages` collection (a permissive draft-07 schema requiring `title` + recognising `description` / `sidebar.{order,group}`). Wired into the plugin's `configResolved`/`configureServer` paths alongside `content-types.d.ts` emission.
  - **`.vscode-settings` snippet** (`writeVscodeSnippetFile`) — produces a `<root>/.pyreon/vscode-settings.json` snippet mapping each generated schema to its content glob via the standard `yaml.schemas` setting. Users copy-paste into their real `.vscode/settings.json` (we never touch their actual settings file).

  16 new specs in `src/tests/sidebar-toc.test.ts` (5 for `groupEntries`, 2 for `filterHeadings`, 3 for `defaultPermissiveSchema`, 2 for `renderVscodeSnippet`, 4 for `emitFrontmatterSchemas` + `writeVscodeSnippetFile`).

  382/382 specs passing. 11/11 validate-fast gates. typecheck + lint clean. Bundle budget bumped accordingly (Sidebar+Toc+schema emitter adds ~1.3 KB gzipped — well under any caller's perf budget; the layout pieces tree-shake away when unused).

  **Browser smoke tests for Sidebar+Toc deferred to PR 7** (docs-pyreon spike) — the spike will exercise both inside a real-Chromium environment so the component-render contract gets locked alongside the layout integration.

### Patch Changes

- [#1409](https://github.com/pyreon/pyreon/pull/1409) [`61af593`](https://github.com/pyreon/pyreon/commit/61af5936aa62f1789e6e9a5b1c35544b27382553) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-G — HMR + processor cache (audit C3+H4+H12)

  Three independent fixes:

  - **C3**: per-component virtual sub-modules. Compiled markdown emits
    `import { Foo } from "virtual:zero-content/components/Foo"` per
    referenced component instead of a barrel import. Edits to
    `src/mdx/Foo.tsx` now invalidate only pages that import Foo,
    instead of cascading through every `.md` page.

  - **H4**: schema-edit clears the compile cache. The cache key is
    content-based; a stricter schema added to `content.config.ts`
    would otherwise let unchanged `.md` files skip re-validation.
    `handleHotUpdate` now clears the cache before reloading.

  - **H12**: remark/unified processor reuse. Pre-fix every
    `compileMarkdown` call built a fresh processor (parse + frontmatter
    - gfm + directive + callout + codegroup + optional mdx). Two
      processors (mdx-enabled / mdx-disabled) are now cached at module
      scope; the per-file `remarkCallout` context (`{ source, warnings }`)
      rides through a module-level thread-local set immediately before
      `processor.run`.

  8 new specs cover the three contracts; bisect-verified.

- [#1423](https://github.com/pyreon/pyreon/pull/1423) [`b7ff610`](https://github.com/pyreon/pyreon/commit/b7ff610b1789e8c01d3fb4b29b5f71b6744ff0e8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Three structural fixes to the markdown pipeline:

  1. **VitePress-style `::: name` directive syntax** is now recognized — previously only the canonical `:::name` (no space) worked, so 87 occurrences of `::: code-group`, `::: tip`, etc. across the official Pyreon docs rendered as literal paragraph text. The pipeline now normalizes `::: name` → `:::name` before parsing, accepting both forms transparently.

  2. **`<CodeGroup>` (and other HTML-emitted components from remark plugins) now register their import**. Previously, raw HTML nodes containing `<CodeGroup labels={...}>` (emitted by the codegroup plugin) flowed through the JSX emitter verbatim without registering `CodeGroup` for import resolution — produced `ReferenceError: CodeGroup is not defined` at render time once the directive-syntax normalization (point 1) started actually firing the plugin. The emit step now scans raw HTML node values for PascalCase tag references and registers them via the same `mdxComponentRef` callback MDX JSX elements use.

  3. **`<CodeGroup>` panels now indicate the active tab via `data-active`** on the `.code-group__panels` container. Consumer stylesheets can target the active panel via `[data-active="N"] > :nth-child(N+1)` rules. Without this, all tab panels rendered stacked.

- [#1403](https://github.com/pyreon/pyreon/pull/1403) [`e9014aa`](https://github.com/pyreon/pyreon/commit/e9014aab2c9ef95030c169304e7586699544a333) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-A audit follow-ups — "fix-the-claims-and-silent-failures". Twelve items from the three-audit synthesis (architecture + perf/memory + DX). Bisect-verified at every layer.

  **Correctness (critical):**

  - **C4 — `defineComponents` brand is now runtime-enforced** (was type-only). The manifest promised the plugin refuses raw `{...}` objects with a clear build error; pre-fix no runtime check ever fired. `defineComponents` + `mergeComponents` stamp a non-enumerable `COMPONENTS_BRAND` symbol; `validateConfigShape` (called from `loadConfig`) rejects raw object literals on both top-level `components:` and per-collection `components:` fields with an actionable fix hint. Validation is no longer `__DEV__`-gated — a production-mode CI build no longer silently accepts `{ Foo: undefined }` typos.
  - **C9 — unclosed `:::tip` fence heuristic.** When a callout body extends near the file end (within 3 lines) with many headings (≥2) or many children (≥30), `remark-callout` emits a non-fatal warning the Vite plugin pipes through `this.warn(...)`. Pre-fix `:::tip` without a matching `:::` silently consumed the rest of the file.
  - **C10 — Shiki highlighter cache invalidates on `opts` change.** The first call's themes/langs used to be sticky for the process lifetime — a `theme: 'pyreon-dark'` edit in `content.config.ts` was ignored until full restart. Now the cached instance is keyed on a stable, order-independent JSON-string of `opts`; mismatched calls dispose the old instance and rebuild.

  **Correctness (high):**

  - **H5 — error path labels are Vite-root-relative.** `shortId` stripped to the last `/src/` segment, which broke click-to-open in monorepos with multiple `src/` directories. New `reportPath` emits `./packages/foo/src/content/docs/zero.md` (clickable in IDEs + terminals). Falls back to the legacy `shortId` when root isn't captured yet (config-load edge case).
  - **H6 — `:::warn` typo gets a `did-you-mean…?` hint.** Ratio-bound Levenshtein (≤ 50% of the longer string) — catches `warn` → `warning` (3 / 7), `not` → `note` (1 / 4); rejects noise like `qux` → `tip` (3 / 3).

  **Drift locks (low — single source of truth):**

  - **L10 — built-in components list deduped** into `src/_shared/built-ins.ts` (frozen, alphabetical). Scanner + validator both import; can't drift on contents or order.
  - **L11 — slug derivation deduped** into `src/_shared/derive-slug.ts`. `slugFromPath` is the shared primitive; `pipeline/parse.ts:deriveSlug` and the virtual-module's emitted `__zcSlug` both reduce to it via different prefix-stripping strategies (their contracts are different, but the index-collapse / extension-strip / Windows-normalise logic is one source).
  - **L12 — MiniSearch options deduped** into `src/_shared/minisearch-options.ts`. Both build-time `index-builder` and runtime `search-runtime` consume the same `MINISEARCH_OPTIONS` so `loadJSON`'s field-ref + scoring contract stays consistent.

  **Docs (low):**

  - **C1 + L1 + L14** — README rewritten: drops the false `import { z }` claim (the package doesn't re-export zod; BYO Standard Schema validator), names the `private: true` install caveat, adds a Conventions table covering every path/filename the plugin treats specially.
  - **L5** — PR-N tracking comments in `plugin.ts` replaced with a Responsibilities block.
  - Manifest's `defineConfig` description + mistakes list rewritten to lead with "BYO validator".

  **Tests (bisect-verified):**

  - `_shared-drift-lock.test.ts` — 6 specs locking L10 + L11 + L12.
  - `_callout-diagnostics.test.ts` — 13 specs locking C9 + H5 + H6.
  - `_components-brand.test.ts` — 8 specs locking C4.
  - 3 new C10 regression specs in `highlighter.test.ts`.

  Total: 434 specs pass (was 404). Each new fix bisect-verified by stashing the corresponding shared module — the matching specs fail loudly, restored → all pass.

- [#1404](https://github.com/pyreon/pyreon/pull/1404) [`be5437d`](https://github.com/pyreon/pyreon/commit/be5437dcb9e5c09b30c5ec3c87614a748bd3a5d8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - PR-B audit C2 — kill the 2× markdown pipeline pass.

  Pre-fix the markdown pipeline ran TWICE per SSG build:

  1. Once for the outer client build's `transform` hook.
  2. Once for the SSG inner SSR sub-build's `transform` hook.

  Both invocations have their own plugin instance, so per-instance state couldn't bridge them — but they share the same Node process. A module-level cache keyed on `(id, FNV-1a(code), FNV-1a(opts))` survives across plugin-instance boundaries within the process, so the second transform skips remark + Shiki + esbuild entirely.

  **Measured wall-clock impact on `examples/docs-zero` (93 docs pages):**

  - Baseline (cache off): 13.21s
  - With cache (cache on): 8.75s
  - **~34% faster** on every SSG build.

  Cache mechanics:

  - LRU bound at 4096 entries (effectively unbounded for normal docs sites).
  - HMR / content changes invalidate naturally (FNV-1a key shifts).
  - Theme swaps in `content.config.ts` invalidate (the `highlighter` opts shape changes).
  - Search-index population on cache-hit path: re-stashes only when the collection+slug entry is absent (the outer build's `searchEntries` populates once; the inner SSR build's map is discarded anyway).

  Regression coverage: `_compile-cache.test.ts` — 8 specs covering:

  - Public `_resetCompileCacheForTesting` helper exists + is callable.
  - Plugin instance memoises identical input (cache HIT skips `compileMarkdown`).
  - Cache invalidates on content change (cache MISS triggers re-compile).
  - **Cross-instance sharing** — the SSG outer→inner SSR scenario: a second `content()` plugin instance hits the cache populated by the first.
  - Opts difference invalidates (`highlight: false` → `highlight: true` re-compiles).
  - Pipeline is byte-stable on repeat compiles (cache safety).
  - Content change → different output.
  - ID change → different slug.

  Bisect-verified: reverting `plugin.ts:transform`'s cache HIT branch fails 3 of the 6 plugin-level specs (cache-helper test + pipeline-idempotency specs still pass since they don't depend on the plugin).

- [#1422](https://github.com/pyreon/pyreon/pull/1422) [`c8c8b57`](https://github.com/pyreon/pyreon/commit/c8c8b57968cc2a4858602d9cecf05072d93a60a6) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fix two long-standing bugs in `@pyreon/zero-content`:

  1. **Search broken on subpath deploys** — `buildSearchIndex` hardcoded `url: "/search-index-X.json"` in the catalog, so a site deployed under `/<base>/` fetched from `https://host/search-index-X.json` (404) instead of `https://host/<base>/search-index-X.json`. The runtime `loadSearchIndex()` had the same bug — defaulted to `/search-index.json` regardless of `base`. Fixed:

     - `BuildIndexArgs` accepts a new optional `base` field; emitted catalog URLs are prefixed.
     - `loadSearchIndex()` default now derives from `import.meta.env.BASE_URL` (the standard Vite global), so consumers don't need any explicit configuration.
     - `@pyreon/zero` passes the resolved Vite `base` from `configResolved` into `buildSearchIndex` automatically.

  2. **Copy button missing `data-copied` attribute** — the `<CodeBlock>` copy button flipped its TEXT between "Copy" and "Copied" but exposed no DOM attribute for the copied state, so consumers couldn't style the confirm visual via CSS. Now emits `data-copied="true"` (or omits it) reactively.

- [#1397](https://github.com/pyreon/pyreon/pull/1397) [`6cdae79`](https://github.com/pyreon/pyreon/commit/6cdae79903cd00c96410dcc6bad39669d9b8898b) Thanks [@vitbokisch](https://github.com/vitbokisch)! - perf(zero-content): truncate search-index body to ~1500 chars per page

  For a 91-page docs site the full stripped-markdown body exceeded the
  plugin's own 300KB warn threshold (719KB observed on docs-zero). Most
  search hits land in title (boost 3) / headings (boost 2) / description
  (boost 1.5) anyway, so truncating the body to the first 1500 chars
  (at a word boundary) shrinks the index without measurably affecting
  result quality. Measured impact: 719KB → 198KB on docs-zero (-72%).

  The 1500-char limit is currently hardcoded; tuning per project lands
  when a real use case surfaces.

- [#1397](https://github.com/pyreon/pyreon/pull/1397) [`6cdae79`](https://github.com/pyreon/pyreon/commit/6cdae79903cd00c96410dcc6bad39669d9b8898b) Thanks [@vitbokisch](https://github.com/vitbokisch)! - feat(zero-content): wire build-time search index emission

  The plugin now writes `dist/search-index.json` (catalog) +
  `dist/search-index-<collection>.json` (per-collection chunk) during the
  build's `closeBundle` hook — closing the long-standing `// PR 5: search
index emission` TODO at the top of `src/plugin.ts`.

  Per-collection entries are accumulated by `transform()` for every
  `.md` / `.mdx` file under a collection's path. The body text is
  extracted via the existing `stripMarkdown` helper (strips code fences,
  HTML tags, link syntax, headings, emphasis); title comes from
  frontmatter; headings from the markdown pipeline result. Searchable
  collections (default for `type: 'pages'`) get an index entry; opt out
  with `searchable: false` on the collection definition.

  The runtime `<Search>` component from `@pyreon/zero-content/search`
  already fetches `/search-index.json` lazily on first Cmd+K open — this
  PR closes the build side so the fetch resolves instead of 404'ing.

  Per-build state cleared in `buildStart` so dev → build doesn't leak
  stale dev-mode entries; SSG sub-builds also skip (gated on
  `resolvedConfig.command === 'build'`).

- [#1426](https://github.com/pyreon/pyreon/pull/1426) [`3f74a1e`](https://github.com/pyreon/pyreon/commit/3f74a1e580937e0774df23cb7356fde137107095) Thanks [@vitbokisch](https://github.com/vitbokisch)! - `<Search>` no longer yanks the page to the bottom when opened.

  The component focuses the search input on open to make typing immediate. The `<search>` element itself is typically mounted near the end of the document tree (e.g. at the bottom of the app shell), so the `<dialog>` rendered inside it lives in document flow at THAT position even though `.pyreon-search__backdrop` is `position: fixed`. The browser's default focus behaviour scrolls the focused input into view — which on a tall scrollable page jumps the document down by several thousand pixels (while the visible fixed backdrop stays anchored at the top).

  `focus({ preventScroll: true })` opts out of the auto-scroll. The fixed backdrop already shows the dialog at the right visual position; the scroll-into-view was pure collateral damage.

- [#1424](https://github.com/pyreon/pyreon/pull/1424) [`a6a9abd`](https://github.com/pyreon/pyreon/commit/a6a9abd51cfd285abeb769d6c8886e4b1f8fa1c8) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Two markdown-pipeline + runtime bugs surfaced by Playwright comparison of the docs-zero preview vs the original VitePress docs:

  1. **Markdown tables produced literal `unhandled mdast node "table"` comments instead of rendering as HTML tables.** remark-gfm parses pipe-tables into `table`/`tableRow`/`tableCell` mdast nodes, but the JSX emitter had no case for any of them — they fell through to the default unhandled-node branch and emitted the visible comment marker in the rendered page. Affects every page with a markdown table (CLAUDE.md-style "package overview" pages: dozens of pages, including the docs Overview).

     Now emits standard `<table><thead><tr><th>...</th></tr></thead><tbody><tr><td>...</td></tr></tbody></table>` with per-column alignment from the `table` node's `align` field threaded into each cell as `style={{ textAlign: 'left'|'right'|'center' }}`. Header cells use `<th>`; body cells use `<td>`. Tables without a body row emit `<thead>` alone.

  2. **`<Search>` rendered `[object Object]` as its only content when the dialog opened.** The component's reactive child was `{() => state.open() && <div>...</div>}` — when `state.open()` is true, the `&&` short-circuit returns the VNode itself. Some runtime paths normalize VNodes correctly from accessors; this specific accessor + `&&`-return shape produced a `String(VNode)` toString → `[object Object]`. Switched to a ternary returning `null` on the closed branch, which the runtime treats consistently as "no child" regardless of accessor shape.

- [#1417](https://github.com/pyreon/pyreon/pull/1417) [`9472463`](https://github.com/pyreon/pyreon/commit/947246396303c676498a45cdf3ea1711f72a5f96) Thanks [@vitbokisch](https://github.com/vitbokisch)! - refactor(zero-content): use core `cx()` + `createUniqueId()` helpers

  Replace string-concat class merging with `@pyreon/core`'s `cx()` across
  9 components (Tabs, PropTable, Details, APICard, Mermaid, Playground,
  Toc, CompatMatrix, Math). Mechanical sweep — output is identical.

  Mermaid's hand-rolled module-level id counter (`pyreon-mermaid-${n}`)
  replaced with `createUniqueId()` from `@pyreon/core`. The framework
  ID generator is SSR-safe and won't collide across re-mounts.

  No behaviour change. 662/662 tests pass.

- [#1416](https://github.com/pyreon/pyreon/pull/1416) [`b90e67c`](https://github.com/pyreon/pyreon/commit/b90e67c296cc39b2438490f4330b836b78395c8d) Thanks [@vitbokisch](https://github.com/vitbokisch)! - `@pyreon/zero`: add RSS 2.0 feed support to the SEO surface

  - New `generateRssFeed({ title, origin, items, ... })` builder
  - New `toRfc822` helper (ISO-8601 → RFC-822 date conversion)
  - `seoPlugin({ rss: {...} })` auto-emits `dist/rss.xml` at build time
  - `seoMiddleware` serves `/rss.xml` during dev
  - Exported from `@pyreon/zero/server`

  This consolidates SEO into one canonical source — RSS now lives alongside
  sitemap, robots, and JSON-LD generators in `seo.ts`.

  `@pyreon/zero-content`: deprecate duplicated SEO builders

  - `seo/rss.ts` is now a thin backward-compat adapter that delegates to
    `@pyreon/zero`'s `generateRssFeed`. Preserves the `baseUrl` field
    name. New code should import from `@pyreon/zero` directly.
  - `seo/sitemap.ts` and `seo/llms-txt.ts` marked `@deprecated`. Zero's
    `seoPlugin` (sitemap) and `aiPlugin` (llms.txt) are richer
    alternatives with i18n, hreflang, manifest-aware route enumeration,
    and dev-server middleware.

  `@pyreon/vitest-config`: add `@pyreon/zero/server` + `@pyreon/zero/client`
  subpath aliases so workspace test runs resolve them under the `bun`
  condition.

- Updated dependencies [[`0e38332`](https://github.com/pyreon/pyreon/commit/0e3833212e93ec90994edfccb5f2966f9eb0e926), [`4529407`](https://github.com/pyreon/pyreon/commit/4529407d69ba0875568b5c78ff14e2850aa2d690), [`0c1ea1e`](https://github.com/pyreon/pyreon/commit/0c1ea1e89e4228e84367efd5d2cb334808955a25), [`e36bbe5`](https://github.com/pyreon/pyreon/commit/e36bbe52e7f1417a703b4e6ce23281c448d9132f), [`3d90e89`](https://github.com/pyreon/pyreon/commit/3d90e89b824d346a33732af929acdbc7fdd81094), [`75c39ea`](https://github.com/pyreon/pyreon/commit/75c39eac7cc8f4fc1f99586521c27a50bc9f9fb8), [`65ccdf2`](https://github.com/pyreon/pyreon/commit/65ccdf2ad95a16b676b58948acea51f957e5cf62), [`fc26160`](https://github.com/pyreon/pyreon/commit/fc26160ac2d3afba0adde20f61d94a4199519b59), [`510a410`](https://github.com/pyreon/pyreon/commit/510a410f196bb732d963bd357a6bc10993f794fd), [`510a410`](https://github.com/pyreon/pyreon/commit/510a410f196bb732d963bd357a6bc10993f794fd), [`a359e29`](https://github.com/pyreon/pyreon/commit/a359e2917567419655dd31c5d093d0a4479ba021), [`9eb24f6`](https://github.com/pyreon/pyreon/commit/9eb24f604e6e4be62ef4ad3ba33e0c3fa28e9906), [`7f89196`](https://github.com/pyreon/pyreon/commit/7f89196dd3d99f61b0bba032481b9d389fdd8264), [`5a38b69`](https://github.com/pyreon/pyreon/commit/5a38b69a2a2dc9a331c2e6a8a11375eebc532c63), [`698f514`](https://github.com/pyreon/pyreon/commit/698f514f44160e1955582b4573014bddba45a38e), [`f21a439`](https://github.com/pyreon/pyreon/commit/f21a439cfefd219b1c13f1b8d99dbfbbe949fd34), [`d543f36`](https://github.com/pyreon/pyreon/commit/d543f36150f11fe94b08fabed0887914fa9deb9f), [`8a9bc52`](https://github.com/pyreon/pyreon/commit/8a9bc52318841868badf907963bf99d7937ab735), [`6cdae79`](https://github.com/pyreon/pyreon/commit/6cdae79903cd00c96410dcc6bad39669d9b8898b), [`b90e67c`](https://github.com/pyreon/pyreon/commit/b90e67c296cc39b2438490f4330b836b78395c8d), [`25ddda0`](https://github.com/pyreon/pyreon/commit/25ddda0d540199a7177cf0ccd4b0cab78912986a)]:
  - @pyreon/core@0.33.0
  - @pyreon/runtime-dom@0.33.0
  - @pyreon/reactivity@0.33.0
  - @pyreon/router@0.33.0
  - @pyreon/zero@0.33.0
  - @pyreon/head@0.33.0

## 0.1.0

### Minor Changes

- [#1370](https://github.com/pyreon/pyreon/pull/1370) [`071eb58`](https://github.com/pyreon/pyreon/commit/071eb581908ffde132c52e5c20e6616fab7dafec) Thanks [@vitbokisch](https://github.com/vitbokisch)! - New package `@pyreon/zero-content` — PR 1 of the dogfood-Pyreon-docs plan.

  Foundation only: package skeleton + the markdown-to-Pyreon-JSX pipeline foundation. Lets users start authoring `.md` files that compile to Pyreon components; later PRs add Shiki (PR 2), MDX components (PR 3), typed content collections (PR 4), search (PR 5), sidebar polish (PR 6).

  **Shipped in PR 1:**

  - **Package skeleton** mirroring `@pyreon/meta`/`@pyreon/zero` conventions (rolldown build, vitest config, manifest for `gen-docs`, MIT, README)
  - **Config helpers**: `defineConfig`, `defineCollection`, `defineComponents`, `mergeComponents` — pass-through factories with type inference + brand-symbol validation. Dev-mode `defineComponents` throws `TypeError` on non-function values (catches `{ Foo: undefined }` typos with the offending key named).
  - **Vite plugin** at `@pyreon/zero-content/plugin` (default export). Hooks `transform` on `.md`/`.mdx` files, returns Pyreon JSX. `enforce: 'pre'` so the user's `pyreon()` plugin processes the JSX after.
  - **Markdown pipeline** via `unified` + `remark-parse` + `remark-frontmatter` + `gray-matter`:
    - Frontmatter extracted as a plain JS object (zod validation lands in PR 4)
    - mdast → Pyreon JSX string emitter covering heading, paragraph, text, strong, em, inlineCode, code (with `data-lang`), link, list (ul/ol), listItem, blockquote, thematicBreak, image, break, raw html
    - Headings level 2-3 captured into a `headings: Heading[]` export for the TOC rail (PR 6)
    - GFM-style slug generation (matches `markdown-it-anchor` output so existing internal anchors keep working when content ports in PR 7)
    - JSX-sensitive characters escaped (`<`, `>`, `{`, `}`, `&`) so Pyreon's compiler sees literal text, not embedded JSX or expressions
    - Compiled `.tsx` module exports `frontmatter`, `headings`, `slug`, `default` (the page component wrapped in `<article class="content">`)

  **Tests + validation:** 75 specs across 4 files:

  - `emit-jsx.test.ts` (43 specs) — per-node behavior, slug edge cases, escape edge cases
  - `parse.test.ts` (12 specs) — end-to-end markdown → module shape, frontmatter parsing, heading capture, slug derivation incl. Windows paths
  - `plugin.test.ts` (8 specs) — plugin contract, `transform` for `.md`/`.mdx`, pass-through for non-markdown
  - `config.test.ts` (12 specs) — helper pass-through, `defineComponents` dev-mode validation, `mergeComponents` precedence

  **Bisect-verified**: neutering `emitJsx` to return an empty body fails 29 of 75 tests (the markdown-shape assertions); helper-direct tests still pass (validates the slugify/escape/config helpers are independently covered). Restored → 75/75 pass.

  **Gates green**: typecheck + lint + 11/11 `validate-fast` + `bundle-budgets` (new entry at 2048 bytes; actual gzipped main entry ~1.57 KB).

  **Next**: PR 2 plugs in Shiki + Pyreon themes + `pyreon-remark-callout` / `pyreon-remark-codegroup` plugins, ports `<Callout>` / `<CodeGroup>` from `examples/docs-pyreon`.

- [#1370](https://github.com/pyreon/pyreon/pull/1370) [`071eb58`](https://github.com/pyreon/pyreon/commit/071eb581908ffde132c52e5c20e6616fab7dafec) Thanks [@vitbokisch](https://github.com/vitbokisch)! - zero-content PR 2: Shiki syntax highlighting + custom markdown blocks (callout, code-group).

  Adds the pieces that turn the PR 1 markdown → Pyreon JSX foundation into production-quality content rendering:

  - **Shiki integration** — shared highlighter instance with `github-light` + `github-dark` dual theme baked into every code block. Default language set covers `typescript`/`tsx`/`javascript`/`jsx`/`json`/`bash`/`shell`/`html`/`css`/`scss`/`markdown`/`mdx`/`yaml`/`toml`/`diff`/`text`. Unknown languages fall back to `text` (no throw). Build-time cost only — the SSR output ships pre-rendered HTML.
  - **`:::tip` / `:::warning` / `:::note` / `:::danger` / `:::info`** container directives → `<Callout type="...">` JSX. Title attribute supported via `:::tip{title="..."}`; HTML-special characters in the title get entity-escaped. Body content renders through the full markdown pipeline.
  - **`:::code-group`** container with `[label]` meta on code fences → `<CodeGroup labels={[...]}>` tabbed JSX. SSR ships tab 0 visible; client-side tab switching is a single signal write (CSS class swap, no VNode reconciliation).
  - **New built-in components**: `<Callout>`, `<CodeGroup>`, `<CodeBlock>` exported from `@pyreon/zero-content`. Emit shapes locked by 28 new specs across `callout.test.ts` / `codegroup.test.ts` / `highlighter.test.ts`, all bisect-verified (revert the matching remark plugin / highlighter → each spec fails with the documented assertion).
  - **`compileMarkdown` is now async** — required for Shiki's async `codeToHtml`. `CompileOptions.highlight: false` opts out for tests / quick previews. The Vite plugin's `transform` hook is async.

  122/122 specs passing (75 PR 1 + 28 new + 19 PR 1 specs updated for async). Pipeline now: parse → frontmatter → gfm → directive → callout → codegroup → emit-jsx (with optional Shiki callback).

### Patch Changes

- Updated dependencies [[`7532eae`](https://github.com/pyreon/pyreon/commit/7532eaeff493327bb19f6c2adc94151638d61ceb)]:
  - @pyreon/zero@0.33.0
  - @pyreon/core@0.33.0
  - @pyreon/head@0.33.0
  - @pyreon/reactivity@0.33.0
  - @pyreon/router@0.33.0
  - @pyreon/runtime-dom@0.33.0
