All WordPress HTML Templates Forms & Webhooks AI & Tools
HTML Templates

Build Tooling for Static Templates: Vite, Gulp and When Plain Files Win

A practitioner’s guide to the HTML template build process: configuring a Vite static HTML site for multi-page, where Gulp 5 still wins, and when no build

A real working moment illustrating the theme of an article about vite static html site. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

We inherited a 42-page marketing site last March. The handover zip contained a gulpfile.js last touched in 2018, and npm install died instantly: node-sass trying to compile native bindings against Node 20, no Python 2 on the machine, no prebuilt binary for that combination. Fixing the build took longer than the actual content changes the client had asked for.

The reflex at that point is to throw it all away and move to Vite. Sometimes that’s right. Often you’re installing a bundler to solve a problem that is, honestly, “compile some Sass and copy some files”, and you’ll spend a day fighting MPA inputs and hashed filenames to get back to where you started.

So here’s the version written after doing this on both template products and client builds: what a vite static html site actually requires, where Gulp still earns its place, and the cases where the correct build process is no build process.

Key Takeaways

  • Vite treats every HTML file as an entry point, so a multi-page site needs each one declared in build.rollupOptions.input. Generate that list with a glob, never maintain it by hand.
  • Gulp 5 isn’t a bundler and never was. It wins when the job is “compile, minify and copy while keeping the file structure and filenames exactly as authored”, which is the normal case for template distribution.
  • The real reason most static sites grow a build step is HTML partials, not JavaScript bundling. If that’s your only driver, Eleventy solves it better than either tool.
  • Native CSS nesting, custom properties and @layer now cover most of what people used Sass for. npx sass --watch plus a one-line minify on deploy is a legitimate production setup for small sites.
  • Hashed asset filenames are a cost, not a feature. Only accept them if you genuinely need long-cache immutability, because they break hand-edited links and client-side string-built URLs.

The three jobs a build actually does

Strip away tooling preferences and there are only three things a static site build does for you:

  1. Transform: Sass to CSS, TypeScript to JS, autoprefixing, image conversion to WebP or AVIF.
  2. Compose: HTML partials and layouts, data-driven page generation, bundling many JS modules into fewer files.
  3. Optimise and fingerprint: minify, tree-shake, hash filenames for cache busting, inline critical CSS.

Vite is excellent at 1 and 3, and does 2 only for JavaScript. Gulp does 1 and 3 as a pipeline of your choosing and leaves 2 to a plugin. Plain files do none of it, which is fine when you need none of it. Write down which of those three you actually need before you pick a tool. Most arguments about gulp vs vite are two people solving different problems.

A real working moment that fits an article section about

Vite for a static HTML site: what works and what bites

Vite 7 requires Node 20.19 or newer (or 22.12+), and the dev server is genuinely the best part. Native ESM, no bundling in dev, HMR that lands CSS changes in well under 100ms even on a stylesheet tree with several hundred partials. After years of Browsersync reloading a full page on every save, that alone justifies the switch for day-to-day work.

Where it bites is multi-page. Vite’s dev server will happily serve /about.html with zero config, but vite build only emits pages it knows about. Miss one and it silently vanishes from dist. Glob the inputs:

// vite.config.js
import { defineConfig } from 'vite';
import { resolve, relative, extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';

const pages = Object.fromEntries(
  glob.sync('**/*.html', { ignore: ['dist/', 'node_modules/'] })
    .map(file => [
      // key becomes the output path, so keep the folder structure
      relative('.', file.slice(0, file.length - extname(file).length)),
      fileURLToPath(new URL(file, import.meta.url))
    ])
);

export default defineConfig({
  appType: 'mpa',          // no SPA fallback, 404 on unknown routes like a real host
  build: {
    rollupOptions: { input: pages },
    assetsInlineLimit: 0,  // stop small SVGs becoming base64 in your CSS
    cssCodeSplit: true
  },
  server: { port: 5173 }
});

Three things will catch you after that. First, Vite rewrites and hashes any asset referenced from an attribute it understands, so <img src="images/hero.jpg"> becomes /assets/hero-B1x9k2.jpg. Anything built as a string in JavaScript ('images/' + slug + '.jpg') is invisible to the bundler and will 404 in production. Put those files in public/, which is copied verbatim.

Second, legacy UMD scripts. Template ecosystems are full of jQuery plugins that expect a global and a specific load order. Importing them into a module graph changes that order in ways that are hard to predict. Keep them as classic script tags from public/ until you have time to replace them properly.

Third, subdirectory deploys. base: './' works for flat sites and gets awkward once your pages live in nested folders, because relative CSS url() resolution shifts. If the site will live at example.com/docs/, set an absolute base and move on.

Where Gulp still wins

Gulp 5 shipped in 2024 with ESM support and a modern glob implementation. The “Gulp is dead” takes mostly came from people who wanted it to be a bundler. It isn’t. It’s a stream-based task runner, and that’s precisely why it still wins in one scenario: when the output must mirror the input, filename for filename.

That’s our situation with Canvas. Over 200 demos and 1400+ page templates, distributed to buyers who open index.html, copy the markup they want and edit the CSS. Hashed filenames would be hostile. Bundling 90 jQuery plugins into one chunk would be hostile. What’s needed is: compile Sass, autoprefix, minify, write style.css and style.min.css side by side, every time, predictably.

// gulpfile.js (ESM, Gulp 5)
import gulp from 'gulp';
import * as dartSass from 'sass';
import gulpSass from 'gulp-sass';
import postcss from 'gulp-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
import rename from 'gulp-rename';

const sass = gulpSass(dartSass);

export function styles() {
  return gulp.src('scss/*.scss', { sourcemaps: true })
    .pipe(sass.sync({ quietDeps: true }).on('error', sass.logError))
    .pipe(postcss([autoprefixer()]))
    .pipe(gulp.dest('css', { sourcemaps: '.' }))      // readable build for buyers
    .pipe(postcss([cssnano({ preset: 'default' })]))
    .pipe(rename({ suffix: '.min' }))
    .pipe(gulp.dest('css'));                           // minified build for production
}

export const watch = () => gulp.watch('scss/**/*.scss', styles);

Two notes from running this in anger. Use sass.sync() rather than the async API: on a large Sass tree it’s measurably faster because the Dart VM isn’t hopping between the JS event loop, and the legacy async JS API is deprecated anyway. Set quietDeps: true or your terminal fills with @import deprecation warnings from Bootstrap’s own source, which you can’t fix and shouldn’t have to read 400 times a day.

If you’re staring at a node-sass project right now, the migration is usually just swapping to sass (dart-sass), replacing gulp-sourcemaps with Gulp’s built-in sourcemaps option, and deleting whatever gulp-util plugin chain has rotted. We’ve done that in under an hour on projects we’ve never seen before.

When plain files win

Here’s the position that annoys people: for a site under roughly 10 pages with no repeated markup, the correct html template build process is none. No package.json, no lockfile, no dependency that breaks when Node goes to 24.

The reason this is viable now and wasn’t in 2019 is CSS itself. Native nesting shipped across Chrome, Safari and Firefox, and the relaxed parsing rules landed in 2024, so & behaves the way you expect. Custom properties replaced variables. @layer replaced most of what people were doing with import order discipline. color-mix() replaced darken(). What’s left of Sass for a small project is mixins and partials. If you want those, one command gives them to you:

# no config file, no plugins, no build system
npx sass scss/style.scss:css/style.css --watch --style=expanded

on deploy, one pass for production

npx lightningcss-cli --minify --bundle --targets '>= 0.25%' scss/style.css -o css/style.min.css

The trade-offs are real, so be clear-eyed. You get no filename hashing, so cache busting is a manual ?v=3 query or a host-level header policy. You get no automatic image optimisation. And you have to resist CSS @import at runtime, because each one is a serialised request discovered only after the parent file parses, which is exactly the waterfall we describe in eliminating render-blocking resources on a static site. Concatenate at build time or ship separate <link> tags. Never a chain of CSS imports.

Where plain files stop working: the moment you copy a navigation block into the eleventh file. That’s the signal. Ignoring it is how sites end up with four slightly different headers. Our navigation patterns piece covers the markup side; the maintenance side is pure tooling.

Partials are the real driver, and neither tool solves them well

Vite has no HTML include mechanism. You can bolt one on with vite-plugin-handlebars or a small transformIndexHtml hook. Gulp has gulp-file-include and posthtml-include. Both work. Both feel like scaffolding you built yourself at 1am, because that’s what they are.

If composition is your primary need, use a static site generator. Eleventy 3 is the honest answer for content-shaped sites: Nunjucks layouts, a _data folder, zero client-side runtime, and it will happily consume the HTML you already have. We’ve moved client sites from a Gulp-plus-include setup to Eleventy in a day and deleted 80% of the gulpfile.

A rough split that has held up across projects:

  • Vite: app-shaped. Real JavaScript, components, a dashboard, something with state.
  • Eleventy (often with Vite for the asset layer): content-shaped. Many similar pages, shared layouts, data files, a blog.
  • Gulp: distribution-shaped. Output must match input structurally, consumers edit the files, no bundling wanted.
  • Nothing: brochure-shaped. A handful of pages, a designer or client edits HTML directly, longevity matters more than ergonomics.

That last row is underrated. A site with no build still runs in five years. A 2019 Webpack config does not.

Picking without regret

Ask who edits the files after you leave. A client on a shared host with FileZilla cannot run npm run build, which means any tool that transforms filenames has just locked them out of their own site. That constraint alone has decided the tooling on more of our projects than performance ever has. It’s the same question worth asking before you commit to a template at all, as we argue in how to evaluate an HTML template.

Then check the dependency count. A static site build with 900 packages in the lockfile is a liability with a two-year fuse. Vite with no plugins pulls in a modest tree built around esbuild and Rollup. A Gulp pipeline with 14 single-purpose plugins is where the rot happens, because each plugin is a thin wrapper that stops being maintained while the underlying library moves on. Prefer calling libraries directly inside a Gulp task over installing gulp-whatever.

Keep a dumb escape hatch. Whatever you pick, make sure a developer can open dist, edit a file and see the change. Builds whose output is unreadable are builds nobody can debug under pressure.

Frequently Asked Questions

Is Gulp dead in 2026?

No. Gulp 5 is maintained, supports ESM and runs fine on current Node. What died is Gulp as a bundler, because esbuild and Rollup do that far better. Use it as a task runner for compile-and-copy pipelines and it’s still the cleanest tool for the job.

Can Vite build a plain HTML site with no framework?

Yes, and it’s good at it, with one condition: every HTML page must be listed in build.rollupOptions.input or it won’t appear in dist. Set appType: 'mpa' so the dev server stops serving the SPA fallback and returns 404s like your real host will. Beyond that you get the dev server, HMR and automatic minification with no extra config.

Do I still need Sass?

For a small site, probably not. Nesting, custom properties, @layer and color-mix() are all native now. Sass still earns its place on large codebases for @use module scoping, loops that generate utility classes, and compile-time maps, which is why most Bootstrap-based template systems are still built on it.

Why do my images 404 after vite build?

Because the reference wasn’t statically analysable. Vite rewrites and hashes assets it finds in HTML attributes and in CSS url(), but a path assembled in JavaScript at runtime is just a string to the bundler. Move those files into public/, where they’re copied with their original names and paths intact.

Should I commit the dist folder?

Commit it if the deploy target is FTP or a host with no build step, and the client may need to recover without you. Otherwise don’t: build in CI and keep the repo clean, since a committed dist produces a merge conflict on every pull request touching CSS. Pick one convention and document it in the README. The mixed state is the worst outcome.

The decision

Count your pages, count your partials, and name the person who edits the files next year. Under 10 pages with no repetition: skip the build, use the Sass CLI if you want partials, minify on deploy. Many similar pages: Eleventy, with Vite handling assets if you want the dev server. Real application JavaScript: Vite, and budget half a day for the MPA input config and asset path surprises. Shipping files that other people will edit by hand: Gulp 5, dart-sass, as few plugins as you can get away with.

Then delete the tooling you’re not using. The build you don’t have is the one that never breaks.

eleventy vs vite static site gulp 5 dart sass pipeline gulp vs vite 2026 html template build process migrating node-sass to dart-sass gulp static site build without node vite multi page app rollupOptions input vite static html site