This document explains how live reload and block-editor hot module replacement work in the theme’s development workflow, and how to configure them — including for HTTPS local environments.
The theme has two development servers. They complement the WordPress site; you continue browsing the site’s normal URL. Running npm start runs both in parallel:
| Watcher | Server | Purpose |
|---|---|---|
npm run start:assets |
BrowserSync, port 3001 | Live reload for the frontend via snippet mode; your site URL stays unchanged. |
npm run start:blocks |
webpack-dev-server / Fast Refresh, port 8887 by default (configurable via BLOCKS_DEV_SERVER_PORT) |
Hot module replacement for block editor React components; requires block sources. Block state is preserved across updates — no full page reload needed. |
For BrowserSync:
For block editor HMR, JS/JSX changes to block components hot-swap in the editor instantly.
npm start
Webpack starts watching for file changes and BrowserSync starts on port 3001. Open your local site and edits will reflect automatically.
The BrowserSync client script is only enqueued when WP_ENVIRONMENT_TYPE is set to local. Add this to your wp-config.php if it isn’t already:
define( 'WP_ENVIRONMENT_TYPE', 'local' );
For block editor HMR (Fast Refresh), also add:
define( 'SCRIPT_DEBUG', true );
Without SCRIPT_DEBUG, WordPress does not support Fast Refresh.
start:assets + BrowserSync)start:assets runs the asset watcher in watch mode using webpack.config.js.assets/build/.start:blocks + Fast Refresh)start:blocks starts a webpack dev server, using webpack.blocks.config.js.webpack.blocks.config.js is a thin wrapper over @wordpress/scripts’ default config. It exists only to strip the devServer.proxy option: webpack-dev-server v5 (pinned via the overrides block in package.json) requires proxy to be an array, while wp-scripts still emits the v4 object form, which v5 rejects with options.proxy should be an array. The wrapper also sets the dev-server port from BLOCKS_DEV_SERVER_PORT.
BrowserSync watches the following:
assets/build/**/***/*.php (excluding vendor/)**/*.htmlThe client script is enqueued by PHP from {scheme}://{host}:3001/browser-sync/browser-sync-client.js. The scheme (http or https) and host are derived automatically from the WordPress site URL using is_ssl() and home_url().
BrowserSync is only added to the scripts webpack config. Adding it to all three configs (scripts, styles, moduleScripts) would start three BrowserSync instances on the same port.
Copy .env.local.example to .env.local and set your local site hostname:
WP_HOST=yoursite.local
WP_HOST is your local site’s hostname (without protocol or port). Set it to match your local hostname exactly.
.env.local is gitignored.
If port 3001 is already taken (e.g. two local sites running at once), set a different port in .env.local:
BS_PORT=3002
Then define the matching constant in wp-config.php so PHP enqueues the client from the right URL:
define( 'ELEMENTARY_THEME_BROWSER_SYNC_URL', 'https://yoursite.local:3002/browser-sync/browser-sync-client.js' );
ELEMENTARY_THEME_BROWSER_SYNC_URL overrides the auto-detected URL entirely, so it also works for remote setups (ddev, reverse proxy) where the BrowserSync server is on a different host or IP.
The block Fast Refresh dev server runs on port 8887 by default. If that port is already in use (e.g. two local sites running start:blocks at once), set a different port in .env.local:
BLOCKS_DEV_SERVER_PORT=8889
webpack.blocks.config.js reads this value and applies it to the dev server. No matching wp-config.php constant is needed — the editor loads block scripts from the dev server directly.
If your local site runs on HTTPS, also add the SSL cert paths:
WP_SSL_KEY=/path/to/yoursite.local.key
WP_SSL_CERT=/path/to/yoursite.local.crt
This is required to avoid mixed content errors — the BrowserSync client script on port 3001 must also be served over HTTPS. Since SSL certs are domain-based, the same cert your local site uses also covers port 3001.
Finding cert paths in LocalWP (macOS):
~/Library/Application Support/Local/run/router/nginx/certs/<domain>.key
~/Library/Application Support/Local/run/router/nginx/certs/<domain>.crt
HMR (BrowserSync live reload) is controlled by a single master switch in .env.local, honoured by both the build (BrowserSync server) and PHP (client enqueue):
ENABLE_HMR=false
Default is on — the key only needs setting to turn HMR off. Off values are 0, false, no, and off (case-insensitive). With it off, npm start skips the BrowserSync server entirely and PHP skips the client, so there is no live reload and no console noise from a client pointing at a server that isn’t running. The browser-sync dev dependencies stay installed, so flipping it back on needs no reinstall.
You can also toggle it from npm run init (manage mode → Toggle features → HMR), which just flips ENABLE_HMR in .env.local for you. Since .env.local is gitignored, this is a per-developer local setting.
To keep the BrowserSync server running but stop PHP from enqueuing its client (e.g. when working purely in the block editor), set this in .env.local:
DISABLE_BS=true
This prevents PHP from enqueuing the BrowserSync client script. The BrowserSync server still starts (webpack still runs it), but the browser won’t connect to it. For a full off switch (server included), use ENABLE_HMR=false above.
By default, PHP constructs the client URL from the site’s scheme and host:
{scheme}://{host}:3001/browser-sync/browser-sync-client.js
To override it entirely — for a non-standard port, a remote dev server, or a reverse proxy setup — define this constant in wp-config.php:
define( 'ELEMENTARY_THEME_BROWSER_SYNC_URL', 'https://yoursite.local:3002/browser-sync/browser-sync-client.js' );
This takes precedence over the auto-detected URL.
BrowserSync port: BrowserSync requires its own port (3001) separate from your local site. Snippet mode keeps the site URL unchanged — proxy mode would change the URL and break WordPress redirects and cookie domains.
WDS host validation: WDS runs on localhost:8887. For custom local hostnames (e.g. yoursite.local), allowedHosts: 'all' is set in the webpack devServer config so the HMR WebSocket connection is accepted.