Pug + SCSS pipeline
CoolAdmin's source pipeline — Pug templates with a single nav-data file, SCSS split into 56 partials across two stylesheets, and a Vite dev server with HMR. Built artifacts ship in the repo so end users skip the build.
Last updated May 22, 2026
CoolAdmin v3.2 introduced a Pug + SCSS source pipeline. The motivation: stop editing 35 HTML files when you want to add a sidebar item.
The pipeline is opt-in for contributors. End users still clone the repo and open index.html — the build artifacts ship committed.
What npm run dev boots
npm install # one-time
npm run dev # three concurrent watchers
Three processes run in parallel via concurrently:
| Process | What it watches | What it produces |
|---|---|---|
pug | src/pug/**/*.pug, src/pug/partials/content/*.html | Root *.html files |
sass | src/scss/**/*.scss | css/theme.css + css/app.css |
vite | Browser-side HMR over http://localhost:3000 | (just a dev server, no bundling) |
Edit any source — browser reloads in under a second.
npm run build runs the Pug + Sass compile once without watchers. There is no production bundling step beyond that.
The Pug source tree
src/pug/
├── layouts/
│ ├── _default.pug # Dashboard layout (sidebar + topbar + main)
│ ├── _auth.pug # Centered single-column (login, register, forgot password)
│ └── _error.pug # 404 / 500 / maintenance (no sidebar, no topbar)
├── partials/
│ ├── _head.pug # +head(pageMeta) mixin — emits the entire <head>
│ ├── _nav-data.pug # 🌟 SINGLE SOURCE OF TRUTH FOR THE NAV
│ ├── sidebar.pug # Desktop sidebar (reads _nav-data)
│ ├── header-desktop.pug # Topbar (search, dropdowns, account menu)
│ ├── header-mobile.pug # Mobile drawer (reads _nav-data)
│ ├── footer-scripts.pug # Common <script> stack
│ └── content/
│ └── <page>.html # Per-page inner markup as raw HTML
└── pages/ # One file per route (35 total)
└── <page>.pug
The layouts and partials are pure templating — no data, no logic. Pages set metadata in block variables and bring in their content fragment via include ../partials/content/<page>.
Pages — three layouts, one anatomy
Every page in src/pug/pages/ follows the same minimal shape:
extends ../layouts/_default
block variables
- var pageMeta = {
- title: 'Operations dashboard | CoolAdmin',
- description: 'KPIs, sparklines, revenue chart, and activity feed.'
- }
- var activePage = 'dashboard'
block content
include ../partials/content/index.html
Three things:
- Which layout —
_defaultfor dashboards,_authfor login/register/forget-pass,_errorfor 404/500/maintenance. pageMeta— title and description used by the+headmixin to emit<title>,<meta name="description">, Open Graph tags, and Twitter Card tags. The auth layout addsnoindexautomatically.activePage— string matching akeyin_nav-data.pug. The sidebar highlights the corresponding item.
Available blocks
| Block | Where | Use for |
|---|---|---|
variables | all layouts | pageMeta, activePage, bodyClass, skipLinkText |
content | all layouts | the inner page markup |
extra_head | all | extra <link> / <style> in <head> (e.g. Leaflet CSS) |
vendor_scripts | default, auth | vendor <script> BEFORE main JS (Chart.js, FullCalendar, Leaflet) |
post_main | default | content as sibling of <main> (e.g. Bootstrap modal definitions) |
extra_scripts | all | inline <script> blocks loaded LAST, after main JS |
⚠️ Pug block is replace, not append. When you override block variables in a page, the layout’s defaults aren’t merged in — you must restate pageMeta, activePage, etc. for every page. That’s why every page in src/pug/pages/ repeats those declarations.
The single source of truth — _nav-data.pug
This is the file that justifies the pipeline:
//- Edit this file to add, rename, or reorder navigation items.
-
var navItems = [
{
icon: 'fa-solid fa-tachometer-alt',
label: 'Dashboard',
children: [
{ label: 'Dashboard 1', href: 'index.html' },
{ label: 'Dashboard 2', href: 'index2.html' },
{ label: 'Dashboard 3', href: 'index3.html' },
{ label: 'Dashboard 4', href: 'index4.html' }
]
},
{
icon: 'fa-solid fa-chart-bar',
label: 'Charts',
href: 'chart.html'
},
{ icon: 'fa-solid fa-calendar-alt', label: 'Calendar', href: 'calendar.html' },
{ icon: 'fa-solid fa-inbox', label: 'Inbox', href: 'inbox.html' },
// ...
]
Edit the array, run npm run build:pug, every page rebuilds with the new menu. Both the desktop sidebar and the mobile drawer read from this same array — no duplication.
Item fields:
icon— Font Awesome 7 class (fa-solid fa-*orfa-regular fa-*)label— display texthref— URL (omit on parent items that only have children)children— array of{ label, href }for a collapsible submenu
The sidebar highlights items by matching each href against the page’s activePage local.
Adding a new page
Two ways: via the migration helper, or by hand.
With the helper
node scripts/migrate-page.js my-new-page
The helper extracts the inner content and inline scripts from my-new-page.html (at the repo root) and creates:
src/pug/pages/my-new-page.pugsrc/pug/partials/content/my-new-page.html
It detects which layout you need (default / auth / error) by looking at the source’s structure. Used during v3.2’s full migration of all 35 pages — useful when you copy a page from elsewhere and want to bring it into the pipeline.
By hand
# 1. Create the Pug page
echo 'extends ../layouts/_default
block variables
- var pageMeta = { title: "Reports | CoolAdmin", description: "Q2 reports" }
- var activePage = "reports"
block content
include ../partials/content/reports.html
' > src/pug/pages/reports.pug
# 2. Drop the inner content
echo '<h1>Reports</h1>' > src/pug/partials/content/reports.html
# 3. Add the nav entry to _nav-data.pug
# (edit by hand — append to navItems array)
# 4. Build
npm run build:pug
Restart the dev server if it’s running (Pug additions register at startup).
The SCSS source tree
Two entry files, each @use-ing a stack of partials in a deliberate order:
src/scss/
├── theme.scss # 20 ITCSS partials → css/theme.css (legacy stylesheet)
│ ├── _variables, _generic, _elements, _objects
│ ├── components/
│ │ ├── _buttons, _form, _header, _sidebar, _overview
│ │ ├── _cards, _charts, _tables, _footer
│ │ ├── _breadcrumb, _statistic, _progress, _alert, _switch
│ └── _utilities, _modern-additions
└── app.scss # 36 partials under app/ → css/app.css (modern overlay)
├── _variables, _overlay-base, _legacy-overrides
├── _topbar-layout, _responsive-sidebar, _dropdowns
├── _forms-bootstrap, _auth
├── _inbox, _inbox-pane, _kanban, _data-table, _invoice, _wizard
├── _card-showcase, _section-cards, _project-list, _rank-list, _pricing
├── _notifications, _empty-states, _toast
├── _command-palette, _theme-switcher, _theme-presets, _skeleton, _error-pages, _docs
└── _mobile-refinements, _mobile-topbar, _mobile-topbar-final, _hamburger,
_header-grid, _mobile-header-restore (load order is load-bearing — see below)
@use order is load-bearing
The mobile-related app partials are deliberately layered:
@use 'app/mobile-refinements';
@use 'app/mobile-topbar';
@use 'app/mobile-topbar-final';
@use 'app/hamburger';
@use 'app/header-grid';
@use 'app/mobile-header-restore';
Each layer either overrides or restores something from the previous one. Don’t alphabetize this stack — the cascade is the spec. The naming (-final, -restore) is a deliberate signal that ordering matters.
Sass compiles both stacks back to byte-equivalent CSS that’s verified against the pre-refactor output via minified diff. If you refactor a partial, compile + diff against the previous css/*.css to confirm nothing regressed.
End-user mode
For someone who just wants to use CoolAdmin as a starting point:
git clone https://github.com/puikinsh/CoolAdmin.git
cd CoolAdmin
open index.html
No Node, no build, no dist/. The HTML and CSS in the repo root are the deliverable. The Pug + SCSS pipeline never runs.
This split — committed build artifacts + source pipeline — is what lets the template serve both audiences without forcing one workflow on everyone.
See also
- Architecture — the bigger-picture overview the pipeline fits into
- Theming — what the SCSS variables actually do
- Deployment — shipping the built artifacts