Adding a new page
Three small edits, no boilerplate.
1. Create the HTML file
Make src/team-chat.html (any name — the slug becomes the URL):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Team chat · Adminator</title>
<script>
(function () {
try {
var saved = localStorage.getItem('dash26-theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', saved || (prefersDark ? 'dark' : 'light'));
} catch (e) { document.documentElement.setAttribute('data-theme', 'light'); }
})();
</script>
</head>
<body data-active="team-chat" data-crumbs="Communications | Team chat">
<div class="shell">
<div data-shell-sidebar></div>
<div class="main">
<div data-shell-topbar></div>
<main class="content">
<section class="hero">
<div class="hero-text">
<span class="eyebrow">Communications</span>
<h1 class="hero-title">Team chat</h1>
<p class="hero-sub">Page-specific content goes here.</p>
</div>
</section>
<!-- More <section class="card"> blocks here -->
</main>
<div data-shell-footer></div>
</div>
</div>
</body>
</html>
The simplest starting point is to copy src/blank.html and edit from there.
2. Register it in webpack
Open webpack/plugins/htmlPlugin.js and add to the titles map:
const titles = {
'index': 'Adminator · Dashboard',
// …existing entries…
'team-chat': 'Adminator · Team chat', // ← add this
};
The key matches the filename (without .html). Webpack will build dist/team-chat.html next time it compiles.
3. Add it to the sidebar
Open src/assets/scripts/2026/Shell.js and add an entry to the appropriate section’s items array:
{
label: 'Communications',
items: [
// …existing items…
{
key: 'team-chat', // matches data-active on body
text: 'Team chat', // sidebar label
href: 'team-chat.html', // link target
badge: { kind: 'new', text: 'NEW' }, // optional
icon: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8..."/>', // SVG path
},
],
},
Find an icon at any free SVG icon library — the inline path data is what goes in. The viewBox is fixed at 0 0 24 24 by the renderer.
4. Restart the dev server
# Ctrl+C then…
npm start
Webpack picks up new HTML templates only on restart (the template files are read once when htmlPlugin.js runs).
The new page is live at http://localhost:4000/team-chat.html, the sidebar shows the new item, and clicking it navigates correctly.
What if it’s a standalone page (no shell)?
Skip the placeholder <div>s and data-active/data-crumbs on the body. Use one of the standalone CSS shells (.auth-shell, .error-shell) or write your own layout inside <body>.
Example — a custom maintenance page:
<body>
<div class="error-shell">
<div class="error-card">
<span class="error-eyebrow">Status</span>
<div class="error-code">503</div>
<h1 class="error-title">Scheduled maintenance</h1>
<p class="error-sub">We'll be back at 03:00 UTC.</p>
</div>
</div>
</body>
Still register in webpack/plugins/htmlPlugin.js, but leave it out of the sidebar NAV (no point linking to a maintenance page from your normal nav).
Adding a submenu item
If your new item belongs under an existing parent (e.g. another table type under “Tables”), add it to the parent’s children array:
{
key: 'tables',
text: 'Tables',
icon: '<rect x="3" y="4" width="18" height="16" rx="2"/>...',
children: [
{ key: 'basic-table', text: 'Basic Table', href: 'basic-table.html' },
{ key: 'datatable', text: 'Data Table', href: 'datatable.html' },
{ key: 'pivot', text: 'Pivot Table', href: 'pivot.html' }, // ← new
],
},
The parent group will auto-expand when any child has data-active matching its key.