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.
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::Contextobject. 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 thetera::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
outputfolder.
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>© 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 thatpage.htmlinherits frombase.html.{{ page.title }}: This is a variable placeholder. Tera will look for apageobject in its context, and then for atitlefield within it.{{ page.date | date(format="%Y-%m-%d") }}: This demonstrates a Tera filter. Thedatefilter formats the date string.{% if page.author %}: Conditional rendering. The author will only be displayed if thepage.authorvariable exists in the context.{{ page.body | safe }}: This is critical. Thebodyvariable will contain the HTML rendered from Markdown. Thesafefilter tells Tera not to auto-escape this content. Withoutsafe, all HTML tags inpage.bodywould be converted to their entity equivalents (e.g.,<p>would become<p>), 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:
- Dependencies: Added
teraandanyhowtoCargo.toml.anyhowis used to simplify error propagation. ContentStruct Update: Addedfile_pathandrelative_pathtoContentfor better tracking and output path generation.extrafield added toFrontmatterfor flexible metadata.SiteStruct:- Encapsulates the
terainstance,content_dir,output_dir, andtemplates_dir. This makes our SSG more modular and easier to manage. Site::new: InitializesTera. The pattern{}/**/*.htmltells Tera to load all.htmlfiles recursively from thetemplatesdirectory. This is crucial for Tera to discoverbase.htmlandpage.html.
- Encapsulates the
Site::render_contentFunction:- This is the core rendering logic.
TeraContext::new(): Creates an empty context for this specific page.context.insert("page", &frontmatter_value): We serialize ourFrontmatterinto aserde_json::Valueand 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 underpage.body.self.tera.render(template_name, &context): Calls Tera to render the specified template (page.html) with the prepared context.- Error handling with
with_contextfromanyhowprovides more informative error messages.
Site::buildFunction:- Output Directory Management: Cleans and recreates the
outputdirectory 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: truein their frontmatter. - Rendering: Calls
self.render_contentfor 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.mdbecomesoutput/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.
- Output Directory Management: Cleans and recreates the
mainFunction: Initializes logging and orchestrates theSitecreation andbuildprocess.
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 frombase.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 currentrender_contentuseswith_contextwhich 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
.htmltemplate 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 todebugorinfoto 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::newis 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::buildmethod currently processes files sequentially. In future chapters, we’ll introduce concurrency usingrayonortokioto 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<and>, preventing malicious script injection (XSS). | safeFilter: 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| safewould be a significant security vulnerability. Always be cautious when usingsafe.
Logging and Monitoring
- Detailed Logging: Ensure your SSG logs are informative. We’re using
logandenv_logger. Good log messages should indicate:- Which file is being processed.
- Which template is being used.
- Successes (
INFO). - Warnings (e.g.,
WARNfor 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: Addedteraandanyhowdependencies.templates/: New directory containingbase.htmlandpage.html.src/main.rs:- Updated
Contentstruct withfile_pathandrelative_path. - New
Sitestruct to manage Tera and the build process. Site::newfor Tera initialization.Site::render_contentto render individual content items using Tera.Site::buildnow orchestrates reading, parsing, rendering, and writing.mainfunction updated to use theSitestruct.
- Updated
Key Concepts Implemented:
- Tera Initialization: Loading templates from a specified directory.
- Tera Context: Creating and populating a
TeraContextwithfrontmatterandhtml_bodydata. - Template Inheritance: Using
{% extends "base.html" %}and{% block ... %}for reusable layouts. - Variable Injection: Displaying data using
{{ variable_name }}. - Filters: Using
| dateand| safefor 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
“Template not found” error during
cargo run:- Issue: Tera cannot locate
base.htmlorpage.html. - Solution:
- Double-check that the
templatesdirectory exists at the root of your project (next toCargo.toml). - Ensure
templates/base.htmlandtemplates/page.htmlexist within that directory. - Verify the glob pattern in
Site::new:Tera::new(&format!("{}/**/*.html", templates_dir.display())). Make suretemplates_dircorrectly points to./templates. - Check for typos in
{% extends "base.html" %}or the template name passed totera.render().
- Double-check that the
- Issue: Tera cannot locate
Tera syntax errors (e.g., “Lexical error at line X, col Y”):
- Issue: You have a typo or incorrect syntax within your
.htmltemplate 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).
- Unclosed
- Issue: You have a typo or incorrect syntax within your
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, usedebug!(target: "tera_context", "Context: {:?}", context);beforeself.tera.render. Run withRUST_LOG=debug cargo runto inspect theTeraContextand ensurepage.title,page.body, etc., are present and have the expected values. Check for typos incontext.insert("page.body", ...)vs.{{ page.body }}in the template.
- Solution: In
- Issue 2: Raw HTML tags appearing instead of rendered HTML.
- Solution: You likely forgot the
| safefilter for yourpage.bodyvariable:{{ page.body | safe }}. Withoutsafe, Tera auto-escapes the HTML, treating it as plain text.
- Solution: You likely forgot the
- Issue 1: Data missing/incorrectly passed to context.
outputdirectory not created or files not written:- Issue: Permissions error or incorrect path handling.
- Solution:
- Check the console output for
INFOorERRORmessages related to directory creation or file writing. - Ensure the directory where you run
cargo runhas write permissions. - Verify that
self.output_dirandoutput_pathare constructing the correct absolute paths. Usedebug!(target: "file_paths", "Output path: {:?}", output_path);to inspect.
- Check the console output for
Testing & Verification
To thoroughly test and verify the work done in this chapter:
Run a full build:
cargo runObserve the console output. Ensure there are no
ERRORmessages.INFOmessages should confirm files are being generated.Inspect the
outputdirectory:- Confirm the
outputdirectory exists. - Verify the structure:
output/posts/first-post/index.html. - Ensure no unexpected files or directories are present.
- Confirm the
Open generated HTML in a browser:
- Navigate to
output/posts/first-post/index.htmlin your web browser. - Visual Check:
- Does it look like a complete HTML page?
- Is the header and footer from
base.htmlvisible? - Is the title in the browser tab correct?
- Is the main content (
My First Post with Teraand 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 (<p>). - Check that the title, date, and author from the frontmatter are correctly inserted.
- Navigate to
Test draft content:
- Modify
content/posts/first-post.mdtodraft = true. - Run
cargo runagain. - The
INFOlog 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).
- Modify
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.