Skip to main content

Narrative CMS: Major Performance Improvements and Feature Enhancements

DRAFT

About the author: I'm Charles Sieg, a cloud architect and platform engineer who builds apps, services, and infrastructure for Fortune 1000 clients through Vantalect. If your organization is rethinking its software strategy in the age of AI-assisted engineering, let's talk.

Today's update to Narrative CMS includes multiple performance optimizations, intelligent caching systems, and enhanced features that transform the development experience from "fast enough" to "instant feedback."


Table of Contents

Overview

Today's development session yielded eight improvements spanning performance, features, and documentation.

Image Optimization with WebP Support

Narrative now automatically optimizes images and generates WebP versions for browsers that support them.

What This Means

When you add an image to your content, Narrative:

  • Optimizes it in its original format (JPEG, PNG, or GIF)
  • Generates a WebP version (typically 25-35% smaller)
  • Renders it as a responsive <picture> element in your HTML
  • Delivers WebP to modern browsers, original format as fallback

Example

Write this in your Markdown: markdown ![My Photo](images/photo.jpg)

Narrative generates: html <picture> <source srcset="images/photo.webp" type="image/webp"> <img src="images/photo.jpg" alt="My Photo"> </picture>

Modern browsers use the smaller WebP version. Older browsers get the optimized JPEG.

Intelligent Incremental Builds

Incremental builds now regenerate only the files that changed.

The Problem

Traditional static site generators rebuild everything on every change. A single-word edit in one blog post triggers regeneration of all 300+ pages. This meant 5-7 second build times even for tiny edits.

The Solution

Dependency tracking that understands content relationships:

When you edit a blog post:

  • Rebuilds only that post
  • Rebuilds the blog index (since it lists all posts)
  • Rebuilds affected tag pages
  • Rebuilds pagination pages
  • Skips everything else

When you edit a stylesheet:

  • Recompiles CSS only
  • No HTML regeneration

When you add an image:

  • Optimizes that image only
  • No HTML regeneration

Performance Impact

Before:

  • Any file change: 5-7 seconds (full rebuild)

After:

  • Single post edit: < 1 second
  • Stylesheet change: < 1 second
  • Image addition: < 1 second
  • Multiple changes: Still < 2 seconds

Save a file and see changes immediately.

Incremental Deployments

Deployment speed received the same treatment as build performance.

Hash-Based Change Detection

Narrative now tracks MD5 hashes of every deployed file. When you deploy:

  1. Computes hash of each file
  2. Compares with hash from last deployment
  3. Uploads only files that changed
  4. Skips unchanged files entirely

Performance Impact

Before:

  • Every deployment uploaded all 150 files (~30 seconds)

After:

  • Changed one file: Upload 1 file (< 2 seconds)
  • Changed five files: Upload 5 files (< 5 seconds)
  • 90%+ reduction in deployment time

Modular Pipeline Architecture

The build system now uses a pipeline architecture with three independent processors:

Image Pipeline: Handles image optimization with aggressive caching. Once an image is optimized, it is never re-optimized, even across different sites.

Stylesheet Pipeline: Compiles SCSS to CSS. Always recompiles due to @import dependencies, but skips HTML generation.

Content Pipeline: Parses Markdown, renders templates, generates HTML. Only runs when content or templates change.

Each pipeline runs independently. A failure in one pipeline does not stop the others. This isolation makes the system easier to maintain.

Enhanced Code Tabs Shortcode

The code_tabs shortcode gained two new features for technical writing.

Feature 1: Captions

Add explanatory text below code examples:

<div class="code-tabs mb-8" id="code-tabs-4566">
  <div class="flex flex-wrap border-b border-gray-200 dark:border-gray-700 mb-0">
    <button onclick="switchTab('code-tabs-4566', 0)" class="tab-button px-4 py-2 font-medium text-sm transition-colors bg-gray-50 dark:bg-gray-600 border-b-2 border-primary-600 text-primary-600 dark:text-gray-100" data-tab="0">Python</button>
    <button onclick="switchTab('code-tabs-4566', 1)" class="tab-button px-4 py-2 font-medium text-sm transition-colors text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" data-tab="1">Terraform</button>
  </div>
  <div class="tab-panels">
    <div class="tab-panel block" data-tab="0">
      <div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">pulumi_aws</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">aws</span>
<span class="n">lambda_fn</span> <span class="o">=</span> <span class="n">aws</span><span class="o">.</span><span class="n">lambda_</span><span class="o">.</span><span class="n">Function</span><span class="p">(</span><span class="s2">&quot;my-function&quot;</span><span class="p">)</span>
</pre></div>

    </div>
    <div class="tab-panel hidden" data-tab="1">
      <div class="highlight"><pre><span></span><span class="kr">resource</span><span class="w"> </span><span class="nc">&quot;aws_lambda_function&quot;</span><span class="w"> </span><span class="nv">&quot;example&quot;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">function_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;my-function&quot;</span>
<span class="p">}</span>
</pre></div>

    </div>
  </div>
  <div class="figcaption mt-0 px-4">Lambda configuration in different tools</div>
</div>
<script>
  if (typeof switchTab === "undefined") {
    function switchTab(groupId, tabIndex) {
      const group = document.getElementById(groupId);
      if (!group) return;

      // Update buttons
      const buttons = group.querySelectorAll(".tab-button");
      buttons.forEach((btn, idx) => {
        if (idx === tabIndex) {
          btn.classList.add("bg-gray-50", "dark:bg-gray-600", "border-b-2", "border-primary-600", "text-primary-600", "dark:text-gray-100");
          btn.classList.remove("text-gray-500", "hover:text-gray-700", "dark:text-gray-400", "dark:hover:text-gray-300");
        } else {
          btn.classList.remove("bg-gray-50", "dark:bg-gray-600", "border-b-2", "border-primary-600", "text-primary-600", "dark:text-gray-100");
          btn.classList.add("text-gray-500", "hover:text-gray-700", "dark:text-gray-400", "dark:hover:text-gray-300");
        }
      });

      // Update panels
      const panels = group.querySelectorAll(".tab-panel");
      panels.forEach((panel, idx) => {
        if (idx === tabIndex) {
          panel.classList.remove("hidden");
          panel.classList.add("block");
        } else {
          panel.classList.add("hidden");
          panel.classList.remove("block");
        }
      });
    }
  }
</script>

The caption appears in subtle styling below the tabbed interface, providing context for the code.

Feature 2: Custom Tab Labels

Override automatic labels to distinguish between tools using the same language:

<div class="code-tabs mb-8" id="code-tabs-5035">
  <div class="flex flex-wrap border-b border-gray-200 dark:border-gray-700 mb-0">
    <button onclick="switchTab('code-tabs-5035', 0)" class="tab-button px-4 py-2 font-medium text-sm transition-colors bg-gray-50 dark:bg-gray-600 border-b-2 border-primary-600 text-primary-600 dark:text-gray-100" data-tab="0">Pulumi</button>
    <button onclick="switchTab('code-tabs-5035', 1)" class="tab-button px-4 py-2 font-medium text-sm transition-colors text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" data-tab="1">Boto3</button>
    <button onclick="switchTab('code-tabs-5035', 2)" class="tab-button px-4 py-2 font-medium text-sm transition-colors text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" data-tab="2">CDK</button>
  </div>
  <div class="tab-panels">
    <div class="tab-panel block" data-tab="0">
      <div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">pulumi_aws</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">aws</span>
<span class="n">notebook</span> <span class="o">=</span> <span class="n">aws</span><span class="o">.</span><span class="n">sagemaker</span><span class="o">.</span><span class="n">NotebookInstance</span><span class="p">(</span><span class="s2">&quot;nb&quot;</span><span class="p">)</span>
</pre></div>

    </div>
    <div class="tab-panel hidden" data-tab="1">
      <div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">boto3</span>
<span class="n">sm</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">&#39;sagemaker&#39;</span><span class="p">)</span>
<span class="n">sm</span><span class="o">.</span><span class="n">create_notebook_instance</span><span class="p">(</span><span class="n">NotebookInstanceName</span><span class="o">=</span><span class="s1">&#39;nb&#39;</span><span class="p">)</span>
</pre></div>

    </div>
    <div class="tab-panel hidden" data-tab="2">
      <div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">aws_cdk</span><span class="w"> </span><span class="kn">import</span> <span class="n">aws_sagemaker</span> <span class="k">as</span> <span class="n">sagemaker</span>
<span class="n">notebook</span> <span class="o">=</span> <span class="n">sagemaker</span><span class="o">.</span><span class="n">CfnNotebookInstance</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&quot;nb&quot;</span><span class="p">)</span>
</pre></div>

    </div>
  </div>
</div>
<script>
  if (typeof switchTab === "undefined") {
    function switchTab(groupId, tabIndex) {
      const group = document.getElementById(groupId);
      if (!group) return;

      // Update buttons
      const buttons = group.querySelectorAll(".tab-button");
      buttons.forEach((btn, idx) => {
        if (idx === tabIndex) {
          btn.classList.add("bg-gray-50", "dark:bg-gray-600", "border-b-2", "border-primary-600", "text-primary-600", "dark:text-gray-100");
          btn.classList.remove("text-gray-500", "hover:text-gray-700", "dark:text-gray-400", "dark:hover:text-gray-300");
        } else {
          btn.classList.remove("bg-gray-50", "dark:bg-gray-600", "border-b-2", "border-primary-600", "text-primary-600", "dark:text-gray-100");
          btn.classList.add("text-gray-500", "hover:text-gray-700", "dark:text-gray-400", "dark:hover:text-gray-300");
        }
      });

      // Update panels
      const panels = group.querySelectorAll(".tab-panel");
      panels.forEach((panel, idx) => {
        if (idx === tabIndex) {
          panel.classList.remove("hidden");
          panel.classList.add("block");
        } else {
          panel.classList.add("hidden");
          panel.classList.remove("block");
        }
      });
    }
  }
</script>

All three tabs show Python code, with clear labels indicating which framework each example uses. This works well for comparing different approaches to the same task.

Comprehensive Documentation

Documentation received a full update:

Test Coverage Documentation: Complete inventory of all 340 unit tests across 35 test files, with detailed coverage statistics (53% overall) and module-by-module breakdown.

Plugin Documentation: All 8 shortcodes fully documented with usage examples, parameters, and best practices.

Architecture Documentation: The pipeline architecture, dependency tracking system, and caching mechanisms all comprehensively documented.

All documentation is current and complete.

File Watcher Improvements

The development file watcher now detects both .md and .markdown file extensions. Previously, only .md files triggered rebuilds.

Both extensions now trigger incremental builds on save.

Deployment Button Optimization

The frontend's "Deploy to Staging" and "Deploy to Production" buttons now use incremental builds instead of full rebuilds. Deployments complete faster while still ensuring all changes are captured and uploaded.

The Development Workflow

All of this work happened in a single day using AI-assisted development with Claude Code:

  1. Identify performance bottlenecks
  2. Design solutions through conversation
  3. Implement and test features
  4. Document everything comprehensively
  5. Commit with detailed messages

AI-assisted development enables this kind of improvement in hours instead of weeks.

Performance Summary

Build Times

Scenario Before After Improvement
Full build 5-7 seconds 2-3 seconds ~60% faster
Single file change 5-7 seconds < 1 second ~85% faster
SCSS change 5-7 seconds < 1 second ~85% faster
Image change 5-7 seconds < 1 second ~85% faster

Deployment Times

Scenario Before After Improvement
Typical deployment ~30 seconds < 2 seconds ~93% faster
5 file changes ~30 seconds < 5 seconds ~83% faster

Development Experience

Before: Save file → wait 5-7 seconds → see changes

After: Save file → wait < 1 second → see changes

The feedback loop is now sub-second.

What This Means for the Site

These improvements change the writing and development experience in practice:

Writing posts: Preview updates as you write. Formatting changes appear immediately.

Styling tweaks: CSS changes appear in under a second.

Adding images: Drop in an image; it is optimized and rendered in under a second.

Deploying: Push to production in seconds. Incremental uploads replace full-site transfers.

This site uses all of these optimizations. Pages load faster with WebP images. Deploys complete faster with incremental uploads. Development sessions produce more output with sub-second rebuilds.

The AI Advantage

This level of improvement in a single day demonstrates the value of AI-assisted development. The core benefit is rapid iteration:

  • Describe what you want
  • Get an implementation
  • Test it immediately
  • Refine based on results
  • Move to the next feature

Cycle time from idea to working feature drops from days to hours. Cycle time from identifying a bottleneck to resolving it drops from weeks to a single afternoon.

Looking Ahead

With this performance foundation in place, future priorities include:

  • Parallel pipeline execution (architecture supports it, just needs implementation)
  • Selective page regeneration for even faster content builds
  • Template dependency graph for smarter partial change detection
  • Additional shortcodes for rich content features

Each of these builds on today's work.


Reach out via Twitter or LinkedIn to share your experience with static site performance or AI-assisted development.

Let's Build Something!

I help teams ship cloud infrastructure that actually works at scale. Whether you're modernizing a legacy platform, designing a multi-region architecture from scratch, or figuring out how AI fits into your engineering workflow, I've seen your problem before. Let me help.

Currently taking on select consulting engagements through Vantalect.