Deployment
CoolAdmin's deploy story is the easy kind — static HTML + CSS + JS, no Node runtime. Drop the repo on any host that serves static files. This page covers static hosts (Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3), CDN configuration, and what to do about Bootstrap loaded from CDN.
Last updated May 22, 2026
CoolAdmin builds to plain static files — there’s no dist/ directory because the built HTML and CSS already ship at the repo root. Any host that serves static files works.
Two deployment models:
- Use the repo as-is. The 35 root
*.htmlfiles +css/+js/+vendor/+images/are everything you need. Upload them. - Run the build first, then deploy the result. If you’ve edited the Pug or SCSS source, run
npm run buildto regenerate the static artifacts. The output overwrites the files already in the repo root.
Either way, what you ship is a static directory.
What’s in the deploy
CoolAdmin/ ← upload this
├── *.html (35 pages)
├── css/
│ ├── theme.css (legacy)
│ ├── app.css (modern overlay)
│ └── font-face.css
├── js/
│ ├── vanilla-utils.js
│ ├── bootstrap5-init.js
│ ├── main-vanilla.js
│ └── modern-plugins.js
├── vendor/ (Bootstrap, Chart.js, FullCalendar, Font Awesome)
└── images/
Things you can skip:
src/— Pug + SCSS sources. Build artifacts live at the repo root; deploy doesn’t need them.scripts/— Node build scripts. Not used at runtime.node_modules/— local-only.package.json/package-lock.json— only needed by contributors who run the build.CLAUDE.md/CHANGELOG.md/RELEASE.md— repo docs, not runtime.
A clean deploy is around 3–4 MB total (most of which is vendor/ and images/).
Static hosts
Cloudflare Pages
# Connect a GitHub repo via the Cloudflare dashboard:
# Build command: (leave blank — repo is pre-built)
# Build output dir: /
# Node version: (not needed)
Or use Wrangler to deploy a local copy:
npx wrangler pages deploy . --project-name=cooladmin --branch=main
If you want the build to run on every push (so contributors don’t have to commit built artifacts), set:
- Build command:
npm install && npm run build - Build output directory:
.
Cloudflare Pages will run the build server-side and serve the result.
Netlify
netlify.toml at the repo root:
[build]
# If contributors commit build artifacts, leave command blank
publish = "."
# If you want Netlify to build on push:
# [build]
# command = "npm install && npm run build"
# publish = "."
# Cache aggressive on vendor + css + js
[[headers]]
for = "/vendor/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/css/*"
[headers.values]
Cache-Control = "public, max-age=86400, must-revalidate"
[[headers]]
for = "/js/*"
[headers.values]
Cache-Control = "public, max-age=86400, must-revalidate"
[[headers]]
for = "/*.html"
[headers.values]
Cache-Control = "public, max-age=0, must-revalidate"
Vercel
vercel.json:
{
"buildCommand": null,
"outputDirectory": ".",
"headers": [
{
"source": "/vendor/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
},
{
"source": "/(css|js)/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=86400, must-revalidate" }]
},
{
"source": "/(.*\\.html)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
}
]
}
Or via CLI: vercel --prod.
GitHub Pages
A .github/workflows/deploy.yml:
name: Deploy to Pages
on:
push:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
# Skip these two steps if you commit built artifacts directly
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run build
- uses: actions/upload-pages-artifact@v3
with: { path: . }
- uses: actions/deploy-pages@v4
In repo settings, set Pages source to “GitHub Actions”. Pushes to master deploy.
S3 + CloudFront
# Sync everything except node_modules / src / scripts
aws s3 sync . s3://your-bucket/ --delete \
--exclude "node_modules/*" \
--exclude "src/*" \
--exclude "scripts/*" \
--exclude ".git/*" \
--exclude "package*" \
--cache-control "public, max-age=0, must-revalidate" \
--include "*.html"
aws s3 sync . s3://your-bucket/ \
--include "vendor/*" \
--cache-control "public, max-age=31536000, immutable"
aws s3 sync . s3://your-bucket/ \
--include "css/*" --include "js/*" \
--cache-control "public, max-age=86400, must-revalidate"
Invalidate /*.html and /css/* and /js/* in CloudFront after each deploy.
Cloudflare R2
rclone sync . r2remote:bucket-name/ \
--exclude "node_modules/**" \
--exclude "src/**" \
--exclude "scripts/**" \
--exclude ".git/**" \
--exclude "package*" \
--progress
Wrap with a Worker or set up a custom domain for HTTPS + caching.
CDN dependencies
A few resources load from external CDNs by default. Decide whether to keep them or vendor them locally:
| Resource | URL | Why it’s external |
|---|---|---|
| Poppins font | fonts.googleapis.com | Google Fonts CDN — universally cached, no SRI hash on font files |
| Leaflet (map.html) | unpkg.com/leaflet@1.9.4 | Vendored CSS would need its own asset folder; loading from CDN keeps the per-page tax low |
If you’re shipping for offline use or a network-restricted environment, vendor them:
# Poppins — download woff2 files into vendor/fonts/
# Then update css/font-face.css to point to local paths
# Leaflet — already partially vendored in vendor/leaflet/ for some templates; otherwise:
npm install leaflet
cp -r node_modules/leaflet/dist vendor/leaflet
# Update map.html to reference vendor/leaflet/leaflet.js + leaflet.css
Everything else (Bootstrap, Chart.js, FullCalendar, Font Awesome) is already vendored in vendor/.
Cache strategy
Three tiers of cache headers map to three asset groups:
| Tier | Pattern | Cache-Control | Why |
|---|---|---|---|
| Immutable | /vendor/* | public, max-age=31536000, immutable | Vendored libs are versioned by filename (chart.umd.js-4.5.1.min.js); they never change in place |
| Daily | /css/*, /js/* | public, max-age=86400, must-revalidate | App CSS/JS may change between deploys but don’t need second-fresh — daily revalidate strikes the balance |
| No-cache | /*.html | public, max-age=0, must-revalidate | HTML changes most often + has to reference fresh CSS/JS hashes; always revalidate |
The downside of “Daily” for app CSS/JS is that a user with a cached app.css might see ~24h of staleness after a deploy. Two ways to avoid that:
- Add a query string for cache-busting. Update
<link href="css/app.css?v=3.3.0">on each release. The query string changes the resource URL from the browser’s perspective. - Move to filename hashing. Run the SCSS build with a hash in the output filename and template the link tag. More setup, but cleaner cache behavior. CoolAdmin doesn’t do this by default because the repo’s “no build for end users” promise would break.
For most deployments, the Daily tier is fine.
What you don’t need
A few things CoolAdmin deliberately doesn’t require:
- No server-side rendering. Pages are static HTML.
- No client-side routing. Navigation is real
<a href="…">links. - No build-step environment variables. The template has no API URLs to inject; configuration lives in your own code.
- No
.htaccessor rewrite rules. Pages are served by filename (/inbox.html, not/inbox). - No service worker. CoolAdmin doesn’t ship a PWA manifest or service worker by default. Add one if you want offline support — see the
<head>template insrc/pug/partials/_head.pugfor where to inject the link tag.
If you want pretty URLs (/inbox instead of /inbox.html), each host has its own way: Netlify uses redirects, Vercel uses cleanUrls: true, S3 uses Lambda@Edge, Cloudflare uses Workers. CoolAdmin doesn’t ship a specific config for any of them — the template is host-agnostic.
Quick checklist before deploy
# If you've edited source files:
npm run build # rebuild HTML + CSS
# Verify it works locally:
python3 -m http.server 8000
# Open http://localhost:8000
# Check for broken Bootstrap dropdowns / charts / icons by visiting:
# / (operations dashboard)
# /chart.html (Chart.js sanity)
# /calendar.html (FullCalendar sanity)
# /modal.html (Bootstrap interactivity sanity)
# Then upload everything except node_modules/src/scripts/.git
After deploy, smoke-test:
- All 35 pages load (open a few at random)
- Sidebar collapses + expands
- ⌘K palette opens
- Theme switcher applies a preset and survives a refresh
- No 404s in network tab on any page
- Charts render on
chart.html - Calendar populates on
calendar.html
See also
- Architecture — what gets shipped and what doesn’t
- Pug + SCSS pipeline — for the build step you might run before deploy