dioxus_docs_kit/lib.rs
1//! # dioxus-docs-kit
2//!
3//! Reusable documentation site shell and blog engine for Dioxus applications.
4//!
5//! Provides a complete docs layout with sidebar navigation, search modal,
6//! page navigation, OpenAPI API reference pages, and mobile drawer.
7//! Also includes a full blog engine with post listing, tag filtering,
8//! search, reading time, and MDX rendering.
9//!
10//! ## Quick Start — Docs
11//!
12//! ```rust,ignore
13//! use dioxus::prelude::*;
14//! use dioxus_docs_kit::{DocsConfig, DocsRegistry, DocsContext, DocsLayout, DocsPageContent};
15//! use std::sync::LazyLock;
16//!
17//! static DOCS: LazyLock<DocsRegistry> = LazyLock::new(|| {
18//! DocsConfig::new(include_str!("../docs/_nav.json"), doc_content_map())
19//! .with_default_path("getting-started/introduction")
20//! .build()
21//! });
22//! ```
23//!
24//! ## Quick Start — Blog
25//!
26//! ```rust,ignore
27//! use dioxus::prelude::*;
28//! use dioxus_docs_kit::{BlogConfig, BlogRegistry, BlogContext, BlogLayout, BlogList, BlogPostView};
29//! use std::sync::LazyLock;
30//!
31//! dioxus_docs_kit::blog_content_map!();
32//!
33//! static BLOG: LazyLock<BlogRegistry> = LazyLock::new(|| {
34//! BlogConfig::new(include_str!("../blog/_blog.json"), blog_content_map())
35//! .with_posts_per_page(9)
36//! .build()
37//! });
38//! ```
39
40// The tree-sitter C grammars pulled in via `dioxus-code` (for syntax
41// highlighting) reference the libc global `stderr`. Its definition lives in
42// `arborium-sysroot`'s C shim, but that shim links as a plain static archive
43// whose `stdio.o` member only gets pulled in when `stderr` is undefined as the
44// linker reaches it — and on some host toolchains (notably Homebrew LLVM on
45// macOS) it isn't, so the wasm link fails with `undefined symbol: stderr`.
46//
47// We define `stderr` here in the *library* so every consumer that links
48// `dioxus-docs-kit` gets the symbol — defining it only in the docs-kit binary
49// (`src/main.rs`) leaves library consumers (their own app binaries) to hit the
50// same link error. A strong definition preempts the sysroot's lazy archive
51// member, so where the shim already links cleanly its `stdio.o` simply isn't
52// pulled and there is never a duplicate symbol. `fprintf` is a no-op macro in
53// the shim's headers, so `stderr` is referenced but never dereferenced at
54// runtime. `#[used]` keeps the symbol from being dropped from the rlib before
55// it can satisfy the cross-crate reference at final link.
56//
57// This is only needed when the `highlight` feature is enabled, since that is what
58// links the tree-sitter C code (via `dioxus-code`) that references `stderr`.
59#[cfg(all(target_arch = "wasm32", feature = "highlight"))]
60mod wasm_sysroot_stderr {
61 use core::ffi::c_void;
62 static mut DUMMY_FILE: u8 = 0;
63 #[used]
64 #[unsafe(no_mangle)]
65 static mut stderr: *mut c_void = &raw mut DUMMY_FILE as *mut c_void;
66}
67
68pub mod blog;
69pub mod components;
70pub mod config;
71pub mod error;
72pub mod hooks;
73pub mod registry;
74pub(crate) mod search;
75#[cfg(feature = "server")]
76pub mod server;
77
78use dioxus::prelude::*;
79
80/// Precompiled stylesheet covering every class the kit's components emit
81/// (Tailwind utilities, DaisyUI dark/light themes, typography prose, and the
82/// `--dk-*` token surface from `theme.css`).
83///
84/// Link it to run the kit without any Tailwind/Bun setup of your own:
85///
86/// ```rust,ignore
87/// rsx! {
88/// document::Stylesheet { href: dioxus_docs_kit::DOCS_KIT_CSS }
89/// }
90/// ```
91///
92/// If your app already runs its own Tailwind build, skip this and use the
93/// `safelist.html` approach from the README instead — the precompiled sheet
94/// only contains the kit's classes, not yours.
95pub const DOCS_KIT_CSS: Asset = asset!("/assets/docs-kit.css");
96
97// ============================================================================
98// Docs context
99// ============================================================================
100
101/// Navigation bridge that decouples library components from the consumer's Route enum.
102///
103/// The consumer creates this in their docs layout wrapper and provides it via `use_context_provider`.
104#[derive(Clone)]
105#[non_exhaustive]
106pub struct DocsContext {
107 /// Current docs page path (e.g. "getting-started/introduction").
108 pub current_path: ReadSignal<String>,
109 /// Base URL path for docs (e.g. "/docs").
110 pub base_path: String,
111 /// Callback to navigate to a docs page by content path.
112 pub navigate: Callback<String>,
113 /// Optional full site URL (e.g. `https://example.com`). Used as the canonical
114 /// host for emitted `<link rel="canonical">` and `og:url` tags. Independent
115 /// of [`auto_meta`](Self::auto_meta) — set it whenever you want kit helpers
116 /// (sitemap generation, canonical URLs) to know the public origin, even if
117 /// you suppress automatic meta emission.
118 pub site_url: Option<String>,
119 /// When true, the kit emits per-page `<title>`, `<meta name="description">`,
120 /// Open Graph and Twitter Card tags from frontmatter. Set to `false` if your
121 /// app manages its own `<head>` (e.g. brand-specific OG images, structured
122 /// data) and the kit's emissions would conflict. Title and description tags
123 /// always emit when this is on; canonical and `og:url` only emit when
124 /// [`site_url`](Self::site_url) is also set.
125 pub auto_meta: bool,
126 /// When true, [`DocsPageMeta`] emits a
127 /// `<link rel="alternate" type="text/markdown">` pointing at the page's raw
128 /// Markdown source (`<base_path>/<path>.md`), a discoverability hint for AI
129 /// crawlers and "view as Markdown" tooling. Enable this only if your server
130 /// actually serves those `.md` URLs (see `server::SeoRouter` behind the
131 /// `server` feature). Emitted only for MDX pages (OpenAPI endpoint pages
132 /// have no Markdown source), and only when [`auto_meta`](Self::auto_meta)
133 /// is also on.
134 pub markdown_alternate: bool,
135}
136
137impl DocsContext {
138 /// Create a context from the three required fields.
139 ///
140 /// The meta fields default to `site_url: None`, `auto_meta: true`,
141 /// `markdown_alternate: false`; override them with the `with_*` setters.
142 /// Prefer this over a struct literal — new fields get sensible defaults
143 /// here instead of breaking your build.
144 pub fn new(
145 current_path: impl Into<ReadSignal<String>>,
146 base_path: impl Into<String>,
147 navigate: Callback<String>,
148 ) -> Self {
149 Self {
150 current_path: current_path.into(),
151 base_path: base_path.into(),
152 navigate,
153 site_url: None,
154 auto_meta: true,
155 markdown_alternate: false,
156 }
157 }
158
159 /// Set the public site origin (e.g. `"https://example.com"`).
160 pub fn with_site_url(mut self, site_url: impl Into<String>) -> Self {
161 self.site_url = Some(site_url.into());
162 self
163 }
164
165 /// Enable or disable automatic per-page meta emission (default: on).
166 pub fn with_auto_meta(mut self, auto_meta: bool) -> Self {
167 self.auto_meta = auto_meta;
168 self
169 }
170
171 /// Emit `<link rel="alternate" type="text/markdown">` tags (default: off).
172 pub fn with_markdown_alternate(mut self, markdown_alternate: bool) -> Self {
173 self.markdown_alternate = markdown_alternate;
174 self
175 }
176}
177
178// ============================================================================
179// Blog context
180// ============================================================================
181
182/// Navigation bridge for blog pages, decoupled from the consumer's Route enum.
183///
184/// The consumer creates this in their blog layout wrapper and provides it via `use_context_provider`.
185#[derive(Clone)]
186#[non_exhaustive]
187pub struct BlogContext {
188 /// Current blog post slug (empty on the list/index page).
189 pub current_slug: ReadSignal<String>,
190 /// Current category slug, when category routes are enabled.
191 pub current_category: Option<ReadSignal<String>>,
192 /// Base URL path for the blog (e.g. "/blog").
193 pub base_path: String,
194 /// Callback to navigate to a blog post by slug (empty string = blog index).
195 pub navigate: Callback<String>,
196 /// Optional full site URL (e.g. `https://example.com`). Used as the canonical
197 /// host for emitted `<link rel="canonical">`, `og:url`, and JSON-LD URLs.
198 /// Independent of [`auto_meta`](Self::auto_meta) — set it whenever you want
199 /// kit helpers (sitemap/RSS, canonical URLs) to know the public origin, even
200 /// if you suppress automatic meta emission.
201 pub site_url: Option<String>,
202 /// When true, the kit emits per-page `<title>`, `<meta name="description">`,
203 /// Open Graph, Twitter Card, and Article JSON-LD tags from frontmatter. Set
204 /// to `false` if your app manages its own `<head>` (e.g. brand-specific OG
205 /// images, structured data) and the kit's emissions would conflict. Title
206 /// and description tags always emit when this is on; canonical, `og:url`,
207 /// and JSON-LD `@id` only emit when [`site_url`](Self::site_url) is also set.
208 pub auto_meta: bool,
209 /// When true, [`BlogPostMeta`] emits a
210 /// `<link rel="alternate" type="text/markdown">` pointing at the post's raw
211 /// Markdown source (`<base_path>/<slug>.md`), a discoverability hint for AI
212 /// crawlers and "view as Markdown" tooling. Enable this only if your server
213 /// actually serves those `.md` URLs (see `server::SeoRouter` behind the
214 /// `server` feature). Emitted only when [`auto_meta`](Self::auto_meta) is
215 /// also on.
216 pub markdown_alternate: bool,
217}
218
219impl BlogContext {
220 /// Create a context from the three required fields.
221 ///
222 /// The meta fields default to `site_url: None`, `auto_meta: true`,
223 /// `markdown_alternate: false`; override them with the `with_*` setters.
224 /// Prefer this over a struct literal — new fields get sensible defaults
225 /// here instead of breaking your build.
226 pub fn new(
227 current_slug: impl Into<ReadSignal<String>>,
228 base_path: impl Into<String>,
229 navigate: Callback<String>,
230 ) -> Self {
231 Self {
232 current_slug: current_slug.into(),
233 current_category: None,
234 base_path: base_path.into(),
235 navigate,
236 site_url: None,
237 auto_meta: true,
238 markdown_alternate: false,
239 }
240 }
241
242 /// Track category routes for active navigation and mobile drawer dismissal.
243 pub fn with_current_category(mut self, slug: impl Into<ReadSignal<String>>) -> Self {
244 self.current_category = Some(slug.into());
245 self
246 }
247
248 /// Set the public site origin (e.g. `"https://example.com"`).
249 pub fn with_site_url(mut self, site_url: impl Into<String>) -> Self {
250 self.site_url = Some(site_url.into());
251 self
252 }
253
254 /// Enable or disable automatic per-page meta emission (default: on).
255 pub fn with_auto_meta(mut self, auto_meta: bool) -> Self {
256 self.auto_meta = auto_meta;
257 self
258 }
259
260 /// Emit `<link rel="alternate" type="text/markdown">` tags (default: off).
261 pub fn with_markdown_alternate(mut self, markdown_alternate: bool) -> Self {
262 self.markdown_alternate = markdown_alternate;
263 self
264 }
265}
266
267// ============================================================================
268// Docs re-exports
269// ============================================================================
270
271#[cfg(feature = "highlight")]
272pub use config::CodeThemeConfig;
273pub use config::{DocsConfig, ThemeConfig};
274pub use error::DocsKitError;
275pub use registry::DocsRegistry;
276pub use registry::{ApiEndpointEntry, NavConfig, NavGroup, SearchEntry};
277
278pub use components::{
279 ActiveTab, CopyPageButton, CurrentTheme, DocsLayout, DocsPageContent, DocsPageMeta,
280 DocsPageNav, DocsSidebar, DocsVariant, DrawerOpen, LayoutOffsets, MobileDrawer, SearchButton,
281 SearchModal, SearchOpen, ThemeToggle, use_theme_provider,
282};
283
284pub use hooks::{DocsProviders, use_docs_context, use_docs_providers};
285
286pub use dioxus_mdx::{
287 ApiOperation, ApiTag, DocContent, DocTableOfContents, EndpointPage, HttpMethod, OpenApiSpec,
288 ParsedDoc, extract_headers,
289};
290
291#[cfg(feature = "highlight")]
292pub use dioxus_mdx::CodeThemeOverride;
293
294#[cfg(feature = "highlight")]
295pub use dioxus_code::{Code, CodeTheme, Language, SourceCode, Theme};
296
297#[cfg(feature = "mermaid")]
298pub use dioxus_mdx::MermaidDiagram;
299
300// ============================================================================
301// Blog re-exports
302// ============================================================================
303
304pub use blog::hooks::{ActiveTag, CurrentPage};
305pub use blog::types::{
306 Author, BlogCategory, BlogCategoryMetadata, BlogFrontmatter, BlogPost, BlogSearchEntry,
307};
308pub use blog::{BlogConfig, BlogProviders, BlogRegistry, use_blog_providers};
309
310pub use components::{
311 AuthorInfo, BlogCard, BlogCategoryPage, BlogIndexMeta, BlogLayout, BlogList, BlogMobileDrawer,
312 BlogPostMeta, BlogPostNav, BlogPostView, BlogSearchButton, BlogSearchModal, BlogThemeToggle,
313 ReadingProgressBar, ReadingTimeBadge, RelatedPosts, TagFilter,
314};
315
316// ============================================================================
317// Macros
318// ============================================================================
319
320/// Generates a `doc_content_map()` function that returns a
321/// `HashMap<&'static str, &'static str>` from the build-script output.
322///
323/// Place this at module level in your `main.rs`:
324///
325/// ```rust,ignore
326/// dioxus_docs_kit::doc_content_map!();
327/// ```
328///
329/// Requires `dioxus-docs-kit-build` in `[build-dependencies]` and a `build.rs`
330/// that calls `dioxus_docs_kit_build::generate_content_map("docs/_nav.json")`.
331#[macro_export]
332macro_rules! doc_content_map {
333 () => {
334 fn doc_content_map() -> ::std::collections::HashMap<&'static str, &'static str> {
335 include!(concat!(env!("OUT_DIR"), "/doc_content_generated.rs"))
336 }
337 };
338}
339
340/// Generates a `blog_content_map()` function that returns a
341/// `HashMap<&'static str, &'static str>` from the build-script output.
342///
343/// Place this at module level in your `main.rs`:
344///
345/// ```rust,ignore
346/// dioxus_docs_kit::blog_content_map!();
347/// ```
348///
349/// Requires `dioxus-docs-kit-build` in `[build-dependencies]` and a `build.rs`
350/// that calls `dioxus_docs_kit_build::generate_blog_content_map("blog/_blog.json")`.
351#[macro_export]
352macro_rules! blog_content_map {
353 () => {
354 fn blog_content_map() -> ::std::collections::HashMap<&'static str, &'static str> {
355 include!(concat!(env!("OUT_DIR"), "/blog_content_generated.rs"))
356 }
357 };
358}