G Gentelella v4.0.0

Deployment

Build Gentelella for production with one command. The output is plain static files — drop them on any host that serves HTML. This page covers production builds, static hosts (Netlify, Vercel, GitHub Pages, S3, Cloudflare Pages), subpath deploys, and CDN configuration.

Last updated May 22, 2026

Gentelella builds to plain static filesdist/ contains HTML, JS, CSS, fonts, images, and the service worker. No Node runtime, no serverless functions, no edge config. Any host that serves static files works.

Building for production

# Minified, hashed, sourcemap-hidden — what you'd deploy
npm run build

# Non-minified — useful for debugging the production output
npm run build:dev

# Build at a subpath (e.g. https://example.com/admin/)
BASE_PATH=/admin/ npm run build

Both build flavors write to dist/. The minified build runs Terser with three passes, drops console.log, and produces content-hashed filenames for every asset.

What’s in dist/

dist/
├── production/
│   ├── index.html              # Operations dashboard
│   ├── inbox.html              # Inbox client
│   ├── kanban.html             # Drag-drop board
│   ├── theme.html              # Live theme generator
│   ├── playground.html         # Component playground
│   ├── offline.html            # SW fallback
│   └── ...                     # 58 pages total
├── js/
│   ├── main-[hash].js          # Entry chunk for index.html
│   ├── inbox-[hash].js         # Lazy-loaded inbox module
│   ├── kanban-[hash].js
│   ├── vendor-echarts-[hash].js
│   ├── vendor-tables-[hash].js
│   ├── vendor-maps-[hash].js
│   └── ...
├── assets/
│   └── main-[hash].css         # Compiled SCSS, one bundle
├── fonts/                      # If any are self-hosted (default uses Google Fonts CDN)
├── images/
│   └── favicon-[hash].svg      # ...and other content-hashed images
├── sw.js                       # Service worker (not hashed — needs stable URL)
├── site.webmanifest            # PWA manifest
└── stats.html                  # Bundle analyzer treemap

Every page links to the same compiled CSS and the same main-[hash].js entry. Page-specific modules (inbox, kanban, calendar, settings, charts vendor chunk, etc.) load on demand via dynamic import — see Architecture for the lazy-load pattern.

Deploying to static hosts

Cloudflare Pages

# Build, then deploy via Wrangler
npm run build
npx wrangler pages deploy dist --project-name=your-project

Or connect the repo to Cloudflare Pages via the dashboard:

  • Build command: npm run build
  • Build output directory: dist
  • Node version: 20

For preview deploys on every PR, the GitHub integration handles it automatically.

Netlify

Drag-and-drop deploy: build locally with npm run build, then drag dist/ into Netlify’s deploy zone.

Continuous deploy from a git repo — add a netlify.toml at the project root:

[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "20"

# Service-worker scope must match the deployed path
[[headers]]
  for = "/sw.js"
  [headers.values]
    Cache-Control = "no-cache"
    Service-Worker-Allowed = "/"

# Cache hashed assets aggressively
[[headers]]
  for = "/js/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/images/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

Vercel

Create a vercel.json:

{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": null,
  "headers": [
    {
      "source": "/(js|assets|images|fonts)/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    },
    {
      "source": "/sw.js",
      "headers": [
        { "key": "Cache-Control", "value": "no-cache" }
      ]
    }
  ]
}

Or use the CLI: vercel --prod.

GitHub Pages

GitHub Pages serves the contents of a branch or a /docs folder. The cleanest path is a GitHub Actions workflow:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: BASE_PATH=/${{ github.event.repository.name }}/ npm run build
      - uses: actions/upload-pages-artifact@v3
        with: { path: dist }
      - uses: actions/deploy-pages@v4

The BASE_PATH is the repo name with leading/trailing slashes — GitHub Pages serves the repo at https://username.github.io/repo-name/.

S3 + CloudFront

npm run build

# Long-cache hashed assets
aws s3 sync dist/ s3://your-bucket/ --delete \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "*.html" --exclude "sw.js" --exclude "site.webmanifest"

# No-cache for HTML so users always get fresh references
aws s3 sync dist/ s3://your-bucket/ \
  --cache-control "public, max-age=0, must-revalidate" \
  --include "*.html" --include "sw.js" --include "site.webmanifest"

The two-pass sync sets long-cache for hashed assets and no-cache for HTML + service worker. The SW must not be aggressively cached — otherwise users get stuck on an old version that never updates.

In CloudFront, invalidate the HTML paths after every deploy (/*.html, /sw.js, /site.webmanifest).

Cloudflare R2

Like S3 but with no egress fees. Use rclone:

npm run build
rclone sync dist/ r2remote:bucket-name/ --progress

Behind a Cloudflare Worker or a custom domain, R2 serves static files at zero egress cost. Wrap with a Worker if you need custom routing, redirects, or A/B tests.

NPM publishing

If you maintain a fork of Gentelella and want to publish your version, the package ships both src/ and a pre-built dist/:

# Update version in package.json
npm run lint           # must pass
npm run build          # produces dist/
npm publish

Consumers install with:

npm install your-gentelella-fork

And import from node_modules/your-gentelella-fork/dist/ — handy when shipping a Rails / Django / Laravel integration where the backend serves the compiled assets.

CDN configuration

A few headers worth setting:

# JS / CSS / fonts / images — hashed, safe to cache forever
Cache-Control: public, max-age=31536000, immutable

# HTML — must revalidate
Cache-Control: public, max-age=0, must-revalidate

# Service worker — strictly no-cache (or short max-age, e.g. 5 minutes)
Cache-Control: no-cache
Service-Worker-Allowed: /

# Compress everything text-based
Content-Encoding: gzip       (or br for Brotli)

The Service-Worker-Allowed: / header is needed if you’re serving the SW from a subpath but want it to control the root scope. Default Vite output places sw.js at the root, so this is rarely needed.

Most modern CDNs (Cloudflare, Vercel, Netlify, CloudFront) apply Brotli automatically — saves ~15% over gzip.

Subpath deploys

Common when hosting under a path like https://example.com/admin/:

BASE_PATH=/admin/ npm run build

What changes:

  • Every asset URL in HTML gets /admin/ prefixed
  • The shell-injection plugin uses the base when generating manifest links
  • The service worker resolves its scope against the base
  • The PWA manifest’s start_url works because it’s relative

Verify locally with the same env var:

BASE_PATH=/admin/ npm run preview
# Opens http://localhost:9174/admin/production/index.html

If the base path is wrong (e.g. you deployed under /dashboard/ but built with /admin/), pages render but every asset 404s. Always rebuild when changing the deploy path.

Quick checklist before deploy

npm run lint            # ESLint across src/ — fix any errors
npm run build           # produces dist/
ls -la dist/            # sanity check the output
du -sh dist/            # ~5 MB total (vendor chunks dominate)

After deploy, check:

  • HTML pages load (/production/index.html, /production/inbox.html, etc.)
  • Service worker registers (Chrome devtools → Application → Service Workers)
  • PWA install icon appears in the address bar (Chrome/Edge)
  • Dark mode toggles persist across refreshes
  • No console errors

What you don’t need

A few things the template 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 .htaccess or rewrite rules — pages are served by filename (/production/inbox.html, not /inbox)

If you want pretty URLs (/inbox instead of /production/inbox.html), each host has its own way: Netlify uses redirects, Vercel uses cleanUrls: true, CloudFront uses Lambda@Edge, Cloudflare uses Workers. Gentelella doesn’t ship a config for any of them — the template itself is host-agnostic.

See also

  • Vite build — how the output is produced
  • PWA setup — service-worker registration in production builds only
  • Architecture — what the lazy-loading pattern means for asset cache strategy