dioxus-docs-kit 0.7.0

Reusable documentation site shell for Dioxus applications
Documentation
# dioxus-docs-kit

Reusable documentation site kit for [Dioxus](https://dioxuslabs.com/) applications.

Drop-in layout with sidebar navigation, full-text search, page navigation, OpenAPI API reference pages, mobile drawer, and theme toggle. Built on [dioxus-mdx](https://crates.io/crates/dioxus-mdx) for content rendering.

## Quick Start

Add to your `Cargo.toml`:

```toml
[dependencies]
dioxus-docs-kit = "0.7"
```

### 1. Build a Registry

The `DocsRegistry` holds all parsed content, the navigation tree, the search index, and any OpenAPI specs:

```rust
use dioxus_docs_kit::{DocsConfig, DocsRegistry};
use std::sync::LazyLock;

static DOCS: LazyLock<DocsRegistry> = LazyLock::new(|| {
    DocsConfig::new(
        include_str!("../docs/_nav.json"),
        doc_content_map(), // HashMap<&'static str, &'static str> generated by build.rs
    )
    .with_openapi("api-reference", include_str!("../docs/api-reference/spec.yaml"))
    .build()
});
```

### 2. Wire Into Your Router

Create a thin layout wrapper that provides the `DocsContext` and registry to the library components. The `use_docs_providers` hook bundles all the context setup into one call:

```rust
use dioxus::prelude::*;
use dioxus_docs_kit::{DocsLayout, use_docs_context, use_docs_providers};

#[component]
fn MyDocsLayout() -> Element {
    let nav = use_navigator();
    let route = use_route::<Route>();

    // A plain String — extract the slug from your route, e.g. slug.join("/").
    let current_path = /* ... */;

    // `use_docs_context` rewraps the path reactively for you. Building the
    // signal yourself with a plain `use_memo(move || ...)` captures the first
    // route and never updates — the sidebar highlight would freeze.
    let docs_ctx = use_docs_context(
        current_path,
        "/docs",
        Callback::new(move |path: String| {
            nav.push(/* build route from path */);
        }),
    )
    .with_site_url("https://your-site.com"); // optional: canonical/OG URLs

    let providers = use_docs_providers(&DOCS, docs_ctx);
    // providers.search_open / providers.drawer_open are available for
    // wiring a custom header.

    rsx! {
        DocsLayout {
            Outlet::<Route> {}
        }
    }
}
```

### 3. Add Routes

```rust
#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[layout(MyDocsLayout)]
        #[route("/docs")]
        DocsIndex {},
        #[route("/docs/:..slug")]
        DocsPage { slug: Vec<String> },
}
```

That's it. The library handles sidebar rendering, search, page content, previous/next navigation, and mobile responsiveness.

## Components

| Component | Description |
|-----------|-------------|
| `DocsLayout` | Full page layout with sidebar, content area, and table of contents |
| `DocsSidebar` | Navigation sidebar built from `_nav.json` |
| `DocsPageContent` | Renders MDX docs or OpenAPI endpoint pages |
| `DocsPageNav` | Previous/next page navigation |
| `SearchModal` | Full-text search across all docs |
| `SearchButton` | Trigger button for the search modal |
| `MobileDrawer` | Mobile navigation drawer |
| `ThemeToggle` | Light/dark theme switcher |

## Navigation Config

Define your docs structure in `_nav.json`. `tabs` is a flat list of tab names, and each group optionally names the tab it belongs to:

```json
{
  "tabs": ["Docs", "Guides"],
  "groups": [
    {
      "group": "Getting Started",
      "tab": "Docs",
      "pages": [
        "getting-started/introduction",
        "getting-started/quickstart"
      ]
    }
  ]
}
```

## Content Pipeline

All doc content is embedded at compile time via `include_str!()`. A typical `build.rs` reads `_nav.json`, collects all referenced `.mdx` files, and generates a `HashMap<&'static str, &'static str>` mapping paths to content.

See the [example project](https://github.com/hauju/dioxus-docs-kit) for a complete `build.rs` implementation.

## Styling Setup

### Zero-setup: the precompiled stylesheet

The crate ships a compiled stylesheet (`DOCS_KIT_CSS`) covering every class its
components emit — Tailwind utilities, DaisyUI dark/light themes, typography
prose, and the `--dk-*` theming tokens. Link it and skip the rest of this
section — no Tailwind, no Bun, no safelist:

```rust
rsx! {
    document::Stylesheet { href: dioxus_docs_kit::DOCS_KIT_CSS }
}
```

It only contains the *kit's* classes; if your own pages use Tailwind utilities
the kit doesn't, run your own build instead. That path requires **Tailwind CSS
4**, **DaisyUI 5**, and **@tailwindcss/typography**:

### Install dependencies

```sh
bun add tailwindcss @tailwindcss/typography daisyui
```

### Configure Tailwind

Add to your `tailwind.css`:

```css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
    themes: dark --default, light;
}

@source "./src/**/*.{rs,html,css}";
```

### Include dioxus-docs-kit classes

When using as a **crates.io dependency**, Tailwind can't scan the crate source
(it lives in `~/.cargo` with machine-specific paths). Copy `safelist.html` from the
crate into your project root and add it as a source:

```css
@source "./safelist.html";
```

The safelist includes all classes from both `dioxus-docs-kit` and `dioxus-mdx`, including
dynamic runtime classes that Tailwind cannot detect from source scanning alone.

When using as a **workspace path dependency**, you can point directly at the source instead:

```css
@source "./crates/dioxus-docs-kit/src/**/*.rs";
@source "./crates/dioxus-mdx/src/**/*.rs";
```

## Theming

The kit exposes a public theming surface so consumers can restyle without
forking or fighting DaisyUI internals.

### Public CSS variables

Copy `theme.css` from the crate into your project and import it alongside
Tailwind:

```css
@import "tailwindcss";
@import "./theme.css";
```

All tokens have DaisyUI fallbacks, so DaisyUI users inherit the current theme;
non-DaisyUI users get a sensible neutral default. Override any of them:

```css
.my-site .dk-root {
  --dk-accent: #f0a57c;
  --dk-font-heading: 'Instrument Serif', serif;
  --dk-radius-lg: 20px;
}
```

| Token | Purpose |
|-------|---------|
| `--dk-bg`, `--dk-bg-sub`, `--dk-bg-alt` | Surface colors |
| `--dk-fg`, `--dk-muted`, `--dk-dim` | Foreground / text colors |
| `--dk-border` | Border color |
| `--dk-accent`, `--dk-accent-fg`, `--dk-accent-soft` | Accent |
| `--dk-radius-sm`, `--dk-radius`, `--dk-radius-lg` | Corner radii |
| `--dk-font-body`, `--dk-font-heading`, `--dk-font-mono` | Typography |
| `--dk-article-width`, `--dk-sidebar-width`, `--dk-toc-width` | Layout widths |

### Stable `dk-*` classes

Structural nodes carry semver-stable `dk-*` class names. Target these in your
own CSS instead of DaisyUI or internal classes:

| Class | What it wraps |
|-------|---------------|
| `dk-root`, `dk-docs-root` | Outermost docs wrapper |
| `dk-header`, `dk-shell`, `dk-sidebar`, `dk-main`, `dk-toc` | Layout regions |
| `dk-tabs`, `dk-tab`, `dk-tab-active` | Tab bar |
| `dk-nav`, `dk-nav-group`, `dk-nav-group-title`, `dk-nav-item`, `dk-nav-item-active` | Sidebar navigation |
| `dk-article`, `dk-article-header`, `dk-article-title`, `dk-article-description`, `dk-article-body` | Article regions |
| `dk-pagination`, `dk-page-prev`, `dk-page-next` | Previous/next links |
| `dk-search-trigger`, `dk-search-dialog`, `dk-search-input`, `dk-search-results`, `dk-search-result` | Search |
| `dk-drawer` | Mobile drawer |

### Slots on `DocsLayout`

`DocsLayout` accepts optional element slots. Each slot is wrapped in a
`dk-*-slot` class for CSS hooks.

```rust
DocsLayout {
    announcement_bar: Some(rsx!{ AnnouncementBar {} }),
    sidebar_header: Some(rsx!{ MyProductSwitcher {} }),
    sidebar_footer: Some(rsx!{ EditOnGitHub {} }),
    footer: Some(rsx!{ SiteFooter {} }),
    Outlet::<Route> {}
}
```

`DocsPageContent` additionally accepts an `article_footer` slot (rendered
below the article body, before pagination) — useful for "Was this helpful?"
widgets.

### Density variants

`DocsLayout` accepts a `variant` prop for two built-in density presets:

```rust
DocsLayout { variant: DocsVariant::Reference, Outlet::<Route> {} }
```

| Variant | Feel | `--dk-article-width` |
|---------|------|----------------------|
| `Prose` (default) | Wide margins, serif-friendly, long-form reading | `72ch` |
| `Reference` | Tighter column, smaller type, denser headings | `64ch` |

The variant is emitted as a class on `dk-root` (`dk-variant-prose` or
`dk-variant-reference`) so consumers can layer further tweaks.

### Pre-built theme examples

`examples/themes/` ships three drop-in visual identities built entirely on
top of `--dk-*` tokens:

- `warm-editorial.css` — amber accent, serif headings, cream surfaces
- `brutalist-light.css` — high contrast, zero-radius, monospace
- `default.css` — baseline (nothing overridden)

See [THEMING.md](./THEMING.md) for the full roadmap and proposals open for
community contribution.

## Features

- `web` (default) — enables web-specific features (propagated to `dioxus-mdx`)
- `mermaid` (default) — renders ` ```mermaid ` fences as diagrams
- `highlight` (default) — syntax-highlights code blocks via [`dioxus-code`]https://crates.io/crates/dioxus-code. Disable (`default-features = false`) to drop the dependency and its C-compiling tree-sitter grammars: no C toolchain (or wasm `stderr` shim) is needed and the binary is smaller, but code blocks render as plain (uncolored) text. Turning it off also removes the `dioxus-code` re-exports and `DocsConfig::with_code_theme[s]`.
- `lang-*` — one tree-sitter grammar each, on top of `highlight`. Enabled by default: `lang-bash`, `lang-css`, `lang-dockerfile`, `lang-html`, `lang-javascript`, `lang-json`, `lang-markdown`, `lang-python`, `lang-toml`, `lang-typescript`, `lang-yaml`. Also available: `lang-rust` (a no-op — Rust always highlights), `lang-c-sharp`, `lang-cpp`, `lang-tsx`. Any other language goes through `dioxus-code` directly; see [Syntax Highlighting]#syntax-highlighting.
- `openapi` (default) — renders API reference pages from OpenAPI specs. Required by `DocsConfig::with_openapi()`, which is absent without it. Disable (`default-features = false`) to drop `openapiv3` and `serde_yaml` (and its `unsafe-libyaml`) from the build; docs pages, blog and frontmatter are unaffected.
- `server` — Axum route builders for crawler-facing endpoints

With the `server` feature, `SeoRouter` generates per-page raw-Markdown routes, `llms.txt` / `llms-full.txt`, sitemaps, blog RSS, and robots.txt as plain Axum routes (server functions would JSON-encode the bodies):

```rust
use dioxus_docs_kit::server::SeoRouter;

dioxus::server::serve(|| async {
    let seo = SeoRouter::new("https://your-site.com", "My Docs", "Documentation")
        .with_docs(&DOCS, "/docs")
        .into_router();
    Ok(dioxus::server::router(App).merge(seo))
});
```

## Syntax Highlighting

Code blocks render through [`dioxus-code`](https://crates.io/crates/dioxus-code). Every language is a separate tree-sitter grammar compiled into your binary, so each one sits behind its own `lang-*` feature. Enabled by default:

`bash`, `css`, `dockerfile`, `html`, `javascript`, `json`, `markdown`, `python`, `rust`, `toml`, `typescript`, `yaml`

Rust needs no feature of its own — `dioxus-code`'s `runtime` always compiles it, so `highlight` alone highlights Rust.

`lang-c-sharp`, `lang-cpp` and `lang-tsx` exist too but are deliberately **off** by default: dropping the C# and C++ grammars cut this repo's own release wasm from 19.0 MB to 9.9 MB (data section 15.2 MB to 6.3 MB), and TSX (1.5 MB, React-only) followed. Turn them back on if your docs fence those languages:

```toml
[dependencies]
dioxus-docs-kit = { version = "0.7", features = ["lang-c-sharp", "lang-cpp", "lang-tsx"] }
```

To trim the set down to what your docs actually fence:

```toml
[dependencies]
dioxus-docs-kit = { version = "0.7", default-features = false, features = [
    "web", "mermaid", "highlight", "openapi", "lang-bash", "lang-json", "lang-toml",
] }
```

### Any other language

The kit carries `lang-*` features only for the languages above. For anything else `dioxus-code` supports — Go, Zig, Kotlin, SQL, and ~90 more — add `dioxus-code` to your own `Cargo.toml` with the grammar you want. Cargo unifies that feature into the same `dioxus-code` the kit already depends on, so `Language::from_slug` (which the kit uses to resolve a fence) is compiled with the union of the features and picks the grammar up. No kit changes needed:

```toml
[dependencies]
dioxus-docs-kit = "0.7"
dioxus-code = { version = "0.1", default-features = false, features = ["runtime", "lang-go"] }
```

That route matches on `dioxus-code`'s canonical language slug, so use it in the fence (` ```go `). The friendly aliases (` ```c++ `, ` ```yml `, ` ```sh `) only exist for the languages the kit has its own feature for. See the [dioxus-code feature list](https://github.com/DioxusLabs/dioxus-code/blob/main/Cargo.toml) for every available flag.

A fence whose grammar is not compiled into the build renders as plain, uncolored text in the same markup — it does not fail or fall back to an unrelated grammar.

## Production build

Bundle with debug symbols off:

```sh
dx bundle --web --release --debug-symbols false
```

`dx` keeps DWARF by default and `wasm-opt` aborts on it ("compile unit size was incorrect"), after which `dx` silently ships the *unoptimized* wasm-bindgen output — for this repo's own site that was 25 MB instead of 7 MB. Two more things worth copying from this repo's root `Cargo.toml` and CI workflow:

- `[profile.wasm-release]` / `[profile.server-release]` — the profiles `dx` actually builds with (`opt-level = "z"`, fat LTO, `panic = "abort"` for the wasm; `strip = true` for the server).
- Brotli sidecars: write a `.br` next to every `.wasm`/`.js`/`.css` under the bundle's `public/` directory (`brotli -q 11 -k`). `dioxus-server` serves them automatically with `content-encoding: br`, taking the wasm from ~7 MB to ~1.4 MB on the wire. `scripts/wasm-size.sh` in this repo reports both numbers and can gate CI on the raw size.

## License

MIT