The five-minute install gets you a running site. It does not get you a developer workstation. In 2026 a solid WordPress developer setup is a local stack you can recreate, a lint pipeline that matches core, safe debugging that never paints errors on the front end, and block tooling that ships the same assets you test.
This guide is the checklist we use when provisioning a new theme or plugin repo: environment type, wp-env, @wordpress/scripts, debug constants, and a thin hardening layer in wp-config.php and mu-plugins.
Local environment with wp-env
Hand-rolled Apache and PHP installs drift. One machine runs PHP 8.2, another still has 8.1, and block builds fail only on CI. The official path is wp-env: Docker containers for WordPress, MySQL, and optional tools, driven from a .wp-env.json in the project root.
Install once globally or as a project dependency:
npm install --save-dev @wordpress/env
npx wp-env startA minimal config for a plugin under development:
{
"core": "WordPress/WordPress#6.7",
"plugins": [ "." ],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"SCRIPT_DEBUG": true
}
}Why this beats a generic stack for plugin work:
- Core version is pinned in git, so “works on my machine” stops being a debate.
wp-env run cligives you WP-CLI inside the same containers your browser hits.- Tests and e2e scripts in
@wordpress/scriptsexpect this layout.
Pair it with the block editor development environment docs when you need Node version notes, create-block, and the recommended editor extensions.
For agency work that is still classic PHP themes, you can keep a host PHP binary for one-off scripts, but keep the site itself in wp-env so media, cron, and rewrite behaviour match staging.
Useful daily commands once the stack is up:
npx wp-env run cli wp plugin list
npx wp-env run cli wp cache flush
npx wp-env logs
npx wp-env stopMap a custom domain through your hosts file only when the project needs cookie or SSO behaviour that localhost breaks. Otherwise the default URL is enough and keeps certificates simple.
When a teammate clones the repo, the onboarding note should be three lines: install Docker, npm install, npx wp-env start. Anything longer usually means the environment escaped into undocumented host packages.
Environment type and wp-config discipline
Since WordPress 5.5, WP_ENVIRONMENT_TYPE is the switch every plugin should read. Define it early in wp-config.php:
define( 'WP_ENVIRONMENT_TYPE', 'local' ); // local | development | staging | productionThen branch behaviour without inventing your own constants:
if ( wp_get_environment_type() === 'production' ) {
// Caching on, verbose logging off.
} else {
define( 'SCRIPT_DEBUG', true );
}Hardening constants that belong in every production boilerplate:
define( 'DISALLOW_FILE_EDIT', true );
define( 'FORCE_SSL_ADMIN', true );
define( 'WP_POST_REVISIONS', 5 );
define( 'AUTOMATIC_UPDATER_DISABLED', true ); // when deploys own core updatesUse DISALLOW_FILE_MODS only when the whole tree is immutable and updates land through CI. On managed hosts that still patch plugins in the dashboard, that constant will fight the host.
Authentication keys and salts are not decoration. Rotate them after a compromise; every session ends immediately. Store rotation in your deploy runbook, not in a sticky note.
Linting with @wordpress/scripts
Linting is how you stay compatible with Gutenberg without memorising every ESLint rule. The package @wordpress/scripts wraps webpack, Babel, ESLint, Stylelint, Jest, and Playwright behind familiar npm scripts.
Typical package.json surface for a block plugin:
{
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build",
"lint:js": "wp-scripts lint-js",
"lint:css": "wp-scripts lint-css",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"format": "wp-scripts format",
"packages-update": "wp-scripts packages-update"
}
}Run npm run lint:js in CI on every pull request. Format with Prettier via wp-scripts format so diffs stay about logic, not quote style.
PHP still needs its own gate. Install WordPress Coding Standards (WPCS) through Composer and run PHPCS against themes and plugins. Do not expect @wordpress/scripts to replace that; it owns the JS and CSS half of the stack.
A practical CI order that catches most regressions:
composer phpcs(or your PHPCS wrapper)npm run lint:jsandnpm run lint:cssnpm run buildso a broken webpack config fails before review- Optional:
wp-env startthen plugin PHPUnit or Playwright from the scripts package
Ignore generated build/ output in git reviews unless the project deliberately commits compiled assets for hosts without Node. Prefer building in CI or on deploy. EditorConfig plus the default Prettier config from the scripts package keeps indentation fights out of pull requests.
If a legacy theme still ships jQuery-era admin scripts, introduce @wordpress/scripts for new blocks first rather than rewriting every enqueue on day one. Mixed stacks are fine; mixed rules are not. Pick one ESLint config per package directory.
Debugging without leaking errors
Never show PHP errors to visitors. Log them.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log by default
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );Better: point WP_DEBUG_LOG at a path outside the document root, and deny HTTP access to any leftover debug.log under wp-content. On production, leave WP_DEBUG false unless you are mid-incident. Turn SAVEQUERIES on only for short profiling sessions; it costs memory and should never stay on overnight.
Complement constants with tools that explain what happened:
- Query Monitor for hooks, HTTP calls, and slow queries in the admin bar
- Xdebug attached to the wp-env PHP container when you need step-through
- Browser DevTools with
SCRIPT_DEBUGso you load unminified core scripts while chasing a conflict
When a white screen hits staging, check debug.log, the host error log, and wp-env logs (if local) before installing another “debug” plugin. Most production outages from misconfiguration show up as a single fatal in the log, not as a missing feature.
Reproduce the bug on a throwaway wp-env instance with the same plugin set before changing production constants. If the issue only appears under object cache or a CDN, note that in the ticket; local Docker will not invent Redis for you unless you add it to the env config.
Block tooling in 2026
Interactive blocks are no longer optional side projects. The current default path is:
- Scaffold with
npx @wordpress/create-block my-block --variant dynamic(or static, depending on render strategy) - Develop against wp-env so
block.jsonregistration matches a real admin - Use
npm startfor watch builds andnpm run buildfor production assets - Register the block from PHP with
register_block_type( __DIR__ . '/build' )when metadata lives inblock.json
Keep editor and front-end scripts separate in block.json so you do not ship editor-only React to every visitor. Prefer viewScript / viewStyle for front-end interactivity instead of dumping everything into one bundle.
For themes that still enqueue classic assets, migrate new interactive pieces to blocks incrementally. A hybrid theme can ship block patterns and a few custom blocks while the rest of the templates remain PHP. That is normal in 2026; a full Site Editor rewrite is a product decision, not a tooling requirement.
When dependencies drift, npx wp-scripts packages-update refreshes @wordpress/* packages within the scripts toolchain. Pin major versions in package-lock.json and review the lockfile in PRs the same way you review Composer updates.
Thin core cleanup with an mu-plugin
You do not need a “disable everything” plugin from the directory. A small must-use plugin keeps the rules in version control:
<?php
/**
* Plugin Name: Lean core
*/
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'xmlrpc_enabled', '__return_false' );Disable XML-RPC only when you are sure no remote clients need it. Disable emoji scripts when you control brand icons and want fewer front-end requests. Leave RSS alone on content sites; brochure sites can shut feeds off with an intentional hook, not a random snippet from a forum.
Launch checklist
Before a client site goes live:
WP_ENVIRONMENT_TYPEisproduction.DISALLOW_FILE_EDITis true; SSL admin is forced.- Revisions are capped; debug display is off.
- Local and CI both start from the same
.wp-env.jsonand pass@wordpress/scriptslint and build. - Block assets are production builds, not watch-mode leftovers.
- Salts are unique;
debug.logis not web-accessible.
A WordPress developer setup that you can recreate from git is worth more than a clever wp-config.php alone. Local parity, lint gates, and honest logging are what keep block work and classic PHP work from fighting each other.
Need a senior review of an existing stack or a greenfield block plugin? Talk to a WordPress developer at WPPoland.






