tooling

Gulp + Nunjucks: The Pragmatic Workflow

2026-02-02 tooling 2 min read

In an era of complex 'Dragons' like Vite and Eleventy, there is immense value in a build system that is explicit, fast, and deterministic. For landing pages and microsites, the Gulp + Nunjucks stack remains a powerhouse of productivity.

Modern Comparison

Before diving into the code, let's look at where this stack sits in the 2026 landscape:

FeatureEleventy + ViteGulp + NunjucksSvelteKit
ComplexityHigh (The Dragon)Medium (Explicit)Low (Integrated)
DXSync-HeavyReliable / ManualSeamless
Best ForLarge SSG SitesMarketing / MicrositesInteractive Apps

1. Core Project Structure

A clean architecture is the foundation of a scalable static site. By separating source logic from the distribution build, we ensure the dist/ folder is always ready for Firebase or S3 hosting.

text
/
├── dist/             # Compiled output (Production Ready)
├── src/
│   ├── data/         # JSON data models
│   ├── views/        # Nunjucks templates
│   │   └── _parts/   # Macros, Layouts, Partials
│   ├── scss/         # SCSS entry points
│   └── js/           # Modern JavaScript
├── gulpfile.js
└── package.json

2. The Gulp Pipeline

The beauty of Gulp 4 is task orchestration. We use a series-parallel approach to ensure the build folder is wiped clean before the new assets are streamed in.

javascript
const { src, dest, watch, parallel, series } = require('gulp');
const nunjucksRender = require('gulp-nunjucks-render');
const sass = require('gulp-sass')(require('sass'));
const cleanCSS = require('gulp-clean-css');
const del = require('del');

function clean() {
  return del(['dist']);
}

function styles() {
  return src('src/scss/main.scss')
    .pipe(sass().on('error', sass.logError))
    .pipe(cleanCSS())
    .pipe(dest('dist/css'));
}

function templates() {
  return src('src/views/*.{html,njk}')
    .pipe(nunjucksRender({
      path: ['src/views/_parts'],
      data: { siteName: 'KLPGo Static' }
    }))
    .pipe(dest('dist'));
}

exports.build = series(clean, parallel(styles, templates));

3. Nunjucks Macros: The Razor Ancestor

Macros are the secret weapon of this stack. If you've ever worked with Razor View Components or Svelte Components, Nunjucks Macros will feel familiar. They allow you to encapsulate HTML logic into reusable functions.

jinja2
{# src/views/_parts/macros.njk #}
{% macro card(title, content, type='info') %}
<div class="card shadow-sm border-{{ type }}">
  <div class="card-body">
    <h5 class="card-title text-capitalize">{{ title }}</h5>
    <p class="card-text">{{ content }}</p>
  </div>
</div>
{% endmacro %}

4. Data Modeling without a Database

Nunjucks allows for sophisticated data manipulation right in the template. This is perfect for generating lists, galleries, or even blog feeds from local arrays.

jinja2
{% set features = [
  { name: 'Fast', icon: 'zap' },
  { name: 'Reliable', icon: 'shield' },
  { name: 'Static', icon: 'lock' }
] %}

<div class="row">
  {% for item in features %}
    <div class="col"> {{ item.name }} </div>
  {% endfor %}
</div>

Conclusion

While the industry pushes toward increasingly complex build tools, the Gulp + Nunjucks workflow remains a 'defensible' choice. It offers the perfect balance of templating power and architectural simplicity, making it my go-to for HTML-first development in 2026.