Overview
dioxus-docs-kit includes a full blog engine that embeds all posts at compile time. You get post listing with pagination, tag filtering, full-text search, reading time estimates, author metadata, and MDX rendering — all from a single binary with zero runtime file I/O.
Content Structure
Create a blog/ directory at your project root with a _blog.json manifest and .mdx post files:
blog/
├── _blog.json
├── hello-world.mdx
├── building-with-dioxus.mdx
└── rust-web-future.mdxThe _blog.json Manifest
The manifest defines authors and lists all post slugs (filenames without .mdx):
{
"authors": {
"hauke": {
"name": "Hauke Jung",
"bio": "Rust developer and Dioxus enthusiast",
"url": "https://github.com/hauju"
}
},
"posts": [
"hello-world",
"building-with-dioxus",
"rust-web-future"
]
}Author fields
| Field | Required | Description |
|---|---|---|
name |
Yes | Display name |
avatar |
No | URL or asset path for the author's avatar image |
bio |
No | Short biography |
url |
No | Link to the author's website or profile |
Post Frontmatter
Each .mdx file needs YAML frontmatter at the top:
---
title: "Hello World"
description: "Welcome to our blog!"
date: "2026-03-20"
author: "hauke"
tags: ["announcement", "dioxus"]
coverImage: "/assets/cover.png"
draft: false
---
Your post content in MDX format...Frontmatter fields
| Field | Required | Description |
|---|---|---|
title |
Yes | Post title |
description |
No | Short summary shown in post cards |
date |
Yes | ISO 8601 date string (e.g. "2026-03-20") — used for sorting |
author |
Yes | Author ID matching a key in _blog.json authors |
tags |
No | Array of tag strings for filtering |
coverImage |
No | Cover image path |
draft |
No | Set to true to hide from listing (default: false) |
Setup
Add the build script
In your build.rs, add the blog bundle alongside your docs bundle:
fn main() {
dioxus_docs_kit_build::DocsBuild::new("docs/_nav.json").generate();
dioxus_docs_kit_build::BlogBuild::new("blog/_blog.json").generate();
}The blog directory is inferred from the manifest path parent (e.g. "blog/_blog.json" reads from blog/). Posts are parsed, drafts dropped and the search index built here, so the browser only deserializes the result.
Create the blog registry
In main.rs, load the bundle with the blog_bundle!() macro and build a BlogRegistry:
use dioxus_docs_kit::{
BlogConfig, BlogContext, BlogLayout, BlogList, BlogPostView,
BlogRegistry, BlogSearchButton, use_blog_providers,
};
use std::sync::LazyLock;
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()
});Add routes
Add blog routes to your Route enum. The blog needs two routes: an index (list) page and a single post page:
#[derive(Debug, Clone, Routable, PartialEq)]
enum Route {
#[layout(MyBlogLayout)]
#[route("/blog")]
BlogIndex {},
#[route("/blog/:slug")]
BlogPage { slug: String },
}Create the blog layout wrapper
The blog layout wrapper provides BlogContext and wires up the registry, just like the docs layout does for documentation:
#[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(),
}));
// The constructor defaults the meta fields; chain `.with_site_url()`
// etc. to override them.
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 });
}
}),
);
let providers = use_blog_providers(&BLOG, blog_ctx);
let search_open = providers.search_open;
rsx! {
BlogLayout {
header: rsx! {
div { class: "navbar bg-base-200 border-b border-base-300 px-4",
div { class: "flex-1",
// Your site logo / nav links
}
div { class: "flex-none flex items-center gap-1",
BlogSearchButton { search_open }
}
}
},
Outlet::<Route> {}
}
}
}The BlogProviders struct gives you search_open, drawer_open, active_tag, and current_page signals to wire into your header.
Create the page components
Add the index and post view components:
#[component]
fn BlogIndex() -> Element {
rsx! {
BlogList {
hero: rsx! {
div { class: "text-center mb-12",
h1 { class: "text-4xl font-bold tracking-tight mb-3",
"Blog"
}
p { class: "text-lg text-base-content/60",
"Thoughts on Rust, Dioxus, and web development."
}
}
}
}
}
}
#[component]
fn BlogPage(slug: String) -> Element {
rsx! {
BlogPostView { slug }
}
}The BlogList component accepts an optional hero prop for custom content above the post grid. It includes built-in tag filtering and pagination.
Configuration Options
The BlogConfig builder supports these options:
| Method | Default | Description |
|---|---|---|
.with_posts_per_page(n) |
9 |
Number of posts per page |
.with_date_format(fmt) |
"%B %d, %Y" |
Date display format (%Y, %m, %d, %B for month name) |
.with_theme(name) |
— | Set a fixed theme (no toggle) |
.with_theme_toggle(light, dark, default) |
— | Enable light/dark theme switching |
Available Components
BlogLayout
Main layout shell with header slot, search modal, and mobile drawer
BlogList
Post grid with tag filtering, pagination, and optional hero section
BlogPostView
Full post renderer with author info, reading time, tags, and prev/next navigation
BlogSearchModal
Full-text search across all blog posts
BlogSearchButton
Button that opens the search modal
TagFilter
Tag filter bar (included in BlogList, also usable standalone)
AuthorInfo
Author avatar, name, and bio display
ReadingTimeBadge
Estimated reading time badge
BlogPostNav
Previous/next post navigation links
BlogThemeToggle
Light/dark theme toggle for the blog layout
RSS Feed Generation
The BlogRegistry can generate an RSS feed:
let rss_xml = BLOG.generate_rss(
"My Blog", // site title
"https://example.com", // site URL
"/blog", // blog base path
);Don't serve this from a #[get] server function — server functions JSON-encode the body. With the server feature, SeoRouter::new(...).with_blog(&BLOG, "/blog") registers /blog/rss.xml (plus per-post .md routes and the blog sitemap) as plain Axum routes with the correct content type. See the customization guide for the full setup.
LLMs.txt Generation
Similar to the docs registry, the blog supports llms.txt output:
let llms_txt = BLOG.generate_llms_txt(
"My Blog",
"A blog about Rust and Dioxus",
"https://example.com",
"/blog",
);Category Pages
Category pages give each topic a permanent URL, such as /blog/categories/rust. Posts are matched by their existing frontmatter tags. Featured posts are included, drafts are excluded, and a post can appear in several categories.
Enable category URLs when building the registry:
BlogConfig::new(dioxus_docs_kit::blog_bundle!())
.with_category_base_path("/blog/categories")
.with_posts_per_page(9)
.build()Register both routes under your blog layout and point them at BlogCategoryPage; its page prop defaults to 1. The path must match with_category_base_path; you can use /categories or /topics instead.
use dioxus_docs_kit::BlogCategoryPage;
#[derive(Debug, Clone, Routable, PartialEq)]
enum Route {
#[layout(MyBlogLayout)]
#[route("/blog")]
BlogIndex {},
#[route("/blog/categories/:slug", BlogCategoryPage)]
BlogCategory { slug: String },
#[route("/blog/categories/:slug/page/:page", BlogCategoryPage)]
BlogCategoryPaginated { slug: String, page: usize },
#[route("/blog/:slug")]
BlogPage { slug: String },
}In MyBlogLayout, derive the current category from the route alongside current_slug and pass it to BlogContext. This is required once with_category_base_path is set: it highlights the active category and closes the mobile drawer after category navigation.
let current_category = use_memo(use_reactive!(|route| match route {
Route::BlogCategory { slug } | Route::BlogCategoryPaginated { slug, .. } => slug,
_ => String::new(),
}));
// Chain these onto your BlogContext::new(...) call:
// .with_current_category(current_category)
// .with_site_url("https://example.com")TagFilter then renders category links automatically. Post badges and card badges also link to their categories. Without with_category_base_path, tag filters retain their existing local behavior.
Titles, descriptions, and images
Add an optional categories object to _blog.json. Keys must match the original tag exactly:
{
"authors": { "hauke": { "name": "Hauke Jung" } },
"posts": ["hello-world", "building-with-dioxus", "rust-web-future"],
"categories": {
"rust": {
"title": "Rust",
"description": "Practical guides to building web applications with Rust.",
"image": "/assets/rust-topic.webp"
},
"C++": {
"slug": "cpp",
"title": "C++"
}
}
}All metadata fields are optional. The default title is the tag, the default description is Browse articles about {title}., and the URL slug is generated from the tag. Use slug to choose a stable URL or resolve collisions such as C++ and C#. Slugs must be unique and contain only letters, numbers, or hyphens. Invalid or colliding slugs cause try_build() to return an error.
Only tags attached to published posts create categories. Metadata alone does not create an empty page. Images appear in the category header and social metadata; root-relative images become absolute social URLs when site_url is set.
Pagination and search metadata
Category page numbers start at one. The first page uses /blog/categories/rust; later pages use /blog/categories/rust/page/2. Pagination links work with direct loads, refresh, and browser history. The page size comes from with_posts_per_page and must be greater than zero.
Category pages emit titles, descriptions, Open Graph, and Twitter metadata. Setting BlogContext::with_site_url also emits a canonical URL for each page. A /page/1 URL canonicalizes to the category's first page. with_auto_meta(false) disables this automatic metadata for valid pages.
Unknown categories and out-of-range pages render a not-found page with noindex. With the kit's server feature enabled, they also return HTTP 404 during server rendering. A client-only host must configure its own HTTP status handling.
BlogRegistry::generate_sitemap includes every valid category page automatically, as does SeoRouter::with_blog. RSS remains a feed of posts. Use category_url_for_tag, category_url, or category_posts_page when building custom navigation and layouts.