Pug Template Inheritance: Building Maintainable Layout Hierarchies Without the Copy-Paste Trap
Pug’s extends/block inheritance lets you define a single parent layout and override only the sections that differ per page — eliminating copy‑paste duplication while keeping asset injection composable via append/prepend.
07 Jun 2026, 01:07 UTC

The Problem: Layout Duplication That Scales Poorly
Every Node.js project reaches a point where the base HTML structure — doctype, head, navigation, footer — gets copied across dozens of view files. A change to the analytics script or a new CSS variable means editing twenty templates. Pug’s extends and block mechanism solves this by letting you define a single parent layout and override only the pieces that differ per page. The result is a hierarchy that stays readable even as the application grows.
How Inheritance Works in Practice
A parent template declares named slots with block. A child template uses extends to inherit the parent and then redefines only the blocks it needs. Consider this minimal pair:
// layout.pug
doctype html
html
head
title #{title} | My App
block head
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='/css/app.css')
body
header
include partials/nav
main
block content
p Default page content
footer
include partials/footer
block scripts
script(src='/js/app.js')
// pages/about.pug
extends ../layout.pug
block head
//- Page-specific meta tags
meta(name='description', content='About our team')
block content
h1 About Us
p We build things.
block append scripts
script(src='/js/about.js')
The about.pug template replaces block content entirely, adds a description meta tag inside block head (the parent’s default meta tags remain because the child didn’t use append or prepend), and injects an extra script after the parent’s app.js via block append scripts. Run npx pug --pretty pages/about.pug to see the compiled HTML and verify the merge order.
Append and Prepend: Adding Without Replacing
The append and prepend modifiers are the key to composable asset injection. A marketing landing page might need a heavy animation library; an admin dashboard needs a date‑picker. Neither should force the other’s scripts onto every page. With block append scripts in the child, the parent’s base bundle stays, and the page‑specific file loads after it. block prepend styles works the same way for CSS — critical for overriding variables before the main stylesheet loads.
This pattern also keeps the parent layout clean. The parent defines what blocks exist; children decide how to fill them. No conditional logic in the layout, no if page === 'about' scattered through the base template.
Integration with Express and Precompilation
In Express, set app.set('view engine', 'pug') and app.set('views', './views'). Then res.render('pages/about', { title: 'About' }) resolves views/pages/about.pug, passes title to both parent and child, and sends the rendered HTML. For production, precompile templates to JavaScript functions:
npx pug --client --no-debug -o public/js/views views
This writes public/js/views/pages/about.js (and a matching layout.js) that the browser can execute without the Pug runtime. Include the generated file and call template(locals) on the client — useful for progressive enhancement or single‑page transitions. The trade‑off: the client bundle grows by ~30 KB minified, and template logic becomes visible in the browser. For SEO‑critical pages, server‑side rendering with hydration remains the safer default.
Pitfalls That Bite Teams
- Indentation errors are the most common compile failure. Pug is whitespace‑sensitive; mixed tabs and spaces throw
PugError: Invalid indentationwith a line/column pointer. Teams typically enforce 2‑space indentation via.editorconfigorprettier-plugin-pugin CI. - Block scoping trips up deep hierarchies. If
layout.pugdefinesblock contentandsection.pugextends it and redefinesblock contentwithout a new name, a grandchild extendingsection.pugcannot reach the original layout’s block. Plan block names for the full depth you need. - Dynamic block names are not supported. You cannot compute a block identifier at runtime; it must be a literal at compile time. If you need truly dynamic composition, use mixins or partials via
includeinstead.
Verification Checklist Before Merging
- Render a child template with
npx pug --pretty child.pugand confirm parent defaults appear where blocks aren’t overridden. - Add
block append scriptsin a child and verify both parent and child script tags exist in output, in the correct order. - Run the precompilation command above and load the generated JS in a test HTML page; call the exported function with locals to ensure client‑side rendering works.
- Introduce a deliberate indentation mismatch (tab vs. spaces) and confirm the error message points to the exact line.
- Set
app.locals.basedir = __dirname + '/views'in Express and testextends ../../layoutfrom a nested subdirectory — absolute paths prevent fragile relative paths.
Closing: Start Small, Structure Early
You don’t need a grand layout hierarchy on day one. Begin with a single layout.pug that defines block head, block content, and block scripts. As pages diverge, add block append for assets and new named blocks for recurring sections (sidebar, breadcrumbs, modals). The inheritance model keeps changes localized: updating the navigation means editing one partial, not twenty templates. That’s the engineering decision that pays off at scale.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.