Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

docs(mdbook): build process

Đang mở
#31 0 bình luận 0 reaction 1 người được giao Xem trên GitHub

@aRustyDev đang làm issue này rồi.

Từ ngày 5/1/2026.

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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

Ngôn ngữ chính
Rust
Star
0
Fork
1
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của aRustyDev/mdbook-htmx

Tất cả issue của aRustyDev/mdbook-htmx

Issue tương tự

Thêm issue về Rust

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.