Welcome to Chapter 2 of our journey to build a modern Static Site Generator (SSG) in Rust! In the previous chapter, we laid the foundational project structure. Now, we’ll focus on making our SSG usable and configurable. A well-designed Command Line Interface (CLI) is crucial for any developer tool, allowing users to easily create new projects, build sites, and manage various operations. Alongside the CLI, robust configuration management ensures that our SSG can adapt to different project requirements and user preferences without needing code changes.

This chapter will guide you through implementing a user-friendly CLI using the clap crate, which is a powerful and popular choice for parsing command-line arguments in Rust. We’ll define essential commands like new for initializing a new SSG project and build for triggering the site generation process. Concurrently, we will establish a flexible configuration system using serde and toml to manage project-specific settings such as content directories, output paths, and site metadata. By the end of this chapter, you’ll have a functional CLI that can initialize a new SSG project and load its configuration, setting the stage for content processing in subsequent chapters.

Planning & Design

Before diving into code, let’s outline the design for our CLI commands and configuration structure.

CLI Commands

Our SSG will initially support two core commands:

  1. new <project_name>: This command will initialize a new SSG project in a specified directory. It will create a basic directory structure (e.g., content/, templates/) and a default configuration file (config.toml).
  2. build: This command will trigger the site generation process. It will read the project’s configuration, process content, apply templates, and output the static files to the designated directory.

Later, we might add commands like serve for local development servers, watch for live reloading, and deploy for automated deployments, but we’ll start with the fundamentals.

Configuration Structure

The project’s configuration will reside in a config.toml file at the root of each SSG project. Using TOML (Tom’s Obvious, Minimal Language) provides a human-readable and easy-to-parse format for structured data. Our initial configuration will include:

  • base_url: The base URL for the deployed site (e.g., https://example.com).
  • title: The main title of the website.
  • description: A brief description of the website.
  • source_dir: The directory where content files (Markdown, etc.) are located, relative to the project root. Defaults to content.
  • output_dir: The directory where generated static files will be placed, relative to the project root. Defaults to public.
  • template_dir: The directory where template files (Tera templates) are located, relative to the project root. Defaults to templates.
  • static_dir: The directory for static assets (images, CSS, JS), relative to the project root. Defaults to static.

Project File Structure

To keep our code organized, we’ll introduce new modules for CLI parsing and configuration management.

ssg_builder/
├── src/
│   ├── main.rs         # Entry point, orchestrates CLI commands
│   ├── cli.rs          # Defines CLI arguments and subcommands using clap
│   ├── config.rs       # Defines SiteConfig struct and loading logic
│   └── utils.rs        # Common utility functions (e.g., file system operations)
├── Cargo.toml
└── .gitignore

Architecture Flow for CLI and Configuration

The following diagram illustrates how our CLI and configuration components will interact:

flowchart TD User[User] --> CLI_App[SSG CLI Application] CLI_App --->|Parses Arguments| CLI_Parser[CLI Parser clap] CLI_Parser --->|Command Matched| Main_Orchestrator[main.rs Orchestrator] Main_Orchestrator --> New_Handler[New Command] New_Handler --> Create_Dirs[Create Project Directories] New_Handler --> Generate_Config[Generate Default config.toml] New_Handler --> Init_Content[Initialize content and templates] New_Handler --> Success_New[Log Success] Main_Orchestrator --> Build_Handler[Build Command] Build_Handler --> Load_Config[Load config.toml] Load_Config --> Config_Parser[Config Parser serde toml] Config_Parser --> Site_Config[SiteConfig Object] Site_Config --> Build_Process_Placeholder[Placeholder Start Build Process] Build_Process_Placeholder --> Success_Build[Log Config Loaded] Create_Dirs & Generate_Config & Init_Content --> FS_Ops[File System Operations utils.rs] Load_Config --> FS_Ops

Step-by-Step Implementation

Let’s begin by updating our Cargo.toml with the necessary dependencies.

a) Setup/Configuration

First, open your Cargo.toml file and add the following dependencies. We’re including clap for CLI parsing, serde for serialization/deserialization, toml as the specific format for our configuration, anyhow for simplified error handling, and tracing with tracing-subscriber for robust logging.

# ssg_builder/Cargo.toml
[package]
name = "ssg_builder"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.5.1", features = ["derive"] } # For CLI argument parsing
serde = { version = "1.0.197", features = ["derive"] } # For serializing/deserializing config
toml = "0.8.10" # For TOML config files
anyhow = "1.0.80" # For simplified error handling
tracing = "0.1.40" # For logging
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } # For configuring tracing

Save Cargo.toml. Now, let’s create the new modules.

b) Core Implementation

i. src/utils.rs - File System Utilities

We’ll need some basic utility functions for file system operations, especially for the new command. Create src/utils.rs and add the following:

// ssg_builder/src/utils.rs
use std::{fs, io, path::Path};
use anyhow::{Result, Context};
use tracing::{info, error};

/// Creates a directory at the given path if it doesn't already exist.
pub fn create_dir_if_not_exists(path: &Path) -> Result<()> {
    if !path.exists() {
        info!("Creating directory: {}", path.display());
        fs::create_dir_all(path)
            .with_context(|| format!("Failed to create directory: {}", path.display()))?;
    } else {
        info!("Directory already exists: {}", path.display());
    }
    Ok(())
}

/// Recursively copies contents from `src` to `dst`.
/// This is useful for copying boilerplate files or static assets.
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
    let src = src.as_ref();
    let dst = dst.as_ref();

    info!("Copying directory from {} to {}", src.display(), dst.display());

    create_dir_if_not_exists(dst)?;

    for entry in fs::read_dir(src)
        .with_context(|| format!("Failed to read source directory: {}", src.display()))?
    {
        let entry = entry?;
        let ty = entry.file_type()?;
        let path = entry.path();
        let relative_path = path.strip_prefix(src)?;
        let dest_path = dst.join(relative_path);

        if ty.is_dir() {
            copy_dir_all(&path, &dest_path)?;
        } else {
            info!("Copying file: {} to {}", path.display(), dest_path.display());
            fs::copy(&path, &dest_path)
                .with_context(|| format!("Failed to copy file from {} to {}", path.display(), dest_path.display()))?;
        }
    }
    Ok(())
}

Explanation:

  • create_dir_if_not_exists: A simple function to ensure a directory exists, creating it and its parents if not. We use anyhow::Result for ergonomic error handling and Context to add descriptive messages to errors.
  • copy_dir_all: This recursive function copies an entire directory and its contents. It’s essential for setting up initial project templates or copying static assets. We include tracing info! logs to provide feedback during these operations.
ii. src/config.rs - Site Configuration

Next, define the structure for our site’s configuration. Create src/config.rs:

// ssg_builder/src/config.rs
use serde::{Deserialize, Serialize};
use std::{fs, path::{Path, PathBuf}};
use anyhow::{Result, Context};
use tracing::{info, warn, error};

/// Represents the overall configuration for the static site.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SiteConfig {
    /// The base URL for the deployed site (e.g., "https://example.com").
    #[serde(default = "default_base_url")]
    pub base_url: String,
    /// The main title of the website.
    #[serde(default = "default_title")]
    pub title: String,
    /// A brief description of the website.
    #[serde(default = "default_description")]
    pub description: String,
    /// The directory where content files (Markdown, etc.) are located, relative to the project root.
    #[serde(default = "default_source_dir")]
    pub source_dir: PathBuf,
    /// The directory where generated static files will be placed, relative to the project root.
    #[serde(default = "default_output_dir")]
    pub output_dir: PathBuf,
    /// The directory where template files (Tera templates) are located, relative to the project root.
    #[serde(default = "default_template_dir")]
    pub template_dir: PathBuf,
    /// The directory for static assets (images, CSS, JS), relative to the project root.
    #[serde(default = "default_static_dir")]
    pub static_dir: PathBuf,
}

// Default values for SiteConfig fields
fn default_base_url() -> String { "http://localhost:8080".to_string() }
fn default_title() -> String { "My Awesome SSG Site".to_string() }
fn default_description() -> String { "A static site generated by our Rust SSG.".to_string() }
fn default_source_dir() -> PathBuf { PathBuf::from("content") }
fn default_output_dir() -> PathBuf { PathBuf::from("public") }
fn default_template_dir() -> PathBuf { PathBuf::from("templates") }
fn default_static_dir() -> PathBuf { PathBuf::from("static") }

impl SiteConfig {
    /// Loads the site configuration from a TOML file.
    /// If the file doesn't exist, it returns a default configuration.
    ///
    /// # Arguments
    /// * `path` - The path to the `config.toml` file.
    pub fn load(path: &Path) -> Result<Self> {
        info!("Attempting to load configuration from: {}", path.display());
        if !path.exists() {
            warn!("Configuration file not found at {}. Using default configuration.", path.display());
            return Ok(Self::default());
        }

        let config_str = fs::read_to_string(path)
            .with_context(|| format!("Failed to read configuration file: {}", path.display()))?;

        let config: Self = toml::from_str(&config_str)
            .with_context(|| format!("Failed to parse TOML configuration from: {}", path.display()))?;

        info!("Configuration loaded successfully.");
        Ok(config)
    }

    /// Returns a default `SiteConfig`.
    pub fn default() -> Self {
        Self {
            base_url: default_base_url(),
            title: default_title(),
            description: default_description(),
            source_dir: default_source_dir(),
            output_dir: default_output_dir(),
            template_dir: default_template_dir(),
            static_dir: default_static_dir(),
        }
    }

    /// Saves the current configuration to a TOML file.
    ///
    /// # Arguments
    /// * `path` - The path where the `config.toml` file should be saved.
    pub fn save(&self, path: &Path) -> Result<()> {
        info!("Saving configuration to: {}", path.display());
        let toml_string = toml::to_string_pretty(self)
            .context("Failed to serialize SiteConfig to TOML")?;
        fs::write(path, toml_string)
            .with_context(|| format!("Failed to write configuration to file: {}", path.display()))?;
        info!("Configuration saved successfully.");
        Ok(())
    }
}

Explanation:

  • SiteConfig Struct: This struct uses #[derive(Debug, Deserialize, Serialize, Clone)] from serde to automatically implement traits for debugging, deserialization (reading from TOML), serialization (writing to TOML), and cloning.
  • #[serde(default = "..."): This attribute is critical for making our configuration robust. If a field is missing in config.toml, serde will use the specified default function instead of returning an error. This allows users to only specify what they want to override.
  • Default Functions: Separate functions like default_base_url() provide default values for each field.
  • load() Method: This method attempts to read config.toml. If the file doesn’t exist, it logs a warning and returns a default SiteConfig instance, making the SSG usable even without an explicit config file (though it’s recommended). It uses anyhow for error propagation.
  • default() Method: Provides a convenient way to get a SiteConfig with all default values.
  • save() Method: Serializes the current SiteConfig to a pretty-printed TOML string and writes it to a file. This is useful for the new command to create a boilerplate config.
iii. src/cli.rs - CLI Definition

Now, let’s define our CLI structure using clap. Create src/cli.rs:

// ssg_builder/src/cli.rs
use clap::{Parser, Subcommand};
use std::path::PathBuf;

/// A blazing-fast static site generator built in Rust.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Creates a new static site project
    New(NewCommand),
    /// Builds the static site
    Build(BuildCommand),
    // Future commands could go here, e.g., Serve(ServeCommand)
}

/// Arguments for the 'new' command
#[derive(Parser, Debug)]
pub struct NewCommand {
    /// The name of the new project directory
    #[arg(name = "NAME")]
    pub name: String,
    /// Path to the directory where the new project will be created (defaults to current directory)
    #[arg(short, long, value_name = "PATH")]
    pub path: Option<PathBuf>,
}

/// Arguments for the 'build' command
#[derive(Parser, Debug)]
pub struct BuildCommand {
    /// Path to the project root directory (where config.toml is located)
    #[arg(short, long, value_name = "PATH", default_value = ".")]
    pub path: PathBuf,
}

Explanation:

  • Cli Struct: This is the main entry point for clap. #[derive(Parser, Debug)] automatically generates the argument parsing logic. #[command(...)] provides metadata like author, version, and a description.
  • Commands Enum: This enum defines our subcommands (New, Build). #[derive(Subcommand, Debug)] enables clap to parse these.
  • NewCommand Struct: Holds arguments specific to the new command. #[arg(name = "NAME")] defines a positional argument, while #[arg(short, long, value_name = "PATH")] defines an optional argument with a short (-p) and long (--path) flag.
  • BuildCommand Struct: Holds arguments specific to the build command, including an optional --path to specify the project root, defaulting to the current directory (.).
iv. src/main.rs - CLI Entry Point and Orchestration

Finally, let’s update src/main.rs to integrate our CLI and configuration modules. This file will parse the command-line arguments and dispatch to the appropriate handlers.

// ssg_builder/src/main.rs
mod cli;
mod config;
mod utils;

use clap::Parser;
use cli::{Cli, Commands, NewCommand, BuildCommand};
use config::SiteConfig;
use utils::{create_dir_if_not_exists, copy_dir_all};
use anyhow::{Result, Context};
use tracing::{info, error, Level};
use tracing_subscriber::{fmt, EnvFilter};
use std::{path::{Path, PathBuf}, fs};

fn main() -> Result<()> {
    // Initialize logging
    // Uses RUST_LOG environment variable for filtering, e.g., RUST_LOG=info cargo run
    fmt()
        .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into()))
        .init();

    info!("Starting SSG Builder...");

    let cli = Cli::parse();

    match &cli.command {
        Commands::New(new_args) => handle_new_command(new_args),
        Commands::Build(build_args) => handle_build_command(build_args),
    }
}

/// Handles the 'new' command: initializes a new SSG project.
fn handle_new_command(args: &NewCommand) -> Result<()> {
    let project_root = args.path.as_deref().unwrap_or(Path::new(".")).join(&args.name);
    info!("Initializing new project at: {}", project_root.display());

    // 1. Create project root directory
    create_dir_if_not_exists(&project_root)
        .context("Failed to create project root directory")?;

    // 2. Create essential subdirectories
    let content_dir = project_root.join("content");
    create_dir_if_not_exists(&content_dir)?;

    let templates_dir = project_root.join("templates");
    create_dir_if_not_exists(&templates_dir)?;

    let static_dir = project_root.join("static");
    create_dir_if_not_exists(&static_dir)?;

    let output_dir = project_root.join("public"); // Default output dir, not created by `new`

    // 3. Generate default config.toml
    let config_path = project_root.join("config.toml");
    let default_config = SiteConfig::default();
    default_config.save(&config_path)
        .context("Failed to save default config.toml")?;

    // 4. Create a dummy content file
    let dummy_content_path = content_dir.join("index.md");
    fs::write(&dummy_content_path, "# Welcome\n\nThis is your first page.")
        .with_context(|| format!("Failed to create dummy content file: {}", dummy_content_path.display()))?;
    info!("Created dummy content file: {}", dummy_content_path.display());

    // 5. Create a dummy template file
    let dummy_template_path = templates_dir.join("base.html");
    fs::write(&dummy_template_path, "<!DOCTYPE html>\n<html>\n<head><title>{{ config.title }}</title></head>\n<body>\n<h1>{{ config.title }}</h1>\n<p>{{ config.description }}</p>\n</body>\n</html>")
        .with_context(|| format!("Failed to create dummy template file: {}", dummy_template_path.display()))?;
    info!("Created dummy template file: {}", dummy_template_path.display());


    info!("Successfully created new SSG project: {}", args.name);
    Ok(())
}

/// Handles the 'build' command: loads configuration and prepares for site generation.
fn handle_build_command(args: &BuildCommand) -> Result<()> {
    let project_root = &args.path;
    info!("Building site from project root: {}", project_root.display());

    // 1. Load configuration
    let config_path = project_root.join("config.toml");
    let site_config = SiteConfig::load(&config_path)
        .context("Failed to load site configuration")?;

    info!("Site configuration loaded: {:#?}", site_config);

    // Placeholder for the actual build logic in future chapters
    info!("Build command executed. Content processing and rendering will happen here.");

    Ok(())
}

Explanation:

  • Module Imports: We import our newly created cli, config, and utils modules.
  • Logging Initialization: tracing is initialized to provide structured logging. EnvFilter::from_default_env().add_directive(Level::INFO.into()) means that by default, INFO level messages and above will be logged, but this can be overridden by setting the RUST_LOG environment variable (e.g., RUST_LOG=debug cargo run).
  • CLI Parsing: Cli::parse() uses clap to parse command-line arguments into our Cli struct.
  • Command Dispatch: A match statement handles different subcommands, calling handle_new_command or handle_build_command accordingly.
  • handle_new_command:
    • Determines the project_root based on arguments.
    • Uses create_dir_if_not_exists to set up project_root, content, templates, and static directories.
    • Creates a config.toml using SiteConfig::default().save().
    • Adds a dummy content/index.md and templates/base.html to provide immediate, runnable content.
  • handle_build_command:
    • Determines the project_root.
    • Loads the config.toml using SiteConfig::load().
    • Logs the loaded configuration (using info!) for verification.
    • Includes a placeholder message for the actual build logic, which will be implemented in later chapters.
  • Error Handling: anyhow::Result is used as the return type for main and handler functions, simplifying error propagation. The ? operator and .context() calls provide clear error messages.

c) Testing This Component

Let’s test our CLI and configuration setup.

  1. Build the project:

    cargo build
    
  2. Test the new command: From your ssg_builder project root:

    cargo run new my-first-site
    

    You should see output similar to:

    INFO ssg_builder: Starting SSG Builder...
    INFO ssg_builder: Initializing new project at: my-first-site
    INFO ssg_builder::utils: Creating directory: my-first-site
    INFO ssg_builder::utils: Creating directory: my-first-site/content
    INFO ssg_builder::utils: Creating directory: my-first-site/templates
    INFO ssg_builder::utils: Creating directory: my-first-site/static
    INFO ssg_builder::config: Saving configuration to: my-first-site/config.toml
    INFO ssg_builder::config: Configuration saved successfully.
    INFO ssg_builder: Created dummy content file: my-first-site/content/index.md
    INFO ssg_builder: Created dummy template file: my-first-site/templates/base.html
    INFO ssg_builder: Successfully created new SSG project: my-first-site
    

    Verify that a new directory my-first-site has been created, containing content/index.md, templates/base.html, static/, and config.toml. Open my-first-site/config.toml to see the default configuration.

  3. Test the build command: Navigate into your newly created project directory:

    cd my-first-site
    

    Now, run the build command from within my-first-site (the . path argument is implicit):

    cargo run build
    

    You should see output similar to:

    INFO ssg_builder: Starting SSG Builder...
    INFO ssg_builder: Building site from project root: .
    INFO ssg_builder::config: Attempting to load configuration from: config.toml
    INFO ssg_builder::config: Configuration loaded successfully.
    INFO ssg_builder: Site configuration loaded: SiteConfig {
        base_url: "http://localhost:8080",
        title: "My Awesome SSG Site",
        description: "A static site generated by our Rust SSG.",
        source_dir: "content",
        output_dir: "public",
        template_dir: "templates",
        static_dir: "static",
    }
    INFO ssg_builder: Build command executed. Content processing and rendering will happen here.
    

    This confirms that our build command correctly loads the config.toml file.

Production Considerations

Error Handling

We’ve integrated anyhow for simplified error handling. This crate is excellent for application-level errors where you just need to propagate failures with context, rather than defining a complex error hierarchy. For library code, a custom Error enum is often preferred, but for an application’s main flow, anyhow significantly reduces boilerplate. The .context() method adds valuable debugging information to error messages.

Performance Optimization

  • CLI Parsing: clap is highly optimized for performance, making argument parsing negligible in terms of execution time.
  • Configuration Loading: Reading and parsing a TOML file is a fast operation, typically occurring only once at the start of a build process. For large configurations, serde and toml are efficient.
  • File System Operations: While our current utils.rs functions are basic, for very large projects, optimizing file system traversal (e.g., using walkdir for more control, or asynchronous I/O) might be considered. For now, the standard library functions are sufficient and robust.

Security Considerations

  • Configuration Files: config.toml should not contain sensitive information (e.g., API keys, database credentials). If such information is needed for deployment or build-time operations, it should be passed via environment variables, which are not committed to version control.
  • Input Validation: clap handles basic argument type validation. For file paths, ensure that operations respect file system permissions and do not allow arbitrary file access outside the project scope (e.g., preventing directory traversal attacks if user input is directly used in path construction without proper sanitization). Our current PathBuf usage helps mitigate some of these risks.

Logging and Monitoring

We’ve set up tracing for logging. In a production environment, tracing allows for highly configurable logging backends. You could integrate with tracing-appender for file logging, or send logs to external monitoring systems. Using Level::INFO by default provides good visibility into the SSG’s operations without being overly verbose. For debugging, RUST_LOG=debug or RUST_LOG=trace can be used.

Code Review Checkpoint

At this point, we have successfully implemented:

  • src/cli.rs: Defines the new and build commands using clap.
  • src/config.rs: Defines the SiteConfig struct, including default values, and methods to load and save configuration from/to config.toml using serde and toml.
  • src/utils.rs: Provides utility functions for file system operations like creating directories and copying files.
  • src/main.rs: The main entry point that initializes logging, parses CLI arguments, and dispatches to appropriate handlers for new and build commands. It also demonstrates how to initialize a new project with default files and how to load an existing project’s configuration.
  • Cargo.toml: Updated with necessary dependencies (clap, serde, toml, anyhow, tracing, tracing-subscriber).

The project structure now looks like this:

ssg_builder/
├── src/
│   ├── main.rs
│   ├── cli.rs
│   ├── config.rs
│   └── utils.rs
├── Cargo.toml
└── .gitignore

And if you ran cargo run new my-first-site, you’d also have:

my-first-site/
├── content/
│   └── index.md
├── templates/
│   └── base.html
├── static/
├── public/ (will be created on build)
└── config.toml

This setup provides a solid foundation for user interaction and project-specific customization, which are essential for any production-ready SSG.

Common Issues & Solutions

  1. Error: clap parsing failures (e.g., “The following required arguments were not provided: ”)

    • Issue: You might have forgotten to provide the project name for the new command, or used an unknown flag.
    • Solution: Ensure you follow the command syntax. For new, it’s cargo run new <project-name>. For build, it’s cargo run build. Run cargo run -- --help to see all available commands and options.
    • Prevention: clap’s generated help messages are very informative. Encourage users to use --help for each command (e.g., cargo run -- new --help).
  2. Error: toml::de::Error or serde_toml::Error (Failed to parse TOML configuration)

    • Issue: The config.toml file is malformed (e.g., syntax error, missing a required field without a default).
    • Solution: Carefully check your config.toml for syntax errors. Ensure all keys and values are correctly formatted according to TOML specifications. If you modified SiteConfig to remove a #[serde(default = "...")] attribute for a field, that field becomes mandatory in the TOML.
    • Prevention: Use a TOML linter or editor extension. Our SiteConfig uses default attributes for all fields, making it resilient to missing fields.
  3. Error: File system permissions issues (e.g., “Permission denied”)

    • Issue: The SSG tries to create directories or write files in a location where the current user does not have write permissions.
    • Solution:
      • Ensure you have write permissions in the directory where you are running cargo run new.
      • If running build, ensure the output_dir (default public) can be created/written to in the project root.
      • Run the command in a user-owned directory (e.g., your home directory or a project specific folder).
    • Prevention: Design the SSG to operate within a designated project directory, and clearly document permission requirements.

Testing & Verification

To verify everything is correct:

  1. Clean up: Remove the my-first-site directory created earlier:

    rm -rf my-first-site
    
  2. Run new command again:

    cargo run new my-new-project --path ./test_projects
    

    This time, we’re creating it in a test_projects subdirectory.

    • Verify:
      • test_projects/my-new-project directory exists.
      • Inside my-new-project, content/, templates/, static/ directories exist.
      • my-new-project/config.toml exists and contains the default configuration.
      • my-new-project/content/index.md and my-new-project/templates/base.html exist with their dummy content.
  3. Run build command on the new project:

    cargo run build --path ./test_projects/my-new-project
    
    • Verify:
      • The logs show “Site configuration loaded” and print the details of the config.toml from test_projects/my-new-project.
      • No errors are reported.

These steps confirm that our CLI correctly parses arguments, the new command sets up a project structure with a default configuration, and the build command can successfully load that configuration from an arbitrary path.

Summary & Next Steps

In this chapter, we’ve taken significant strides in making our SSG usable and extensible. We designed and implemented a robust CLI using clap, enabling users to initialize new projects and trigger builds. We also established a flexible configuration system with serde and toml, allowing project settings to be easily defined and loaded. The new command now scaffolds a basic project structure, including a default config.toml, dummy content, and a basic template, providing an immediate starting point for development. The build command successfully loads this configuration, preparing the groundwork for the actual site generation process.

This foundation is critical for building a production-ready SSG. With a clear CLI and configuration, developers can interact with our tool efficiently and customize their sites without touching the core SSG code.

In Chapter 3: Content Structure and Frontmatter Parsing, we will delve into how our SSG will consume content. We’ll design a flexible content file structure, implement frontmatter parsing (using serde and yaml-frontmatter), and begin to understand how raw content is transformed into structured data that our SSG can process.