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:
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.ssg-cli(Binary Crate): This will be the command-line interface application that orchestrates the SSG build process. It will interact withssg-coreto 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.
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-corecan be used byssg-clior potentially by other tools (e.g., a GUI, a test harness). - Shared Dependencies: Common dependencies (like
serdefor 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.
Create the project directory: Open your terminal and run:
mkdir ssg-builder cd ssg-builderThis creates our
ssg-builderdirectory, which will serve as the root of our Rust workspace.Create the workspace
Cargo.toml: Inside thessg-builderdirectory, create a file namedCargo.tomlwith 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 thisCargo.tomlas the manifest for a workspace.members = [...]lists the directories that contain theCargo.tomlfiles for each crate belonging to this workspace. We’ll create these directories and theirCargo.tomlfiles 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.
Create the
ssg-corelibrary crate: From thessg-builderroot directory, run:cargo new ssg-core --libThis command creates a new directory
ssg-corewith its ownCargo.tomland a basicsrc/lib.rs. The--libflag specifies that it should be a library crate, not a binary.Inspect
ssg-core/Cargo.toml: The generatedssg-core/Cargo.tomlwill 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 hereExplanation: This is a standard
Cargo.tomlfor a library crate. We’ll add specific dependencies for parsing and templating here as we progress.Add initial code to
ssg-core/src/lib.rs: Let’s add a simple function tossg-coreto 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 (returnsOk(())) or fails with an error message (returnsErr(String)). This is a fundamental Rust pattern for error handling and will be used extensively throughout our project.println!: For now, we useprintln!for simple logging. In a production environment, we would integrate a robust logging crate liketracingorlog.#[cfg(test)] mod tests: This block contains unit tests for our library.cargo testwill 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.
Create the
ssg-clibinary crate: From thessg-builderroot directory, run:cargo new ssg-cliThis command creates a new directory
ssg-cliwith its ownCargo.tomland a basicsrc/main.rs. By default,cargo newcreates a binary crate.Inspect
ssg-cli/Cargo.toml: The generatedssg-cli/Cargo.tomlwill 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 hereAdd
ssg-coreas a dependency tossg-cli: Editssg-cli/Cargo.tomlto includessg-corein 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 crateExplanation:
ssg-core = { path = "../ssg-core" }: This defines a path dependency. It tells Cargo that thessg-corecrate is located relative to thisCargo.tomlat../ssg-core. This is how crates within the same workspace depend on each other.
Add initial code to
ssg-cli/src/main.rs: Now, let’s modifyssg-cli/src/main.rsto call the function fromssg-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 ourinitialize_ssg_corefunction into scope, allowingssg-clito use it.match initialize_ssg_core(): We use amatchstatement to handle theResultreturned byinitialize_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.
Build the entire workspace: From the
ssg-builderroot directory, run:cargo buildThis command will compile both
ssg-coreandssg-cli. You should see output indicating that both crates are being built. Cargo’s workspace awareness ensures thatssg-coreis built beforessg-clibecausessg-clidepends on it.Run the
ssg-cliapplication: To run a specific binary within a workspace, use the-p(or--package) flag:cargo run -p ssg-cliYou 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-clisuccessfully called the function fromssg-core.Run tests for
ssg-core: To run tests for a specific crate:cargo test -p ssg-coreYou 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.00sThis verifies that our
ssg-corelibrary’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, onlyssg-coreand its dependents (ssg-cliin 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
Resulttype 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 (liketracing) in a later chapter.
Code Review Checkpoint
At this point, we have successfully:
- Created the
ssg-builderroot directory. - Defined the workspace
Cargo.toml. - Created the
ssg-corelibrary crate with a placeholder function and a unit test. - Created the
ssg-clibinary crate. - Configured
ssg-clito depend onssg-coreusing a path dependency. - Implemented
ssg-clito call thessg-corefunction and handle itsResult.
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
error: failed to getssg-coreas a dependency ofssg-cli`- Issue: This typically means there’s a typo in the
pathspecified inssg-cli/Cargo.tomlor thessg-coredirectory doesn’t exist where Cargo expects it. - Solution: Double-check the
path = "../ssg-core"entry inssg-builder/ssg-cli/Cargo.toml. Ensure thessg-coredirectory is directly adjacent tossg-cliwithin thessg-builderroot. Also, verify thatssg-core/Cargo.tomlexists.
- Issue: This typically means there’s a typo in the
error[E0432]: unresolved importssg_core::initialize_ssg_core``- Issue: This means
ssg-clicannot find theinitialize_ssg_corefunction. - Solution:
- Ensure
ssg-coreis listed inssg-cli/Cargo.tomlunder[dependencies]. - Verify that
initialize_ssg_coreinssg-core/src/lib.rsis markedpub(public). Withoutpub, it’s private to thessg-corecrate. - Check for typos in the
usestatement inssg-cli/src/main.rs.
- Ensure
- Issue: This means
error: packagessg-coreis not a member of the workspace- Issue: You might see this if you try to run
cargo build -p ssg-corewithoutssg-corebeing listed in the workspaceCargo.toml. - Solution: Ensure that
ssg-core(andssg-cli) are correctly listed under themembersarray inssg-builder/Cargo.toml.
- Issue: You might see this if you try to run
Testing & Verification
To ensure everything is correctly configured and working, perform the following checks from the ssg-builder root directory:
Full Workspace Build:
cargo buildExpected: The build should complete successfully without errors, compiling both
ssg-coreandssg-cli.Run the CLI Application:
cargo run -p ssg-cliExpected: You should see the console output indicating
ssg-clistarting,ssg-coreinitializing, and thenssg-clicompleting 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).Run Core Library Tests:
cargo test -p ssg-coreExpected: All tests within
ssg-coreshould pass. You should seetest 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.