docs(adr): ADR-0012: MDBook Renderer Trait Implementation
@aRustyDev ci sta già lavorando.
Dal 5/1/2026.
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
ADR-0012: MDBook Renderer Trait Implementation
Status
Accepted
Context
mdbook-htmx is an alternative backend for MDBook. To integrate properly with MDBook's build pipeline, we need to understand and implement the Renderer trait from mdbook-renderer crate, and correctly consume the RenderContext JSON structure passed via stdin.
This ADR documents the integration approach and the data structures we receive from MDBook.
Decision Drivers
- Compatibility - Must work with standard
mdbook buildcommand - Future-Proofing - Handle MDBook version changes gracefully
- Extension - Support custom frontmatter beyond MDBook's standard fields
- Error Handling - Fail gracefully with actionable error messages
Decision
Implement as a standalone binary that reads RenderContext JSON from stdin.
This is the standard pattern for MDBook backends, allowing:
- Installation via
cargo install mdbook-htmx - Invocation via
[output.htmx]in book.toml - No changes required to MDBook itself
RenderContext Structure
MDBook passes a JSON representation of RenderContext via stdin:
{
"version": "0.4.40",
"root": "/path/to/book",
"book": {
"items": [
{
"Chapter": {
"name": "Introduction",
"content": "# Introduction\n\nWelcome to...",
"number": [1],
"sub_items": [],
"path": "intro.md",
"source_path": "intro.md",
"parent_names": []
}
},
"Separator",
{
"PartTitle": "Getting Started"
},
{
"Chapter": {
"name": "Installation",
"content": "# Installation\n\n...",
"number": [2],
"sub_items": [
{
"Chapter": {
"name": "Linux",
"content": "...",
"number": [2, 1],
"sub_items": [],
"path": "install/linux.md",
"source_path": "install/linux.md",
"parent_names": ["Installation"]
}
}
],
"path": "install/index.md",
"source_path": "install/README.md",
"parent_names": []
}
}
]
},
"config": {
"book": {
"title": "Example Book",
"authors": ["John Doe"],
"description": "An example book",
"src": "src",
"language": "en"
},
"build": {
"build-dir": "book",
"create-missing": true,
"use-default-preprocessors": true
},
"output": {
"htmx": {
"version": "2.0.4",
"boost": true,
"template-engine": "tera"
}
}
},
"destination": "/path/to/book/htmx"
}
Key Fields
| Field | Type | Description |
|---|---|---|
version |
String | MDBook version (for compatibility checks) |
root |
PathBuf | Book's root directory |
book.items |
Vec | Tree of chapters, separators, part titles |
config |
Config | Full book.toml configuration |
destination |
PathBuf | Output directory for this renderer |
BookItem Variants
enum BookItem {
Chapter(Chapter),
Separator,
PartTitle(String),
}
Chapter Structure
| Field | Type | Description |
|---|---|---|
name |
String | Chapter title |
content |
String | Raw markdown content |
number |
Option<Vec> | Section number (e.g., [1, 2] = "1.2.") |
sub_items |
Vec | Nested chapters |
path |
Option | Logical path (README → index.md) |
source_path |
Option | Actual file path |
parent_names |
Vec | Ancestor chapter names |
Implementation
Entry Point
use std::io;
use mdbook_renderer::RenderContext;
fn main() -> anyhow::Result<()> {
// Initialize logging
env_logger::init();
// Check for "supports" subcommand
if let Some(arg) = std::env::args().nth(1) {
if arg == "supports" {
// We support all renderers
return Ok(());
}
}
// Read RenderContext from stdin
let ctx = RenderContext::from_json(&mut io::stdin())?;
// Validate MDBook version
check_version(&ctx.version)?;
// Load our configuration
let config: HtmxConfig = ctx.config
.get_deserialized("output.htmx")?
.unwrap_or_default();
// Render the book
let renderer = HtmxRenderer::new(config);
renderer.render(&ctx)?;
Ok(())
}
Version Compatibility
fn check_version(version: &str) -> anyhow::Result<()> {
let semver: semver::Version = version.parse()?;
// Require MDBook 0.4.21+ for RenderContext stability
let minimum = semver::Version::new(0, 4, 21);
if semver < minimum {
anyhow::bail!(
"mdbook-htmx requires MDBook {} or later, found {}",
minimum, version
);
}
Ok(())
}
Accessing Custom Configuration
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmxConfig {
pub version: String,
pub boost: bool,
pub swap_strategy: String,
pub target: String,
pub push_url: bool,
pub template_engine: String,
pub theme: Option<String>,
pub output_mode: OutputMode,
pub navigation: NavigationConfig,
pub authz: AuthzConfig,
pub authn: AuthnConfig,
pub search: SearchConfig,
pub assets: AssetsConfig,
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum OutputMode {
Full,
Fragments,
#[default]
Both,
}
Rendering Pipeline
- Parse RenderContext - Deserialize JSON from stdin
- Load Configuration - Extract
[output.htmx]settings - Initialize Tera - Load templates from theme or defaults
- Register Custom Filters - Add
asset(),url(), etc. - Iterate Chapters - Walk
book.itemsdepth-first - Extract Frontmatter - Parse YAML from chapter content
- Render HTML - Apply Tera templates to each chapter
- Generate Manifest - Build manifest.json from collected metadata
- Build Search Index - Generate search-index.json
- Copy Assets - Handle CSS, JS, images
impl Renderer for HtmxRenderer {
fn name(&self) -> &str {
"htmx"
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
// Ensure destination exists
fs::create_dir_all(&ctx.destination)?;
// Initialize template engine
let tera = self.init_tera(ctx)?;
// Collect page metadata
let mut pages = Vec::new();
// Render each chapter
for item in ctx.book.iter() {
if let BookItem::Chapter(ch) = item {
if ch.is_draft_chapter() {
continue;
}
let page = self.render_chapter(ctx, &tera, ch)?;
pages.push(page);
}
}
// Generate manifest
self.write_manifest(ctx, &pages)?;
// Generate search index
if self.config.search.enabled {
self.write_search_index(ctx, &pages)?;
}
// Copy assets
self.copy_assets(ctx)?;
Ok(())
}
}
Custom Tera Filters
Register filters for template use:
fn init_tera(&self, ctx: &RenderContext) -> Result<Tera> {
let mut tera = Tera::default();
// Load templates from theme or defaults
let templates_dir = self.config.theme
.as_ref()
.map(|t| ctx.root.join(t))
.unwrap_or_else(|| self.default_templates());
tera.add_template_files(/* ... */)?;
// Register custom filters
let destination = ctx.destination.clone();
tera.register_function("asset", make_asset_fn(destination));
// Content filters
tera.register_filter("markdown", markdown_filter);
tera.register_filter("toc", toc_filter);
Ok(tera)
}
fn make_asset_fn(destination: PathBuf) -> impl Function {
Box::new(move |args: &HashMap<String, Value>| -> tera::Result<Value> {
match args.get("path") {
Some(val) => {
let path: String = from_value(val.clone())?;
// In production, would add content hash
Ok(to_value(format!("/assets/{}", path))?)
}
None => Err("asset() requires 'path' argument".into()),
}
})
}
Error Handling
Fail with clear, actionable errors:
// Bad: process::exit(1) with no message
// Good: anyhow with context
fn render_chapter(&self, ctx: &RenderContext, tera: &Tera, ch: &Chapter)
-> Result<PageMeta>
{
let path = ch.path.as_ref()
.ok_or_else(|| anyhow::anyhow!(
"Chapter '{}' has no path (draft chapter?)", ch.name
))?;
let frontmatter = extract_frontmatter(&ch.content)
.with_context(|| format!(
"Failed to parse frontmatter in '{}'", path.display()
))?;
let html = tera.render("page.html", &context)
.with_context(|| format!(
"Failed to render template for '{}'", path.display()
))?;
Ok(PageMeta { /* ... */ })
}
Consequences
Positive
- Standard MDBook integration pattern
- Receives full book structure including preprocessor output
- Access to complete configuration
- Works with existing MDBook tooling (
mdbook watch, etc.)
Negative
- No incremental builds (MDBook limitation)
- Must re-render entire book on any change
- Large books may have slow build times
Mitigation
- Cache rendered fragments by content hash
- Parallelize chapter rendering with Rayon
- Consider implementing incremental rendering in future MDBook versions
Alternatives Considered
Library Integration
Implement as library linked into MDBook.
Rejected because:
- Requires MDBook code changes
- Harder to distribute
- No standard pattern
Preprocessor Instead of Backend
Implement as a preprocessor that modifies content.
Rejected because:
- Preprocessors can't control output structure
- Can't generate manifest.json
- Wrong abstraction level
References
- Lingua principale
- Rust
- Stelle
- 0
- Fork
- 1
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di aRustyDev/mdbook-htmx
-
feat(ci): Implement cross-repo-validate compliance for docs syncForse di nuovo libera @aRustyDev l’ha presa 283 giorni fa e non c’è nessuna pull request aperta. Apertaci enhancement
aRustyDev/mdbook-htmx#58 · 1 commento · 1 assegnatario ·
-
docs(htmx): Explain how HTMX worksForse di nuovo libera @aRustyDev l’ha presa 285 giorni fa e non c’è nessuna pull request aperta. Aperta
aRustyDev/mdbook-htmx#48 · 3 commenti · 1 assegnatario ·
-
docs(mdbook-htmx): Parent Issue for documentationForse di nuovo libera @aRustyDev l’ha presa 285 giorni fa e non c’è nessuna pull request aperta. Apertadocumentation
aRustyDev/mdbook-htmx#47 · 1 assegnatario ·
-
docs(examples): Secure Self-Hosted Meilisearch via CloudflareForse di nuovo libera @aRustyDev l’ha presa 285 giorni fa e non c’è nessuna pull request aperta. Apertadocumentation
aRustyDev/mdbook-htmx#46 · 1 assegnatario ·
-
docs(examples): Kubernetes with MeilisearchForse di nuovo libera @aRustyDev l’ha presa 285 giorni fa e non c’è nessuna pull request aperta. Apertadocumentation
aRustyDev/mdbook-htmx#45 · 1 assegnatario ·
Tutte le issue di aRustyDev/mdbook-htmx
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
[Bug] ComponentType classifies 0x8A8A-0xEAEA as GREASE; mls-extensions §7.5 makes them private useAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
I maintainer di solito rispondono entro 1 giorno
-
The book has a Nix install section that chelis.ch lacks, and it names two of the three systemsApertaarea:ecosystem documentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Chelis-Lang/chelis#3866 ·
I maintainer di solito rispondono entro 1 giorno
-
[安卓Feature]: 数字键盘左侧符号区域大小优化Apertaenhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
metasequoiaime/msime#8620 ·
I maintainer di solito rispondono entro 1 giorno
-
agent-detection macos p2 triaged
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
I maintainer di solito rispondono entro 1 giorno