Banner of Replaying CSS Builds: A Recipe-Based Manager for Pelican Themes

Replaying CSS Builds: A Recipe-Based Manager for Pelican Themes


Category: Programming » Web Development

📅 June 01, 2026   |   👁️ Views: 22

Author:   mosaid

The Render‑Blocking Problem

Every CSS file a browser encounters before rendering the page adds a network round‑trip and delays the first paint. In a Pelican theme, it’s common to split styles across many small files – typography, code highlighting, comments, special layouts. The result is a handful of <link> tags in the <head>, each a render‑blocking request.

During development I found myself staring at Lighthouse reports that pointed to exactly this overhead. I had the source files in separate, well‑organised chunks, but serving them individually was hurting performance. The answer is obvious: bundle them into a single file. But how do you maintain that bundle while you’re actively editing the originals?

The Development Loop That Slowed Me Down

The manual workflow looked like this:



# 1. Edit one of the source CSS files
vim theme/static/css/mycss/articles-cards.css

# 2. Concatenate all the pieces
cat base.css ... articles-cards.css ... > bundle.css

# 3. Purge unused selectors (if I remembered)
npx purgecss --css bundle.css --content output/**/*.html --output bundle.purged.css

# 4. Minify
npx csso bundle.purged.css --output bundle.min.css --comments none

# 5. Rebuild Pelican (slow, ~20–40 seconds)
make html

# 6. Copy the final file into the output directory
cp bundle.min.css output/theme/css/newcss/

# 7. Refresh the browser

That’s seven steps, two manual command invocations that I would regularly forget, and a full Pelican rebuild just to see whether my CSS change had the desired effect. The loop was slow, error‑prone, and frustrating.

The real pain came when I wanted to test several variants. I would create bundles for different templates (articles, courses, blogs), purge them against their respective content, and then have to re‑run the whole chain after every small tweak. I needed a tool that could remember what I did and do it again automatically.

The CSS Bundle Experiment Manager

I wrote a Python script that turns the manual workflow into a reproducible pipeline. It stores every operation – combine, purge, minify – as a recipe and can replay the full sequence from the original source files all the way to the deployed result. The script lives in the project root and uses an interactive CLI.

Here’s what a typical session looks like:



$ python css_manager.py
================================================================================
CSS BUNDLE EXPERIMENT MANAGER
================================================================================
 1) List all assets
 2) Create a bundle (combine)
 3) Show bundle details
 4) Show bundle tree
 5) Delete a managed asset
 6) Purge a CSS file (purgecss)
 7) Minify a CSS file (csso)
 8) Copy a managed asset to site dirs
 9) Deploy again (replay & copy to newcss)
10) Exit

Choice: 9

After building a bundle once (options 2, 6, 7), option 9 replays everything and copies the output to output/theme/css/newcss/ – the directory my template references during development. I can edit any source file, press 9, and refresh the browser. No Pelican rebuild, no manual concatenation.

Architecture

The manager tracks two types of assets:

Original assets: the source CSS files listed in a BASE_FILES configuration list. These are read‑only from the tool’s perspective.

Managed assets: any CSS file produced by combining, purging, or minifying. Each managed asset carries a recipe that describes exactly how it was built.

The metadata is stored in a JSON file (css-experiments/managed/metadata.json) that survives across sessions. The in‑memory representation is a simple dictionary keyed by absolute path:



{
  "/path/to/bundle.css": {
    "type": "combined",
    "sources": ["/path/to/base.css", "/path/to/cards.css", ...],
    "recipe": {
      "operation": "combine",
      "sources": ["/path/to/base.css", "/path/to/cards.css"]
    }
  }
}

Workflow: From Source to Deployed Bundle

1. Combine

You choose two or more assets (originals or other managed bundles) and give the output a name. The script concatenates them with clear delimiter comments and saves the result in a managed directory (css-experiments/managed/). The sources list is flattened: if you include a bundle that itself was created from A and B, the final metadata lists A and B as sources, not the intermediate bundle. This gives you a clean dependency tree.

2. Purge

Select a CSS file (original or managed) and choose one or more content patterns that cover the HTML pages where the styles will be used. The script runs purgecss and registers the output as a new managed asset. The recipe stores the source and the content patterns so they can be reused later.

3. Minify

Similar to purging – pick a CSS file, choose an output name, and the script runs csso. The recipe records the source file.

4. Copy

The generic copy option (8) lets you copy any managed asset to standard site directories. For quick tests, though, I rely on option 9 – it always copies into output/theme/css/newcss/.

The Recipe System

The heart of the tool is the replay_asset function. It walks the recipe graph recursively, ensuring that every upstream dependency is rebuilt before the target asset is regenerated. This guarantees that if you edit articles-cards.css and ask to redeploy a bundle that includes it, the bundle is reassembled from the updated source.

Replay logic for each operation:

  • Combine: re‑reads every source file and writes the concatenated output.
  • Purge: invokes purgecss with the stored content patterns.
  • Minify: invokes csso.

Because the recipes are plain JSON, the system is fully transparent. You can inspect or even hand‑edit metadata.json to tweak a pipeline without going through the interactive menu.

“Deploy Again” – The Shortcut That Saves the Day

The deploy_again function is a one‑shot replay and deploy. It presents a list of replayable assets, lets you pick one, then calls replay_asset and copies the resulting file to output/theme/css/newcss/. This is the only option I use during active development.

Here’s the code that makes it possible:



def replay_asset(asset_key, _visited=None):
    if _visited is None:
        _visited = set()
    if asset_key in _visited:
        print("⚠ Circular dependency detected, skipping.")
        return True
    _visited.add(asset_key)

    info = metadata[asset_key]
    if info["type"] == "original":
        return True

    recipe = info.get("recipe")
    if not recipe:
        print(f"❌ No recipe stored for {short_name(asset_key)} – cannot replay.")
        return False

    op = recipe["operation"]

    if op == "combine":
        for src_key in recipe["sources"]:
            if metadata[src_key]["type"] != "original":
                replay_asset(src_key, _visited)
        # ... write concatenated file ...

    elif op == "purge":
        src_key = recipe["source"]
        if metadata[src_key]["type"] != "original":
            replay_asset(src_key, _visited)
        # ... run purgecss ...

    elif op == "minify":
        src_key = recipe["source"]
        if metadata[src_key]["type"] != "original":
            replay_asset(src_key, _visited)
        # ... run csso ...

The recursion guarantees that if your minified bundle depends on a purged file that depends on a combined bundle, the entire chain is executed in the correct order.

Integration with Pelican

I modified my theme templates to include a single CSS link that points to the file inside newcss/. For example, in article.html:



<link rel="stylesheet" href="{{ SITEURL }}/theme/css/newcss/article-bundle.min.css">

During development, that file is generated by the manager and overwritten on every “deploy again”. Once I’m satisfied, I copy the final bundle into the standard theme CSS folder, update the template link, and rebuild the full site to freeze the production version.

Performance Impact

Consolidating multiple CSS files into one per template eliminated all extra render‑blocking requests. For my article template, the number of CSS requests dropped from 7 to 1. Combined with purging, the total CSS payload shrank by roughly 40% because unused Bootstrap and syntax‑highlighting rules were removed.

The replay itself is fast: a combine + purge + minify pipeline takes about 2–3 seconds on my machine. That’s compared to the 20‑second Pelican rebuild I used to endure.

Why Not Use an Existing Bundler?

Tools like Webpack or Parcel are overkill for a static site that’s already assembled by Pelican. I wanted something that:

  • Understands Pelican’s output directory structure
  • Works with pre‑existing CSS files without a build step
  • Keeps the pipeline transparent and inspectable
  • Integrates with the CLI rather than a configuration file

The recipe system is the differentiator. It’s not just a build script; it’s a build journal that can be replayed on demand.

Potential Improvements

The script is currently a standalone interactive tool. In the future I plan to add:

Watch mode: use watchdog or a simple polling loop to replay recipes automatically when source files change.

Multiple output profiles: support deploying to different directories for different environments (staging, production).

Diff‑aware purging: only re‑purge if the source CSS or HTML content has changed.

Final Thoughts

This tool removed the biggest friction point in my Pelican theme development. I can now iterate on styles as fast as I can switch to the browser and hit refresh. The recipe system means I never lose track of how a bundle was built, and I can hand off the entire pipeline to a colleague (or my future self) by just sharing the metadata.json file.

The source code is available in my Pelican tools repository. If you’re battling render‑blocking CSS or a slow build‑and‑test loop, this approach might fit your workflow as well as it did mine.


← The Ultimate Neovim Configuration: A Modular, Reproducible Developer Environment 5 Reasons Most People Stick With Windows Even Though Linux Is Free →

Leave a comment