Welcome to Chapter 5! In the previous chapters, we laid the groundwork for our Static Site Generator (SSG) by implementing robust content parsing, frontmatter extraction, and Markdown-to-HTML conversion. We now have the raw content and metadata, but it’s not yet wrapped in a presentable web page. This chapter is where we bridge that gap, transforming our processed data into beautiful, structured HTML using a powerful templating engine.

A templating engine is crucial for an SSG. It allows us to define the structural layout of our website (headers, footers, navigation, sidebars) once, and then inject dynamic content (like post titles, author names, and the main body HTML) into these predefined templates. This separation of concerns—content from presentation—is a cornerstone of maintainable web development. For our Rust SSG, we’ll be using Tera, a powerful and flexible template engine inspired by Jinja2 and Django templates. Tera is a native Rust solution, offering excellent performance and seamless integration with our existing Rust codebase, making it an ideal choice for a production-ready SSG.

By the end of this chapter, you will have integrated Tera into our SSG pipeline. We’ll define a basic site layout, a template for individual content pages, and modify our build process to render our parsed Markdown content into full HTML files. This will be a significant step towards generating a complete, browsable static website, ready for deployment.

Planning & Design

Integrating a templating engine requires careful consideration of how data flows from our content processing pipeline into the templates, and how the templates themselves are structured.

Component Architecture

The templating engine sits as a critical step after content parsing and Markdown rendering. It takes the structured data we’ve extracted and uses it to fill placeholders in our HTML templates.

flowchart TD A[Raw Content File] --> B[Frontmatter Parser] B --> C{Frontmatter Data} B --> D[Markdown Body] D --> E[pulldown cmark Markdown to HTML] E --> F{Rendered HTML Body} subgraph Templating_Engine_Integration["Templating Engine Integration"] G[Tera Context Creation] C --> G F --> G G --> H[Tera Renderer] H --> I[Final HTML Output] end I --> J[Write to Output Directory]

Explanation of the Flow:

  • Raw Content File: Our starting point, containing frontmatter and Markdown.
  • Frontmatter Parser: Extracts metadata (e.g., title, date, slug).
  • Markdown Body: The main content, which is then converted to HTML.
  • Tera Context Creation: A crucial step where we gather all the necessary data (frontmatter, rendered Markdown HTML) and package it into a tera::Context object. This context is essentially a key-value store that templates can access.
  • Tera Renderer: The Tera engine takes the selected template (e.g., page.html) and the tera::Context, then processes them to produce the final HTML.
  • Final HTML Output: The complete, styled, and content-filled HTML file.
  • Write to Output Directory: The generated HTML is saved to the appropriate location in our output folder.

File Structure for Templates

A well-organized templates directory is essential for scalability. We’ll follow a common convention:

.
├── Cargo.toml
├── src
│   └── main.rs
├── content
│   └── posts
│       └── my-first-post.md
└── templates
    ├── base.html       // The main structural layout, extended by others
    ├── page.html       // Template for individual content pages (like blog posts)
    └── macros.html     // (Optional, for reusable Tera macros later)

Step-by-Step Implementation

Let’s integrate Tera into our SSG. We’ll start by adding the dependency, then define our templates, and finally update our main.rs to use Tera for rendering.

a) Setup/Configuration

First, we need to add tera to our project’s dependencies.

Open your Cargo.toml and add tera under [dependencies]:

# Cargo.toml

[package]
name = "rust_ssg"
version = "0.1.0"
edition = "2021"

[dependencies]
# ... other dependencies from previous chapters (serde, serde_yaml, pulldown-cmark, etc.)
tera = "1.19" # Use the latest stable version
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
pulldown-cmark = "0.9"
anyhow = "1.0" # For improved error handling
log = "0.4"
env_logger = "0.11"

Next, create the templates directory and the initial template files:

mkdir -p templates
touch templates/base.html templates/page.html

Now, let’s define the content for these template files.

templates/base.html:

This file will contain the fundamental HTML structure that all other pages will inherit. It includes common elements like DOCTYPE, head, and body, with “blocks” that child templates can override.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Rust SSG Site{% endblock title %}</title>
    <link rel="stylesheet" href="/static/style.css"> {# Placeholder for CSS #}
    {% block head %}{% endblock head %}
</head>
<body>
    <header>
        <h1><a href="/">Rust SSG</a></h1>
        <nav>
            <ul>
                <li><a href="/posts">Posts</a></li>
                <li><a href="/about">About</a></li>
            </ul>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock content %}
    </main>

    <footer>
        <p>&copy; 2026 Rust SSG. All rights reserved.</p>
    </footer>

    {% block scripts %}{% endblock scripts %}
</body>
</html>

Explanation:

  • {% block title %}: A placeholder for the page title. Child templates can override this.
  • {% block head %}: For additional <head> elements like custom CSS or meta tags.
  • {% block content %}: The main area where unique page content will be injected.
  • The header and footer are static elements that will appear on all pages extending base.html.

templates/page.html:

This template will be used for rendering individual content pages, like blog posts or articles. It extends base.html and fills in its blocks.

<!-- templates/page.html -->
{% extends "base.html" %}

{% block title %}{{ page.title }} - My Rust SSG Site{% endblock title %}

{% block content %}
<article>
    <header>
        <h1>{{ page.title }}</h1>
        <p class="meta">
            Published on: {{ page.date | date(format="%Y-%m-%d") }}
            {% if page.author %} by {{ page.author }}{% endif %}
        </p>
    </header>
    <div class="content">
        {{ page.body | safe }}
    </div>
</article>
{% endblock content %}

Explanation:

  • {% extends "base.html" %}: This line tells Tera that page.html inherits from base.html.
  • {{ page.title }}: This is a variable placeholder. Tera will look for a page object in its context, and then for a title field within it.
  • {{ page.date | date(format="%Y-%m-%d") }}: This demonstrates a Tera filter. The date filter formats the date string.
  • {% if page.author %}: Conditional rendering. The author will only be displayed if the page.author variable exists in the context.
  • {{ page.body | safe }}: This is critical. The body variable will contain the HTML rendered from Markdown. The safe filter tells Tera not to auto-escape this content. Without safe, all HTML tags in page.body would be converted to their entity equivalents (e.g., <p> would become &lt;p&gt;), rendering raw HTML text instead of actual HTML. This is a security feature to prevent XSS attacks when displaying user-generated content, but in our case, we trust our own Markdown-to-HTML conversion.

b) Core Implementation

Now, let’s modify our src/main.rs to initialize Tera and use it to render our content. We’ll assume you have a Content struct defined from previous chapters (which includes frontmatter and html_body).

First, let’s refine our Content struct to make it more suitable for Tera. Tera contexts typically work well with serde_json::Value or types that implement serde::Serialize. Our Frontmatter struct already uses serde, so we can leverage that.

src/main.rs (or src/pipeline.rs if you’ve modularized):

We’ll introduce a Site struct to hold our Tera instance and manage the build process. This is a common pattern for SSGs.

// src/main.rs (or create src/site.rs and move logic there)

use std::{fs, path::{Path, PathBuf}};
use serde::{Deserialize, Serialize};
use pulldown_cmark::{Parser, Options, html};
use anyhow::{Result, Context}; // Using anyhow for simplified error handling
use tera::{Tera, Context as TeraContext}; // Alias Context to avoid conflict with our own Context struct
use log::{info, error, debug};
use env_logger::Env;

// --- Existing structs from previous chapters (simplified for brevity) ---

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Frontmatter {
    pub title: String,
    #[serde(default = "default_date_str")]
    pub date: String, // Storing as String for now, will parse properly later
    pub slug: Option<String>,
    pub draft: Option<bool>,
    pub author: Option<String>,
    // Add other fields as needed
    #[serde(flatten)] // Allows arbitrary extra fields in frontmatter
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

fn default_date_str() -> String {
    "1970-01-01".to_string() // A sensible default
}

#[derive(Debug, Serialize, Clone)]
pub struct Content {
    pub frontmatter: Frontmatter,
    pub html_body: String,
    pub file_path: PathBuf, // Original path of the content file
    pub relative_path: PathBuf, // Path relative to the content directory
}

impl Content {
    /// Parses content from a given path, extracting frontmatter and converting Markdown to HTML.
    pub fn from_file(file_path: &Path, base_content_dir: &Path) -> Result<Self> {
        debug!("Processing content file: {:?}", file_path);
        let content_str = fs::read_to_string(file_path)
            .with_context(|| format!("Failed to read content file: {:?}", file_path))?;

        let parts: Vec<&str> = content_str.splitn(3, "+++").collect();

        if parts.len() < 3 {
            anyhow::bail!("Invalid frontmatter format in file: {:?}", file_path);
        }

        let frontmatter_str = parts[1];
        let markdown_body = parts[2];

        let frontmatter: Frontmatter = serde_yaml::from_str(frontmatter_str)
            .with_context(|| format!("Failed to parse frontmatter for file: {:?}", file_path))?;

        let mut options = Options::empty();
        options.insert(Options::ENABLE_FOOTNOTES);
        options.insert(Options::ENABLE_TABLES);
        options.insert(Options::ENABLE_TASKLISTS);
        options.insert(Options::ENABLE_STRIKETHROUGH);

        let parser = Parser::new_ext(markdown_body, options);
        let mut html_output = String::new();
        html::push_html(&mut html_output, parser);

        let relative_path = file_path.strip_prefix(base_content_dir)?
            .to_path_buf();

        Ok(Content {
            frontmatter,
            html_body: html_output,
            file_path: file_path.to_path_buf(),
            relative_path,
        })
    }
}

// --- New `Site` struct and build logic ---

pub struct Site {
    pub tera: Tera,
    pub content_dir: PathBuf,
    pub output_dir: PathBuf,
    pub templates_dir: PathBuf,
}

impl Site {
    pub fn new(content_dir: &Path, output_dir: &Path, templates_dir: &Path) -> Result<Self> {
        info!("Initializing Tera from templates directory: {:?}", templates_dir);
        let tera = Tera::new(&format!("{}/**/*.html", templates_dir.display()))
            .with_context(|| format!("Failed to initialize Tera from {:?}", templates_dir))?;

        Ok(Site {
            tera,
            content_dir: content_dir.to_path_buf(),
            output_dir: output_dir.to_path_buf(),
            templates_dir: templates_dir.to_path_buf(),
        })
    }

    /// Renders a single `Content` item into its final HTML.
    pub fn render_content(&self, content: &Content) -> Result<String> {
        let mut context = TeraContext::new();

        // Insert frontmatter data directly into the context under a 'page' key
        // This makes `{{ page.title }}` possible in templates.
        // We need to convert Frontmatter to a serde_json::Value for TeraContext.
        let frontmatter_value = serde_json::to_value(&content.frontmatter)
            .context("Failed to serialize frontmatter to JSON for Tera context")?;
        context.insert("page", &frontmatter_value);

        // Insert the rendered HTML body
        context.insert("page.body", &content.html_body);

        // Determine which template to use. For now, we'll hardcode 'page.html'.
        // Later, we can make this dynamic based on content type or frontmatter.
        let template_name = "page.html";
        debug!("Rendering {:?} using template: {}", content.file_path, template_name);

        self.tera.render(template_name, &context)
            .with_context(|| format!("Failed to render template '{}' for file {:?}", template_name, content.file_path))
    }

    /// Builds the entire site: reads content, processes it, and renders HTML.
    pub fn build(&self) -> Result<()> {
        info!("Starting site build...");
        if self.output_dir.exists() {
            fs::remove_dir_all(&self.output_dir)
                .with_context(|| format!("Failed to clean output directory: {:?}", self.output_dir))?;
            debug!("Cleaned output directory: {:?}", self.output_dir);
        }
        fs::create_dir_all(&self.output_dir)
            .with_context(|| format!("Failed to create output directory: {:?}", self.output_dir))?;
        debug!("Created output directory: {:?}", self.output_dir);

        for entry in fs::read_dir(&self.content_dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() && path.extension().map_or(false, |ext| ext == "md") {
                let content = Content::from_file(&path, &self.content_dir)?;
                if content.frontmatter.draft.unwrap_or(false) {
                    info!("Skipping draft content: {:?}", path);
                    continue;
                }

                let rendered_html = self.render_content(&content)?;

                // Determine output path. For now, simplified:
                // content/posts/my-post.md -> output/posts/my-post/index.html
                let mut output_path = self.output_dir.clone();
                let file_stem = content.relative_path.file_stem()
                    .and_then(|s| s.to_str())
                    .context("Content file has no stem")?;

                let parent_dir = content.relative_path.parent().unwrap_or_else(|| Path::new(""));

                output_path.push(parent_dir);
                output_path.push(file_stem);
                output_path.push("index.html"); // Standard for clean URLs

                fs::create_dir_all(output_path.parent().unwrap())
                    .with_context(|| format!("Failed to create output directory for: {:?}", output_path))?;
                fs::write(&output_path, rendered_html)
                    .with_context(|| format!("Failed to write output file: {:?}", output_path))?;
                info!("Generated: {:?}", output_path);
            }
        }

        info!("Site build complete.");
        Ok(())
    }
}

fn main() -> Result<()> {
    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

    info!("Starting Rust SSG application...");

    let content_dir = PathBuf::from("./content");
    let output_dir = PathBuf::from("./output");
    let templates_dir = PathBuf::from("./templates");

    let site = Site::new(&content_dir, &output_dir, &templates_dir)?;
    site.build()?;

    Ok(())
}

Key Changes and Explanations:

  1. Dependencies: Added tera and anyhow to Cargo.toml. anyhow is used to simplify error propagation.
  2. Content Struct Update: Added file_path and relative_path to Content for better tracking and output path generation. extra field added to Frontmatter for flexible metadata.
  3. Site Struct:
    • Encapsulates the tera instance, content_dir, output_dir, and templates_dir. This makes our SSG more modular and easier to manage.
    • Site::new: Initializes Tera. The pattern {}/**/*.html tells Tera to load all .html files recursively from the templates directory. This is crucial for Tera to discover base.html and page.html.
  4. Site::render_content Function:
    • This is the core rendering logic.
    • TeraContext::new(): Creates an empty context for this specific page.
    • context.insert("page", &frontmatter_value): We serialize our Frontmatter into a serde_json::Value and insert it under the key "page". This allows us to access frontmatter fields in templates like {{ page.title }}.
    • context.insert("page.body", &content.html_body): The rendered Markdown HTML is inserted under page.body.
    • self.tera.render(template_name, &context): Calls Tera to render the specified template (page.html) with the prepared context.
    • Error handling with with_context from anyhow provides more informative error messages.
  5. Site::build Function:
    • Output Directory Management: Cleans and recreates the output directory before each build to ensure a fresh output.
    • Content Iteration: Iterates through Markdown files in the content_dir.
    • Draft Handling: Skips files marked as draft: true in their frontmatter.
    • Rendering: Calls self.render_content for each valid content file.
    • Output Path Generation: This is a crucial part of an SSG. We’re implementing a simple scheme: content/posts/my-post.md becomes output/posts/my-post/index.html. This creates “pretty URLs” (e.g., yoursite.com/posts/my-post/).
    • File Writing: Creates necessary parent directories and writes the final HTML to disk.
  6. main Function: Initializes logging and orchestrates the Site creation and build process.

c) Testing This Component

To test our new templating integration, we need a sample Markdown file in our content directory.

Create content/posts/first-post.md:

+++
title = "My First Post with Tera"
date = "2026-03-02"
slug = "my-first-post-with-tera"
author = "AI Expert"
draft = false
tags = ["rust", "tera", "ssg"]
categories = ["development"]
+++

This is the **body** of my very first post, rendered using **Tera**!

-   It supports Markdown lists.
-   And *inline* formatting.

```rust
fn main() {
    println!("Hello, Tera!");
}

Let’s see if the code block also works!

Some pre-formatted text.

Now, run your SSG:

cargo run

You should see output similar to this (with appropriate log levels):

INFO  rust_ssg > Starting Rust SSG application...
INFO  rust_ssg > Initializing Tera from templates directory: "templates"
DEBUG rust_ssg > Created output directory: "output"
DEBUG rust_ssg > Processing content file: "content/posts/first-post.md"
DEBUG rust_ssg > Rendering "content/posts/first-post.md" using template: page.html
INFO  rust_ssg > Generated: "output/posts/first-post/index.html"
INFO  rust_ssg > Site build complete.

After running, check the output directory. You should find a structure like:

output
└── posts
    └── first-post
        └── index.html

Open output/posts/first-post/index.html in your web browser. You should see:

  • The <!DOCTYPE html> and <html> structure from base.html.
  • The title in the browser tab should be “My First Post with Tera - My Rust SSG Site”.
  • The <h1> in the header should display “My First Post with Tera”.
  • The date and author should be present.
  • The Markdown content, including the code block, should be rendered correctly as HTML within the <div class="content">.

If this all works, congratulations! You’ve successfully integrated Tera into your SSG pipeline.

Production Considerations

When building a production-ready SSG, several factors related to templating need to be considered:

Error Handling

  • Template Not Found: Tera will return an error if a template specified (e.g., page.html) doesn’t exist. Our current render_content uses with_context which will propagate this error. In a more complex SSG, you might want to log this error and potentially fall back to a generic error page template or skip the problematic content entirely, rather than crashing the build.

  • Template Syntax Errors: If there’s an error in your .html template files (e.g., unclosed blocks, invalid filters), Tera will catch this during initialization (Tera::new) or rendering. These errors are usually quite descriptive, pointing to the line number in the template. Ensure your logging level is set to debug or info to see these messages.

  • Missing Context Data: If a template tries to access a variable that doesn’t exist in the TeraContext (e.g., {{ page.non_existent_field }}), Tera will, by default, render an empty string. While this can be convenient, it can also mask bugs. For production, consider enabling strict mode if you want to be notified of missing variables explicitly. This can be configured during Tera initialization.

    // Example of strict mode (not implemented in main.rs currently)
    // let mut tera = Tera::new("templates/**/*.html")?;
    // tera.autoescape_on(vec![".html", ".sql"]); // Example, default is fine
    // tera.set_strict_mode(true); // Will error on missing variables
    

Performance Optimization

  • Template Caching: Tera automatically caches parsed templates in memory when Tera::new is called. This means parsing only happens once at startup, and subsequent renders are very fast. This is a significant performance benefit for SSGs, as templates are rendered many times during a build.
  • Minification: The HTML output by Tera is not minified by default. For production, you’ll want to minify the final HTML (and CSS/JS) to reduce file sizes. This will be a separate step in our build pipeline later, potentially using a Rust crate for HTML minification.
  • Parallel Rendering: As our SSG grows, we’ll want to process and render multiple content files in parallel. The Site::build method currently processes files sequentially. In future chapters, we’ll introduce concurrency using rayon or tokio to speed up builds by rendering pages on multiple CPU cores.

Security Considerations

  • Auto-escaping: Tera, like most modern templating engines, auto-escapes content by default. This means if you insert {{ some_variable_with_html_tags }} into a template, the < and > characters will be converted to &lt; and &gt;, preventing malicious script injection (XSS).
  • | safe Filter: We used {{ page.body | safe }} for our Markdown-rendered HTML. This explicitly tells Tera not to escape the content. This is safe in our context because we control the Markdown input and the conversion process. However, if you were to allow untrusted users to directly inject HTML into your templates without sanitization, using | safe would be a significant security vulnerability. Always be cautious when using safe.

Logging and Monitoring

  • Detailed Logging: Ensure your SSG logs are informative. We’re using log and env_logger. Good log messages should indicate:
    • Which file is being processed.
    • Which template is being used.
    • Successes (INFO).
    • Warnings (e.g., WARN for missing optional frontmatter fields).
    • Errors (ERROR) with full context.
  • Build Metrics: In a more advanced SSG, you might want to log build duration, number of pages rendered, and other metrics to track performance over time, especially for large sites.

Code Review Checkpoint

At this point, our project structure and core main.rs (or src/pipeline.rs / src/site.rs) should look something like this:

Files Created/Modified:

  • Cargo.toml: Added tera and anyhow dependencies.
  • templates/: New directory containing base.html and page.html.
  • src/main.rs:
    • Updated Content struct with file_path and relative_path.
    • New Site struct to manage Tera and the build process.
    • Site::new for Tera initialization.
    • Site::render_content to render individual content items using Tera.
    • Site::build now orchestrates reading, parsing, rendering, and writing.
    • main function updated to use the Site struct.

Key Concepts Implemented:

  • Tera Initialization: Loading templates from a specified directory.
  • Tera Context: Creating and populating a TeraContext with frontmatter and html_body data.
  • Template Inheritance: Using {% extends "base.html" %} and {% block ... %} for reusable layouts.
  • Variable Injection: Displaying data using {{ variable_name }}.
  • Filters: Using | date and | safe for data transformation and security.
  • Conditional Logic: Using {% if ... %} for dynamic content display.
  • Output Path Generation: A basic scheme for creating clean URLs (e.g., output/post-slug/index.html).

This checkpoint marks a significant milestone: our SSG can now take raw Markdown content, parse its metadata, convert the body to HTML, and then wrap it all in a structured, consistent web page using a powerful templating engine.

Common Issues & Solutions

  1. “Template not found” error during cargo run:

    • Issue: Tera cannot locate base.html or page.html.
    • Solution:
      • Double-check that the templates directory exists at the root of your project (next to Cargo.toml).
      • Ensure templates/base.html and templates/page.html exist within that directory.
      • Verify the glob pattern in Site::new: Tera::new(&format!("{}/**/*.html", templates_dir.display())). Make sure templates_dir correctly points to ./templates.
      • Check for typos in {% extends "base.html" %} or the template name passed to tera.render().
  2. Tera syntax errors (e.g., “Lexical error at line X, col Y”):

    • Issue: You have a typo or incorrect syntax within your .html template files.
    • Solution: The error message from Tera is usually very precise, pointing to the exact line and column. Carefully review that line for:
      • Unclosed {% ... %} or {{ ... }} blocks.
      • Missing quotes around strings.
      • Incorrect filter names or arguments.
      • Unescaped special characters (e.g., { or } characters that are not part of Tera syntax need to be escaped if strict mode is on, or simply not used in that context).
  3. Content (title, body, date) not appearing in the generated HTML, or showing raw HTML tags:

    • Issue 1: Data missing/incorrectly passed to context.
      • Solution: In Site::render_content, use debug!(target: "tera_context", "Context: {:?}", context); before self.tera.render. Run with RUST_LOG=debug cargo run to inspect the TeraContext and ensure page.title, page.body, etc., are present and have the expected values. Check for typos in context.insert("page.body", ...) vs. {{ page.body }} in the template.
    • Issue 2: Raw HTML tags appearing instead of rendered HTML.
      • Solution: You likely forgot the | safe filter for your page.body variable: {{ page.body | safe }}. Without safe, Tera auto-escapes the HTML, treating it as plain text.
  4. output directory not created or files not written:

    • Issue: Permissions error or incorrect path handling.
    • Solution:
      • Check the console output for INFO or ERROR messages related to directory creation or file writing.
      • Ensure the directory where you run cargo run has write permissions.
      • Verify that self.output_dir and output_path are constructing the correct absolute paths. Use debug!(target: "file_paths", "Output path: {:?}", output_path); to inspect.

Testing & Verification

To thoroughly test and verify the work done in this chapter:

  1. Run a full build:

    cargo run
    

    Observe the console output. Ensure there are no ERROR messages. INFO messages should confirm files are being generated.

  2. Inspect the output directory:

    • Confirm the output directory exists.
    • Verify the structure: output/posts/first-post/index.html.
    • Ensure no unexpected files or directories are present.
  3. Open generated HTML in a browser:

    • Navigate to output/posts/first-post/index.html in your web browser.
    • Visual Check:
      • Does it look like a complete HTML page?
      • Is the header and footer from base.html visible?
      • Is the title in the browser tab correct?
      • Is the main content (My First Post with Tera and the Markdown body) rendered as expected?
      • Are the date and author fields correctly displayed?
    • Source Code Check (View Page Source):
      • Right-click the page and select “View Page Source”.
      • Confirm the <!DOCTYPE html> and basic HTML structure.
      • Verify that the Markdown content is rendered as actual HTML tags (<p>, <ul>, <code>) and not escaped entities (&lt;p&gt;).
      • Check that the title, date, and author from the frontmatter are correctly inserted.
  4. Test draft content:

    • Modify content/posts/first-post.md to draft = true.
    • Run cargo run again.
    • The INFO log should show “Skipping draft content: …”
    • The output/posts/first-post/ directory should not be regenerated (or if it was removed, it shouldn’t be created again).

By performing these checks, you can confidently verify that your Tera templating integration is working correctly and producing valid HTML output.

Summary & Next Steps

In this chapter, we successfully integrated Tera, a powerful Rust templating engine, into our static site generator. We established a foundational base.html layout, created a page.html template to render our content, and updated our build pipeline to leverage Tera for HTML generation. We also discussed crucial production considerations like error handling, performance, and security.

Our SSG can now:

  • Read Markdown files.
  • Parse frontmatter (YAML/TOML).
  • Convert Markdown body to HTML.
  • Render the parsed content and frontmatter into full HTML pages using Tera templates.
  • Generate clean URLs and write output to disk.

This marks a significant leap in our project, bringing us much closer to a functional SSG. However, our current templating is quite basic. Modern SSGs, inspired by frameworks like Astro, allow for component-driven rendering directly within Markdown, and even partial hydration of these components.

In Chapter 6: Implementing Component Support and Custom Syntax, we will dive into more advanced templating features. We’ll explore how to introduce custom component syntax (similar to JSX) directly into our Markdown, design a mechanism to parse these components, and lay the groundwork for rendering them, moving towards a more dynamic and modular content creation experience.