Skip to main content

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// ============================================================================
81// Docs context
82// ============================================================================
83
84/// Navigation bridge that decouples library components from the consumer's Route enum.
85///
86/// The consumer creates this in their docs layout wrapper and provides it via `use_context_provider`.
87#[derive(Clone)]
88#[non_exhaustive]
89pub struct DocsContext {
90    /// Current docs page path (e.g. "getting-started/introduction").
91    pub current_path: ReadSignal<String>,
92    /// Base URL path for docs (e.g. "/docs").
93    pub base_path: String,
94    /// Callback to navigate to a docs page by content path.
95    pub navigate: Callback<String>,
96    /// Optional full site URL (e.g. `https://example.com`). Used as the canonical
97    /// host for emitted `<link rel="canonical">` and `og:url` tags. Independent
98    /// of [`auto_meta`](Self::auto_meta) — set it whenever you want kit helpers
99    /// (sitemap generation, canonical URLs) to know the public origin, even if
100    /// you suppress automatic meta emission.
101    pub site_url: Option<String>,
102    /// When true, the kit emits per-page `<title>`, `<meta name="description">`,
103    /// Open Graph and Twitter Card tags from frontmatter. Set to `false` if your
104    /// app manages its own `<head>` (e.g. brand-specific OG images, structured
105    /// data) and the kit's emissions would conflict. Title and description tags
106    /// always emit when this is on; canonical and `og:url` only emit when
107    /// [`site_url`](Self::site_url) is also set.
108    pub auto_meta: bool,
109    /// When true, [`DocsPageMeta`] emits a
110    /// `<link rel="alternate" type="text/markdown">` pointing at the page's raw
111    /// Markdown source (`<base_path>/<path>.md`), a discoverability hint for AI
112    /// crawlers and "view as Markdown" tooling. Enable this only if your server
113    /// actually serves those `.md` URLs (see `server::SeoRouter` behind the
114    /// `server` feature). Emitted only for MDX pages (OpenAPI endpoint pages
115    /// have no Markdown source), and only when [`auto_meta`](Self::auto_meta)
116    /// is also on.
117    pub markdown_alternate: bool,
118}
119
120impl DocsContext {
121    /// Create a context from the three required fields.
122    ///
123    /// The meta fields default to `site_url: None`, `auto_meta: true`,
124    /// `markdown_alternate: false`; override them with the `with_*` setters.
125    /// Prefer this over a struct literal — new fields get sensible defaults
126    /// here instead of breaking your build.
127    pub fn new(
128        current_path: impl Into<ReadSignal<String>>,
129        base_path: impl Into<String>,
130        navigate: Callback<String>,
131    ) -> Self {
132        Self {
133            current_path: current_path.into(),
134            base_path: base_path.into(),
135            navigate,
136            site_url: None,
137            auto_meta: true,
138            markdown_alternate: false,
139        }
140    }
141
142    /// Set the public site origin (e.g. `"https://example.com"`).
143    pub fn with_site_url(mut self, site_url: impl Into<String>) -> Self {
144        self.site_url = Some(site_url.into());
145        self
146    }
147
148    /// Enable or disable automatic per-page meta emission (default: on).
149    pub fn with_auto_meta(mut self, auto_meta: bool) -> Self {
150        self.auto_meta = auto_meta;
151        self
152    }
153
154    /// Emit `<link rel="alternate" type="text/markdown">` tags (default: off).
155    pub fn with_markdown_alternate(mut self, markdown_alternate: bool) -> Self {
156        self.markdown_alternate = markdown_alternate;
157        self
158    }
159}
160
161// ============================================================================
162// Blog context
163// ============================================================================
164
165/// Navigation bridge for blog pages, decoupled from the consumer's Route enum.
166///
167/// The consumer creates this in their blog layout wrapper and provides it via `use_context_provider`.
168#[derive(Clone)]
169#[non_exhaustive]
170pub struct BlogContext {
171    /// Current blog post slug (empty on the list/index page).
172    pub current_slug: ReadSignal<String>,
173    /// Base URL path for the blog (e.g. "/blog").
174    pub base_path: String,
175    /// Callback to navigate to a blog post by slug (empty string = blog index).
176    pub navigate: Callback<String>,
177    /// Optional full site URL (e.g. `https://example.com`). Used as the canonical
178    /// host for emitted `<link rel="canonical">`, `og:url`, and JSON-LD URLs.
179    /// Independent of [`auto_meta`](Self::auto_meta) — set it whenever you want
180    /// kit helpers (sitemap/RSS, canonical URLs) to know the public origin, even
181    /// if you suppress automatic meta emission.
182    pub site_url: Option<String>,
183    /// When true, the kit emits per-page `<title>`, `<meta name="description">`,
184    /// Open Graph, Twitter Card, and Article JSON-LD tags from frontmatter. Set
185    /// to `false` if your app manages its own `<head>` (e.g. brand-specific OG
186    /// images, structured data) and the kit's emissions would conflict. Title
187    /// and description tags always emit when this is on; canonical, `og:url`,
188    /// and JSON-LD `@id` only emit when [`site_url`](Self::site_url) is also set.
189    pub auto_meta: bool,
190    /// When true, [`BlogPostMeta`] emits a
191    /// `<link rel="alternate" type="text/markdown">` pointing at the post's raw
192    /// Markdown source (`<base_path>/<slug>.md`), a discoverability hint for AI
193    /// crawlers and "view as Markdown" tooling. Enable this only if your server
194    /// actually serves those `.md` URLs (see `server::SeoRouter` behind the
195    /// `server` feature). Emitted only when [`auto_meta`](Self::auto_meta) is
196    /// also on.
197    pub markdown_alternate: bool,
198}
199
200impl BlogContext {
201    /// Create a context from the three required fields.
202    ///
203    /// The meta fields default to `site_url: None`, `auto_meta: true`,
204    /// `markdown_alternate: false`; override them with the `with_*` setters.
205    /// Prefer this over a struct literal — new fields get sensible defaults
206    /// here instead of breaking your build.
207    pub fn new(
208        current_slug: impl Into<ReadSignal<String>>,
209        base_path: impl Into<String>,
210        navigate: Callback<String>,
211    ) -> Self {
212        Self {
213            current_slug: current_slug.into(),
214            base_path: base_path.into(),
215            navigate,
216            site_url: None,
217            auto_meta: true,
218            markdown_alternate: false,
219        }
220    }
221
222    /// Set the public site origin (e.g. `"https://example.com"`).
223    pub fn with_site_url(mut self, site_url: impl Into<String>) -> Self {
224        self.site_url = Some(site_url.into());
225        self
226    }
227
228    /// Enable or disable automatic per-page meta emission (default: on).
229    pub fn with_auto_meta(mut self, auto_meta: bool) -> Self {
230        self.auto_meta = auto_meta;
231        self
232    }
233
234    /// Emit `<link rel="alternate" type="text/markdown">` tags (default: off).
235    pub fn with_markdown_alternate(mut self, markdown_alternate: bool) -> Self {
236        self.markdown_alternate = markdown_alternate;
237        self
238    }
239}
240
241// ============================================================================
242// Docs re-exports
243// ============================================================================
244
245#[cfg(feature = "highlight")]
246pub use config::CodeThemeConfig;
247pub use config::{DocsConfig, ThemeConfig};
248pub use error::DocsKitError;
249pub use registry::DocsRegistry;
250pub use registry::{ApiEndpointEntry, NavConfig, NavGroup, SearchEntry};
251
252pub use components::{
253    ActiveTab, CopyPageButton, CurrentTheme, DocsLayout, DocsPageContent, DocsPageMeta,
254    DocsPageNav, DocsSidebar, DocsVariant, DrawerOpen, LayoutOffsets, MobileDrawer, SearchButton,
255    SearchModal, SearchOpen, ThemeToggle, use_theme_provider,
256};
257
258pub use hooks::{DocsProviders, use_docs_context, use_docs_providers};
259
260pub use dioxus_mdx::{
261    ApiOperation, ApiTag, DocContent, DocTableOfContents, EndpointPage, HttpMethod, OpenApiSpec,
262    ParsedDoc, extract_headers,
263};
264
265#[cfg(feature = "highlight")]
266pub use dioxus_mdx::CodeThemeOverride;
267
268#[cfg(feature = "highlight")]
269pub use dioxus_code::{Code, CodeTheme, Language, SourceCode, Theme};
270
271#[cfg(feature = "mermaid")]
272pub use dioxus_mdx::MermaidDiagram;
273
274// ============================================================================
275// Blog re-exports
276// ============================================================================
277
278pub use blog::hooks::{ActiveTag, CurrentPage};
279pub use blog::types::{Author, BlogFrontmatter, BlogPost, BlogSearchEntry};
280pub use blog::{BlogConfig, BlogProviders, BlogRegistry, use_blog_providers};
281
282pub use components::{
283    AuthorInfo, BlogCard, BlogIndexMeta, BlogLayout, BlogList, BlogMobileDrawer, BlogPostMeta,
284    BlogPostNav, BlogPostView, BlogSearchButton, BlogSearchModal, BlogThemeToggle,
285    ReadingProgressBar, ReadingTimeBadge, RelatedPosts, TagFilter,
286};
287
288// ============================================================================
289// Macros
290// ============================================================================
291
292/// Generates a `doc_content_map()` function that returns a
293/// `HashMap<&'static str, &'static str>` from the build-script output.
294///
295/// Place this at module level in your `main.rs`:
296///
297/// ```rust,ignore
298/// dioxus_docs_kit::doc_content_map!();
299/// ```
300///
301/// Requires `dioxus-docs-kit-build` in `[build-dependencies]` and a `build.rs`
302/// that calls `dioxus_docs_kit_build::generate_content_map("docs/_nav.json")`.
303#[macro_export]
304macro_rules! doc_content_map {
305    () => {
306        fn doc_content_map() -> ::std::collections::HashMap<&'static str, &'static str> {
307            include!(concat!(env!("OUT_DIR"), "/doc_content_generated.rs"))
308        }
309    };
310}
311
312/// Generates a `blog_content_map()` function that returns a
313/// `HashMap<&'static str, &'static str>` from the build-script output.
314///
315/// Place this at module level in your `main.rs`:
316///
317/// ```rust,ignore
318/// dioxus_docs_kit::blog_content_map!();
319/// ```
320///
321/// Requires `dioxus-docs-kit-build` in `[build-dependencies]` and a `build.rs`
322/// that calls `dioxus_docs_kit_build::generate_blog_content_map("blog/_blog.json")`.
323#[macro_export]
324macro_rules! blog_content_map {
325    () => {
326        fn blog_content_map() -> ::std::collections::HashMap<&'static str, &'static str> {
327            include!(concat!(env!("OUT_DIR"), "/blog_content_generated.rs"))
328        }
329    };
330}