When you write CSS or JavaScript for a WordPress site, the loop is always the same: save the code, switch to the front-end tab, reload, look. This snippet adds a simple live reload to WordPress: save a Scripts Organizer code block, and the front-end tab reloads itself with the new code.
It only runs for administrators, only checks while the tab is visible, and only reloads when a code block really changed. The old version of this post reloaded the page every 1.5 seconds, for every visitor, whether anything changed or not.
The snippet
<?php
/**
* Live reload: the front end reloads itself when you save a Scripts Organizer code block.
* Only for administrators, only in the browser tab you are looking at.
*/
// ===== Config =====
function dpc_live_reload_config() {
$config = array(
// Milliseconds between checks while the tab is visible.
'interval' => 1500,
// Saving a post of these types reloads the page. 'scorg' = code blocks, 'scorg_scss' = SCSS partials.
'post_types' => array( 'scorg', 'scorg_scss' ),
);
// ===== End of config =====
return (array) apply_filters( 'dpc_live_reload_config', $config );
}
function dpc_live_reload_post_types() {
return (array) ( dpc_live_reload_config()['post_types'] ?? array() );
}
// 1. Remember that something changed. The stamp is written at the very end of the save
// request, after Scripts Organizer has written its files.
add_action( 'save_post', 'dpc_live_reload_watch' );
add_action( 'added_post_meta', 'dpc_live_reload_watch_meta', 10, 3 );
add_action( 'updated_post_meta', 'dpc_live_reload_watch_meta', 10, 3 );
add_action( 'deleted_post_meta', 'dpc_live_reload_watch_meta', 10, 3 );
function dpc_live_reload_watch( $post_id ) {
if ( in_array( get_post_type( $post_id ), dpc_live_reload_post_types(), true ) && ! has_action( 'shutdown', 'dpc_live_reload_bump' ) ) {
add_action( 'shutdown', 'dpc_live_reload_bump' );
}
}
function dpc_live_reload_watch_meta( $meta_id, $post_id, $meta_key ) {
// The editor's lock and last-editor fields change while you only look at a code block.
if ( 0 !== strpos( (string) $meta_key, '_edit_' ) ) {
dpc_live_reload_watch( $post_id );
}
}
function dpc_live_reload_bump() {
update_option( 'dpc_live_reload_stamp', (string) microtime( true ), false );
}
// 2. A small REST endpoint the page asks for the stamp.
add_action( 'rest_api_init', 'dpc_live_reload_route' );
function dpc_live_reload_route() {
register_rest_route(
'dpc/v1',
'/live-reload',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'dpc_live_reload_stamp',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);
}
function dpc_live_reload_stamp() {
$response = rest_ensure_response( array( 'stamp' => (string) get_option( 'dpc_live_reload_stamp', '' ) ) );
$response->header( 'Cache-Control', 'no-store' );
return $response;
}
// 3. The polling script, printed on the front end for administrators only.
add_action( 'wp_enqueue_scripts', 'dpc_live_reload_script' );
function dpc_live_reload_script() {
if ( ! current_user_can( 'manage_options' ) || is_customize_preview() ) {
return;
}
$config = dpc_live_reload_config();
wp_register_script( 'dpc-live-reload', false, array(), '1.0', array( 'in_footer' => true ) );
wp_enqueue_script( 'dpc-live-reload' );
wp_add_inline_script(
'dpc-live-reload',
'window.dpcLiveReload = ' . wp_json_encode(
array(
'url' => esc_url_raw( rest_url( 'dpc/v1/live-reload' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'interval' => max( 500, (int) ( $config['interval'] ?? 1500 ) ),
)
) . ';',
'before'
);
wp_add_inline_script( 'dpc-live-reload', <<<'JS'
(function (cfg) {
// Page builders and the Customizer show the page in a frame; never reload those.
if (!cfg || window.self !== window.top) {
return;
}
var stamp = null;
var busy = false;
function check() {
if (busy || document.visibilityState !== 'visible') {
return;
}
busy = true;
fetch(cfg.url, { headers: { 'X-WP-Nonce': cfg.nonce }, credentials: 'same-origin', cache: 'no-store' })
.then(function (response) {
return response.ok ? response.json() : null;
})
.then(function (data) {
if (!data) {
return;
}
if (stamp !== null && data.stamp !== stamp) {
window.location.reload();
return;
}
stamp = data.stamp;
})
.catch(function () {})
.then(function () {
busy = false;
});
}
check();
setInterval(check, cfg.interval);
// Switching back to this tab checks at once.
document.addEventListener('visibilitychange', check);
})(window.dpcLiveReload);
JS
);
}
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty.
Everywhere is needed: the save is noticed in wp-admin and admin-ajax.php, the stamp is read through the REST API, and the script runs on the front end. Disable the code block when you are done developing.
How it works
- When a code block or SCSS partial is saved, or its settings change, the snippet schedules
dpc_live_reload_bump()onshutdown. It writes the current time to the optiondpc_live_reload_stampat the end of the request, after Scripts Organizer has written its files. - Changes to
_edit_lockand_edit_lastare ignored. WordPress updates them while a code block is only open in the editor. - The REST route
dpc/v1/live-reloadreturns the stamp. Itspermission_callbackallows only users who canmanage_options, and the script sends thewp_restnonce so cookie authentication works. - On the front end, the script asks for the stamp every 1.5 seconds while the tab is visible, and at once when you switch back to it. When the stamp changes, it calls
location.reload(). - It does nothing in a frame, so page builders and the Customizer preview are never reloaded under you.
Customise it
- Slower or faster checks: change
interval. Below 500 ms is not allowed. - Reload when you save pages or posts too: add
'page'and'post'topost_types. - Theme files are not covered. For a build-tool based setup, see Gutenberg Blocks Development – Live Reload.
Gotchas
- Each check is a small REST request that loads WordPress. Fine on a local or staging site, but don’t leave it enabled on a busy production site.
- A full page cache or a CDN must not cache
/wp-json/dpc/v1/live-reload. The response sendsCache-Control: no-store, which most caches respect. - A form you are typing in on the front end is lost when the page reloads.
In the Scripts Organizer library Import it with one click from Scripts Organizer.