Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.DS_Store
node_modules
.wp-env-port

#files not allowed by the guidelines
thumbs.db
Expand Down
8 changes: 8 additions & 0 deletions .wp-env.gutenberg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"core": "WordPress/WordPress",
"plugins": [ "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip" ],
"themes": [ "https://downloads.wordpress.org/theme/twentytwentythree.latest-stable.zip" ],

@MaggieCabrera MaggieCabrera Mar 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atlas needs TT3 as a dependency

"mappings": {
"wp-content/themes": "."
}
}
7 changes: 7 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"core": "WordPress/WordPress",
"themes": [ "https://downloads.wordpress.org/theme/twentytwentythree.latest-stable.zip" ],
"mappings": {
"wp-content/themes": "."
}
}
32 changes: 31 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,36 @@ Themes submitted to WordPress.org must:

**WordPress Playground (no local setup required)**: Use the Playground links in `README.md` to preview themes in-browser.

**Local development**: Clone this repo into your WordPress install's `wp-content/themes/` directory. Each theme directory will appear as a separate theme in the admin.
**wp-env (recommended for local development)**: Requires Docker. Boots a sandboxed WordPress instance with all themes from this repo mounted and the Twenty Twenty-Three parent theme pre-installed.

```bash
# Standard WordPress (bundled editor)
npm run env:start

# Activate a specific theme on start
npm run env:start -- --theme blue-note

# WordPress + Gutenberg plugin (latest stable)
npm run env:start:gutenberg -- --theme blue-note

npm run env:stop # Stop the environment (preserves data)
npm run env:destroy # Remove containers and volumes (fresh start)
```

Site runs at `http://localhost:8888` (or the next available port if 8888 is taken) — credentials: `admin` / `password`.

Pass `--theme <theme-slug>` to activate a specific theme. The slug must match the theme's directory name (e.g. `blue-note`, `archivist`). If the environment is already running, `env:start --theme <slug>` will skip startup and just switch the active theme.

The active port is stored in `.wp-env-port` (git-ignored) so that `env:stop` and `env:destroy` always target the correct instance.

All theme directories are live-mounted, so changes are reflected immediately without restarting. To use a local Gutenberg checkout instead of the downloaded plugin, add a `.wp-env.override.json` at the repo root:

```json
{
"plugins": [ "../gutenberg" ]
}
```

**Manual local development**: Clone this repo into your WordPress install's `wp-content/themes/` directory. Each theme directory will appear as a separate theme in the admin.

Recommended plugin: Create Block Theme (helps generate theme files from the editor).
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@ You can boot up the Playground with the selected theme by clicking one of the li

#### If you want to work locally:

##### Option A — wp-env (recommended)

Requires [Docker](https://www.docker.com/get-started/). Boots a self-contained WordPress instance with all themes in this repo available and ready to use — no manual WordPress setup needed.

```bash
# Install dependencies (first time only)
npm install

# Start the environment
npm run env:start

# Start with a specific theme already active
npm run env:start -- --theme blue-note

# Stop / tear down
npm run env:stop
npm run env:destroy
```

The site runs at `http://localhost:8888` (or the next available port if 8888 is in use). Login with `admin` / `password`.

All theme directories are live-mounted — edits are reflected immediately without restarting.

##### Option B — manual setup

1. Set up a WordPress instance, here is a [handy guide to install WordPress locally](https://wordpress.org/support/article/installing-wordpress-on-your-own-computer/)
2. Clone / download this repository into your `/wp-content/themes/` directory
3. You may want to install the [Create Block Theme plugin](https://wordpress.org/plugins/create-block-theme/) to help you generate the theme files if you want to build your Theme directly on the Site Editor.
Expand All @@ -50,7 +75,7 @@ If it's your first time building a Block Theme, we suggest checking the Resource
#### Requirements for local setup

- WordPress 6.1+
- PHP 5.6+
- PHP 7.4+
- License: [GPLv2](http://www.gnu.org/licenses/gpl-2.0.html) or later

#### For New Contributors
Expand Down
144 changes: 144 additions & 0 deletions env.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* Wraps wp-env with auto-detected ports and theme activation.
* Saves the active port to .wp-env-port so stop/destroy target the right instance.
*
* Usage:
* node env.mjs start [--theme <theme-slug>] [--config <file>]
* node env.mjs stop
* node env.mjs destroy
*
* npm run env:start -- --theme blue-note
* npm run env:stop
* npm run env:destroy
*/
import { createServer } from 'net';
import { spawn } from 'child_process';
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';

const PORT_FILE = '.wp-env-port';

function isPortFree( port ) {
return new Promise( ( resolve ) => {
const server = createServer();
server.on( 'error', () => resolve( false ) );
server.listen( port, () => server.close( () => resolve( true ) ) );
} );
}

async function findFreePort( start = 8888 ) {
return ( await isPortFree( start ) ) ? start : findFreePort( start + 1 );
}

function readSavedPort() {
if ( existsSync( PORT_FILE ) ) {
const port = parseInt( readFileSync( PORT_FILE, 'utf8' ).trim(), 10 );
if ( ! isNaN( port ) ) return port;
}
return null;
}

function runWpEnv( wpEnvArgs, env ) {
return new Promise( ( resolve ) => {
const child = spawn( 'wp-env', wpEnvArgs, { stdio: 'inherit', env } );
child.on( 'exit', ( code ) => resolve( code ?? 0 ) );
} );
}

const args = process.argv.slice( 2 );
const command = args[ 0 ];

// --- stop / destroy ---
if ( command === 'stop' || command === 'destroy' ) {
const savedPort = readSavedPort();
const wpEnvEnv = { ...process.env };
if ( savedPort ) {
wpEnvEnv.WP_ENV_PORT = String( savedPort );
wpEnvEnv.WP_ENV_TESTS_PORT = String( savedPort + 1 );
}
const code = await runWpEnv( args, wpEnvEnv );
if ( code === 0 && command === 'destroy' ) {
try { unlinkSync( PORT_FILE ); } catch {}
}
process.exit( code );
}

// --- start ---

// Extract --theme <slug> from args, pass the rest to wp-env.
const themeIndex = args.indexOf( '--theme' );
let themeSlug = null;
if ( themeIndex !== -1 ) {
themeSlug = args[ themeIndex + 1 ];
args.splice( themeIndex, 2 );
}

const savedPort = readSavedPort();
const alreadyRunning = savedPort && ! ( await isPortFree( savedPort ) );

if ( alreadyRunning ) {
// Instance is already running — skip wp-env start, just activate the theme.
console.log( `wp-env is already running on port ${ savedPort }.` );
if ( themeSlug ) {
activateTheme( themeSlug, buildEnv( savedPort ) );
} else {
process.exit( 0 );
}
} else {
const port = await findFreePort();
const testsPort = await findFreePort( port + 1 );

if ( port !== 8888 ) {
console.log( `Port 8888 is in use, starting on port ${ port } instead.` );
}

const wpEnvEnv = buildEnv( port, testsPort );
const child = spawn( 'wp-env', args, { stdio: 'inherit', env: wpEnvEnv } );

child.on( 'exit', ( code ) => {
if ( code !== 0 ) {
process.exit( code );
}

// Persist port so stop/destroy can target this instance.
writeFileSync( PORT_FILE, String( port ) );

if ( ! themeSlug ) {
process.exit( 0 );
}

activateTheme( themeSlug, wpEnvEnv );
} );
}

function buildEnv( port, testsPort = port + 1 ) {
return { ...process.env, WP_ENV_PORT: String( port ), WP_ENV_TESTS_PORT: String( testsPort ) };
}

function activateTheme( slug, wpEnvEnv ) {
console.log( `Activating theme: ${ slug }` );
const activate = spawn( 'wp-env', [ 'run', 'cli', 'wp', 'theme', 'activate', slug ], {
stdio: [ 'inherit', 'pipe', 'pipe' ],
env: wpEnvEnv,
} );

let activateOutput = '';
activate.stdout.on( 'data', ( data ) => {
const text = data.toString();
activateOutput += text;
process.stdout.write( text );
} );
activate.stderr.on( 'data', ( data ) => {
const text = data.toString();
activateOutput += text;
process.stderr.write( text );
} );

activate.on( 'exit', ( activateCode ) => {
if ( activateCode !== 0 || ! activateOutput.includes( 'Success:' ) ) {
console.error( `Error: Failed to activate theme '${ slug }'. Make sure the theme slug is correct.` );
process.exit( 1 );
}
process.exit( 0 );
} );
}
Loading