A multi-page site
Let's put it all together: the Morning Café site with three pages — home, menu and contacts. The pages share a layout, a header and a footer, and the build script finds all the pages by itself and writes the HTML files.
Project structure
The pages move to the src/pages/ folder, while the shared parts stay in src/:
cafe/
├── build.js
├── dist/ ← index.html, menu.html, contacts.html
└── src/
├── layout.pug
├── mixins.pug
├── includes/
│ ├── header.pug
│ └── footer.pug
└── pages/
├── index.pug
├── menu.pug
└── contacts.pugThe file src/mixins.pug is the same as in the lesson on includes: the drink(name, price) mixin.
A script for all pages
Replace build.js:
// build.js
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
import pug from "pug";
const data = {
year: new Date().getFullYear(),
phone: "+1 555 0100",
menu: [
{ name: "Espresso", price: 2 },
{ name: "Cappuccino", price: 3 },
{ name: "Hot chocolate", price: 3 },
],
};
mkdirSync("dist", { recursive: true });
for (const file of readdirSync("src/pages")) {
const page = file.replace(".pug", "");
const html = pug.renderFile(`src/pages/${file}`, { pretty: true, page, ...data });
writeFileSync(`dist/${page}.html`, html);
console.log(`Done: dist/${page}.html`);
}The loop takes every file from src/pages/ and passes the shared data to the template, plus a page variable — the page name without the extension. A new page appears on the site as soon as you create a .pug file for it.
Layout, header and footer
Create src/includes/header.pug. The link to the current page gets the active class:
header
a.logo(href="index.html") Morning Café
nav
each title, slug in { index: "Home", menu: "Menu", contacts: "Contacts" }
a(href=slug + ".html" class=page === slug ? "active" : undefined)= titleThe footer is src/includes/footer.pug:
footer
p © #{year} Morning Café · #{phone}The layout src/layout.pug includes both parts. Paths in include are counted from the layout itself:
doctype html
html(lang="en")
head
meta(charset="utf-8")
block title
title Morning Café
link(rel="stylesheet" href="style.css")
body
include includes/header.pug
main
block content
include includes/footer.pugThe pages
Home is src/pages/index.pug:
extends ../layout.pug
block content
h1 Fresh coffee every day
p Come for breakfast from 8:00.Contacts is src/pages/contacts.pug:
extends ../layout.pug
block title
title Contacts — Morning Café
block content
h1 Contacts
p Call us: #{phone}The menu is src/pages/menu.pug. Mixins are included with an include line next to extends:
extends ../layout.pug
include ../mixins.pug
block title
title Menu — Morning Café
block content
h1 Menu
each item in menu
+drink(item.name, item.price)The result
Run npm run build: the terminal prints three "Done" lines, and dist/ gets three pages. This is dist/menu.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Menu — Morning Café</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header><a class="logo" href="index.html">Morning Café</a>
<nav><a href="index.html">Home</a><a class="active" href="menu.html">Menu</a><a href="contacts.html">Contacts</a>
</nav>
</header>
<main>
<h1>Menu</h1>
<div class="card">
<h3>Espresso</h3>
<p class="price">$2</p>
</div>
<div class="card">
<h3>Cappuccino</h3>
<p class="price">$3</p>
</div>
<div class="card">
<h3>Hot chocolate</h3>
<p class="price">$3</p>
</div>
</main>
<footer>
<p>© 2026 Morning Café · +1 555 0100</p>
</footer>
</body>
</html>The "Menu" link has the active class, and the prices come from the data in build.js. Put style.css into dist/ and open dist/index.html — the header links take you between the pages.
Common mistake
Putting something other than pages into src/pages/, such as mixins or the header. The script tries to build such a file as a page of its own and creates a stray HTML file. Keep only pages in src/pages/, and the shared parts in src/ and src/includes/.