HTML5 Boilerplate (h5bp) is a professional front-end template for building fast, robust, and adaptable web apps or sites. It is the product of more than ten years of iterative development by 200+ contributors and ships as a small, hand-curated set of HTML, CSS scaffolding, icons, manifests, and webpack configuration that you drop into a new project as the foundation.
This guide walks you through every supported way to start a project, the contents of the template, the build commands you will run day-to-day, and the practical extension points (Open Graph, theme color, web app manifest, search, social, security.txt) that the template ships ready to wire up.
Note
HTML5 Boilerplate does not impose a development philosophy or framework. The whole point is that you are free to architect your code however you want — the template just gives you a sane, modern starting line. Current release is v9.0.1 under the MIT license, requiring Node.js
>=20.
1. Quick Start
The fastest way to start a brand-new project is the create-html5-boilerplate scaffolder. It downloads the latest published template from npm, unpacks it into a folder you name, and leaves you ready to install dependencies — no global tools, no clones, no zip extraction.
npx create-html5-boilerplate my-site
cd my-site
npm install
npm run start
> webpack serve --open --config webpack.config.dev.js
[webpack-dev-server] Project is running at:
[webpack-dev-server] http://localhost:8080/
Your default browser opens automatically to a "Hello world! This is HTML5 Boilerplate." page with hot-module replacement and live reload enabled. Edit index.html, js/app.js, or css/style.css and the browser refreshes immediately.
Tip
If
npx create-html5-boilerplateis not yet available on your machine, the command auto-downloads the scaffolder package on first run. There is no install step. The package is published atcreate-html5-boilerplateon npm.
What you just got
After scaffolding you have a project folder containing the content of what was previously the upstream repo's src/ folder — meaning your project root is the template. You will see (among other files) index.html, 404.html, robots.txt, site.webmanifest, the icon set, js/app.js, and three webpack config files.
Try it: edit your first page
- Open
index.htmlin your editor. - Change
<html lang="">to<html lang="en">. - Set
<title>My First Site</title>. - Save. The browser auto-refreshes.
What to try next
- Section 2 — every supported install method (zip download, GitHub template repo,
npm install, git clone of the upstream repo). - Section 3 — the meta tags, manifest, and dotfiles you need to fill in before going to production.
- Section 4 — the webpack scripts and the layout of the shipped files.
- Section 5 — recipe-style walkthroughs for the most common day-one tasks (replace icons, configure Open Graph, set caching headers via the companion server configs, deploy to GitHub Pages / Netlify / Vercel, run linters, build for production).
2. Installation
System requirements
| Requirement | Minimum | Notes |
|---|---|---|
| Node.js | >=20 |
Enforced via engines.node in src/package.json. CI runs Node 22 and Node 24. |
| Volta-pinned Node | 20.19.1 |
Used for reproducible local builds via Volta. |
| Operating system | Any | macOS, Linux, Windows (CRLF normalization handled by .gitattributes). |
| Browser (for previewing) | Latest stable | The template targets Browserslist default. |
Internet Explorer is not supported (dropped in v8.0.0). Modernizr is not bundled (removed in v9.0.0). There are no runtime dependencies for the published site — the deliverable is pure static output (dist/).
Important
You need Node.js for the development workflow (running webpack, building production bundles). You do not need Node.js to host the site in production — the build output (
dist/) is plain HTML, CSS, JS, and images that any static host can serve.
Pick your install method
create-html5-boilerplate (recommended)
Scaffold a brand-new project. Fastest path. No global install required.
npx create-html5-boilerplate my-site
cd my-site
npm install
npm run start
The scaffolder fetches the latest published html5-boilerplate package from npm and unpacks dist/ into the folder you specify.
GitHub Template Repo
Use the HTML5 Boilerplate template repository on GitHub. Click Use this template → Create a new repository to seed a brand new GitHub repo from the latest main branch.
Then on your machine:
git clone https://github.com/your-username/your-new-repo.git my-site
cd my-site
npm install
npm run start
The template repo is auto-mirrored from the upstream main branch by the push-to-template.yml workflow, so it always reflects the latest release.
npm install (existing project)
Install the package into an existing project and copy the contents of node_modules/html5-boilerplate/dist/ into your project root:
npm install html5-boilerplate
yarn add html5-boilerplate
The dist/ folder is the actual deliverable — everything else in the upstream repo is build tooling for the project itself.
Download zip
Grab a tagged release zip from GitHub Releases. The zip is html5-boilerplate_v9.0.1.zip (or the current version) and contains the same files as dist/.
mkdir my-site
cd my-site
curl -LO https://github.com/h5bp/html5-boilerplate/releases/latest/download/html5-boilerplate_v9.0.1.zip
unzip html5-boilerplate_v9.0.1.zip
npm install
npm run start
On Windows PowerShell:
Expand-Archive html5-boilerplate_v9.0.1.zip -DestinationPath .
git clone (upstream)
Only useful if you intend to contribute back to the project itself. The upstream repo includes the gulpfile, test suite, and CHANGELOG that are not part of the deliverable — those are build tooling for the maintainers.
git clone https://github.com/h5bp/html5-boilerplate.git
cd html5-boilerplate
npm install
npm run build
See the Administrator & Developer Guide for the full upstream contribution workflow.
Tip
For 99% of users starting a new site,
npx create-html5-boilerplate my-siteis the right answer. The other methods exist for migration scenarios (drop the template into an existing app) or air-gapped/offline use (the zip).
Verify your install
After npm install you can confirm the dev server starts:
npm run start
> webpack serve --open --config webpack.config.dev.js
asset js/app.js 28 bytes [emitted] (name: app)
webpack 5.105.3 compiled successfully
Then build the production bundle:
npm run build
> webpack --config webpack.config.prod.js
asset index.html 656 bytes [emitted]
asset js/app.js 26 bytes [emitted] [minimized] (name: app)
asset 404.html 1.74 KiB [emitted] [from: 404.html] [copied]
... (icons, manifest, robots.txt, css/style.css copied)
webpack 5.105.3 compiled successfully
The dist/ folder is what you upload to your web host.
Warning
The development bundle includes
inline-source-mapsource maps and is unminified. Always runnpm run buildbefore deploying to production — the dev bundle is much larger and exposes your source.
Install troubleshooting
EBADENGINEon Node 18 or older — the template requires Node 20+. Runnvm install 20 && nvm use 20, or use Volta (volta install node@20).npm error code EACCESduring global install — you do not need a global install.npx create-html5-boilerplateruns the scaffolder one-off without touching your global node_modules.gulp: command not found— that's the upstream maintainer build tool. Downstream users usenpm run startandnpm run buildinstead. If you cloned the upstream repo by mistake, see the AdminGuide.
3. Configuration
The template's "configuration" is the set of meta tags, manifest fields, and dotfiles that you fill in once per project. There is no central config file — each piece lives in the asset that needs it.
Files you customize per project
| File | What you change | Required for |
|---|---|---|
index.html |
<title>, <meta name="description">, <html lang>, <meta name="theme-color">, all og:* tags |
Every site |
site.webmanifest |
name, short_name, additional icon sizes, display, theme_color, background_color |
Installable PWAs |
robots.txt |
Disallow: paths, optional Sitemap: directive |
Sites with private routes or a sitemap |
favicon.ico, icon.svg, icon.png |
Replace with your own assets | Branding |
404.html |
Friendly error page text | Production sites |
package.json (your project) |
name, version, scripts, dev dependencies |
Every site |
index.html — annotated
The shipped index.html is intentionally minimal:
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" href="css/style.css">
<meta name="description" content="">
<meta property="og:title" content="">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<meta property="og:image:alt" content="">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon.png">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
</head>
<body>
<!-- Add your site or application content here -->
<p>Hello world! This is HTML5 Boilerplate.</p>
<script src="js/app.js"></script>
</body>
</html>
The blank lang="", blank <title>, blank <meta name="description">, and blank og:* properties are intentional placeholders that you fill in for every page.
Common option combinations
| Goal | Tags / fields to set |
|---|---|
| Basic SEO | <title>, <meta name="description">, <html lang="..."> |
| Rich social preview | All og:* fields including og:image:alt |
| Installable PWA | site.webmanifest filled in, both 192×192 + 512×512 icons, HTTPS, valid start_url |
| Block crawlers from one path | Add Disallow: /admin/ to robots.txt |
| Block all indexing (staging) | Replace Disallow: with Disallow: / AND add <meta name="robots" content="noindex"> to index.html |
| Disable iOS phone-number auto-linking | Add <meta name="format-detection" content="telephone=no"> |
| Disable Chrome auto-translation | Add <meta name="google" content="notranslate"> |
The shipped site.webmanifest
{
"short_name": "",
"name": "",
"icons": [{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "/?utm_source=homescreen",
"background_color": "#fafafa",
"theme_color": "#fafafa"
}
Fill in short_name and name. Add a 512×512 icon entry to satisfy PWA install criteria.
The shipped robots.txt
# https://www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:
This explicitly allows every crawler to visit every page. The empty Disallow: line is correct robots.txt syntax for "no paths are blocked."
Caution
robots.txtis not access control. URLs disallowed there can still be indexed (just not crawled), and the file itself is publicly readable — listing private paths inDisallow:advertises their existence. Use authentication for anything that must stay private.
Dotfiles
| File | What it does |
|---|---|
.editorconfig |
Forces 2-space indent, LF endings, UTF-8, trailing-whitespace trim across editors |
.gitattributes |
Forces LF line endings on .css, .html, .js, .json, .md, .mjs, .sh, .txt, .xml, .yml |
.gitignore |
Ignores node_modules, dist/package-lock.json, archive output |
Don't delete the .editorconfig or .gitattributes — they prevent cross-platform contributors from accidentally introducing CRLF / indentation drift.
Important
The full architectural reference for these files (and the upstream test suite that asserts the exact byte content of
style.css) lives in the Administrator & Developer Guide section 8 (Developer Internals).
4. Core Usage
The day-to-day surface for downstream users is just two commands and a small set of files you edit. This section covers exactly what each command does and what each shipped file is for.
The two scripts you run
npm run start
> webpack serve --open --config webpack.config.dev.js
This starts the webpack dev server with liveReload, hot-module replacement, and open: true (auto-opens your default browser). The server uses an inline source map (devtool: 'inline-source-map') so debugging works directly in the browser DevTools.
npm run build
> webpack --config webpack.config.prod.js
This produces a dist/ folder containing index.html (templated by HtmlWebpackPlugin), 404.html, the icon set, js/app.js (bundled), js/vendor/* (copied verbatim), css/, img/, robots.txt, and site.webmanifest. Upload the contents of dist/ to any static host.
There is no other npm script you typically run. (The default npm test script in src/package.json is echo "Error: no test specified" && exit 1 — a placeholder for you to replace with your own test runner.)
Note
The
npm run startandnpm run buildscripts live insrc/package.json(the per-sitepackage.jsonthat ships with the template), not in the upstream repo's rootpackage.json. After scaffolding, your project root is what was previouslysrc/.
Shipped file inventory
When you scaffold a new project, you get exactly this layout. (The build output in dist/ is the same files, plus a bundled js/app.js.)
.
├── 404.html # Placeholder 404 error page (inline-styled, no external deps)
├── css/
│ └── style.css # Sourced from h5bp/main.css with autoprefixer + version banner
├── favicon.ico # Default browser tab icon
├── icon.png # 192×192 PNG icon (Apple Touch Icon, manifest)
├── icon.svg # Scalable icon for modern browsers
├── img/ # Drop your images here
│ └── .gitkeep
├── index.html # The default HTML skeleton
├── js/
│ ├── app.js # Your entry-point JavaScript (empty)
│ └── vendor/ # Third-party libraries (copied as-is by webpack)
│ └── .gitkeep
├── package.json # Webpack scripts + devDependencies
├── robots.txt # Allow-everything by default
├── site.webmanifest # Web app manifest stub
├── webpack.common.js # Shared webpack config (entry + output)
├── webpack.config.dev.js # Dev server overrides
├── webpack.config.prod.js# Production build overrides
├── .editorconfig # Cross-editor formatting rules
├── .gitattributes # Force LF line endings on text files
└── .gitignore # node_modules, archive, lockfiles
Total source footprint: about 41 files in the upstream repo, dominated by the icon set, manifests, and webpack scaffolding. The HTML skeleton itself is intentionally minimal — fewer than 25 lines of body markup.
Tip
The template is "delete-key friendly." If you don't need a piece (Open Graph, the manifest, the 404 page, the SVG icon), just delete it AND remove its CopyPlugin entry from
webpack.config.prod.js. There are no hidden cross-references that will break.
index.html — the skeleton
index.html is the document you'll edit most. Its purpose is to:
- Declare a UTF-8 charset and a responsive viewport.
- Link the bundled stylesheet (
css/style.css) and the bundled script (js/app.js). - Reference the icon set (favicon
.ico, scalable.svg, and Apple Touch icon.png). - Reference the web app manifest.
- Carry placeholder Open Graph metadata for social sharing.
The webpack production build runs this file through HtmlWebpackPlugin, which auto-injects the bundled <script> tag (so manual edits to the <script src="js/app.js"> line are not strictly required — webpack rewrites it).
404.html — error page
404.html is a self-contained, inline-styled error page with no external CSS or JS dependencies. It uses a CSS table layout to vertically center the error message and a single @media (max-width: 280px) query to stay legible on narrow screens.
Note
The 404 page intentionally inlines its CSS. Internet Explorer's older "friendly error pages" feature replaced custom 404 responses smaller than 512 bytes; the inlined CSS keeps the page above that threshold and avoids a blank fallback in legacy contexts.
app.js — your entry point
js/app.js ships empty. It is the webpack entry point declared in webpack.common.js:
const path = require('path');
module.exports = {
entry: { app: './js/app.js' },
output: {
path: path.resolve(__dirname, 'dist'),
clean: true,
filename: './js/app.js',
},
};
Replace its contents with your application code, or wire in a framework — webpack will bundle whatever you import.
js/vendor/ — drop-in libraries
Anything you place under js/vendor/ is copied verbatim into dist/js/vendor/ by the production webpack CopyPlugin. This is the right place for prebuilt third-party scripts you do not want bundled (analytics snippets, polyfills served as-is, jQuery if you still need it).
css/style.css — base styles
The template ships with style.css derived from h5bp/main.css. The base styles do not include a CSS reset framework like Normalize.css — that was intentionally dropped in v9.0.0. Instead, you get a small set of opinionated defaults: typography readability, text-shadow protection during selection, alignment fixes for img, video, fieldset, textarea, and a styled outdated-browser prompt.
Icons
| File | Purpose | When the browser uses it |
|---|---|---|
favicon.ico |
Tab icon, bookmarks | All browsers; auto-discovered if at site root |
icon.svg |
Scalable tab icon | Modern browsers that support image/svg+xml favicons |
icon.png (192×192) |
Apple Touch Icon, home screen | iOS and Android home-screen install |
Replace them with your own. For multi-resolution Apple Touch Icons across older iOS versions, see Section 5.
Webpack — three files, one entry
webpack.common.js — entry + output (shared)
webpack.config.dev.js — merges common + dev-server settings
webpack.config.prod.js — merges common + HtmlWebpackPlugin + CopyPlugin patterns
The two derived configs use webpack-merge to compose. If you need to add a webpack rule used by both modes (e.g., a TypeScript loader), put it in webpack.common.js.
The CopyPlugin patterns in webpack.config.prod.js enumerate every static file that round-trips from your project root to dist/:
new CopyPlugin({
patterns: [
{ from: 'img', to: 'img' },
{ from: 'css', to: 'css' },
{ from: 'js/vendor', to: 'js/vendor' },
{ from: 'icon.svg', to: 'icon.svg' },
{ from: 'favicon.ico', to: 'favicon.ico' },
{ from: 'robots.txt', to: 'robots.txt' },
{ from: 'icon.png', to: 'icon.png' },
{ from: '404.html', to: '404.html' },
{ from: 'site.webmanifest', to: 'site.webmanifest' },
],
}),
When you add a new static file, add a CopyPlugin entry for it. Without an entry, the file is not copied into dist/ and your deploy will be missing that asset.
Warning
webpack.config.prod.jsis the source of truth for "what files end up in the production build." Forgetting to add a new asset's CopyPlugin pattern is the single most common deploy bug for this template.
5. Common Tasks
Recipe-style walkthroughs of the most common things you'll do once. Each recipe lists the goal, the prerequisites, the exact steps, and how to verify it worked.
Recipe: Start a brand-new project
Goal: A fresh HTML5 Boilerplate project running locally with hot reload.
Prerequisites: Node.js >=20. Optionally Volta for pinned Node management.
Steps:
npx create-html5-boilerplate my-site
cd my-site
npm install
npm run start
Verify: Your default browser opens to http://localhost:8080/ and shows "Hello world! This is HTML5 Boilerplate." Editing index.html triggers an auto-refresh.
If it fails: See the troubleshooting section. Common causes are wrong Node version (EBADENGINE) and port 8080 already in use (EACCES or EADDRINUSE).
Recipe: Customize meta tags for SEO and social sharing
Goal: A page that renders correctly when shared on Facebook, LinkedIn, Slack, Discord, iMessage, and surfaces with the right title in Google.
Prerequisites: Project scaffolded. A 1200×630 PNG saved to img/social-preview.png.
Steps: Edit index.html:
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acme Widget Co — Quality widgets since 1923</title>
<meta name="description" content="Acme Widget Co builds artisan widgets for the discerning factory.">
<meta property="og:url" content="https://www.acmewidget.example/">
<meta property="og:title" content="Acme Widget Co — Quality widgets since 1923">
<meta property="og:type" content="website">
<meta property="og:description" content="Acme Widget Co builds artisan widgets for the discerning factory.">
<meta property="og:image" content="https://www.acmewidget.example/img/social-preview.png">
<meta property="og:image:alt" content="A row of polished brass widgets on a workbench.">
<meta property="og:site_name" content="Acme Widget Co">
<meta name="theme-color" content="#bf3f00">
Verify: Use Facebook's Sharing Debugger, LinkedIn's Post Inspector, and the Twitter/X Card Validator. Each platform shows your image, title, and description as configured.
If it fails: Social platforms cache scraped Open Graph metadata for hours. Use the platform's debug tool to force a re-scrape.
Tip
Always provide
og:image:alt. It is read by screen readers when the image is announced and is required for accessible link previews. The empty placeholder shipped inindex.htmlexists exactly because you must fill this in.
Recipe: Configure caching headers via the companion server configs
HTML5 Boilerplate intentionally does not ship server configs in the template anymore — the .htaccess file was removed in v9.0.0. The h5bp organization maintains separate, server-specific repos with hardened defaults (security headers, content-type mappings, gzip/brotli, immutable caching for hashed assets):
| Server | Companion repo |
|---|---|
| Apache | h5bp/server-configs-apache |
| Nginx | h5bp/server-configs-nginx |
| IIS | h5bp/server-configs-iis |
| Lighttpd | h5bp/server-configs-lighttpd |
| Node.js | h5bp/server-configs-node |
| Google App Engine | h5bp/server-configs-gae |
Goal: Cache hashed bundles aggressively (max-age=31536000, immutable) and HTML conservatively (max-age=0, must-revalidate).
Prerequisites: A static host that respects .htaccess (Apache), nginx.conf, IIS web.config, or your platform's caching directives.
Steps (Apache example): Pull the relevant snippet from h5bp/server-configs-apache:
# .htaccess fragment from h5bp/server-configs-apache
<IfModule mod_headers.c>
# Hashed JS/CSS — cache forever
<FilesMatch "\.(js|css)$">
Header set Cache-Control "max-age=31536000, immutable"
</FilesMatch>
# HTML — never cache long
<FilesMatch "\.html$">
Header set Cache-Control "max-age=0, must-revalidate"
</FilesMatch>
</IfModule>
Steps (Nginx example): From h5bp/server-configs-nginx:
location ~* \.(js|css)$ {
add_header Cache-Control "max-age=31536000, immutable";
}
location ~* \.html$ {
add_header Cache-Control "max-age=0, must-revalidate";
}
Verify: Use curl -I or Chrome DevTools' Network tab to inspect the Cache-Control header on a deployed asset.
If it fails: Most managed static hosts (Netlify, Vercel, Cloudflare Pages, GitHub Pages) override your Cache-Control headers with their own. Check the platform's caching docs — many of them already use sensible defaults equivalent to the h5bp configs.
Important
Do not copy the entire
server-configs-*repos into your project blindly. Pull only the snippets that match your hosting environment. The configs include security headers (HSTS, X-Frame-Options, Content-Security-Policy) that you must understand before enabling.
Recipe: Integrate with a build tool (TypeScript, Sass, frameworks)
The bundled webpack config is intentionally minimal. To add TypeScript, Sass, or a framework, edit webpack.common.js (so both dev and prod inherit the change) and add the relevant loader.
Goal: Add TypeScript compilation.
Prerequisites: Project scaffolded. npm install already run.
Steps:
npm install --save-dev typescript ts-loader
Edit webpack.common.js:
const path = require('path');
module.exports = {
entry: { app: './js/app.ts' },
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
],
},
resolve: { extensions: ['.tsx', '.ts', '.js'] },
output: {
path: path.resolve(__dirname, 'dist'),
clean: true,
filename: './js/app.js',
},
};
Rename js/app.js to js/app.ts. Add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"sourceMap": true
},
"include": ["js/**/*"]
}
Verify: npm run start. The dev server compiles app.ts and serves the resulting JavaScript.
If it fails: Confirm ts-loader is in devDependencies, not dependencies. Check that tsconfig.json is in the project root (where webpack runs). Issue #3229 tracks adding official TypeScript docs to the upstream project.
Recipe: Deploy to GitHub Pages
Goal: A site at https://your-username.github.io/your-repo/ updated on every push to main.
Prerequisites: A GitHub repo containing your scaffolded project.
Steps:
- In your repo root, create
.github/workflows/deploy.yml:
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
- In your repo's GitHub settings, set Pages → Source to GitHub Actions.
- Push to
main.
Verify: GitHub Actions workflow run shows the deploy step in green. Your site is live at the URL printed in the workflow summary.
If it fails: Confirm your repo's Pages settings use GitHub Actions as the source, not "Deploy from a branch." If asset paths break (404s on js/app.js), you may need to set webpack's output.publicPath to /your-repo/ to match the repo path GitHub Pages serves under.
Recipe: Deploy to Netlify
Goal: A site auto-deploying on every push.
Steps:
- Push your project to GitHub.
- In Netlify, click Add new site → Import an existing project and select the repo.
- Set:
- Build command:
npm run build - Publish directory:
dist
- Build command:
- Click Deploy.
Verify: Netlify reports a successful build. Your site is at the auto-generated *.netlify.app URL.
If it fails: Netlify's default Node version may be older than 20. In your repo, add a .nvmrc file containing 20 or set NODE_VERSION = "20" in netlify.toml.
Recipe: Deploy to Vercel
Goal: A site auto-deploying on every push.
Steps:
- Push your project to GitHub.
- In Vercel, click Add New → Project and select the repo.
- Vercel auto-detects "Other" — set:
- Build Command:
npm run build - Output Directory:
dist
- Build Command:
- Click Deploy.
Verify: Vercel reports a successful build. Your site is live.
If it fails: Vercel sometimes assumes public/ as the output directory. Override it explicitly to dist. Set engines.node in package.json to 20.x so Vercel uses Node 20.
Recipe: Run linters (upstream contributor flow)
Goal: Lint your JavaScript and format Markdown / YAML / JSON before committing.
Prerequisites: Only relevant if you cloned the upstream repo to contribute back. Downstream-site users do not need this.
Steps:
npm run prettier
> prettier --write ./**/*.{js,json,md,mjs,yml}
This rewrites every file in place using the rules from .prettierrc.json (single quotes, brackets on the same line, double quotes in YAML).
To run ESLint via gulp:
gulp lint:js
The flat config in eslint.config.mjs enforces 2-space indent, single quotes, and required semicolons across src/js/*.js, src/*.js, and test/*.mjs.
Verify: No errors reported. The exit code is 0.
If it fails: Most lint failures are easy fixes. The strictest rules are around indentation and quote style — both are auto-fixed by npm run prettier.
Recipe: Build for production
Goal: A clean, minified dist/ folder ready to upload to any static host.
Prerequisites: Project ready. All assets in place. index.html filled in.
Steps:
npm run build
Confirm dist/ contains exactly:
dist/
├── 404.html
├── css/style.css
├── favicon.ico
├── icon.png
├── icon.svg
├── img/
├── index.html
├── js/app.js
├── js/vendor/ (if you put files in src/js/vendor/)
├── package.json
├── robots.txt
├── site.webmanifest
├── webpack.common.js
├── webpack.config.dev.js
├── webpack.config.prod.js
├── .editorconfig
├── .gitattributes
└── .gitignore
Deploy: Upload the contents of dist/ (not the dist/ folder itself) to your web host's document root.
Verify: curl -I https://your-site.example/ returns 200 OK. curl https://your-site.example/site.webmanifest | head shows your manifest. curl https://your-site.example/robots.txt shows your crawler config.
Tip
If you want to include the
package.json, dotfiles, and webpack configs in your repo but not in your published site, add a post-build cleanup step or add them to a.gitignore-style deploy ignore list. Most hosts (Netlify, Vercel, GitHub Pages) ignore those files automatically when servingdist/.
6. Troubleshooting
Error: npm run start fails with EACCES: permission denied or EADDRINUSE
Cause: webpack-dev-server can't bind to port 8080 because another process is using it (or your firewall is blocking it).
Solution: Add a port to the devServer block in webpack.config.dev.js:
devServer: {
liveReload: true,
hot: true,
open: true,
static: ['./'],
port: 9000,
}
Prevention: Pick a port your team agrees on; avoid privileged ports (<1024) on Unix-like systems.
Error: Module not found: Error: Can't resolve './index.html' in production build
Cause: webpack.config.prod.js uses HtmlWebpackPlugin({ template: './index.html' }). If you renamed or moved index.html, the plugin can't find it.
Solution: Update the template path to match your file's actual location:
new HtmlWebpackPlugin({ template: './src/index.html' })
Prevention: If you rename index.html, search the project for the old name and update every reference.
Error: Browser tab shows generic favicon instead of your icon
Cause: Browsers cache favicons aggressively. Your replacement isn't being fetched.
Solution: Append a query parameter to the favicon link to bust the cache during development:
<link rel="icon" href="/favicon.ico?v=2" sizes="any">
In production, change the filename (e.g., favicon-v2.ico) and update the link.
Prevention: Hard-refresh (Ctrl+F5 / Cmd+Shift+R) when previewing icon changes; browsers won't pick up a new favicon on a soft reload.
Error: theme-color is ignored on iOS
Cause: iOS Safari only honors theme-color for sites added to the home screen, and only in iOS 15 and later. In a regular Safari tab on older iOS, the meta tag is ignored entirely.
Solution: This is browser behavior, not a bug. Test on Chrome on Android or iOS 15+ for visible effect. There is no fallback that works in older Safari.
Prevention: Don't rely on theme-color being globally supported — design your branding to work without it.
Error: og: previews don't update on Facebook/LinkedIn after editing index.html
Cause: Social platforms cache scraped Open Graph metadata for hours or days.
Solution: Use the platform's debug tool to force a re-scrape:
- Facebook: Sharing Debugger
- LinkedIn: Post Inspector
- Twitter/X: Card Validator
Prevention: Get your og:* tags right before sharing the URL anywhere. The first scrape is sticky.
Error: Web app manifest not picked up — no install prompt appears
Cause: PWA install criteria are strict. Common gaps: site not served over HTTPS (or localhost), manifest's start_url is unreachable, missing 192×192 and 512×512 icons, no service worker registered.
Solution: Open Chrome DevTools → Application → Manifest. It lists exactly which criteria are unmet. Add the missing pieces (HTML5 Boilerplate intentionally ships only a 192×192 icon — add 512×512 for installability).
Prevention: Test PWA install on localhost (HTTPS is auto-relaxed there) before deploying.
Error: Live reload doesn't trigger on index.html edits
Cause: The dev server's static setting is ['./'], which serves files from the project root. If you moved index.html into a subfolder without updating static, the watcher misses it.
Solution: Update webpack.config.dev.js:
devServer: {
static: ['./src'],
// ...
}
Prevention: If you reorganize the project layout, update static and the HtmlWebpackPlugin({ template }) path together.
Error: npm install fails with EBADENGINE on Node 18 or older
Cause: The template's package.json specifies "engines": { "node": ">=20" }.
Solution: Upgrade Node:
nvm install 20
nvm use 20
The template's volta field pins Node 20.19.1 if you use Volta.
Prevention: Use a Node version manager (nvm, Volta, asdf) and check node --version matches the template's requirement before running install.
Error: GitHub auto-publishes a Twitter Card preview that looks broken
Cause: Twitter has deprecated Twitter Cards and now falls back to Open Graph metadata. Old <meta name="twitter:*"> tags can conflict with newer og: tags.
Solution: Remove the twitter:* tags entirely and rely on og:title, og:description, og:image, og:image:alt. Twitter/X uses Open Graph as its source of truth now.
Prevention: Don't add twitter:* tags to new projects. Open Graph is the present and future of social previews.
Error: Modernizr is missing — feature detection no longer works
Cause: Modernizr was removed in v9.0.0. The template no longer ships any feature-detection library.
Solution: Either drop the Modernizr dependency entirely (most modern browsers support enough of HTML5/CSS3 that detection is unnecessary), or npm install modernizr and configure your own custom build. The template won't add it back.
Prevention: Use @supports CSS at-rules for CSS feature detection — they cover most modern needs without a runtime library.
Error: GitHub Pages 404s on js/app.js
Cause: GitHub Pages serves your repo under https://USERNAME.github.io/REPO/. The webpack-emitted <script src="js/app.js"> resolves relative to the document, which is fine — but if you have an absolute-path resource like <link rel="manifest" href="/site.webmanifest">, the leading / points at https://USERNAME.github.io/, not your repo's subpath.
Solution: Either deploy under a custom domain (which lives at the root) or change all leading-slash paths to relative paths in index.html:
<link rel="manifest" href="site.webmanifest">
<link rel="icon" href="favicon.ico" sizes="any">
Prevention: Use relative paths exclusively if you intend to host under a subpath.
Error: Test suite fails with mismatch on style.css banner
Cause: The upstream test/file_content.mjs test asserts that dist/css/style.css contains an exact-byte banner. Line ending conversion to CRLF on Windows breaks the byte match. (This affects upstream contributors, not downstream users.)
Solution: Make sure .gitattributes is in place and your local repo has been re-checked-out with git rm --cached -r . && git reset --hard HEAD to apply LF normalization. (Tests on Windows were explicitly fixed in v9.0.1.)
Prevention: Configure git's core.autocrlf to input on Windows, or rely on .gitattributes (which the template already provides).
7. FAQ
Q: Do I need to upgrade my site each time a new version of HTML5 Boilerplate is released?
No. Just as you don't normally replace the foundation of a house once it's built. There's nothing stopping you from working in the latest changes, but assess the cost/benefit first. Most version bumps are tooling and metadata updates that don't change the on-page output.
Q: Where can I get help with support questions?
Ask on Stack Overflow with the html5boilerplate tag. The h5bp org also enables GitHub Discussions, but Stack Overflow is the more active venue. Bugs and feature requests go to GitHub Issues.
Q: Can I use HTML5 Boilerplate with React, Vue, Svelte, or another framework?
Yes. The template doesn't impose any framework — replace app.js with your framework's entry point and adjust webpack as needed. Most teams using a modern framework will eventually outgrow the bundled webpack config and adopt the framework's CLI (vite, next, nuxt, svelte-kit, etc.) instead — that's fine. Use the boilerplate's index.html, icons, manifest, and metadata patterns as a starting point even when you swap out the build system.
Q: Why was Modernizr removed in v9.0.0?
Modern browsers cover enough of HTML5/CSS3 that feature detection has become a niche need. Carrying Modernizr by default added bytes most sites didn't use. Add it back yourself if you specifically need it.
Q: Why was Normalize.css dropped?
The maintainers moved CSS into a separate main.css repo and decided that Normalize's opinionated cross-browser leveling was beyond the scope of a "starting line." main.css includes a smaller set of base styles instead.
Q: Why does the project not ship .htaccess anymore?
Server config was removed from the core template in v9.0.0. The h5bp organization maintains separate, server-specific repos with hardened defaults (security headers, content-type mappings, gzip/brotli, immutable caching for hashed assets). See Section 5's recipe on caching headers for the full list of companion repos.
Q: Why is there no JavaScript framework, no router, no state management?
By design. HTML5 Boilerplate is a template, not an application scaffold. It targets the lowest common denominator (a static HTML site or a single-page app you wire up yourself). Tools like create-react-app, vite, and the framework-specific CLIs cover the "give me a working app skeleton" niche.
Q: Should I commit dist/ to source control?
No. dist/ is build output — it's regenerated by npm run build. The default .gitignore already excludes dist/package-lock.json. Add dist/ itself if your project doesn't ship it.
Q: Where do I put third-party libraries that I don't want webpack to bundle?
js/vendor/. The production webpack config copies that folder verbatim into dist/js/vendor/. Reference scripts in your HTML with <script src="js/vendor/library.js"></script>.
Q: Can I delete 404.html if my hosting provider already has a 404 page?
Yes. Delete the file and remove its entry from the webpack.config.prod.js CopyPlugin patterns. Same applies to any of the other static files — the build config is the only place that needs to know.
Q: Can I delete the .editorconfig file?
You can, but you probably shouldn't. It's free and silently improves cross-editor consistency for your team. The only "cost" is one extra dotfile.
Q: How do I add TypeScript?
The template ships pure JavaScript. Install typescript and ts-loader, add a tsconfig.json, and update webpack.common.js to register the .ts loader rule. Section 5 has the full recipe. (#3229 tracks adding official upstream TypeScript docs.)
Q: How is HTML5 Boilerplate different from create-react-app or vite?
| HTML5 Boilerplate | create-react-app / vite | |
|---|---|---|
| Output | Plain HTML/CSS/JS skeleton | Framework-specific app scaffold |
| Build | webpack (configurable) | Locked to the framework's tool |
| Framework | None | React / Vue / Svelte / etc. |
| Best for | Static sites, marketing pages, framework-agnostic apps | Single-page apps in a specific framework |
Q: Does HTML5 Boilerplate include analytics?
No. Google Analytics was removed in v9.0.0. Add your analytics snippet to index.html yourself, or use a privacy-respecting alternative like Plausible or Fathom.
Q: Can I use jQuery?
Yes, just install it. jQuery was bundled in v6 and v7 but removed in v8.0.0. Drop it into js/vendor/ if you want it served verbatim, or npm install jquery and import $ from 'jquery' from app.js if you want webpack to bundle it.
Q: What's the license?
MIT. Use it commercially, modify it, ship it without attribution if you want — all permitted.
Q: Where do I report a bug or contribute?
GitHub issues for bugs and feature requests. Pull requests are reviewed by project maintainers; the upstream repo's .github/CONTRIBUTING.md has the workflow details. Open work items as of this guide include #3411 (modernize npm publishing & auth), #3412 (template-push fix), and #3373 (remove Twitter Cards docs).
Q: How do I disable iOS phone-number auto-detection?
Add to index.html head:
<meta name="format-detection" content="telephone=no">
This stops Safari on iOS from auto-linking sequences of digits as tel: links.
Q: How do I declare a security.txt channel?
Serve .well-known/security.txt:
Contact: mailto:[email removed]
Expires: 2027-01-01T00:00:00Z
Preferred-Languages: en
Canonical: https://www.example.com/.well-known/security.txt
See securitytxt.org for the full specification.
Q: How do I add an RSS or Atom feed link to my page?
<link rel="alternate" type="application/rss+xml" title="My Site RSS" href="/rss.xml">
<link rel="alternate" type="application/atom+xml" title="My Site Atom" href="/atom.xml">
Q: How do I tell browsers about my OpenSearch endpoint?
<link rel="search" title="Search Example" type="application/opensearchdescription+xml" href="/opensearch.xml">
Browsers detect the descriptor and offer to add your search engine.
Q: What does @media (scripting: enabled) do, and is it supported?
It's a modern alternative to <noscript> for hiding/showing content based on JavaScript availability. Supported in current Chrome, Edge, Safari, and Firefox. Useful for avoiding flash of unstyled content (FOUC) on JS-dependent UI.
@media (scripting: enabled) {
.js-only { display: block; }
}
@media (scripting: none) {
.js-only { display: none; }
}
Q: Where is the official documentation?
The upstream repo's docs/ folder (TOC, usage, html, css, js, misc, faq, extend, about-this-repo). The companion site at html5boilerplate.com is also separately maintained.