Super-Simple Includes Documentation

v0.244.0

Conditional Outcomes Without Conditionals

Conditional Outcomes Without Conditionals

SSI has no if/else syntax. This is intentional — conditional logic in templates leads to complexity, unexpected interactions, and hard-to-debug output. Instead, SSI provides four composable patterns that cover the most common conditional use cases without any branching in templates.

Pattern 1: Defaults-to-Empty

The most common conditional need is “add this class/attribute/snippet for some pages but not others.” SSI handles this by setting a default of empty string in the [_] section, then overriding it on specific pages.

Config (strings.toml):

[_]
                active_nav_home = ""
                active_nav_about = ""
                active_nav_blog = ""
                
                [index]
                active_nav_home = " nav--active"
                
                [about]
                active_nav_about = " nav--active"
                
                [blog]
                active_nav_blog = " nav--active"
                

Template (shared nav block, blocks/nav.html):

<nav>
                    <a href="index.html" class="nav-link💬active_nav_home">Home</a>
                    <a href="about.html" class="nav-link💬active_nav_about">About</a>
                    <a href="blog.html" class="nav-link💬active_nav_blog">Blog</a>
                </nav>
                

For index.html, the result is:

<nav>
                    <a href="index.html" class="nav-link nav--active">Home</a>
                    <a href="about.html" class="nav-link">About</a>
                    <a href="blog.html" class="nav-link">Blog</a>
                </nav>
                

The token 💬active_nav_about resolves to empty string, leaving just class="nav-link". The space before the class name is part of the value in strings.toml, so it only appears when the value is non-empty.

Other uses for defaults-to-empty:

  • Optional <meta> content (description, og:image)
  • Optional HTML attributes (aria-current="page", data-*)
  • Optional body classes or section modifiers
  • Optional inline content sections

See Defaults and Fallbacks for the full mechanics.

Pattern 2: Per-Page Include Selection

For larger layout variations — different hero sections, different sidebar content, different page structures — use per-page directory resolution. Each page automatically gets its own version of an include token.

Directory layout:

layouts/
                ├── sidebar.html          ← default (pages with no override get this)
                ├── index/
                │   └── sidebar.html     ← only for index.html
                └── docs/
                    └── sidebar.html     ← only for docs.html
                

Config:

[[step]]
                emoji = "📎"
                path = "layouts/"
                processing = "include"
                type = "html"
                

Template (identical for all pages):

<body>
                    <main>📎content</main>
                    <aside>📎sidebar</aside>
                </body>
                

Each page picks up its own sidebar. Pages with no subdirectory get the default. Adding a new variant is just adding a new subdirectory — no template changes required.

This pattern also works for:

  • Full page layouts (a token that expands to the entire page structure)
  • Hero sections (image-backed vs text-only)
  • Call-to-action blocks (page-specific vs shared fallback)
  • Schema.org markup (product page vs article vs generic)

Pattern 3: Ordered Token Replacement

Because SSI processes include sources in order, content inserted by an earlier source can contain tokens that a later source resolves. This lets one include “activate” or “configure” another.

Example: Per-page data configuring a shared component

A shared block uses a token from a later source:

<!-- blocks/product-card.html -->
                <article class="card card--💬product_type">
                    <h2>💬product_name</h2>
                    <p>💬product_desc</p>
                </article>
                

A TOML file provides the values:

# data.toml
                [laptop]
                product_type = "hardware"
                product_name = "Pro Laptop"
                product_desc = "Fast, lightweight, and reliable."
                
                [phone]
                product_type = "hardware"
                product_name = "Compact Phone"
                product_desc = "Fits in your pocket."
                
                [accessories]
                product_type = "accessories"
                product_name = "Accessories"
                product_desc = "Cables, cases, and more."
                

Config order matters:

[[step]]
                emoji = "📎"
                path = "blocks/"
                processing = "include"
                type = "html"
                
                [[step]]
                emoji = "💬"
                path = "data.toml"
                processing = "include"
                type = "plain"
                options = ["inline"]
                

The 📎product-card step runs first and inserts the block — including its 💬 tokens — into the template. Then the 💬 step runs and resolves those tokens using the page-specific section from data.toml.

The result: a shared component whose appearance is driven by per-page data, with no conditionals in either the component or the template.

Pattern 4: Conditional Section via Fallback Gate

For cases where an entire HTML section should appear or disappear based on whether a data source is active, combine the fallback field with step ordering and SSI’s chain-reaction mechanism.

Use case: an events banner that appears only when events are scheduled.

The gate TOML (events-gate.toml) — present when an event is active:

[_]
                show-banner = "📢event-banner"
                

The empty fallback (fallbacks/events-empty.toml) — always in the repository:

[_]
                show-banner = ""
                

Config (step order matters):

[[step]]
                emoji = "🚩"
                path = "events-gate.toml"
                fallback = "fallbacks/events-empty.toml"
                processing = "include"
                type = "plain"
                options = ["inline"]
                
                # The 📢 step MUST come after 🚩 so the chained token is present when 📢 runs
                [[step]]
                emoji = "📢"
                path = "content/"
                processing = "include"
                type = "html"
                options = ["leftovers-okay"]
                

How the chain reaction works:

When events-gate.toml is present, the 🚩 step inlines show-banner = "📢event-banner" into the page — placing a real 📢event-banner include token where 🚩show-banner appeared. The 📢 step then runs and resolves that token to the banner HTML from content/event-banner.html.

When events-gate.toml is absent, fallback activates and fallbacks/events-empty.toml provides show-banner = "". The empty string replaces 🚩show-banner, leaving no 📢 token for the banner step to find. The banner disappears without any change to the page template or banner HTML.

Step-ordering requirement: the gate step must precede the include step. If 📢 ran before 🚩, the chain-reaction token would not yet exist and the banner would never appear — even when events-gate.toml is present.

fallback fires on file absence, not on empty content. The fallback field activates only when the primary file is missing. An existing file whose value is "" does not trigger fallback. This is why the empty fallback must be a separate file that is always present, not an empty primary file.

Emoji characters in TOML values are not HTML-escaped. The value "📢event-banner" passes through the inline step as a raw string, so the emoji character becomes a real include token for the next step to resolve. TOML plain values are never HTML-escaped, which is what makes chaining work.

This pattern is build-time only. Add or remove events-gate.toml and redeploy to change which sections appear. For runtime variation, use JavaScript.

See example 19 and Include Sources — fallback for full details.

What SSI Cannot Do

These patterns cover the common 90%. Some conditional behaviors genuinely require a different tool:

  • Loops — generating a list of N items from a data source. SSI requires one token per item; use a script to generate the content directory from your data instead.
  • Dynamic conditionals at view time — content that changes per visitor. SSI processes at build time; use JavaScript for runtime variation.
  • Computed values — math, string manipulation, or logic during the build. Pre- process your data into the values you need before running SSI.

For sites that need auto-generated pages from structured data, a planned future feature (auto-templates) will generate page files from content directories automatically.