Welcome to the beginning of an exciting journey where we’ll build a modern, high-performance Static Site Generator (SSG) using Rust. This project aims to create a robust, production-ready system inspired by the architectural elegance of tools like Hugo and the component-driven flexibility of Astro, but with the raw speed and safety benefits of Rust. We’ll cover everything from fundamental parsing to advanced deployment strategies, ensuring you gain a deep understanding of SSG internals.

In this first chapter, our focus is on laying the foundational bricks for our SSG project. We will set up a multi-crate Rust workspace, which is a crucial step for building scalable and maintainable applications. By the end of this chapter, you will have a well-organized project structure with a dedicated library crate for core logic and a separate binary crate for our command-line interface (CLI). This modular approach will allow us to manage dependencies efficiently, promote code reuse, and simplify future development and testing.

There are no prerequisites from previous chapters, as this is our starting point. Our expected outcome is a fully functional Rust workspace, ready to accept our initial content parsing logic in the subsequent chapters. We’ll ensure this setup adheres to Rust’s best practices for project organization, preparing us for a production-grade application from day one.

Planning & Design

Building a complex application like an SSG requires careful architectural planning. A multi-crate workspace in Rust is the ideal structure for this, as it allows us to logically separate different functionalities into distinct crates (libraries or binaries) that can depend on each other.

Component Architecture

Our initial architecture will consist of two main crates within a single workspace:

  1. ssg-core (Library Crate): This will house all the core logic of our SSG. This includes content parsing (Markdown, frontmatter), templating, rendering, data models, and utility functions. By separating this into a library, we make it reusable and testable independently of the CLI.
  2. ssg-cli (Binary Crate): This will be the command-line interface application that orchestrates the SSG build process. It will interact with ssg-core to read configuration, process content, and generate the static site.

This separation ensures a clear division of concerns: ssg-core is about how the SSG works, while ssg-cli is about how a user interacts with it.

flowchart TD SSG_Workspace["SSG Project Workspace (Root)"] SSG_Workspace --> SSG_CLI["ssg-cli (Binary Crate)"] SSG_Workspace --> SSG_Core["ssg-core (Library Crate)"] SSG_CLI -->|Depends on| SSG_Core

File Structure

Our initial project structure will look like this:

ssg-builder/
├── Cargo.toml          # Workspace manifest
├── ssg-cli/            # Binary crate for CLI
│   └── Cargo.toml
│   └── src/
│       └── main.rs
└── ssg-core/           # Library crate for core logic
    └── Cargo.toml
    └── src/
        └── lib.rs

Why a Workspace?

  • Modularity: Each crate can focus on a specific concern, making the codebase easier to understand, manage, and scale.
  • Reusability: ssg-core can be used by ssg-cli or potentially by other tools (e.g., a GUI, a test harness).
  • Shared Dependencies: Common dependencies (like serde for serialization) can be managed at the workspace level, ensuring consistent versions and potentially faster compilation.
  • Incremental Compilation: Cargo’s build system is highly optimized for workspaces, speeding up development cycles by only recompiling changed crates.
  • Easier Testing: We can run tests for specific crates, isolating test environments.

Step-by-Step Implementation

Let’s start by creating our workspace and initial crates.

a) Setup/Configuration: Create the Workspace

First, we’ll create the root directory for our project and initialize the workspace Cargo.toml.

  1. Create the project directory: Open your terminal and run:

    mkdir ssg-builder
    cd ssg-builder
    

    This creates our ssg-builder directory, which will serve as the root of our Rust workspace.

  2. Create the workspace Cargo.toml: Inside the ssg-builder directory, create a file named Cargo.toml with the following content:

    # ssg-builder/Cargo.toml
    [workspace]
    members = [
        "ssg-core",
        "ssg-cli",
    ]
    
    # Optional: Define workspace-wide dependencies or settings here.
    # We'll add common dependencies later when needed.
    [workspace.dependencies]
    # Example: serde = { version = "1.0", features = ["derive"] }
    

    Explanation:

    • [workspace] declares this Cargo.toml as the manifest for a workspace.
    • members = [...] lists the directories that contain the Cargo.toml files for each crate belonging to this workspace. We’ll create these directories and their Cargo.toml files next.
    • [workspace.dependencies] is a feature in modern Cargo that allows you to define common dependencies for all workspace members. This helps ensure consistent versions and reduces boilerplate. We’ll leverage this in future chapters.

b) Core Implementation: Add the ssg-core Library Crate

Now, let’s create our core library crate, ssg-core.

  1. Create the ssg-core library crate: From the ssg-builder root directory, run:

    cargo new ssg-core --lib
    

    This command creates a new directory ssg-core with its own Cargo.toml and a basic src/lib.rs. The --lib flag specifies that it should be a library crate, not a binary.

  2. Inspect ssg-core/Cargo.toml: The generated ssg-core/Cargo.toml will look something like this:

    # ssg-builder/ssg-core/Cargo.toml
    [package]
    name = "ssg-core"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    # Dependencies specific to ssg-core will go here
    

    Explanation: This is a standard Cargo.toml for a library crate. We’ll add specific dependencies for parsing and templating here as we progress.

  3. Add initial code to ssg-core/src/lib.rs: Let’s add a simple function to ssg-core to demonstrate its functionality.

    // ssg-builder/ssg-core/src/lib.rs
    
    /// Initializes the SSG core processing.
    ///
    /// In future chapters, this function will orchestrate the entire
    /// static site generation process, including content parsing,
    /// template rendering, and output generation.
    ///
    /// # Returns
    /// A `Result` indicating success or failure of the initialization.
    /// For now, it always returns `Ok`.
    pub fn initialize_ssg_core() -> Result<(), String> {
        println!("[ssg-core] Core initialization started...");
        // In a production scenario, we'd have proper logging here.
        // For now, `println!` serves as a simple log.
    
        // Placeholder for future complex initialization logic
        // e.g., loading configurations, setting up caches, etc.
    
        println!("[ssg-core] Core initialization complete.");
        Ok(())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_initialize_ssg_core_success() {
            // Test that the function returns Ok(())
            assert!(initialize_ssg_core().is_ok());
        }
    }
    

    Explanation:

    • pub fn initialize_ssg_core(): We define a public function that can be called from other crates.
    • Result<(), String>: This indicates that the function either succeeds (returns Ok(())) or fails with an error message (returns Err(String)). This is a fundamental Rust pattern for error handling and will be used extensively throughout our project.
    • println!: For now, we use println! for simple logging. In a production environment, we would integrate a robust logging crate like tracing or log.
    • #[cfg(test)] mod tests: This block contains unit tests for our library. cargo test will automatically discover and run these. We’ve added a basic test to ensure our function returns successfully.

c) Core Implementation: Add the ssg-cli Binary Crate

Next, we’ll create our command-line application crate, ssg-cli, and make it depend on ssg-core.

  1. Create the ssg-cli binary crate: From the ssg-builder root directory, run:

    cargo new ssg-cli
    

    This command creates a new directory ssg-cli with its own Cargo.toml and a basic src/main.rs. By default, cargo new creates a binary crate.

  2. Inspect ssg-cli/Cargo.toml: The generated ssg-cli/Cargo.toml will look like this:

    # ssg-builder/ssg-cli/Cargo.toml
    [package]
    name = "ssg-cli"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    # Dependencies specific to ssg-cli will go here
    
  3. Add ssg-core as a dependency to ssg-cli: Edit ssg-cli/Cargo.toml to include ssg-core in its dependencies.

    # ssg-builder/ssg-cli/Cargo.toml
    [package]
    name = "ssg-cli"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    ssg-core = { path = "../ssg-core" } # Path dependency to our library crate
    

    Explanation:

    • ssg-core = { path = "../ssg-core" }: This defines a path dependency. It tells Cargo that the ssg-core crate is located relative to this Cargo.toml at ../ssg-core. This is how crates within the same workspace depend on each other.
  4. Add initial code to ssg-cli/src/main.rs: Now, let’s modify ssg-cli/src/main.rs to call the function from ssg-core.

    // ssg-builder/ssg-cli/src/main.rs
    
    use ssg_core::initialize_ssg_core; // Import the public function from ssg-core
    
    fn main() {
        println!("[ssg-cli] Starting SSG build process...");
    
        // Call the core initialization function
        match initialize_ssg_core() {
            Ok(_) => {
                println!("[ssg-cli] SSG core initialized successfully.");
                // Placeholder for actual build logic in future chapters
                println!("[ssg-cli] Static site generation complete (placeholder).");
            },
            Err(e) => {
                eprintln!("[ssg-cli] Error initializing SSG core: {}", e);
                // Exit with a non-zero status code to indicate failure
                std::process::exit(1);
            }
        }
    }
    

    Explanation:

    • use ssg_core::initialize_ssg_core;: This line brings our initialize_ssg_core function into scope, allowing ssg-cli to use it.
    • match initialize_ssg_core(): We use a match statement to handle the Result returned by initialize_ssg_core(). This is standard Rust error handling.
    • eprintln!: This macro prints to standard error, which is appropriate for error messages in CLI applications.
    • std::process::exit(1): Exiting with a non-zero status code is a standard practice for indicating that a program terminated with an error.

d) Testing This Component: Verify Setup

With our workspace and initial crates set up, let’s verify everything is working as expected.

  1. Build the entire workspace: From the ssg-builder root directory, run:

    cargo build
    

    This command will compile both ssg-core and ssg-cli. You should see output indicating that both crates are being built. Cargo’s workspace awareness ensures that ssg-core is built before ssg-cli because ssg-cli depends on it.

  2. Run the ssg-cli application: To run a specific binary within a workspace, use the -p (or --package) flag:

    cargo run -p ssg-cli
    

    You should see the following output:

    [ssg-cli] Starting SSG build process...
    [ssg-core] Core initialization started...
    [ssg-core] Core initialization complete.
    [ssg-cli] SSG core initialized successfully.
    [ssg-cli] Static site generation complete (placeholder).
    

    This confirms that ssg-cli successfully called the function from ssg-core.

  3. Run tests for ssg-core: To run tests for a specific crate:

    cargo test -p ssg-core
    

    You should see output similar to this, indicating the test passed:

    running 1 test
    [ssg-core] Core initialization started...
    [ssg-core] Core initialization complete.
    test tests::test_initialize_ssg_core_success ... ok
    
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    

    This verifies that our ssg-core library’s tests are correctly configured and pass.

Production Considerations

Even at this early stage, our project setup incorporates several production best practices:

  • Modularity and Maintainability: The workspace structure ensures a clear separation of concerns, making the codebase easier to navigate, understand, and maintain as it grows. New features can be added as separate crates (e.g., ssg-parser, ssg-renderer), preventing a monolithic application.
  • Performance (Build Times): Cargo’s incremental compilation within a workspace means that if you only change code in ssg-core, only ssg-core and its dependents (ssg-cli in this case) will be recompiled. This significantly speeds up development cycles compared to recompiling an entire monolithic application.
  • Scalability: The architecture easily scales to include more complex features. For example, a future plugin system could involve dynamically loaded crates, which is more manageable within a workspace.
  • Error Handling: By immediately introducing Rust’s Result type for error handling, we are building a foundation for robust error management, which is critical for production-ready applications. We will enhance this with custom error types and propagation as the project evolves.
  • Logging: While we’re using println! for now, the [crate-name] prefix in our output mimics structured logging, making it easier to parse and understand logs in a production environment. We will replace this with a proper logging framework (like tracing) in a later chapter.

Code Review Checkpoint

At this point, we have successfully:

  • Created the ssg-builder root directory.
  • Defined the workspace Cargo.toml.
  • Created the ssg-core library crate with a placeholder function and a unit test.
  • Created the ssg-cli binary crate.
  • Configured ssg-cli to depend on ssg-core using a path dependency.
  • Implemented ssg-cli to call the ssg-core function and handle its Result.

Your project structure should now look exactly like this:

ssg-builder/
├── Cargo.toml
├── ssg-cli/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
└── ssg-core/
    ├── Cargo.toml
    └── src/
        └── lib.rs

This setup is the backbone of our SSG, providing a solid, extensible foundation for all future development.

Common Issues & Solutions

  1. error: failed to get ssg-coreas a dependency ofssg-cli`

    • Issue: This typically means there’s a typo in the path specified in ssg-cli/Cargo.toml or the ssg-core directory doesn’t exist where Cargo expects it.
    • Solution: Double-check the path = "../ssg-core" entry in ssg-builder/ssg-cli/Cargo.toml. Ensure the ssg-core directory is directly adjacent to ssg-cli within the ssg-builder root. Also, verify that ssg-core/Cargo.toml exists.
  2. error[E0432]: unresolved import ssg_core::initialize_ssg_core``

    • Issue: This means ssg-cli cannot find the initialize_ssg_core function.
    • Solution:
      • Ensure ssg-core is listed in ssg-cli/Cargo.toml under [dependencies].
      • Verify that initialize_ssg_core in ssg-core/src/lib.rs is marked pub (public). Without pub, it’s private to the ssg-core crate.
      • Check for typos in the use statement in ssg-cli/src/main.rs.
  3. error: package ssg-core is not a member of the workspace

    • Issue: You might see this if you try to run cargo build -p ssg-core without ssg-core being listed in the workspace Cargo.toml.
    • Solution: Ensure that ssg-core (and ssg-cli) are correctly listed under the members array in ssg-builder/Cargo.toml.

Testing & Verification

To ensure everything is correctly configured and working, perform the following checks from the ssg-builder root directory:

  1. Full Workspace Build:

    cargo build
    

    Expected: The build should complete successfully without errors, compiling both ssg-core and ssg-cli.

  2. Run the CLI Application:

    cargo run -p ssg-cli
    

    Expected: You should see the console output indicating ssg-cli starting, ssg-core initializing, and then ssg-cli completing its (placeholder) process.

    [ssg-cli] Starting SSG build process...
    [ssg-core] Core initialization started...
    [ssg-core] Core initialization complete.
    [ssg-cli] SSG core initialized successfully.
    [ssg-cli] Static site generation complete (placeholder).
    
  3. Run Core Library Tests:

    cargo test -p ssg-core
    

    Expected: All tests within ssg-core should pass. You should see test result: ok. 1 passed; 0 failed; ....

If all these steps pass, your Rust workspace is successfully set up and ready for the next phase of development!

Summary & Next Steps

In this chapter, we’ve successfully established the foundational structure for our Rust SSG using a multi-crate workspace. We created ssg-core for our library logic and ssg-cli as the entry point, demonstrating how they interact. This modular setup is critical for building a production-grade application that is maintainable, scalable, and performant.

This initial setup doesn’t build any static sites yet, but it provides the robust framework upon which we will build everything else. The clear separation of concerns and the use of Rust’s powerful Result type for error handling are vital best practices we’ve adopted from the outset.

In Chapter 2: Parsing Frontmatter with Serde, we will dive into handling content files. We’ll learn how to read content, parse structured metadata (frontmatter) using serde with YAML and TOML, and define our initial content data structures. This will be our first step towards processing real-world content for our SSG.