dioxus-docs-kit-integration
Integrate dioxus-docs-kit into a Dioxus 0.7 project. Adds compile-time MDX docs, sidebar navigation, full-text search, optional OpenAPI reference pages, an optional blog engine, SEO meta + sitemap, and theme switching (DaisyUI themes or dk-* CSS presets). Handles Cargo.toml dependencies, build.rs setup, docs_bundle / blog_bundle macros, route/layout wiring with use_docs_providers + use_blog_providers, Tailwind CSS safelist, and _nav.json creation. Use when: (1) Adding documentation to a Dioxus project, (2) Setting up dioxus-docs-kit in a new or existing app, (3) "add docs", "integrate docs-kit", "set up documentation site", (4) Adding a blog alongside docs, (5) Migrating from manual context providers to use_docs_providers. Triggers: "dioxus-docs-kit", "add docs to project", "integrate documentation", "docs-kit setup", "add dioxus docs", "add a blog".
npx skills add oxidt/oxidt-docs-kit --skill dioxus-docs-kit-integrationIntegrate dioxus-docs-kit (v0.5) into any Dioxus 0.7 fullstack project.
Pre-flight
Before starting, identify:
- The project's
Cargo.toml— confirmdioxusis 0.7.x and uses thefullstackfeature (server functions for sitemap / llms.txt need it). - The
Routeenum — usually insrc/main.rs. Find existing layout wrappers so the new docs layout fits in. - The
tailwind.css— confirm Tailwind CSS 4 + DaisyUI 5 are in use. - Optional surfaces — ask the user (or infer):
- OpenAPI reference pages? (Need a spec file.)
- Blog engine?
- SEO meta + sitemap? (Need the public site URL.)
Step 1: Dependencies
Add to the consumer project's Cargo.toml:
[dependencies]
dioxus = { version = "0.7.10", features = ["router", "fullstack"] }
dioxus-docs-kit = { version = "0.5", default-features = false }
[build-dependencies]
dioxus-docs-kit-build = "0.5"Wire features so web propagates to the kit:
[features]
default = ["web"]
web = ["dioxus/web", "dioxus-docs-kit/web"]
server = ["dioxus/server", "dioxus-docs-kit/server"]
# Optional: keep mermaid rendering
mermaid = ["dioxus-docs-kit/mermaid"]default-features = false on the kit is important — the kit's own default
includes web, which would conflict with fullstack feature unification.
Syntax highlighting needs no feature: it is always compiled in (hl-lite,
~48 KB of wasm, no dependencies). To recolor tokens, override the --dk-hl-*
CSS custom properties from theme.css; there is no Rust-side code-theme API.
Step 2: Build script
Create or extend build.rs:
fn main() {
dioxus_docs_kit_build::DocsBuild::new("docs/_nav.json")
// Optional: .with_openapi("api-reference", "docs/api-reference/openapi.yaml")
.generate();
// Only if adding the blog:
// dioxus_docs_kit_build::BlogBuild::new("blog/_blog.json").generate();
}The generator reads the nav/manifest JSON, parses every listed .mdx file
(rendering its prose to HTML) plus any OpenAPI spec, precomputes the search
index, and writes one $OUT_DIR/docs_bundle.json. If a path doesn't exist, the
build reports it at compile time. The client links no MDX, Markdown or YAML
parser as a result.
Step 3: Content files
Docs nav
Create docs/_nav.json:
{
"tabs": ["Docs", "API Reference"],
"groups": [
{
"group": "Getting Started",
"tab": "Docs",
"pages": ["getting-started/introduction"]
},
{
"group": "API Reference",
"tab": "API Reference",
"pages": ["api-reference/overview"]
}
]
}tabsis the array of tab labels shown above the sidebar. Omit for a single-tab sidebar.groups[].tabties a group to one tab. Omit for tab-less layouts.groups[].pagesare paths relative todocs/, without the.mdxextension.- Do not list OpenAPI operation IDs in
pages— they're injected dynamically into the group whose name matchesapi_group_name(default"API Reference").
Create docs/getting-started/introduction.mdx:
---
title: Introduction
description: Getting started with the project
---
Welcome to the documentation.Optional: OpenAPI spec
If using the API reference tab, drop the spec at
docs/api-reference/openapi.yaml (JSON also works) and an overview MDX at
docs/api-reference/overview.mdx.
Optional: Blog manifest
Create blog/_blog.json:
{
"authors": {
"alex": {
"name": "Alex Doe",
"bio": "Builds things with Rust",
"url": "https://example.com/@alex"
}
},
"posts": ["hello-world"]
}Each post lives at blog/<slug>.mdx with frontmatter:
---
title: "Hello World"
description: "First post."
date: "2026-03-20"
author: "alex"
tags: ["announcement"]
featured: true
---
Post body.Step 4: Registry + bundle macro
At module level in src/main.rs:
use std::sync::LazyLock;
use dioxus_docs_kit::{DocsConfig, DocsRegistry};
static DOCS: LazyLock<DocsRegistry> = LazyLock::new(|| {
DocsConfig::new(dioxus_docs_kit::docs_bundle!())
.with_default_path("getting-started/introduction")
// Optional: .with_theme_toggle("light", "dark", "dark")
.build()
});If adding the blog, add a parallel block:
use dioxus_docs_kit::{BlogConfig, BlogRegistry};
static BLOG: LazyLock<BlogRegistry> = LazyLock::new(|| {
BlogConfig::new(dioxus_docs_kit::blog_bundle!())
.with_posts_per_page(9)
.with_theme_toggle("light", "dark", "dark")
.build()
});Keep dioxus-docs-kit and dioxus-docs-kit-build on the same version — the
bundle carries a format version the runtime checks.
Step 5: Routes
Extend the Route enum:
#[derive(Debug, Clone, Routable, PartialEq)]
#[rustfmt::skip]
enum Route {
#[layout(MyDocsLayout)]
#[redirect("/docs", || Route::DocsPage { slug: vec!["getting-started".into(), "introduction".into()] })]
#[route("/docs/:..slug")]
DocsPage { slug: Vec<String> },
#[end_layout]
// Only if adding the blog:
#[layout(MyBlogLayout)]
#[route("/blog")]
BlogIndex {},
#[route("/blog/:slug")]
BlogPage { slug: String },
}Step 6: Layout wrappers
use dioxus::prelude::*;
use dioxus_docs_kit::{
DocsLayout, DocsPageContent, SearchButton, ThemeToggle,
use_docs_context, use_docs_providers,
};
#[component]
fn MyDocsLayout() -> Element {
let nav = use_navigator();
let route = use_route::<Route>();
let current_path = match route {
Route::DocsPage { slug } => slug.join("/"),
_ => String::new(),
};
// Pass the path by value: use_docs_context rewraps it reactively. Building
// the signal yourself with a plain use_memo freezes the sidebar highlight.
let docs_ctx = use_docs_context(
current_path,
"/docs",
Callback::new(move |path: String| {
let slug: Vec<String> = path.split('/').map(String::from).collect();
nav.push(Route::DocsPage { slug });
}),
)
.with_site_url("https://example.com"); // optional; omit for no canonical/OG host
let providers = use_docs_providers(&DOCS, docs_ctx);
let search_open = providers.search_open;
let mut drawer_open = providers.drawer_open;
rsx! {
DocsLayout {
header: rsx! {
// Replace with the project's existing navbar styling
div { class: "navbar bg-base-200 border-b border-base-300 px-4 lg:px-8",
div { class: "flex-1 gap-2",
button {
class: "btn btn-ghost btn-sm btn-square lg:hidden",
onclick: move |_| drawer_open.toggle(),
// hamburger icon here
}
Link {
to: Route::DocsPage { slug: vec!["getting-started".into(), "introduction".into()] },
class: "text-xl font-semibold tracking-tight",
"Project Name"
}
}
div { class: "flex-none flex items-center gap-1",
SearchButton { search_open }
ThemeToggle {}
}
}
},
Outlet::<Route> {}
}
}
}
#[component]
fn DocsPage(slug: Vec<String>) -> Element {
rsx! { DocsPageContent { path: slug.join("/") } }
}DocsContext is #[non_exhaustive] — construct it with
DocsContext::new(current_path, base_path, navigate) and layer optional SEO
settings via .with_site_url(...), .with_auto_meta(false),
.with_markdown_alternate(true), .with_llms_txt(true) and
.with_help_link("/docs/guides/report-a-problem") (the last two emit
site-level <link> tags even when auto_meta is off). Struct literals won't
compile outside the kit's own crate.
If you added the blog, add the parallel wrapper:
use dioxus_docs_kit::{
BlogContext, BlogLayout, BlogList, BlogPostView, BlogSearchButton,
use_blog_providers,
};
#[component]
fn MyBlogLayout() -> Element {
let nav = use_navigator();
let route = use_route::<Route>();
let current_slug = use_memo(use_reactive!(|route| match route {
Route::BlogPage { slug } => slug,
_ => String::new(),
}));
let blog_ctx = BlogContext::new(
current_slug,
"/blog",
Callback::new(move |slug: String| {
if slug.is_empty() { nav.push(Route::BlogIndex {}); }
else { nav.push(Route::BlogPage { slug }); }
}),
)
.with_site_url("https://example.com");
let providers = use_blog_providers(&BLOG, blog_ctx);
let search_open = providers.search_open;
rsx! {
BlogLayout {
header: rsx! { /* navbar with BlogSearchButton { search_open } */ },
Outlet::<Route> {}
}
}
}
#[component]
fn BlogIndex() -> Element { rsx! { BlogList {} } }
#[component]
fn BlogPage(slug: String) -> Element { rsx! { BlogPostView { slug } } }Step 7: Tailwind safelist
When the kit is a crates.io dep, Tailwind 4's @source cannot scan
~/.cargo/ reliably. Ship a safelist file.
# From the dioxus-docs-kit repo (or download from GitHub):
cp crates/dioxus-docs-kit/safelist.html <project>/safelist-docs-kit.htmlIn tailwind.css:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
themes: dark --default, light;
}
@source "./src/**/*.{rs,html,css}";
@source "./safelist-docs-kit.html";Remove any old @source paths pointing at ~/.cargo/ and any CI symlinks /
cargo-vendor workarounds that existed to support that.
Step 8 (optional): SEO meta, sitemap, robots, llms.txt
The auto_meta: true field on DocsContext already emits per-page <title>,
description, OpenGraph, and Twitter Card tags from MDX frontmatter. Canonical
URL + og:url emit only when site_url is also set.
Do not use #[get(...)] server functions for these endpoints — server
functions JSON-encode their String return (quoted body, application/json
content type), which robots.txt / sitemap / llms.txt consumers can't parse.
Use SeoRouter (behind the kit's server feature) instead; it wires all
crawler-facing routes as plain Axum routes with correct content types:
per-page raw Markdown (<page>.md), /llms.txt, /llms-full.txt,
per-surface sitemaps plus a /sitemap.xml index, blog RSS, and robots.txt.
use dioxus_docs_kit::server::SeoRouter;
dioxus::server::serve(|| async {
let seo = SeoRouter::new("https://example.com", "My Docs", "Documentation for My Product")
.with_docs(&DOCS, "/docs")
.with_blog(&BLOG, "/blog") // omit if you have no blog
.into_router();
Ok(dioxus::server::router(App).merge(seo))
});If you call DOCS.generate_llms_txt(...) / generate_llms_full_txt(...)
directly instead, note the third argument is the docs base URL (e.g.
"https://example.com/docs"), not a repository link.
If using the blog, add a /sitemap-blog.xml entry that calls
BLOG.generate_sitemap(SITE_URL, "/blog") and include it in the sitemap
index.
Step 9 (optional): Theme presets / dk-* token surface
Beyond DaisyUI themes, the kit exposes a flat --dk-* CSS token surface on
.dk-root. Override any subset in your stylesheet:
.dk-root {
--dk-article-width: 68ch;
--dk-heading-font: "Inter Tight", system-ui, sans-serif;
--dk-accent: oklch(0.65 0.20 250);
--dk-radius: 0.25rem;
}Drop-in presets live in crates/dioxus-docs-kit/examples/themes/
(warm-editorial.css, brutalist-light.css, shadcn-light.css,
shadcn-dark.css, default.css). Copy any to your assets/ and load via
document::Stylesheet or an inline <style> block.
For density, pass variant to DocsLayout:
use dioxus_docs_kit::DocsVariant;
DocsLayout { variant: DocsVariant::Reference, /* ... */ }Prose (default) is wide / serif-friendly; Reference is tighter and denser.
Checklist
After integration, verify:
-
dx servestarts without errors -
/docsredirects to the default page - Sidebar tabs + groups render
- Search modal opens (Ctrl/Cmd+K) and returns hits
- OpenAPI endpoints appear under the API Reference group (if configured)
- Blog index + post render (if configured)
-
<head>shows per-page title/description/OG tags whenauto_meta = true -
/sitemap.xml,/robots.txt,/llms.txtreturn content (if wired) - All component styles render — callouts, code blocks, HTTP method badges, parameter location badges (depend on safelist being loaded)
- Mobile drawer opens/closes
- Theme toggle persists across reloads (if enabled)