docs(mdbook): build process
@aRustyDev ci sta già lavorando.
Dal 5/1/2026.
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
Build Process
This document provides a step-by-step walkthrough of the mdbook-htmx build process, from input to output.
Overview
┌─────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────┐
│ MDBook │────▶│ mdbook-htmx │────▶│ Output Files │
│ (preprocessed) │ │ │ │ │
└─────────────────┘ └─────────────────────────────────┘ └─────────────────┘
│ │
▼ ▼
RenderContext book/htmx/
(JSON stdin) ├── pages/
├── fragments/
├── assets/
└── manifest.json
Build Phases
The build process consists of 7 phases:
| Phase | Name | Description |
|---|---|---|
| 1 | Input Parsing | Parse RenderContext from stdin |
| 2 | Configuration | Load and validate config |
| 3 | Template Init | Initialize template engine |
| 4 | Content Rendering | Render each chapter |
| 5 | Output Generation | Write pages and fragments |
| 6 | Asset Processing | Hash and copy assets |
| 7 | Index Generation | Build manifest and search index |
Phase 1: Input Parsing
MDBook invokes the backend as a subprocess, passing RenderContext as JSON via stdin.
fn main() -> Result<()> {
// Read RenderContext from stdin
let ctx: RenderContext = serde_json::from_reader(io::stdin())?;
// Process the book
render(&ctx)?;
Ok(())
}
The RenderContext contains:
{
"version": "0.4.40",
"root": "/path/to/book",
"book": {
"sections": [...],
"config": {...}
},
"config": {
"book": {...},
"output": {
"htmx": {...}
}
},
"destination": "/path/to/book/book/htmx"
}
Phase 2: Configuration
Load configuration from [output.htmx] section with validation.
#[derive(Debug, Deserialize)]
pub struct HtmxConfig {
pub version: Option<String>,
#[serde(default = "default_htmx_version")]
pub htmx_version: String,
#[serde(default = "default_true")]
pub boost: bool,
#[serde(default)]
pub swap_strategy: SwapStrategy,
#[serde(default = "default_target")]
pub target: String,
#[serde(default = "default_true")]
pub push_url: bool,
#[serde(default)]
pub template_engine: TemplateEngine,
pub theme: Option<PathBuf>,
#[serde(default)]
pub output_mode: OutputMode,
#[serde(default)]
pub navigation: NavigationConfig,
#[serde(default)]
pub authn: AuthnConfig,
#[serde(default)]
pub authz: AuthzConfig,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)]
pub assets: AssetsConfig,
#[serde(default)]
pub scopes: ScopesConfig,
}
fn load_config(ctx: &RenderContext) -> Result<HtmxConfig> {
let config: HtmxConfig = ctx
.config
.get_deserialized_opt("output.htmx")?
.unwrap_or_default();
validate_config(&config)?;
Ok(config)
}
Configuration Validation
fn validate_config(config: &HtmxConfig) -> Result<()> {
// Validate version format
if let Some(version) = &config.version {
if !VERSION_PATTERN.is_match(version) {
return Err(ConfigError::InvalidValue {
field: "version".to_string(),
expected: "semver format (e.g., '1.0')".to_string(),
actual: version.clone(),
}.into());
}
}
// Validate scope configuration
if config.scopes.enabled {
if config.scopes.available.is_empty() {
return Err(ConfigError::MissingField {
field: "scopes.available".to_string(),
}.into());
}
}
Ok(())
}
Phase 3: Template Initialization
Initialize the template engine with default and custom templates.
fn init_tera(config: &HtmxConfig, root: &Path) -> Result<Tera> {
// Load bundled templates
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("layout.html", include_str!("../templates/layout.html")),
("page.html", include_str!("../templates/page.html")),
("sidebar.html", include_str!("../templates/sidebar.html")),
("nav.html", include_str!("../templates/nav.html")),
// ... more templates
])?;
// Override with custom theme if specified
if let Some(theme_path) = &config.theme {
let theme_dir = root.join(theme_path);
if theme_dir.exists() {
let pattern = format!("{}/**/*.html", theme_dir.display());
tera.add_template_files(glob(&pattern)?)?;
}
}
// Register custom functions
tera.register_function("asset", make_asset_fn(&config));
tera.register_function("integrity", make_integrity_fn(&config));
Ok(tera)
}
Phase 4: Content Rendering
Iterate through chapters and render each one.
fn render_chapters(
ctx: &RenderContext,
config: &HtmxConfig,
tera: &Tera,
) -> Result<Vec<RenderedChapter>> {
let mut rendered = Vec::new();
for item in ctx.book.iter() {
match item {
BookItem::Chapter(chapter) => {
// Skip draft chapters
if chapter.path.is_none() {
debug!(name = %chapter.name, "Skipping draft chapter");
continue;
}
let result = render_chapter(chapter, config, tera)?;
rendered.push(result);
}
BookItem::Separator => {
// Handle separators in navigation
}
BookItem::PartTitle(title) => {
// Handle part titles
}
}
}
Ok(rendered)
}
fn render_chapter(
chapter: &Chapter,
config: &HtmxConfig,
tera: &Tera,
) -> Result<RenderedChapter> {
// Extract frontmatter
let (frontmatter, content) = extract_frontmatter(&chapter.content)?;
// Convert markdown to HTML
let html_content = markdown_to_html(&content);
// Build template context
let mut context = Context::new();
context.insert("page", &PageContext {
path: chapter.path.as_ref().unwrap(),
title: &chapter.name,
content: &html_content,
});
context.insert("config", config);
context.insert("frontmatter", &frontmatter);
// Render full page
let full_page = tera.render("page.html", &context)?;
// Render fragment (content only)
let fragment = tera.render("fragment.html", &context)?;
Ok(RenderedChapter {
path: chapter.path.clone().unwrap(),
title: chapter.name.clone(),
full_page,
fragment,
frontmatter,
})
}
Frontmatter Extraction
fn extract_frontmatter(content: &str) -> Result<(Frontmatter, String)> {
if !content.starts_with("---") {
return Ok((Frontmatter::default(), content.to_string()));
}
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() < 3 {
return Err(FrontmatterError::Unclosed.into());
}
let yaml = parts[1].trim();
let markdown = parts[2].trim();
let frontmatter: Frontmatter = serde_yaml::from_str(yaml)?;
validate_frontmatter(&frontmatter)?;
Ok((frontmatter, markdown.to_string()))
}
Phase 5: Output Generation
Write rendered pages and fragments to the destination directory.
fn write_output(
rendered: &[RenderedChapter],
config: &HtmxConfig,
destination: &Path,
) -> Result<()> {
// Create output directories
let pages_dir = destination.join("pages");
let fragments_dir = destination.join("fragments");
fs::create_dir_all(&pages_dir)?;
fs::create_dir_all(&fragments_dir)?;
for chapter in rendered {
// Convert chapter path to output path
let page_path = pages_dir.join(&chapter.path).with_extension("html");
let fragment_path = fragments_dir.join(&chapter.path).with_extension("html");
// Ensure parent directories exist
if let Some(parent) = page_path.parent() {
fs::create_dir_all(parent)?;
}
if let Some(parent) = fragment_path.parent() {
fs::create_dir_all(parent)?;
}
// Write files based on output mode
match config.output_mode {
OutputMode::Full => {
fs::write(&page_path, &chapter.full_page)?;
}
OutputMode::Fragments => {
fs::write(&fragment_path, &chapter.fragment)?;
}
OutputMode::Both => {
fs::write(&page_path, &chapter.full_page)?;
fs::write(&fragment_path, &chapter.fragment)?;
}
}
}
Ok(())
}
Phase 6: Asset Processing
Hash assets for cache busting and copy to output.
fn process_assets(
config: &HtmxConfig,
root: &Path,
destination: &Path,
) -> Result<AssetManifest> {
let assets_src = root.join("assets");
let assets_dest = destination.join("assets");
let mut hasher = AssetHasher::new();
for entry in WalkDir::new(&assets_src) {
let entry = entry?;
if !entry.file_type().is_file() {
continue;
}
let rel_path = entry.path().strip_prefix(&assets_src)?;
if config.assets.hash_files && should_hash(entry.path()) {
// Hash and copy with new name
let hashed_name = hasher.hash_file(entry.path())?;
let dest_path = assets_dest
.join(rel_path.parent().unwrap_or(Path::new("")))
.join(&hashed_name);
fs::create_dir_all(dest_path.parent().unwrap())?;
fs::copy(entry.path(), &dest_path)?;
} else {
// Copy as-is
let dest_path = assets_dest.join(rel_path);
fs::create_dir_all(dest_path.parent().unwrap())?;
fs::copy(entry.path(), &dest_path)?;
}
}
// Copy HTMX library
if config.assets.copy_htmx {
let htmx_content = include_bytes!("../vendor/htmx.min.js");
let htmx_path = assets_dest.join("js/htmx.min.js");
fs::create_dir_all(htmx_path.parent().unwrap())?;
fs::write(&htmx_path, htmx_content)?;
}
// Write asset manifest
hasher.write_manifest(&destination.join("asset-manifest.json"))?;
Ok(hasher.to_manifest())
}
Phase 7: Index Generation
Generate manifest.json and search indexes.
fn generate_indexes(
rendered: &[RenderedChapter],
config: &HtmxConfig,
ctx: &RenderContext,
destination: &Path,
) -> Result<()> {
// Generate main manifest
let manifest = build_manifest(rendered, config, ctx)?;
let manifest_json = serde_json::to_string_pretty(&manifest)?;
fs::write(destination.join("manifest.json"), &manifest_json)?;
// Generate scope-filtered manifests
if config.scopes.enabled {
for scope in &config.scopes.available {
let filtered = filter_manifest_by_scope(&manifest, scope);
let path = destination.join(format!("manifest.{}.json", scope));
fs::write(&path, serde_json::to_string_pretty(&filtered)?)?;
}
}
// Generate search index
if config.search.enabled {
let search_index = build_search_index(rendered, config)?;
let index_json = serde_json::to_string_pretty(&search_index)?;
fs::write(destination.join("search-index.json"), &index_json)?;
// Generate scope-filtered search indexes
if config.search.scope_indexes {
for scope in &config.scopes.available {
let filtered = filter_search_index_by_scope(&search_index, scope);
let path = destination.join(format!("search-index.{}.json", scope));
fs::write(&path, serde_json::to_string_pretty(&filtered)?)?;
}
}
}
Ok(())
}
Manifest Building
fn build_manifest(
rendered: &[RenderedChapter],
config: &HtmxConfig,
ctx: &RenderContext,
) -> Result<Manifest> {
let pages: Vec<ManifestPage> = rendered.iter().map(|chapter| {
ManifestPage {
id: chapter.path.file_stem().unwrap().to_string(),
path: format!("/docs/{}", chapter.path.with_extension("").display()),
title: chapter.title.clone(),
file: format!("pages/{}.html", chapter.path.with_extension("").display()),
fragment: format!("fragments/{}.html", chapter.path.with_extension("").display()),
auth: chapter.frontmatter.auth.clone(),
htmx: chapter.frontmatter.htmx.clone(),
search: chapter.frontmatter.search.clone(),
scopes: chapter.frontmatter.scopes.clone(),
}
}).collect();
Ok(Manifest {
schema: MANIFEST_SCHEMA_URL.to_string(),
version: MANIFEST_VERSION.to_string(),
generated: Utc::now().to_rfc3339(),
book: BookMeta::from(ctx),
pages,
navigation: build_navigation(ctx)?,
authn: Some(config.authn.clone()),
scopes: Some(config.scopes.clone()),
})
}
Build Output
After a successful build:
book/htmx/
├── pages/
│ ├── index.html
│ ├── getting-started.html
│ └── api/
│ └── reference.html
├── fragments/
│ ├── index.html
│ ├── getting-started.html
│ └── api/
│ └── reference.html
├── assets/
│ ├── css/
│ │ └── docs.a1b2c3d4.css
│ └── js/
│ └── htmx.min.js
├── templates/
│ ├── layout.html
│ └── partials/
│ └── *.html
├── manifest.json
├── manifest.developers.json
├── manifest.managers.json
├── search-index.json
├── search-index.developers.json
└── asset-manifest.json
Error Handling
Build errors are reported with context:
error: auth.access is 'roles' but no roles specified
--> src/admin/config.md:3
|
1 | ---
2 | auth:
3 | access: roles
| ^^^^^ missing 'roles' field
4 | ---
|
help: Add 'roles: [role1, role2]' to the auth section
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Build errors |
| 2 | Configuration errors |
| 3 | I/O errors |
| 4 | Internal errors |
CLI Options
# Default build
mdbook build
# Verbose output
mdbook build -v
# Continue despite errors
mdbook build --keep-going
# Warnings as errors
mdbook build --deny warnings
Related Documentation
- 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 284 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 286 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 286 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 286 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 286 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
-
area:desktop priority:p2 type:bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 88/100
fritzprix/libr-agent#2165 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 69/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
area:ecosystem bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
Chelis-Lang/chelis#3942 ·
I maintainer di solito rispondono entro 1 giorno