Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

docs(mdbook): build process

Aperta
#31 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

@aRustyDev ci sta già lavorando.

Dal 5/1/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

documentation

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

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di aRustyDev/mdbook-htmx

Tutte le issue di aRustyDev/mdbook-htmx

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.