Using Pug Mixins to Keep Card Markup DRY and Readable
Learn how to define a reusable card mixin in Pug, pass parameters and optional block content, and understand the trade‑offs of compile‑time reuse.
16 Dec 2025, 21:54 UTC

Problem: Repeating card markup clutters templates
When building a list of product cards, profile cards, or notification cards, the same structural markup appears over and over:
div.card
img.card-img(src=product.image alt=product.name)
div.card-body
h3.card-title= product.name
p.card-text= product.description
if product.footer
div.card-footer
a.btn(href=product.url) View details
Copy‑pasting this block for each card leads to duplication, makes global changes error‑prone, and obscures the intent of the template.
Thesis: A Pug mixin encapsulates the card pattern while keeping data flow visible
By defining a mixin that accepts the card’s data as parameters and exposes an optional block for footer content, you can write the card once and reuse it wherever needed. The mixin is expanded at compile time, so there is no runtime overhead, and the generated HTML is identical to the hand‑written version.
Defining the mixin
The mixin signature includes:
title– required string for the card heading.image– optional image URL; defaults to a placeholder.body– required string for the main text.block– optional content inserted where+blockappears inside the mixin.
mixin card(title, image = 'https://via.placeholder.com/150', body)
div.card
if image
img.card-img(src=image alt=title)
div.card-body
h3.card-title= title
p.card-text= body
+block
Notice the default value for image and the explicit +block call. If the caller does not provide a block, nothing is rendered at that point.
Using the mixin with variations
Now the template that lists cards becomes concise:
each item in products
+card(item.name, item.image, item.description)
if item.url
a.btn(href=item.url) View details
+card('Featured', null, 'Special offer today')
// No footer block – nothing extra is rendered
+card('Event', 'event.jpg', 'Join us')
// Custom block with multiple lines
p.text-center
small.text-muted Register by Friday
Each call supplies the required arguments; the optional image can be omitted to use the placeholder. The block after the mixin call becomes the footer content.
Trade‑offs and limitations
While mixins reduce duplication, they are compile‑time macros:
- Recompilation required. Changing a mixin forces a rebuild of all templates that import it, which can affect hot‑reload speed in development.
- Data flow can be less obvious. Because the mixin expands inline, tracing where a variable originates may require checking the mixin definition, unlike a separate partial where the data is passed explicitly.
- Version sensitivity. The block parameter syntax (
+block) and default argument handling changed between Pug 2.x and 3.x. Verify behavior against the version you are using.
To confirm that a mixin works as expected, render a minimal file with the Pug API and inspect the output:
- Create
test.pugcontaining the mixin definition and a couple of calls. - Run
node -e "const pug = require('pug'); console.log(pug.renderFile('test.pug', {pretty:true}))". - Check that the expected
<div class="card">elements appear, that default images are inserted when omitted, and that block content appears only where supplied.
This verification step does not assume any particular output; it merely confirms that the template expands without errors and that the structure matches your expectations.
Actionable closing
If you find yourself repeating the same markup pattern across multiple templates, start by extracting it into a mixin with clear parameters and an optional block. Keep the mixin focused on a single UI primitive (e.g., a card, a form field, a navigation item) to maintain readability. After adding the mixin, run a quick render test to ensure the generated HTML matches your design, and remember that any change to the mixin will require a full template recompile.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.