@pyreon/zero-content
v0.53.0
Published
Pyreon Zero — content layer for markdown-driven Pyreon sites. Compile-time .md/.mdx → Pyreon JSX, typed content collections (zod), convention-scanned MDX components.
Readme
@pyreon/zero-content
Pyreon Zero's content layer. Compile-time .md / .mdx → Pyreon JSX, typed content collections (Standard Schema-compatible), convention-scanned MDX components.
Tightly coupled to @pyreon/zero — that integration IS the value:
- Markdown pages route through zero's fs-router
- View Transitions API enabled by default
- Theme + dark mode flow through zero's theme system
- Per-route LCP optimization inherits from zero's image/font/script-defer stack
Status
v0 — Pre-publish. The package is currently private: true in this monorepo, so a bun add @pyreon/zero-content from outside the workspace doesn't resolve yet. Use as a workspace:* dep inside the Pyreon repo (see docs) or watch this readme for the un-private flip.
Install
# inside the Pyreon monorepo (now)
"@pyreon/zero-content": "workspace:*"
# once the package is published (later)
bun add @pyreon/zero-contentUsage
// vite.config.ts
import pyreon from '@pyreon/vite-plugin'
import zero from '@pyreon/zero'
import content from '@pyreon/zero-content/plugin'
export default {
plugins: [pyreon(), zero(), content()],
}// content.config.ts
// Bring your own Standard Schema validator (zod / valibot / arktype / typia
// all duck-type onto Standard Schema). `@pyreon/zero-content` does NOT
// re-export `z` — see `@pyreon/validation` for curated adapters.
import { defineConfig, defineCollection } from '@pyreon/zero-content'
import { z } from 'zod'
export default defineConfig({
collections: {
docs: defineCollection({
type: 'pages',
path: 'src/content/docs',
schema: z.object({
title: z.string(),
description: z.string(),
}),
}),
},
})Drop .tsx files in src/mdx/ to use them in any markdown by name:
// src/mdx/Playground.tsx
export function Playground(props: { initial: string }) { /* ... */ }---
title: Reactivity
description: Signals, computed, effect, batch.
---
# Reactivity
<Playground initial="..." />Typed queries anywhere:
import { getCollection } from '@pyreon/zero-content'
const docs = await getCollection('docs')
// ^? Array<{ slug: string; data: { title: string; description: string }; render: () => Promise<ComponentFn> }>Inline live examples — <Example>
Replace iframe-sandboxed <Playground code={…} /> with real .tsx files that mount inline (no iframe). Two <Example> calls with the same share key receive the SAME signal instance, so a click in one example reactively updates the rendered output of another mounted example on the same page — a docs DX no MDX-flavored framework can replicate.
// entry-client.ts — one-time consumer-side registration
import { startClient } from '@pyreon/zero/client'
import { registerExamples } from '@pyreon/zero-content'
import { routes } from 'virtual:zero/routes'
registerExamples(
import.meta.glob('./examples/**/*.tsx') as Record<
string,
() => Promise<unknown>
>,
)
startClient({ routes })// src/examples/counter-button.tsx — real Pyreon component
import { signal, type Signal } from '@pyreon/reactivity'
export default function CounterButton(props: { shared?: Signal<number> }) {
const count = props.shared ?? signal(0)
return (
<button onClick={() => count.update((n) => n + 1)}>
bump (now {() => count()})
</button>
)
}<!-- In any .md / .mdx page -->
<Example file="./examples/counter-button" share="cnt" />
<Example file="./examples/counter-readout" share="cnt" />Real-file authoring (real type-checking, real refactor support, real lint) closes the foot-gun shape behind PR #1434 — strings like '\n' got double-unescaped through iframe srcdoc interpolation and threw SyntaxError. With <Example>, JS escape rules apply once at module evaluation; you write '\n' and it's a newline.
Cross-Example signal sharing (share="key"):
- First
<Example share="key">to mount registers aSignal<unknown>withshareInitial ?? 0 - Subsequent
<Example share="key">calls receive the SAME signal asprops.shared - The example component MUST accept
{ shared?: Signal<T> }and fall back to a local signal when undefined (lets the same component work with or without sharing) - Powered by
getOrCreateSharedSignalfrom@pyreon/zero-content; use it directly for any cross-component shared state without a context
Props:
| Prop | Type | Default | Description |
| -------------- | -------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------- |
| file | string | — | Path key into the registered glob. Extension optional (.tsx/.ts/.jsx/.js tried in order). |
| share | string (optional) | none | Shared-signal registry key. Two <Example> calls with the same share get the same signal. |
| shareInitial | unknown (optional) | 0 | Initial value for a NEW shared signal. Ignored on subsequent registrations of the same key. |
| class | string (optional) | 'pyreon-example' | className on the outer wrapper. |
| title | string (optional) | none | Optional title shown above the rendered example. |
Conventions
Every path/filename the plugin treats specially, in one place. <root> is the Vite root.
| Convention | Purpose |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| <root>/content.config.{ts,mts,js,mjs} | The single source of truth for collections. Auto-discovered at configResolved. |
| <root>/src/content/<collection-name>/**/*.{md,mdx} | Default location for type: 'pages' collection entries (override per-collection via path:). |
| <collection>/index.md | Maps to slug '' so the route is /docs/ (bare-prefix) instead of /docs/index/. |
| <root>/src/mdx/**/*.{ts,tsx,js,jsx} | Convention scan for MDX components. PascalCase named/default exports are auto-available by name. |
| <root>/src/mdx/_*.tsx | Underscore-prefixed files are excluded from the scan — escape-hatch for component bundles. |
| <root>/.pyreon/content-types.d.ts | Auto-emitted per-collection schemas. Add .pyreon/** to your tsconfig include. |
| <root>/.pyreon/schemas/<collection>.json | Frontmatter JSON Schema per collection (consumed by the YAML extension via .vscode-settings). |
| <root>/dist/search-index.json + dist/search-index-<name>.json | Built-time search index emitted at closeBundle (chunked per collection). |
| Built-in components: <Callout>, <CodeGroup>, <CodeBlock> | Always available in any .md/.mdx — no import needed. Source of truth: src/_shared/built-ins.ts. |
| Markdown directives: :::tip, :::warning, :::note, :::danger, :::info | Container directives → <Callout type="…">. Unknown names get a did-you-mean…? warning; forgetting the closing ::: triggers an unclosed-fence heuristic. |
| Markdown directives: :::code-group | Tabbed code blocks → <CodeGroup>. Each child fence carries its label in [brackets]. |
In development, the search middleware builds the same catalog and collection chunks lazily. It collects static metadata through Vite transforms, without evaluating markdown page modules or their application dependencies. Content configuration still loads normally to define the searchable collections.
Diagnostics
The plugin surfaces non-fatal compile warnings through Vite's this.warn(...):
- Unknown callout name —
:::warntriggersUnknown callout directive ::: warn — did you mean ::: warning?(Levenshtein-based suggestion). - Unclosed
:::fence — when a callout body extends near the file end with many headings, the plugin warnsSuspected unclosed :::tip directive — the body spans N block(s) up to line L. Add a closing ::: line. - Unknown JSX component —
<Unknown />fails the build with adid-you-mean…?hint from the union of built-ins +src/mdx/scan.
License
MIT
