Welcome to Chapter 19! In this chapter, we’ll put our Rust-based Static Site Generator (SSG) to the test by building a full-fledged developer documentation site. This is a common and practical application for an SSG, requiring structured content, robust navigation, and efficient search capabilities. By completing this example, you’ll gain a deeper understanding of how to leverage all the features we’ve built, from content parsing and component rendering to routing and search indexing, to create a production-ready content platform.

Building a documentation site allows us to demonstrate the power of our SSG’s content organization, templating flexibility, and the integration of client-side interactivity via partial hydration. We’ll focus on designing a logical content hierarchy, generating dynamic sidebar navigation, and ensuring seamless search functionality. This step is crucial for validating our SSG’s capabilities in a real-world scenario and showcasing its extensibility for various content types.

Before we begin, ensure you have a working version of the SSG developed in previous chapters, especially those covering content parsing, Tera templating, component rendering, routing, and Pagefind integration. We’ll be building upon these foundations to construct our documentation portal. By the end of this chapter, you’ll have a fully functional, static documentation site ready for deployment, demonstrating the efficiency and performance benefits of our Rust SSG.

Planning & Design

A well-structured documentation site is critical for user experience. Our design will prioritize clear content organization, intuitive navigation, and efficient search.

Content Structure

We’ll adopt a hierarchical content structure, similar to many popular documentation platforms. This allows for logical grouping of topics and versioning.

  • content/docs/
    • _index.md (Main documentation landing page)
    • v1.0/
      • _index.md (Version 1.0 landing)
      • getting-started.md
      • installation.md
      • api-reference.md
    • v2.0/
      • _index.md (Version 2.0 landing)
      • whats-new.md
      • getting-started.md
      • advanced/
        • _index.md (Advanced topics landing)
        • concepts.md
        • performance.md
    • faq.md

Each Markdown file will include frontmatter to define its title, description, and crucially, a weight for ordering within its parent, and an optional layout to specify the docs.html template.

Templating and Navigation

We’ll create a dedicated docs.html Tera template. This template will feature:

  • A header with the site title and potentially a version selector.
  • A sidebar for hierarchical navigation, dynamically generated from the content structure.
  • A main content area where the parsed Markdown will be rendered.
  • A footer.
  • Integration points for Pagefind search.

The core challenge for navigation is to traverse the processed Site content, identify all documentation pages, and build a nested data structure that Tera can easily iterate over to render the sidebar. This structure will need to respect the parent and weight fields from the frontmatter.

Component Architecture for Documentation Site

flowchart TD subgraph Content_Input["Content Input (Markdown Files)"] A[docs/index.md] B[docs/v1.0/getting-started.md] C[docs/v2.0/advanced/concepts.md] end subgraph SSG_Core["SSG Core Processing Pipeline"] CI[Content Ingest] --> PF[Parse Frontmatter] PF --> MD[Parse Markdown & Components] MD --> D[Generate AST & Component Hydration Data] D --> CD[Collect Docs Metadata] end subgraph Navigation_System["Navigation System"] CD --> NT[Build Navigation Tree] NT --> NL[Nested Link Data for Tera] end subgraph Template_Rendering["Template Rendering (Tera)"] TR_START(Start Render) --> TR_CTX[Prepare Tera Context] TR_CTX -->|Includes Navigation Data| TR_TEMP[Apply docs.html Template] TR_TEMP --> TR_HTML[Output Static HTML] end subgraph Search_Integration["Search Integration"] TR_HTML --> PI[Pagefind Indexing] end subgraph Output_Deployment["Output & Deployment"] O[Generated Static Site] --> CDN[Deploy to CDN] end A --> CI B --> CI C --> CI NL --> TR_CTX PI --> O

The diagram illustrates how Markdown files are ingested, parsed, and their frontmatter is used to collect metadata. This metadata is then used by the “Navigation System” to build a hierarchical tree, which is passed to the Tera templating engine. The docs.html template uses this data to render the sidebar, while the main content is rendered from the processed Markdown. Finally, Pagefind indexes the generated HTML for client-side search.

Step-by-Step Implementation

We will start by creating a new docs-site directory to house our example documentation project.

a) Setup/Configuration

First, let’s create the project structure for our documentation site.

  1. Create the docs-site directory:

    mkdir docs-site
    cd docs-site
    mkdir content templates static
    
  2. Copy the SSG CLI binary: Assuming your SSG’s compiled binary is named ssg_cli and is located in ../target/release/, copy it into the docs-site directory for convenience.

    cp ../target/release/ssg_cli .
    

    Explanation: We’re creating a self-contained example project. In a real scenario, you’d have ssg_cli installed globally or use cargo run -- build from the SSG’s root directory, specifying the docs-site as the project root.

  3. Create config.toml for the docs site:

    # docs-site/config.toml
    base_url = "http://localhost:8000"
    title = "My Awesome Docs"
    description = "Documentation for My Awesome Project"
    default_template = "docs.html" # Default for all content
    content_dir = "content"
    templates_dir = "templates"
    static_dir = "static"
    output_dir = "public"
    pagefind_enabled = true
    

    Explanation: This config.toml sets up basic site metadata and points to our documentation-specific directories. We’re setting default_template to docs.html, meaning all content will by default use this layout.

  4. Create initial content files: Let’s start with a few basic Markdown files to establish the hierarchy.

    # docs-site/content/docs/_index.md
    +++
    title = "Documentation Home"
    description = "Welcome to the documentation for My Awesome Project."
    weight = 1
    +++
    
    This is the main landing page for our documentation. Use the sidebar to navigate through topics.
    
    # docs-site/content/docs/v1.0/_index.md
    +++
    title = "Version 1.0"
    description = "Documentation for version 1.0 of the project."
    weight = 10
    +++
    
    Explore the features and guides for version 1.0.
    
    # docs-site/content/docs/v1.0/getting-started.md
    +++
    title = "Getting Started"
    description = "How to get started with My Awesome Project v1.0."
    weight = 20
    +++
    
    ## Installation
    
    To install, run:
    
    {{ CodeBlock(lang="bash", title="Install via Cargo") }}
    cargo add my-awesome-project
    {{ /CodeBlock }}
    
    ## First Steps
    
    Initialize your project with:
    
    ```bash
    my-awesome-project init
    
    
    ```markdown
    # docs-site/content/docs/v2.0/_index.md
    +++
    title = "Version 2.0 (Latest)"
    description = "Documentation for the latest version 2.0 of the project."
    weight = 10
    +++
    
    Welcome to the latest version! Discover new features and improvements.
    
    # docs-site/content/docs/v2.0/whats-new.md
    +++
    title = "What's New in v2.0"
    description = "A summary of changes and new features in version 2.0."
    weight = 20
    +++
    
    Version 2.0 brings significant performance improvements and new API endpoints.
    

    Explanation: We’re setting up a docs section with two versions. Notice the weight field, which will be crucial for ordering the navigation. We’re also demonstrating the use of a custom component CodeBlock within the Markdown.

b) Core Implementation

The main task here is to create the docs.html template and enhance our SSG to generate the navigation data for it.

  1. Create templates/docs.html: This will be our primary layout for all documentation pages.

    <!-- docs-site/templates/docs.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{{ page.title }} - {{ config.title }}</title>
        <meta name="description" content="{{ page.description | default(value=config.description) }}">
        <link rel="stylesheet" href="/static/css/style.css">
        <script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
        <style>
            body { font-family: sans-serif; margin: 0; display: flex; min-height: 100vh; }
            .sidebar { width: 280px; background-color: #f8f9fa; padding: 20px; border-right: 1px solid #e9ecef; overflow-y: auto; }
            .sidebar h2 { margin-top: 0; font-size: 1.2em; color: #343a40; }
            .sidebar ul { list-style: none; padding: 0; }
            .sidebar ul li { margin-bottom: 5px; }
            .sidebar ul li a { text-decoration: none; color: #007bff; }
            .sidebar ul li a:hover { text-decoration: underline; }
            .sidebar ul ul { margin-left: 15px; border-left: 1px solid #ced4da; padding-left: 10px; }
            .main-content { flex-grow: 1; padding: 40px; max-width: 900px; margin: 0 auto; }
            .main-content h1 { color: #343a40; }
            .main-content code { background-color: #e9ecef; padding: 2px 4px; border-radius: 3px; font-family: monospace; }
            .main-content pre { background-color: #f1f3f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
            .pagefind-ui { margin-bottom: 20px; }
            /* Simple CodeBlock styling for demonstration */
            .component-code-block {
                background-color: #282c34;
                color: #abb2bf;
                padding: 15px;
                border-radius: 5px;
                overflow-x: auto;
                margin-top: 1em;
                margin-bottom: 1em;
            }
            .component-code-block pre {
                margin: 0;
            }
            .component-code-block .title {
                color: #61afef;
                font-weight: bold;
                margin-bottom: 10px;
                display: block;
            }
        </style>
    </head>
    <body>
        <aside class="sidebar">
            <div class="pagefind-ui" id="search"></div>
            <script>
                window.addEventListener('DOMContentLoaded', () => {
                    new PagefindUI({ element: "#search", showImages: false, showEmptyFilters: false });
                });
            </script>
    
            <h2>{{ config.title }}</h2>
            <nav>
                {{ self::docs_nav() }}
            </nav>
        </aside>
        <main class="main-content">
            {% if page.title %}
                <h1>{{ page.title }}</h1>
            {% endif %}
            {{ content | safe }}
        </main>
    </body>
    </html>
    
    {% macro docs_nav() %}
        <ul>
            {% for item in nav.items %}
                <li>
                    <a href="{{ item.url }}">{{ item.title }}</a>
                    {% if item.children %}
                        <ul>
                            {% for child in item.children %}
                                <li><a href="{{ child.url }}">{{ child.title }}</a></li>
                            {% endfor %}
                        </ul>
                    {% endif %}
                </li>
            {% endfor %}
        </ul>
    {% endmacro %}
    

    Explanation:

    • This Tera template defines the basic HTML structure for our documentation.
    • It includes a sidebar and main-content area.
    • {{ page.title }} and {{ content | safe }} render the page-specific title and the HTML generated from Markdown.
    • {{ config.title }} pulls the site title from config.toml.
    • Crucially, {{ self::docs_nav() }} calls a macro to render the navigation. This macro expects a nav variable in the Tera context, which will contain our hierarchical navigation data.
    • Basic CSS is embedded for quick styling. In a real project, this would be in static/css/style.css.
    • Pagefind UI is initialized in the sidebar for search functionality.
    • Basic styling for the CodeBlock component is also included.
  2. Add static/css/style.css (minimal):

    /* docs-site/static/css/style.css */
    /* This file is referenced by docs.html. Add more styles here as needed. */
    body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        line-height: 1.6;
        color: #333;
    }
    a {
        color: #007bff;
        text-decoration: none;
    }
    a:hover {
        text-decoration: underline;
    }
    /* ... more styles for components, typography, etc. ... */
    
  3. Enhance SSG for Navigation Data Generation: This is the most complex part. We need to collect all pages, filter documentation pages, and build a hierarchical structure. We’ll add a new function to our SSG’s BuildContext or Site struct to achieve this.

    Let’s assume we have a Site struct that holds all Page objects. Each Page has FrontMatter and relative_path.

    First, define a struct to represent a navigation item.

    // src/ssg/models.rs (or a new nav.rs module)
    // Add this struct if not already present
    use serde::{Deserialize, Serialize};
    use std::collections::BTreeMap;
    
    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct NavItem {
        pub title: String,
        pub url: String,
        pub weight: i64,
        pub children: Vec<NavItem>,
    }
    
    impl NavItem {
        pub fn new(title: String, url: String, weight: i64) -> Self {
            Self {
                title,
                url,
                weight,
                children: Vec::new(),
            }
        }
    
        pub fn add_child(&mut self, child: NavItem) {
            self.children.push(child);
            self.children.sort_by_key(|c| c.weight); // Keep children sorted
        }
    }
    

    Explanation: NavItem will represent a single link in our navigation. It has a title, url, weight for ordering, and children to support nested navigation.

    Now, let’s modify the Site struct and add a method to build this navigation tree. This method will typically be called during the build process before individual pages are rendered.

    // src/ssg/site.rs (or build_context.rs)
    use std::path::{Path, PathBuf};
    use std::collections::BTreeMap; // For stable ordering
    use crate::ssg::content::{Page, FrontMatter}; // Assuming Page and FrontMatter are here
    // ... other imports
    
    // Assuming these structs exist from previous chapters
    // pub struct FrontMatter { ... pub weight: Option<i64>, pub layout: Option<String>, ... }
    // pub struct Page { ... pub front_matter: FrontMatter, pub relative_path: PathBuf, pub permalink: String, ... }
    
    pub struct Site {
        pub config: Config,
        pub pages: BTreeMap<PathBuf, Page>, // Map from original path to Page object
        // ... other fields
    }
    
    impl Site {
        // ... existing methods like new, load_content, etc.
    
        /// Builds a hierarchical navigation tree for documentation pages.
        /// Returns a Vec of top-level NavItems.
        pub fn build_docs_navigation(&self) -> Vec<NavItem> {
            let mut nav_map: BTreeMap<PathBuf, NavItem> = BTreeMap::new();
            let mut parents: BTreeMap<PathBuf, PathBuf> = BTreeMap::new();
    
            // First pass: Create all NavItems and identify their parents
            for (original_path, page) in &self.pages {
                // Only consider pages under 'content/docs/' for documentation navigation
                if !original_path.starts_with(&self.config.content_dir.join("docs")) {
                    continue;
                }
    
                let title = page.front_matter.title.clone().unwrap_or_else(|| {
                    original_path.file_stem().unwrap().to_string_lossy().into_owned()
                });
                let url = page.permalink.clone();
                let weight = page.front_matter.weight.unwrap_or(0);
    
                let nav_item = NavItem::new(title, url, weight);
                nav_map.insert(original_path.clone(), nav_item);
    
                // Determine parent path for hierarchical structure
                if let Some(parent_path) = original_path.parent() {
                    // Special handling for _index.md to ensure its parent is its own directory
                    let current_dir = if original_path.file_name().map_or(false, |name| name == "_index.md") {
                        original_path.parent().map(|p| p.to_path_buf())
                    } else {
                        original_path.parent().map(|p| p.to_path_buf())
                    };
    
                    if let Some(dir) = current_dir {
                        if dir != self.config.content_dir.join("docs") && dir != self.config.content_dir {
                           parents.insert(original_path.clone(), dir.clone());
                        }
                    }
                }
            }
    
            // Second pass: Assemble the tree
            let mut root_items: Vec<NavItem> = Vec::new();
            let mut processed_children: BTreeMap<PathBuf, Vec<NavItem>> = BTreeMap::new();
    
            // Collect all items that have a parent
            for (child_path, parent_path) in &parents {
                if let Some(child_item) = nav_map.remove(child_path) { // Remove to avoid adding to root later
                    processed_children.entry(parent_path.clone())
                                      .or_default()
                                      .push(child_item);
                }
            }
    
            // Attach processed children to their parents
            for (parent_path, children) in processed_children {
                if let Some(parent_item) = nav_map.get_mut(&parent_path) {
                    for child in children {
                        parent_item.add_child(child);
                    }
                } else {
                    // If parent_path isn't in nav_map (e.g., it's a directory without an _index.md),
                    // we might need to create a dummy NavItem for it or handle it as a top-level.
                    // For now, these children might be orphaned if their parent has no _index.md.
                    // A robust solution would create dummy NavItems for directories.
                    // For simplicity, let's assume all parent directories have an _index.md.
                    warn!("Parent path {:?} not found in nav_map. Children might be orphaned.", parent_path);
                }
            }
    
            // Any remaining items in nav_map are root-level or _index.md files whose parent is 'docs'
            for (path, item) in nav_map {
                // Only add to root if its parent is the base 'docs' directory
                if path.parent().map_or(false, |p| p == self.config.content_dir.join("docs")) {
                    root_items.push(item);
                } else if path == self.config.content_dir.join("docs").join("_index.md") {
                    // Handle the main docs _index.md as a root item
                    root_items.push(item);
                }
            }
    
            root_items.sort_by_key(|item| item.weight);
            root_items
        }
    }
    

    Explanation:

    • build_docs_navigation iterates through all pages in self.pages.
    • It filters for pages within content/docs/.
    • It creates a NavItem for each relevant page, using its title, permalink, and weight.
    • It uses two BTreeMaps: nav_map to store all NavItems by their original path, and parents to track parent-child relationships.
    • The logic attempts to identify the correct parent directory for each page, handling _index.md files which represent the “root” of their directory.
    • In the second pass, it iterates through the parents map to attach children to their respective parent NavItems.
    • Finally, any remaining NavItems in nav_map that are top-level documentation pages (e.g., directly under content/docs/ or content/docs/_index.md) are added to root_items and sorted by weight.

    Self-Correction/Refinement: The navigation generation logic for complex directory structures can be tricky. The provided code gives a basic hierarchical structure. For truly robust navigation (e.g., handling directories without _index.md as implicit navigation nodes), you might need to:

    1. Pre-scan the content/docs directory to identify all actual directories.
    2. For each directory, if an _index.md exists, use its frontmatter. Otherwise, create a default NavItem for the directory itself.
    3. Then, attach pages and subdirectories as children. This approach ensures all parts of the hierarchy are navigable, even if not every directory has an _index.md. For this example, we’ll assume _index.md files define the directory’s presence in navigation.

    Now, we need to integrate this into our main build process.

    // src/ssg/mod.rs (or main.rs where the build logic resides)
    // ... other imports
    use crate::ssg::site::{Site, NavItem}; // Assuming NavItem is in site.rs for now, or its own module
    use tera::{Context, Tera};
    use std::fs;
    use std::path::{Path, PathBuf};
    
    pub fn build_site(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
        info!("Starting site build...");
    
        // 1. Load content
        let mut site = Site::new(config.clone());
        site.load_content()?; // This populates site.pages
    
        // 2. Initialize Tera
        let tera = Tera::new(&format!("{}/**/*.html", config.templates_dir))?;
    
        // 3. Build docs navigation
        let docs_navigation = site.build_docs_navigation();
        debug!("Generated documentation navigation: {:?}", docs_navigation);
    
        // 4. Create output directory
        let output_path = PathBuf::from(&config.output_dir);
        if output_path.exists() {
            fs::remove_dir_all(&output_path)?;
        }
        fs::create_dir_all(&output_path)?;
    
        // 5. Copy static files
        // ... (existing static file copy logic)
    
        // 6. Render pages
        for (original_path, page) in site.pages.iter() {
            info!("Rendering page: {}", page.relative_path.display());
            let output_file_path = output_path.join(&page.output_file_path);
            let output_dir = output_file_path.parent().ok_or("Invalid output path")?;
            fs::create_dir_all(output_dir)?;
    
            let mut context = Context::new();
            context.insert("config", &site.config);
            context.insert("page", &page);
            context.insert("content", &page.rendered_html);
            // Insert the generated navigation data into the Tera context
            context.insert("nav", &docs_navigation); // 'nav' matches the macro in docs.html
    
            // Determine which template to use
            let template_name = page.front_matter.layout.as_ref()
                                .unwrap_or(&config.default_template)
                                .to_string();
    
            let rendered = tera.render(&template_name, &context)?;
            fs::write(&output_file_path, rendered.as_bytes())?;
            info!("Rendered {} to {}", page.relative_path.display(), output_file_path.display());
        }
    
        // 7. Run Pagefind indexing
        if config.pagefind_enabled {
            crate::ssg::pagefind::run_pagefind_indexing(&output_path)?;
        }
    
        info!("Site build complete!");
        Ok(())
    }
    

    Explanation:

    • We call site.build_docs_navigation() after loading content and before rendering pages.
    • The result, docs_navigation, is then inserted into the Tera Context under the key "nav". This makes it accessible to our docs_nav() macro in docs.html.
    • The template_name is now dynamically selected from page.front_matter.layout or falls back to config.default_template (docs.html in this case).

c) Testing This Component

  1. Build the documentation site: Navigate to your docs-site directory in the terminal.

    ./ssg_cli build
    

    You should see log messages indicating pages are being processed and rendered.

  2. Verify generated files: Check the docs-site/public directory. You should find:

    • public/index.html (for content/docs/_index.md)
    • public/v1.0/index.html
    • public/v1.0/getting-started/index.html
    • public/v2.0/index.html
    • public/v2.0/whats-new/index.html
    • public/static/css/style.css
    • public/pagefind/ directory with search index files.
  3. Serve the site locally: You can use a simple static file server (like Python’s http.server or live-server npm package) to view the generated site.

    # If you have Python installed
    python3 -m http.server --directory public 8000
    

    Open your browser to http://localhost:8000.

  4. Expected Behavior:

    • You should see the “Documentation Home” page.
    • A sidebar should be present on the left, displaying “My Awesome Project” and the navigation links:
      • Documentation Home
      • Version 1.0
        • Getting Started
      • Version 2.0 (Latest)
        • What’s New in v2.0
    • Clicking on “Getting Started” should take you to that page, and you should see the CodeBlock component rendered correctly with syntax highlighting (if your component renderer supports it, or with the basic styling we added).
    • The Pagefind search bar should be visible and functional. Try searching for “installation” or “api”.

Production Considerations

  • Error Handling:

    • Missing weight or title: Our navigation generation gracefully defaults weight to 0 and title to the file stem. For production, you might want to enforce these fields for documentation via a schema validation for frontmatter (perhaps using serde_json::from_value and a custom schema).
    • Orphaned Navigation Items: The current navigation logic assumes directories have _index.md to be present in the navigation. If a directory content/docs/orphan/ exists without _index.md, its children might not appear. A more robust solution would infer navigation items for directories without _index.md.
    • Broken Permalinks: Ensure the permalink generation is robust and handles special characters or very deep nesting.
  • Performance Optimization:

    • Navigation Tree Generation: For sites with thousands of documentation pages, build_docs_navigation could become a bottleneck. Consider caching the generated NavItem tree if the content hasn’t changed. Our incremental build system from Chapter 18 could detect changes in content/docs and trigger a rebuild of only the navigation data.
    • Tera Template Caching: Tera already caches compiled templates.
    • Asset Optimization: Minify CSS/JS in the static directory. Our SSG could integrate with a tool like lightningcss or terser for this.
  • Security Considerations:

    • Content Sanitization: pulldown-cmark generally outputs safe HTML, but if you allow raw HTML or custom components that inject raw HTML, ensure proper sanitization to prevent XSS attacks. Our component rendering should always escape user input unless explicitly designed for raw HTML (e.g., a RawHtml component).
    • Pagefind: Pagefind is a client-side search, so it doesn’t pose direct server-side security risks, but ensure the content being indexed is appropriate.
  • Logging and Monitoring:

    • Log the time taken for navigation generation and overall build process.
    • Monitor build failures and page rendering errors, especially for complex documentation structures. Use tracing or log macros effectively.
  • Deployment:

    • CDN: Static documentation sites are ideal for deployment on Content Delivery Networks (CDNs) like Cloudflare Pages, Netlify, Vercel, or AWS S3/CloudFront. This provides excellent global performance and scalability.
    • CI/CD: Set up a CI/CD pipeline (e.g., GitHub Actions, GitLab CI) to automatically build and deploy the documentation site whenever changes are pushed to the main branch of your content repository. This ensures your documentation is always up-to-date.

Code Review Checkpoint

At this point, you should have:

  • Files created/modified in docs-site:
    • config.toml: Configures the documentation site.
    • content/docs/_index.md: Main docs landing page.
    • content/docs/v1.0/_index.md, content/docs/v1.0/getting-started.md: Example v1.0 content.
    • content/docs/v2.0/_index.md, content/docs/v2.0/whats-new.md: Example v2.0 content.
    • templates/docs.html: The main Tera template for documentation pages, including navigation rendering logic and Pagefind integration.
    • static/css/style.css: Minimal CSS for styling.
  • Files modified in your SSG (src/ssg/):
    • src/ssg/models.rs (or src/ssg/nav.rs): Added NavItem struct.
    • src/ssg/site.rs (or src/ssg/build_context.rs): Added build_docs_navigation method to Site (or BuildContext) to generate the hierarchical navigation data.
    • src/ssg/mod.rs (or src/main.rs): Modified the build_site function to call build_docs_navigation and pass the resulting Vec<NavItem> to the Tera context for rendering.

This checkpoint ensures that the SSG can now process a structured set of Markdown files, generate a dynamic navigation menu, and render them using a specific documentation layout, all while integrating client-side search.

Common Issues & Solutions

  1. Issue: Navigation links are empty or incorrect.

    • Cause: This usually indicates an issue with permalink generation or the build_docs_navigation logic.
    • Debugging:
      • Add debug! logs in build_docs_navigation to inspect the nav_map, parents, and root_items at each stage.
      • Print the page.permalink for each page during rendering.
      • Verify the url field of NavItem instances before they are passed to Tera.
      • Check for incorrect base_url in config.toml.
    • Solution: Carefully review the build_docs_navigation logic, especially how parent paths are determined and how _index.md files are handled. Ensure page.permalink is correctly generated for all content files.
  2. Issue: Custom components (e.g., CodeBlock) are not rendering or appear as raw text.

    • Cause: The component rendering pipeline might not be correctly integrated or the component definition is missing/malformed.
    • Debugging:
      • Check the SSG’s logs during Markdown processing for any errors related to component parsing or rendering.
      • Inspect the page.rendered_html variable before it’s passed to Tera. Does it contain the expected HTML output for the component, or the raw {{ CodeBlock(...) }} string?
      • Verify that content | safe is used in docs.html to prevent Tera from escaping the HTML generated by our component renderer.
    • Solution: Ensure your ContentProcessor (or equivalent) correctly identifies and replaces component syntax with rendered HTML. Confirm that the Tera context is receiving the processed HTML.
  3. Issue: Pagefind search is not working or says “No results”.

    • Cause: Pagefind indexing might not be running, or the pagefind-ui.js script is not correctly loaded.
    • Debugging:
      • Check the console output during the build process to confirm run_pagefind_indexing was called without errors.
      • Verify that the public/pagefind directory exists and contains the necessary index files.
      • Open your browser’s developer console on the live docs site and check the Network tab for pagefind-ui.js loading errors. Also, check the Console tab for any JavaScript errors related to Pagefind initialization.
      • Ensure the pagefind-ui element ID (#search in our example) matches the ID in your docs.html template.
    • Solution: Double-check the pagefind_enabled flag in config.toml. Ensure the pagefind integration code in src/ssg/pagefind.rs is correct and the run_pagefind_indexing function is called. Verify the script path and initialization in docs.html.

Testing & Verification

To thoroughly test and verify the documentation site:

  1. Full Build:

    • Run ./ssg_cli build (or cargo run -- build if running from the SSG’s root) from the docs-site directory.
    • Ensure no errors are reported in the console.
  2. Local Server:

    • Serve the public directory using python3 -m http.server --directory public 8000 or a similar tool.
    • Open http://localhost:8000 in your web browser.
  3. Content Verification:

    • Navigation:
      • Verify the sidebar navigation loads correctly.
      • Ensure all expected pages (Documentation Home, v1.0, v2.0, Getting Started, What’s New) are listed.
      • Check that the hierarchy is correct (e.g., “Getting Started” is nested under “Version 1.0”).
      • Confirm that clicking each navigation link takes you to the correct page.
      • Check the order of items within a level based on their weight in frontmatter.
    • Page Content:
      • Navigate to Getting Started and verify the CodeBlock component renders correctly.
      • Check for any Markdown rendering issues (e.g., lists, links, images).
    • Frontmatter: Verify that the page title and description are correctly displayed (e.g., in the browser tab title and meta description).
  4. Search Functionality:

    • Use the Pagefind search bar in the sidebar.
    • Search for keywords present in your documentation content (e.g., “installation”, “API”, “new features”).
    • Verify that relevant search results appear and link to the correct pages.
  5. Responsiveness (Optional but Recommended):

    • Resize your browser window or use developer tools to simulate mobile devices.
    • Ensure the layout remains usable and readable on smaller screens. (Our current CSS is basic, so this might require more styling work).

By following these steps, you can confidently verify that your SSG is capable of generating a robust and functional developer documentation site.

Summary & Next Steps

In this chapter, we successfully built a real-world developer documentation site using our Rust-based Static Site Generator. We designed a hierarchical content structure, implemented dynamic sidebar navigation, and integrated client-side search with Pagefind. This exercise validated many core features of our SSG, demonstrating its ability to handle structured content, flexible templating, and interactive components in a production-ready context.

We delved into the specifics of:

  • Configuring a documentation project.
  • Creating a dedicated docs.html Tera template.
  • Enhancing our SSG’s build process to generate and pass hierarchical navigation data to templates.
  • Setting up example documentation content with custom components.
  • Testing and verifying the generated site’s functionality.

This chapter showcased how our SSG can be adapted for specific use cases, emphasizing the importance of content organization and efficient data processing for large content sets.

In the next chapter, we will continue building real-world examples by tackling a Learning Platform with Structured Chapters. This will further challenge our SSG’s content management, routing, and sequential navigation capabilities, potentially introducing concepts like progress tracking and lesson dependencies.