Splitting a Large HTML Template Into Reusable Partials with Nunjucks

Splitting a Large HTML Template Into Reusable Partials with Nunjucks

Key Takeaways

  • Nunjucks partials eliminate copy-paste duplication across multi-page HTML projects by letting you define shared components once.
  • A {% include %} statement is the fastest way to start; {% extends %} with block inheritance is the scalable pattern for larger projects.
  • Nunjucks integrates cleanly with Vite, Gulp, and the Node CLI, so you can adopt it without changing your existing build setup dramatically.
  • Template variables allow a single partial to render page-specific content such as the active nav state or the document title.
  • The approach works well with premium HTML templates like the Canvas HTML Template, where dozens of demo pages share identical navigation and footer markup.

Why Partials Matter for Static HTML Projects

Most HTML template purchases arrive as a folder of finished .html files. That is intentional: you get a working reference for every layout. The problem starts when you move into production. Any shared element, a sticky navigation bar, an announcement ribbon, a cookie consent banner, a footer with four columns of links, lives as identical code in every file. Change the phone number in the footer and you are running a find-and-replace across dozens of documents, hoping nothing gets missed.

Static site generators solve this at the framework level, but they usually require adopting an entirely new mental model. Nunjucks sits in a useful middle ground. It is a straightforward templating language that compiles to plain HTML, keeps your file structure intact, and has almost no learning curve if you already know HTML. Maintained by Mozilla and widely used in Eleventy-based projects, it also runs perfectly as a standalone preprocessing step.

If you are already using Vite as your build tool, pairing it with a Nunjucks plugin is a natural extension of the workflow described in Setting Up Vite for a Bootstrap 5 HTML Template Build.

Splitting a Large HTML Template Into Reusable Partials with Nunjucks, abstract concept illustration

Recommended Folder Structure

Before writing a single template tag, decide on a folder structure that separates source files from compiled output. The following layout works well for a mid-sized HTML template project:

src/
  _partials/
    head.njk
    header.njk
    footer.njk
    scripts.njk
  _layouts/
    base.njk
  pages/
    index.njk
    about.njk
    contact.njk
dist/
  index.html
  about.html
  contact.html

The underscore prefix on partials and layouts signals that these files should not be compiled into standalone output pages. Configure your build tool to skip any file or folder whose name starts with an underscore.

Creating a Base Layout with extends and block

Template inheritance is the scalable Nunjucks pattern. You define a base layout with named blocks that child templates can override. Here is a minimal base.njk:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ pageTitle }} | My Site</title>
  {% include "../_partials/head.njk" %}
</head>
<body>

  {% include "../_partials/header.njk" %}

  <main>
    {% block content %}{% endblock %}
  </main>

  {% include "../_partials/footer.njk" %}
  {% include "../_partials/scripts.njk" %}

</body>
</html>

An individual page extends this layout and fills in the content block:

{% extends "../_layouts/base.njk" %}
{% set pageTitle = "About Us" %}

{% block content %}
<section class="py-6">
  <div class="container">
    <h1>About Us</h1>
    <p>Your about page content goes here.</p>
  </div>
</section>
{% endblock %}

The {% set pageTitle = "About Us" %} line passes a variable up into the base layout before rendering. This is how you keep the document title, the active navigation state, and page-specific meta tags dynamic without duplicating the surrounding structure.

html includes static site, abstract technical diagram

Handling Active Navigation State in a Shared Header

A shared header raises an immediate practical question: how does the correct link get the active class when there is only one partial? You solve it with a variable set in each page file and evaluated inside the partial.

In your page file, set an identifier:

{% set activePage = "about" %}

Then in header.njk, use a conditional:

<ul class="navbar-nav">
  <li class="nav-item">
    <a class="nav-link {% if activePage == 'home' %}active{% endif %}"
       href="index.html">Home</a>
  </li>
  <li class="nav-item">
    <a class="nav-link {% if activePage == 'about' %}active{% endif %}"
       href="about.html">About</a>
  </li>
  <li class="nav-item">
    <a class="nav-link {% if activePage == 'contact' %}active{% endif %}"
       href="contact.html">Contact</a>
  </li>
</ul>

The compiled HTML files remain plain, static HTML. No JavaScript dependency is needed for the active state logic, which matters if you care about keeping page weight low and behaviour predictable.

Integrating Nunjucks Into Your Build Tool

There are three practical ways to compile Nunjucks to HTML, depending on your existing toolchain.

Option 1: Vite with vite-plugin-nunjucks. Install the plugin and point it at your source directory. It hooks into Vite’s transform pipeline and outputs compiled HTML files to your dist folder alongside your processed CSS and JavaScript. This is the recommended route if you have already modernised your build as described in the Vite post linked earlier.

Option 2: Gulp with gulp-nunjucks-render. If your project uses Gulp, add the gulp-nunjucks-render package and write a task:

const nunjucksRender = require('gulp-nunjucks-render');

function templates() {
  return src('src/pages/**/*.njk')
    .pipe(nunjucksRender({ path: ['src'] }))
    .pipe(dest('dist'));
}

Option 3: The Nunjucks Node CLI. For simple projects or one-off compilations, the nunjucks-cli package lets you run nunjucks src/pages/index.njk dist/index.html directly from the terminal with no build file required. It is useful for testing a partial structure before committing to a fuller integration.

Regardless of which option you choose, the compiled output is vanilla HTML. You can open the dist folder in a browser directly, deploy it to any static host, or commit it to version control. Plain HTML output also means your customisations stay auditable, a principle that pairs well with the Git-based update strategy covered in Keeping Template Customisations Safe Across Version Updates.

When Nunjucks Partials Are Not the Right Tool

Nunjucks is a build-time solution. No runtime component runs in the browser or on the server, which is its main strength for static HTML projects, but also its hard limit. If your navigation is personalised per user, driven by server-side session data, or assembled from a CMS API at request time, Nunjucks alone will not be sufficient. You need a server-side language, a JavaScript framework with client-side hydration, or a headless CMS with an API-driven rendering pipeline.

There is also a simpler cutoff. If your project has only two or three pages and the structure is unlikely to change, the overhead of setting up a Nunjucks compilation step probably is not worth it. The breakeven point is roughly four or more pages that share at least two structural regions, at which point a single update to a partial pays for the setup time immediately.

For projects that grow beyond static HTML entirely, the comparison in Static HTML vs React for Marketing Sites gives a balanced picture of when to stay static and when to reach for a framework.

Applying This Pattern to a Multi-Demo HTML Template

The Canvas HTML Template ships with more than 50 live demos, each as a complete HTML file with its own head section, navigation, and footer. If you are building a production site from Canvas, the fastest workflow is to identify the three to five layout patterns you actually need, extract them into layouts files, and move all shared markup into partials.

The Canvas header relies on specific class combinations and data attributes for sticky behaviour, dark and light variants, and the mobile off-canvas menu. These are strong candidates for a single header.njk partial with a headerClass variable controlling the variant per page:

<header id="header" class="header-size-sm {{ headerClass | default('') }}">
  <!-- Canvas navigation markup -->
</header>

Set {% set headerClass = "dark" %} in pages that need the dark variant, and leave it unset for pages that use the default. The | default('') filter prevents Nunjucks from rendering the string “undefined” when the variable is not provided. That one pattern can consolidate navigation markup from dozens of files into a single, editable partial.

Frequently Asked Questions

Nunjucks requires a Node environment to compile templates, but the output is plain HTML. You do not need Node on your production server. The compilation step runs locally or in your CI pipeline, and the resulting .html files are deployed like any other static asset.

Yes. Nunjucks only processes the HTML layer. Your CSS preprocessor (Sass, PostCSS) and your JavaScript bundler (Vite, Webpack, Rollup) operate independently. Run all three as separate tasks or as parallel steps in your build tool, and each produces its own output.

{% include %} inserts the full content of another file at the point of the tag. It is useful for standalone reusable fragments like a cookie banner or a social icons block. {% extends %} establishes a parent-child relationship where the child template fills named regions (blocks) defined in the parent. For page-level layouts, {% extends %} is the correct choice because it prevents content from leaking outside defined regions.

Nunjucks supports a global data object at the environment level. When you initialise the Nunjucks environment in your build script, you can pass a data object that is available in every template without being declared per page. This is useful for site-wide values like the company name, the current year for the copyright notice, or social media URLs in the footer.

No. The compiled output is identical to hand-written HTML. There is no runtime overhead because all templating logic is resolved at build time. Consolidating markup into well-maintained partials actually makes it easier to apply performance improvements consistently: add loading="lazy" to images in a shared card partial once, or update preload hints in a single head partial rather than across every page.

Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.

If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.

Skip the setup and build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog