# WordPress Playground 手册

WordPress Playground 手册，介绍浏览器内 WordPress 运行环境。

# Blueprints Docs

Check the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.

Hi! Welcome to WordPress Playground Blueprints documentation.

Blueprints are JSON files for setting up your very own WordPress Playground instance. This subsite (Blueprints Docs) is where you will find all the information you need to use Blueprints.

The WordPress Playground documentation is distributed across four separate hubs (subsites):

- [**Documentation**](https://developer.wordpress.org/playground/) – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
- 👉 [**Blueprints**](https://developer.wordpress.org/playground/blueprints) (you’re here) – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from this Blueprints docs hub.
- [**Developers**](https://developer.wordpress.org/playground/developers) – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in the Developers docs hub.
- [**API Reference**](https://developer.wordpress.org/playground/api) – All the APIs exposed by WordPress Playground

## Navigating the Blueprints documentation hub

This docs hub is focused on Blueprints info and is divided into the following major sections:

- [Getting started with Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started): Quick Start Guide to setting up a WordPress Playground instance using Blueprint JSON files.
- [Tutorial – Blueprints 101](https://developer.wordpress.org/playground/blueprints/tutorial) – Blueprints API crash course. The tutorial will guide you through the complete process of creating a blueprint that loads a theme and plugin (among other things).
- [Blueprint data Format](https://developer.wordpress.org/playground/blueprints/data-format): Blueprint JSON files define your Playground instance with various properties. This section highlights the key properties you need to know.
- [Using Blueprints](https://developer.wordpress.org/playground/blueprints/using-blueprints): Learn in this section different ways to use Blueprints.
- [Steps](https://developer.wordpress.org/playground/blueprints/steps): API Reference of all the available steps that can be set in a blueprint to run tasks such as login, plugin/theme activation, file operations, and more.
- [Blueprint Bundles](https://developer.wordpress.org/playground/blueprints/bundles): Learn how to create and use Blueprint bundles – self-contained packages that include a Blueprint and all its resources.
- [Examples](https://developer.wordpress.org/playground/blueprints/examples): Compilation of Blueprint examples for various WordPress Playground setups, including installing themes/plugins, running PHP code, enabling features, and loading specific WordPress versions.
- [Troubleshoot and debug Blueprints](https://developer.wordpress.org/playground/blueprints/troubleshoot-and-debug): Tips and tools for troubleshooting and debugging Blueprints.

# Developers Docs

Hi! Welcome to WordPress Playground Developer documentation.

The WordPress Playground documentation is distributed across four separate hubs (subsites):

- [**Documentation**](https://developer.wordpress.org/playground/) – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
- [**Blueprints**](https://developer.wordpress.org/playground/blueprints) – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub.
- 👉 [**Developers**](https://developer.wordpress.org/playground/developers) (you’re here) – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in this Developers docs hub.
- [**API Reference**](https://developer.wordpress.org/playground/api) – All the APIs exposed by WordPress Playground

## Navigating the Developers documentation hub

This docs hub is focused on Developers info and is divided into the following major sections:

- [Quick Start Guide for Developers](https://developer.wordpress.org/playground/developers/build-your-first-app): Begin your development journey with Playground by exploring some examples of what you can achieve using Playground APIs.
- [Local Development](https://developer.wordpress.org/playground/developers/local-development): Discover the tools provided by Playground to streamline the process of setting up and managing WordPress sites and build your apps.
- [Playground APIs](https://developer.wordpress.org/playground/developers/apis): Explore the main APIs exposed by WordPress Playground to interact with the Playground: [Query API](https://developer.wordpress.org/playground/developers/apis/query-api/), [Blueprints](https://developer.wordpress.org/playground/blueprints/), [Javascript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/)
- [Architecture](https://developer.wordpress.org/playground/developers/architecture): The specifics of WordPress Playground’s architecture, including its various components and tools.
- [Limitations](https://developer.wordpress.org/playground/developers/limitations/): Learn more about the current limitations of WordPress Playground, which include disabled network connections, browser persistence and recovery constraints, iframe quirks, PHP function execution requirements, and partial WP-CLI support.

# WordPress Playground Docs

**Looking for the official Playground website?**

WordPress Playground website moved to [wordpress.org/playground/](https://wordpress.org/playground/). The site you’re at now hosts the documentation.

👋 Hi! Welcome to WordPress Playground documentation.
Playground is an online tool to experiment and learn about WordPress. This site (Documentation) is where you will find all the information you need to start using Playground.

The WordPress Playground documentation is distributed across four separate hubs (subsites):

- 👉 [**Documentation**](https://developer.wordpress.org/playground/) (you’re here) – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
- [**Blueprints**](https://developer.wordpress.org/playground/blueprints) – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub.
- [**Developers**](https://developer.wordpress.org/playground/developers) – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in the Developers docs hub.
- [**API Reference**](https://developer.wordpress.org/playground/api) – All the APIs exposed by WordPress Playground

## Navigating this documentation hub

This docs hub is focused on starting with WordPress Playground and is divided into the following major sections.

- **[Quick Start Guide](https://developer.wordpress.org/playground/quick-start-guide)**: For those just starting out with WordPress Playground, this is where you can get up and running with WordPress Playground quickly to [start a new WordPress site](https://developer.wordpress.org/playground/quick-start-guide#start-a-new-wordpress-site) and [try a block/theme/plugin](https://developer.wordpress.org/playground/quick-start-guide#try-a-block-a-theme-or-a-plugin) or [test a specific WordPress/PHP version](https://developer.wordpress.org/playground/quick-start-guide#use-a-specific-wordpress-or-php-version).
- **[Playground web instance](https://developer.wordpress.org/playground/web-instance)**: Learn more about the Playground instance you get at https://playground.wordpress.net/
- **[About Playground](https://developer.wordpress.org/playground/about)**: To learn about WordPress Playground, how safe it is, what you can do with and some of its current limitations, visit this section.
  Discover how you can leverage WordPress Playground to [Build](./about/build), [Test](./about/test), and [Launch](./about/launch) your products.
- **[Guides](https://developer.wordpress.org/playground/guides)**: Explore our comprehensive guides to master new skills, find step-by-step instructions, and unlock valuable insights. Dive in to learn and grow!
- **[Contributing](https://developer.wordpress.org/playground/contributing)**: WordPress Playground is an open-source project that welcomes all contributors—from code to design, documentation to triage. Learn here how to contribute.
- **[Links and resources](https://developer.wordpress.org/playground/links-and-resources)**: A nice compilation of useful links and resources related to WordPress Playground.

## First steps

Whether you’re a developer, a non-technical user, or a contributor, these docs will guide you as you start your learning journey:

- [Start using WordPress Playground](https://developer.wordpress.org/playground/quick-start-guide) in 5 minutes (and check out the [demo site](https://playground.wordpress.net/))
- [Get started for developing](https://developer.wordpress.org/playground/developers/build-your-first-app) with WordPress Playground
- Use Playground as a zero-setup [local development environment](https://developer.wordpress.org/playground/developers/local-development/)
- Read about the [limitations](https://developer.wordpress.org/playground/developers/limitations)
- [WordCamp Contributor Day](https://developer.wordpress.org/playground/contributing/contributor-day)

Read [**Introduction to Playground: running WordPress in the browser**](https://developer.wordpress.org/news/2024/04/05/introduction-to-playground-running-wordpress-in-the-browser/) blog post in the [WordPress Developer Blog](https://developer.wordpress.org/news) for a great introduction to WordPress Playground

## Take a deep dive

If you’re a developer or tech user, you may want to check directly the APIs available:

- Read about [Playground APIs](https://developer.wordpress.org/playground/developers/apis/) and basic concepts
- Review [links and resources](https://developer.wordpress.org/playground/links-and-resources)
- Choose the right API for your app:
  - [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) enables basic operations using only query parameters
  - [Blueprints API](https://developer.wordpress.org/playground/blueprints) gives you a great degree of control with a simple JSON file
  - [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api) gives you full control via a JavaScript client from an npm package
- Dive into the [architecture](https://developer.wordpress.org/playground/developers/architecture) and learn how it all works

## Get Involved

WordPress Playground is an open-source project and welcomes all contributors from code to design, and from documentation to triage. Don’t worry, *you don’t need to know WebAssembly* to contribute!

- See the [Contributors Handbook](https://developer.wordpress.org/playground/contributing) for all the details on how you can contribute.
- Join us in the `#playground` channel in Slack (see the [WordPress Slack page](https://make.wordpress.org/chat/) for signup information)

As with all WordPress projects, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [Code of Conduct](https://make.wordpress.org/handbook/community-code-of-conduct/).

## Using Playground with AI assistants

WordPress Playground is designed to work with AI coding agents and AI-powered tools. It runs entirely client-side in WebAssembly — no authentication, no backend required, and isolated to the browser with no persistent side effects outside the sandbox — making it a safe, reliable environment for AI-generated demos and prototypes.

- **[Using Playground with AI agents](https://developer.wordpress.org/playground/guides/agent-skill-wp-playground)** — Install the `wp-playground` skill for Claude Code, Cursor, Gemini CLI, GitHub Copilot, and other coding agents. Describe what you need; the agent runs the commands.
- **[AI-readable site index](https://playground.wordpress.net/llms.txt)** — Machine-readable summary of Playground’s capabilities, APIs, and docs in `llms.txt` format.
- **[AGENTS.md](https://github.com/WordPress/wordpress-playground/blob/trunk/AGENTS.md)** — Guidance for AI coding agents contributing to this codebase.

## License

WordPress Playground is free software released under the terms of the GNU General Public License version 2 or (at your option) any later version. For a complete license, see [LICENSE.md](https://github.com/WordPress/wordpress-playground/blob/trunk/LICENSE).
  
  
![Code is Poetry.](https://i0.wp.com/developer.wordpress.org/files/2026/07/codeispoetry.png?fit=190%2C15&ssl=1)

# Blueprint Bundles

Blueprint bundles are self-contained packages that include a Blueprint declaration (`blueprint.json`) along with all the additional resources required to compile and run it. This makes it easier to distribute and share complete WordPress Playground setups.

## What are Blueprint Bundles?

A Blueprint bundle is a collection of files that includes:

1. A `blueprint.json` file that defines the Blueprint configuration
2. Any additional resources referenced by the Blueprint (themes, plugins, content files, etc.)

Blueprint bundles can be distributed in various formats:

- A ZIP file with a top-level `blueprint.json` file and additional resources
- A directory inside a git repository where `blueprint.json` resides alongside other resources
- A local directory on your computer
- An inline JavaScript object with the relevant files inlined

## Using Blueprint Bundles

### On the Website

The WordPress Playground website supports Blueprint bundles through **New → Blueprint URL** in the Dock or through the `?blueprint-url=` query parameter. You can provide a URL to a ZIP file containing your Blueprint bundle:

```
https://playground.wordpress.net/?blueprint-url=https://example.com/my-blueprint-bundle.zip
```

The ZIP file should contain a `blueprint.json` file at the root level, along with any additional resources referenced by the Blueprint.

### In the CLI

The Playground CLI supports Blueprint bundles through the `--blueprint=` option. You can provide:

- A path to a local directory containing a Blueprint bundle
- A path to a local ZIP file containing a Blueprint bundle
- A URL to a remote Blueprint bundle (http:// or https://)

For example:

```
# Using a local ZIP file
npx @wp-playground/cli --blueprint=./my-blueprint.zip server

# Using a remote URL
npx @wp-playground/cli --blueprint=https://example.com/my-blueprint.zip server

# Using a local directory
npx @wp-playground/cli --blueprint=./my-blueprint-directory server
```

By default, the CLI restricts access to local files for security reasons. If your Blueprint needs to access files in the same parent directory, you need to explicitly grant permission using the `--blueprint-may-read-adjacent-files` flag:

```
npx @wp-playground/cli --blueprint=./my-blueprint.json --blueprint-may-read-adjacent-files server
```

## Creating Blueprint Bundles

### Basic Structure

A basic Blueprint bundle might look like this:

```
my-blueprint-bundle/
├── blueprint.json
├── theme.zip
├── plugin.zip
└── content/
    └── sample-content.wxr
```

### Example Blueprint with Bundled Resources

Here’s an example of a `blueprint.json` file that references bundled resources:

```
{
    "landingPage": "/my-file.txt",
    "steps": [
        {
            "step": "writeFile",
            "path": "/wordpress/my-file.txt",
            "data": {
                "resource": "bundled",
                "path": "/bundled-text-file.txt"
            }
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "bundled",
                "path": "/theme.zip"
            }
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "bundled",
                "path": "/plugin.zip"
            }
        },
        {
            "step": "importWxr",
            "file": {
                "resource": "bundled",
                "path": "/content/sample-content.wxr"
            }
        }
    ]
}
```

In this example, the Blueprint references several bundled resources:

- A text file at `/bundled-text-file.txt`
- A theme ZIP file at `/theme.zip`
- A plugin ZIP file at `/plugin.zip`
- A WXR content file at `/content/sample-content.wxr`

### Creating a ZIP Bundle

To create a ZIP bundle, simply create a directory with your `blueprint.json` and all required resources, then zip it up:

```
# Create a directory for your bundle
mkdir my-blueprint-bundle
cd my-blueprint-bundle

# Create your blueprint.json and add resources
# ...

# Zip it up
zip -r ../my-blueprint-bundle.zip .
```

## ZIP File Structure Flexibility

Blueprint bundles support `blueprint.json` at two locations within a ZIP file:

1. **Root level** (standard): `blueprint.json` sits directly at the ZIP root
2. **One directory deep**: `blueprint.json` sits inside a single top-level directory

This means ZIP files created with macOS’s right-click “Compress” feature (which wraps contents in a folder) work automatically. The `__MACOSX` metadata directory is ignored during detection.

**Example: Both of these ZIP structures work:**

```
# Structure A (root level)
my-bundle.zip/
├── blueprint.json
├── theme.zip
└── plugin.zip

# Structure B (one directory deep — macOS-style)
my-bundle.zip/
├── my-bundle/
│   ├── blueprint.json
│   ├── theme.zip
│   └── plugin.zip
└── __MACOSX/         ← ignored
```

If multiple top-level directories contain a `blueprint.json`, Playground returns an error to avoid ambiguity.

## Troubleshooting

If you encounter issues with Blueprint bundles:

1. Ensure your `blueprint.json` file is at the root level of your ZIP file or inside a single top-level directory
2. Check that all paths in your bundled resource references are correct
3. Verify that your ZIP file is properly formatted
4. When using the CLI, check if you need the `--blueprint-may-read-adjacent-files` flag
5. Ensure all required resources are included in the bundle

# Blueprint data format

A Blueprint JSON file can have many different properties that will be used to define your Playground instance. The most important properties are detailed below.

Here’s an example that uses many of them:

```
{
    "landingPage": "/wp-admin/",
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.5"
    },
    "features": {
        "networking": true
    },
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi8iLCJwcmVmZXJyZWRWZXJzaW9ucyI6eyJwaHAiOiI4LjMiLCJ3cCI6IjYuNSJ9LCJmZWF0dXJlcyI6eyJuZXR3b3JraW5nIjp0cnVlfSwic3RlcHMiOlt7InN0ZXAiOiJsb2dpbiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6InBhc3N3b3JkIn1dfQ==)

## JSON schema

JSON files can be tedious to write and easy to get wrong. To help with that, Playground provides a [JSON schema](https://playground.wordpress.net/blueprint-schema.json) file that you can use to get auto-completion and validation in your editor. Just set the `$schema` property to the following:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
}
```

## Landing page

The `landingPage` property tells Playground which URL to navigate to after the Blueprint has been run. This is a great tool, especially when creating theme or plugin demos. Often, you will want to start Playground in the Site Editor or have a specific post open in the Post Editor. Make sure you use a relative path.

```
{
    "landingPage": "/wp-admin/site-editor.php",
}
```

## Preferred versions

The `preferredVersions` property declares your preferred PHP and WordPress versions. It can contain the following properties:

- `php` (string): Loads the specified PHP version. Accepts `7.4`, `8.0`, `8.1`, `8.2`, `8.3`, `8.4`, `8.5`, `latest`, or `next`. Minor versions like `7.4.1` are not supported. Use `next` to preview the next PHP version from the php-src development branch; it is currently supported by the web runtime only.
- `wp` (string): Loads the specified WordPress version. Accepts the last seven major WordPress versions. As of April 28, 2026, that’s `6.3`, `6.4`, `6.5`, `6.6`, `6.7`, `6.8`, or `6.9`. You can also use the generic values `latest`, `beta`, or `nightly` (alias `trunk`). `beta` resolves to the most recent Beta or Release Candidate of an active release cycle; `nightly`/`trunk` builds straight from the WordPress development branch.

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.7"
    },
}
```

## Features

You can use the `features` property to turn on or off certain features of the Playground instance. It can contain the following properties:

- `networking`: Defaults to `true`. Enables or disables the networking support for Playground. If enabled, [`wp_safe_remote_get`](https://developer.wordpress.org/reference/functions/wp_safe_remote_get/) and similar WordPress functions will actually use `fetch()` to make HTTP requests. If disabled, they will immediately fail instead. You will need this property enabled if you want the user to be able to install plugins or themes.

```
{
    "features": {
        "networking": false
    },
}
```

## Extra libraries

You can preload extra libraries into the Playground instance. The following libraries are supported:

- `wp-cli`: Enables WP-CLI support for Playground. If included, WP-CLI will be installed during boot. If not included, you will get an error message when trying to run WP-CLI commands using the JS API. WP-CLI will be installed by default if the blueprint contains any `wp-cli` steps.

```
{
    "extraLibraries": [ "wp-cli" ],
}
```

## Steps

Arguably the most powerful property, `steps` allows you to configure the Playground instance with preinstalled themes, plugins, demo content, and more. The following example logs the user in with a dedicated username and password. It then installs and activates the Gutenberg plugin. [Learn more about steps](https://developer.wordpress.org/playground/blueprints/steps).

```
{
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "gutenberg"
            }
        },
    ]
}
```

## Common property placement mistakes

Blueprint validation errors often come from putting a valid property in the  
wrong object.

### Activate a plugin or theme

`activate` belongs inside `options`, not inside `pluginData`, `themeData`, or  
directly on the step.

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "wordpress.org/plugins",
        "slug": "gutenberg"
    },
    "options": {
        "activate": true
    }
}
```

### Install plugins with the shorthand

The `plugins` shorthand is a top-level Blueprint property. Do not put it inside  
`preferredVersions`.

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "plugins": ["gutenberg"]
}
```

### Use one plugin install shape

For an `installPlugin` step, use `pluginData`. Do not mix `pluginData` with  
older examples or custom objects such as `pluginZipFile`.

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "wordpress.org/plugins",
        "slug": "woocommerce"
    },
    "options": {
        "activate": true
    }
}
```

The `wordpress.org/plugins` resource needs a separate `slug`. Do not write the  
slug into the `resource` value, such as `"wordpress.org/plugins/woocommerce"`.

### Keep `preferredVersions` limited to versions

`preferredVersions` only accepts `php` and `wp`. Use `features` for networking,  
`plugins` or `installPlugin` for plugins, and `steps` for ordered setup tasks.

### Use explicit steps when order matters

Shorthands such as `plugins`, `login`, `siteOptions`, and `constants` are  
expanded before the `steps` array. If one action must happen before another,  
write both actions as explicit steps in the order you need.

# Blueprints Examples

Check the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.

Let’s see some cool things you can do with Blueprints.

## Install a Theme and a Plugin

```
{
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "coblocks"
            }
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "pendant"
            }
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6Imluc3RhbGxQbHVnaW4iLCJwbHVnaW5EYXRhIjp7InJlc291cmNlIjoid29yZHByZXNzLm9yZy9wbHVnaW5zIiwic2x1ZyI6ImNvYmxvY2tzIn19LHsic3RlcCI6Imluc3RhbGxUaGVtZSIsInRoZW1lRGF0YSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvdGhlbWVzIiwic2x1ZyI6InBlbmRhbnQifX1dfQ==)

## The `meta` object

The optional `meta` object provides descriptive information about your Blueprint. While it doesn’t affect how the Blueprint executes, this information is crucial for display purposes in galleries, Blueprint selectors, and integrated tools like [WordPress Studio](https://developer.wordpress.com/studio/) and [Blueprints Gallery](https://wordpress.github.io/blueprints/).

### Properties

| Field | Type | Description |
| --- | --- | --- |
| **`title`** | `string` | A short, human-readable name for the Blueprint. |
| **`description`** | `string` | A brief summary explaining the setup. |
| **`author`** | `string` | The name or handle of the creator. |
| **`categories`** | `array<string>` | Tags used for filtering and grouping Blueprints. |

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "meta": {
        "title": "Default Playground Setup",
        "description": "A basic setup for a new WordPress site with the latest versions.",
        "author": "Playground Team",
        "categories": ["starter", "default"]
    },
    "landingPage": "/wp-admin/",
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    }
}
```

## Run custom PHP code

```
{
    "steps": [
        {
            "step": "runPHP",
            "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array( 'post_title' => 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1 )); "
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InJ1blBIUCIsImNvZGUiOiI8P3BocCByZXF1aXJlX29uY2UgJy93b3JkcHJlc3Mvd3AtbG9hZC5waHAnOyB3cF9pbnNlcnRfcG9zdChhcnJheSggJ3Bvc3RfdGl0bGUnID0+ICdQb3N0IHRpdGxlJywgJ3Bvc3RfY29udGVudCcgPT4gJ1Bvc3QgY29udGVudCcsICdwb3N0X3N0YXR1cycgPT4gJ3B1Ymxpc2gnLCAncG9zdF9hdXRob3InID0+IDEgKSk7ICJ9XX0=)

## Run Symfony without WordPress

Blueprints can also skip the WordPress download and boot a standalone PHP app.  
This example loads a bundled Symfony app with Composer dependencies already  
installed, so it starts without cloning a Git repository or running a Node/Sass  
build in the browser.

The app demonstrates Symfony attribute routes, autowired services, Twig, and  
HttpClient. Its landing page also explains where to open the Playground file  
browser and Blueprint viewer.

## Enable an option on the Gutenberg Experiments page

Here: Switch on the “new admin views” feature.

```
{
    "steps": [
        {
            "step": "runPHP",
            "code": "<?php require '/wordpress/wp-load.php'; update_option( 'gutenberg-experiments', array( 'gutenberg-dataviews' => true ) );"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InJ1blBIUCIsImNvZGUiOiI8P3BocCByZXF1aXJlICcvd29yZHByZXNzL3dwLWxvYWQucGhwJzsgdXBkYXRlX29wdGlvbiggJ2d1dGVuYmVyZy1leHBlcmltZW50cycsIGFycmF5KCAnZ3V0ZW5iZXJnLWRhdGF2aWV3cycgPT4gdHJ1ZSApICk7In1dfQ==)

## How to work with WP-CLI from the terminal and Playground

You can run WP-CLI commands on a Playground instance either from your terminal or directly within a Blueprint.

To use your terminal, you must first mount the `/wordpress/` directory and ensure the SQLite database integration is configured. This is because Playground’s internal database doesn’t persist on a mounted site, so you must explicitly install the database plugin via a Blueprint. This allows WP-CLI to recognize the WordPress installation and connect to its database.

If you run WP-CLI commands as steps within your Blueprint file, this manual setup is not needed.

The following Blueprint snippet handles this setup:

```
{
    "plugins": [
        "sqlite-database-integration"
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJwbHVnaW5zIjpbInNxbGl0ZS1kYXRhYmFzZS1pbnRlZ3JhdGlvbiJdfQ==)

For a detailed explanation of why this is needed, refer to the [Troubleshoot and Debug Blueprints](https://developer.wordpress.org/playground/blueprints/troubleshoot-and-debug#wp-cli-error-establishing-a-database-connection-on-mounted-sites) section.

## Showcase a product demo

```
{
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "url",
                "url": "https://your-site.com/your-plugin.zip"
            }
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "url",
                "url": "https://your-site.com/your-theme.zip"
            }
        },
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://your-site.com/starter-content.wxr"
            }
        },
        {
            "step": "setSiteOptions",
            "options": {
                "some_required_option_1": "your_favorite_values",
                "some_required_option_2": "your_favorite_values"
            }
        }
    ]
}
```

## Enable networking

```
{
    "landingPage": "/wp-admin/plugin-install.php",
    "features": {
        "networking": true
    },
    "steps": [
        {
            "step": "login"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi9wbHVnaW4taW5zdGFsbC5waHAiLCJmZWF0dXJlcyI6eyJuZXR3b3JraW5nIjp0cnVlfSwic3RlcHMiOlt7InN0ZXAiOiJsb2dpbiJ9XX0=)

## Load PHP code on every request (mu-plugin)

Use the `writeFile` step to add code to `mu-plugins`, where it runs on every request.

```
{
    "landingPage": "/wp-admin/",
    "steps": [
        {
            "step": "login"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/mu-plugins/demo-footer.php",
            "data": "<?php add_filter( 'admin_footer_text', function () { return 'Added by mu-plugin code.'; } );"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi8iLCJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIn0seyJzdGVwIjoid3JpdGVGaWxlIiwicGF0aCI6Ii93b3JkcHJlc3Mvd3AtY29udGVudC9tdS1wbHVnaW5zL2RlbW8tZm9vdGVyLnBocCIsImRhdGEiOiI8P3BocCBhZGRfZmlsdGVyKCAnYWRtaW5fZm9vdGVyX3RleHQnLCBmdW5jdGlvbiAoKSB7IHJldHVybiAnQWRkZWQgYnkgbXUtcGx1Z2luIGNvZGUuJzsgfSApOyJ9XX0=)

## Code editor (as a Gutenberg block)

```
{
    "landingPage": "/wp-admin/post.php?post=4&action=edit",
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "interactive-code-block"
            }
        },
        {
            "step": "runPHP",
            "code": "<?php require '/wordpress/wp-load.php'; wp_insert_post(['post_title' => 'WordPress Playground block demo!','post_content' => '<!-- wp:wordpress-playground/playground /-->', 'post_status' => 'publish', 'post_type' => 'post',]);"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi9wb3N0LnBocD9wb3N0PTQmYWN0aW9uPWVkaXQiLCJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIiwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjoicGFzc3dvcmQifSx7InN0ZXAiOiJpbnN0YWxsUGx1Z2luIiwicGx1Z2luRGF0YSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvcGx1Z2lucyIsInNsdWciOiJpbnRlcmFjdGl2ZS1jb2RlLWJsb2NrIn19LHsic3RlcCI6InJ1blBIUCIsImNvZGUiOiI8P3BocCByZXF1aXJlICcvd29yZHByZXNzL3dwLWxvYWQucGhwJzsgd3BfaW5zZXJ0X3Bvc3QoWydwb3N0X3RpdGxlJyA9PiAnV29yZFByZXNzIFBsYXlncm91bmQgYmxvY2sgZGVtbyEnLCdwb3N0X2NvbnRlbnQnID0+ICc8IS0tIHdwOndvcmRwcmVzcy1wbGF5Z3JvdW5kL3BsYXlncm91bmQgLy0tPicsICdwb3N0X3N0YXR1cycgPT4gJ3B1Ymxpc2gnLCAncG9zdF90eXBlJyA9PiAncG9zdCcsXSk7In1dfQ==)

You can share your own Blueprint examples in [this dedicated wiki](https://github.com/WordPress/wordpress-playground/wiki/Blueprint-examples).

## Load an older WordPress version

Playground only ships with a few recent WordPress releases. If you need to use an older version, this Blueprint can help you: change the version number in `"url": "https://playground.wordpress.net/plugin-proxy.php?url=https://wordpress.org/wordpress-6.2.1.zip"` from `6.2.1` to the release you want to load.

**Note:** the oldest supported WordPress version is `6.2.1`, following the SQLite integration plugin.

```
{
    "landingPage": "/wp-admin",
    "preferredVersions": {
        "wp": "https://playground.wordpress.net/plugin-proxy.php?url=https://wordpress.org/wordpress-6.2.1.zip",
        "php": "8.3"
    },
    "features": {
        "networking": true
    },
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbiIsInByZWZlcnJlZFZlcnNpb25zIjp7IndwIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLndvcmRwcmVzcy5uZXQvcGx1Z2luLXByb3h5LnBocD91cmw9aHR0cHM6Ly93b3JkcHJlc3Mub3JnL3dvcmRwcmVzcy02LjIuMS56aXAiLCJwaHAiOiI4LjMifSwiZmVhdHVyZXMiOnsibmV0d29ya2luZyI6dHJ1ZX0sInN0ZXBzIjpbeyJzdGVwIjoibG9naW4iLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9XX0=)

## Run WordPress from trunk or a specific commit.

WordPress Playground can run `trunk` (the latest commit), the HEAD of a specific branch or a specific commit from the [WordPress/WordPress](https://github.com/WordPress/WordPress) GitHub repository.

You can specify the reference in `"url": "https://playground.wordpress.net/plugin-proxy.php?build-ref=trunk"`.

To specify the latest commit of a particular branch, you can change the reference to the branch version number, eg `6.6`. To run a specific commit, you can use the commit hash from [WordPress/WordPress](https://github.com/WordPress/WordPress), eg `7d7a52367dee9925337e7d901886c2e9b21f70b6`.

**Note:** the oldest supported WordPress version is `6.2.1`, following the SQLite integration plugin.

```
{
    "landingPage": "/wp-admin",
    "login": true,
    "preferredVersions": {
        "php": "8.3",
        "wp": "https://playground.wordpress.net/plugin-proxy.php?build-ref=trunk"
    }
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbiIsImxvZ2luIjp0cnVlLCJwcmVmZXJyZWRWZXJzaW9ucyI6eyJwaHAiOiI4LjMiLCJ3cCI6Imh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L3BsdWdpbi1wcm94eS5waHA/YnVpbGQtcmVmPXRydW5rIn19)

## Using Blueprint Bundles

Here’s an example of a Blueprint that uses bundled resources from a Blueprint bundle:

```
{
    "landingPage": "/",
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "bundled",
                "path": "/my-theme.zip"
            },
            "activate": true
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "bundled",
                "path": "/my-plugin.zip"
            },
            "activate": true
        },
        {
            "step": "writeFile",
            "path": "/wordpress/custom-page.html",
            "data": {
                "resource": "bundled",
                "path": "/assets/custom-page.html"
            }
        }
    ]
}
```

This Blueprint bundle would be zip file containing the following files:

- `/blueprint.json` – The blueprint declaration outlined above
- `/my-theme.zip` – A theme package
- `/my-plugin.zip` – A plugin package
- `/assets/custom-page.html` – A custom HTML file

You can use this Blueprint bundle by:

1. Creating a ZIP file with these files and the blueprint.json
2. Hosting the ZIP file on a server
3. Loading it with `?blueprint-url=https://example.com/my-blueprint-bundle.zip`

For more information on Blueprint bundles, see the [Blueprint Bundles](https://developer.wordpress.org/playground/blueprints/bundles) documentation.

# Getting started with Blueprints

Blueprints are JSON files for setting up your very own WordPress Playground instance. For example:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/wp-admin/",
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        }
    ]
}
```

There are three ways to use Blueprints:

- [Paste a Blueprint into the URL “fragment” on WordPress Playground website](https://developer.wordpress.org/playground/blueprints/using-blueprints#url-fragment).
- [Use them with the JavaScript API](https://developer.wordpress.org/playground/blueprints/using-blueprints#javascript-api).
- [Reference a blueprint JSON file via QueryParam blueprint-url](https://developer.wordpress.org/playground/developers/apis/query-api/)

## What problems are solved by Blueprints?

### No coding skills required

Blueprints are just JSON. You don’t need a development environment, any libraries, or even JavaScript knowledge. You can write them in any text editor.

However, if you do have a development environment, that’s great! You can use the [Blueprint JSON schema](https://playground.wordpress.net/blueprint-schema.json) to get autocompletion and validation.

### HTTP Requests are managed for you

Blueprints fetch any resources you declare for you. You don’t have to worry about managing multiple `fetch()` calls or waiting for them to finish. You can just declare a few links and let Blueprints handle and optimize the downloading pipeline.

### You can link to a Blueprint-preconfigured Playground

Because Blueprints can be pasted in the URL, you can embed or link to a Playground with a specific configuration. For example, clicking this button will open a Playground with PHP 8.3 and a pendant theme installed:

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJwcmVmZXJyZWRWZXJzaW9ucyI6eyJwaHAiOiI4LjMiLCJ3cCI6ImxhdGVzdCJ9LCJzdGVwcyI6W3sic3RlcCI6Imluc3RhbGxUaGVtZSIsInRoZW1lRGF0YSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvdGhlbWVzIiwic2x1ZyI6InBlbmRhbnQifSwib3B0aW9ucyI6eyJhY3RpdmF0ZSI6dHJ1ZX19XX0=)

### Trusted by default

Blueprints are just JSON. Running other people’s Blueprints doesn’t require the element of trust. Since Blueprints cannot execute arbitrary JavaScript, they are limited in what they can do.

With Blueprints, WordPress.org plugin directory may be able to offer live previews of plugins. Plugin authors will just write a custom Blueprint to preconfigure the Playground instance with any site options or starter content they may need.

### Write it once, use it anywhere

Blueprints work both on the web and in node.js. You can run them both in the same JavaScript process, and through a remote Playground Client. They are the universal language of configuration. Where you can run Playground, you can use Blueprints.

# Steps

The `steps` property of a Blueprint is an array of steps to run. For example this Blueprint logs the user in as an admin:

```
{
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIiwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjoicGFzc3dvcmQifV19)

Each step is an object that contains a `step` property that specifies the type of step to run. The rest of the properties depend on the type of step. Learn and try each step type below.

The following step-related topics are addressed on dedicated pages included in this section:

- [Resources References](https://developer.wordpress.org/playground/blueprints/steps/resources) allow you use external files in Blueprints.
- Some steps have a shorthand version. Check the [Shorthands](https://developer.wordpress.org/playground/blueprints/steps/shorthands) section for more information about them.
- For each step listed below, you’ll find both a “Blueprint API” and a “Function API”. Refer to the [API Consistency](https://developer.wordpress.org/playground/blueprints/steps/api-consistency) page for further details.

The [WordPress Playground Step Library](https://akirk.github.io/playground-step-library/#) tool provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also [create your own steps](https://github.com/akirk/playground-step-library/#contributing)!

---

## activatePlugin[​](#ActivatePluginStep "Direct link to ActivatePluginStep")[​](#activatePlugin "Direct link to activatePlugin")

Activates a WordPress plugin (if it’s installed).

### Parameters

- **pluginName** (string)  – Optional. Plugin name to display in the progress bar.
- **pluginPath** (string)  – Path to the plugin directory as absolute path  
  (/wordpress/wp-content/plugins/plugin-name); or the plugin entry file  
  relative to the plugins directory (plugin-name/plugin-name.php).

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "activatePlugin",
            "pluginName": "Gutenberg",
            "pluginPath": "/wordpress/wp-content/plugins/gutenberg"
        }
    ]
}
```

```
import { activatePlugin } from '@wp-playground/blueprints';

activatePlugin(
    playground,
    {
        "step": "activatePlugin",
        "pluginName": "Gutenberg",
        "pluginPath": "/wordpress/wp-content/plugins/gutenberg"
    },
    progress
)
```

---

## activateTheme[​](#ActivateThemeStep "Direct link to ActivateThemeStep")[​](#activateTheme "Direct link to activateTheme")

Activates a WordPress theme (if it’s installed).

### Parameters

- **themeFolderName** (string)  – The name of the theme folder inside wp-content/themes/

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "activateTheme",
            "themeFolderName": "storefront"
        }
    ]
}
```

```
import { activateTheme } from '@wp-playground/blueprints';

activateTheme(
    playground,
    {
        "step": "activateTheme",
        "themeFolderName": "storefront"
    },
    progress
)
```

---

## cp[​](#CpStep "Direct link to CpStep")[​](#cp "Direct link to cp")

Copies a file from one path to another.

### Parameters

- **fromPath** (string)  – Source path
- **toPath** (string)  – Target path

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/index2.php",
    "steps": [
        {
            "step": "cp",
            "fromPath": "/wordpress/index.php",
            "toPath": "/wordpress/index2.php"
        }
    ]
}
```

```
import { cp } from '@wp-playground/blueprints';

cp(
    playground,
    {
        "step": "cp",
        "fromPath": "/wordpress/index.php",
        "toPath": "/wordpress/index2.php"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii9pbmRleDIucGhwIiwic3RlcHMiOlt7InN0ZXAiOiJjcCIsImZyb21QYXRoIjoiL3dvcmRwcmVzcy9pbmRleC5waHAiLCJ0b1BhdGgiOiIvd29yZHByZXNzL2luZGV4Mi5waHAifV19)

---

## defineSiteUrl[​](#DefineSiteUrlStep "Direct link to DefineSiteUrlStep")[​](#defineSiteUrl "Direct link to defineSiteUrl")

Sets [`WP_HOME`](https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#blog-address-url) and [`WP_SITEURL`](https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#wp-siteurl) constants for the WordPress installation.  
Using this step on playground.wordpress.net is moot.  
It is useful when building a custom Playground-based tool, like [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now),  
or deploying Playground on a custom domain.

### Parameters

- **siteUrl** (string)  – The URL

---

## defineWpConfigConsts[​](#DefineWpConfigConstsStep "Direct link to DefineWpConfigConstsStep")[​](#defineWpConfigConsts "Direct link to defineWpConfigConsts")

Defines constants in a [`wp-config.php`](https://developer.wordpress.org/advanced-administration/wordpress/wp-config/) file.  
This step can be called multiple times, and the constants will be merged.

### Parameters

- **consts** (Record)  – The constants to define
- **method** – The method of defining the constants in wp-config.php. Possible values are:

  – rewrite-wp-config: Default. Rewrites the wp-config.php file to  
  explicitly call define() with the requested  
  name and value. This method alters the file  
  on the disk, but it doesn’t conflict with  
  existing define() calls in wp-config.php.

  – define-before-run: Defines the constant before running the requested  
  script. It doesn’t alter any files on the disk, but  
  constants defined this way may conflict with existing  
  define() calls in wp-config.php.
- **virtualize** (boolean)

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "defineWpConfigConsts",
            "consts": {
                "WP_DEBUG": true
            }
        }
    ]
}
```

```
import { defineWpConfigConsts } from '@wp-playground/blueprints';

defineWpConfigConsts(
    playground,
    {
        "step": "defineWpConfigConsts",
        "consts": {
            "WP_DEBUG": true
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImRlZmluZVdwQ29uZmlnQ29uc3RzIiwiY29uc3RzIjp7IldQX0RFQlVHIjp0cnVlfX1dfQ==)

---

## enableMultisite[​](#EnableMultisiteStep "Direct link to EnableMultisiteStep")[​](#enableMultisite "Direct link to enableMultisite")

Defines the [Multisite](https://developer.wordpress.org/advanced-administration/multisite/create-network/) constants in a `wp-config.php` file.  
This step can be called multiple times, and the constants will be merged.

### Parameters

- **wpCliPath** (string)  – wp-cli.phar path

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "enableMultisite"
        }
    ]
}
```

```
import { enableMultisite } from '@wp-playground/blueprints';

enableMultisite(
    playground,
    {
        "step": "enableMultisite"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImVuYWJsZU11bHRpc2l0ZSJ9XX0=)

---

## importThemeStarterContent[​](#ImportThemeStarterContentStep "Direct link to ImportThemeStarterContentStep")[​](#importThemeStarterContent "Direct link to importThemeStarterContent")

Imports a theme Starter Content into WordPress.

### Parameters

- **themeSlug** (string)  – The name of the theme to import content from.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "importThemeStarterContent"
        }
    ]
}
```

```
import { importThemeStarterContent } from '@wp-playground/blueprints';

importThemeStarterContent(
    playground,
    {
        "step": "importThemeStarterContent"
    },
    progress
)
```

---

## importWordPressFiles[​](#ImportWordPressFilesStep "Direct link to ImportWordPressFilesStep")[​](#importWordPressFiles "Direct link to importWordPressFiles")

Imports top-level WordPress files from a given zip file into  
the `documentRoot`. For example, if a zip file contains the  
`wp-content` and `wp-includes` directories, they will replace  
the corresponding directories in Playground’s `documentRoot`.  
Imported copies of Playground-owned runtime artifacts are discarded. For  
example, an archive cannot replace `mu-plugins/sqlite-database-integration`,  
`mu-plugins/0-playground.php`, or a Playground-generated `db.php`. If those  
paths still exist in the importing document root, its copies are retained.  
An unmarked custom `db.php` remains part of the imported site.  
A `formatVersion: 2` archive is otherwise authoritative for user-owned  
`wp-content`: a customized Twenty Twenty-Five theme replaces the boot  
default, while an absent theme remains deleted. For an older archive, stock  
paths omitted by the exporter, such as `plugins/akismet`, `plugins/hello.php`,  
and `themes/twentytwentyfive`, are restored from the importing document root  
only when absent from the archive.

### Parameters

- **pathInZip** (string)  – The path inside the zip file where the WordPress files are.
- **wordPressFilesZip** (ResourceType)  – The zip file containing the top-level WordPress files and  
  directories.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "importWordPressFiles",
            "wordPressFilesZip": {
                "resource": "url",
                "url": "https://mysite.com/import.zip"
            }
        }
    ]
}
```

```
import { importWordPressFiles } from '@wp-playground/blueprints';

importWordPressFiles(
    playground,
    {
        "step": "importWordPressFiles",
        "wordPressFilesZip": await fetchMyFile()
    },
    progress
)
```

---

## importWxr[​](#ImportWxrStep "Direct link to ImportWxrStep")[​](#importWxr "Direct link to importWxr")

Imports a WXR file into WordPress.

### Parameters

- **authorsMap** (Record)  – Remote WXR author usernames keyed to existing local usernames.
- **authorsMode** – How to assign imported WXR authors to local WordPress users.
- **defaultAuthorUsername** (string)  – The fallback local user for imported authors that cannot be mapped.
- **fetchAttachments** (boolean)  – Whether to fetch and import attachment files referenced by the WXR file.
- **file** (ResourceType)  – The file to import
- **importComments** (boolean)  – Whether to import comments from the WXR file.
- **importUsers** (boolean)  – Whether to create local users for imported WXR authors.
- **importer** – The importer to use. Possible values:

  –
- **rewriteUrls** (boolean)  – Whether to rewrite imported URLs to the current site URL.
- **urlMapping** (Record)  – Explicit URL replacements to apply when URL rewriting is enabled.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://your-site.com/starter-content.wxr"
            }
        }
    ]
}
```

```
import { importWxr } from '@wp-playground/blueprints';

importWxr(
    playground,
    {
        "step": "importWxr",
        "file": await fetchMyFile()
    },
    progress
)
```

---

## installPlugin[​](#InstallPluginStep "Direct link to InstallPluginStep")[​](#installPlugin "Direct link to installPlugin")

Installs a WordPress plugin in the Playground.

### Parameters

- **ifAlreadyInstalled** – What to do if the asset already exists.
- **options** (InstallPluginOptions)  – Optional installation options.
- **pluginData** – The plugin files to install. It can be a plugin zip file, a single PHP  
  file, or a directory containing all the plugin files at its root.
- **pluginZipFile** (FileResource)  – @deprecated. Use ‘pluginData’ instead.

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/wp-admin/plugins.php",
    "steps": [
        {
            "step": "login"
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "gutenberg"
            },
            "options": {
                "activate": true
            }
        }
    ]
}
```

```
import { installPlugin } from '@wp-playground/blueprints';

installPlugin(
    playground,
    {
        "step": "installPlugin",
        "pluginData": await fetchMyFile(),
        "options": {
            "activate": true
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi9wbHVnaW5zLnBocCIsInN0ZXBzIjpbeyJzdGVwIjoibG9naW4ifSx7InN0ZXAiOiJpbnN0YWxsUGx1Z2luIiwicGx1Z2luRGF0YSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvcGx1Z2lucyIsInNsdWciOiJndXRlbmJlcmcifSwib3B0aW9ucyI6eyJhY3RpdmF0ZSI6dHJ1ZX19XX0=)

---

## installTheme[​](#InstallThemeStep "Direct link to InstallThemeStep")[​](#installTheme "Direct link to installTheme")

Installs a WordPress theme in the Playground.

### Parameters

- **ifAlreadyInstalled** – What to do if the asset already exists.
- **options** (InstallThemeOptions)  – Optional installation options.
- **themeData** – The theme files to install. It can be either a theme zip file, or a  
  directory containing all the theme files at its root.
- **themeZipFile** (FileResource)  – @deprecated. Use ‘themeData’ instead.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "login"
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "pendant"
            },
            "options": {
                "activate": true,
                "importStarterContent": true
            }
        }
    ]
}
```

```
import { installTheme } from '@wp-playground/blueprints';

installTheme(
    playground,
    {
        "step": "installTheme",
        "themeData": await fetchMyFile(),
        "options": {
            "activate": true,
            "importStarterContent": true
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIn0seyJzdGVwIjoiaW5zdGFsbFRoZW1lIiwidGhlbWVEYXRhIjp7InJlc291cmNlIjoid29yZHByZXNzLm9yZy90aGVtZXMiLCJzbHVnIjoicGVuZGFudCJ9LCJvcHRpb25zIjp7ImFjdGl2YXRlIjp0cnVlLCJpbXBvcnRTdGFydGVyQ29udGVudCI6dHJ1ZX19XX0=)

---

## login[​](#LoginStep "Direct link to LoginStep")[​](#login "Direct link to login")

Logs in to Playground.  
Under the hood, this function sets the `PLAYGROUND_AUTO_LOGIN_AS_USER` constant.  
The `0-auto-login.php` mu-plugin uses that constant to log in the user on the first load.  
This step depends on the `@wp-playground/wordpress` package because  
the plugin is located in and loaded automatically by the `@wp-playground/wordpress` package.

### Parameters

- **password** (string)
- **username** (string)  – The user to log in as. Defaults to ‘admin’.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "login",
            "username": "admin"
        }
    ]
}
```

```
import { login } from '@wp-playground/blueprints';

login(
    playground,
    {
        "step": "login",
        "username": "admin"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIiwidXNlcm5hbWUiOiJhZG1pbiJ9XX0=)

---

## mkdir[​](#MkdirStep "Direct link to MkdirStep")[​](#mkdir "Direct link to mkdir")

Creates a directory at the specified path.

### Parameters

- **path** (string)  – The path of the directory you want to create

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "mkdir",
            "path": "/wordpress/my-new-folder"
        }
    ]
}
```

```
import { mkdir } from '@wp-playground/blueprints';

mkdir(
    playground,
    {
        "step": "mkdir",
        "path": "/wordpress/my-new-folder"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6Im1rZGlyIiwicGF0aCI6Ii93b3JkcHJlc3MvbXktbmV3LWZvbGRlciJ9XX0=)

---

## mv[​](#MvStep "Direct link to MvStep")[​](#mv "Direct link to mv")

Moves a file or directory from one path to another.

### Parameters

- **fromPath** (string)  – Source path
- **toPath** (string)  – Target path

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/index2.php",
    "steps": [
        {
            "step": "mv",
            "fromPath": "/wordpress/index.php",
            "toPath": "/wordpress/index2.php"
        }
    ]
}
```

```
import { mv } from '@wp-playground/blueprints';

mv(
    playground,
    {
        "step": "mv",
        "fromPath": "/wordpress/index.php",
        "toPath": "/wordpress/index2.php"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii9pbmRleDIucGhwIiwic3RlcHMiOlt7InN0ZXAiOiJtdiIsImZyb21QYXRoIjoiL3dvcmRwcmVzcy9pbmRleC5waHAiLCJ0b1BhdGgiOiIvd29yZHByZXNzL2luZGV4Mi5waHAifV19)

---

## resetData[​](#ResetDataStep "Direct link to ResetDataStep")[​](#resetData "Direct link to resetData")

Deletes the selected WordPress content through WordPress APIs so dependent  
records are removed with it. Empty tables have their sequences reset so  
later imports receive the identifiers they would on a site without the  
removed content.

### Parameters

- **contentTypes** – Content types to remove. When omitted, all posts, pages, custom post  
  types, and comments are removed.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "resetData"
        }
    ]
}
```

```
import { resetData } from '@wp-playground/blueprints';

resetData(
    playground,
    {
        "step": "resetData"
    },
    progress
)
```

---

## rm[​](#RmStep "Direct link to RmStep")[​](#rm "Direct link to rm")

Removes a file at the specified path.

### Parameters

- **path** (string)  – The path to remove

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/index.php",
    "steps": [
        {
            "step": "rm",
            "path": "/wordpress/index.php"
        }
    ]
}
```

```
import { rm } from '@wp-playground/blueprints';

rm(
    playground,
    {
        "step": "rm",
        "path": "/wordpress/index.php"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii9pbmRleC5waHAiLCJzdGVwcyI6W3sic3RlcCI6InJtIiwicGF0aCI6Ii93b3JkcHJlc3MvaW5kZXgucGhwIn1dfQ==)

---

## rmdir[​](#RmdirStep "Direct link to RmdirStep")[​](#rmdir "Direct link to rmdir")

Removes a directory at the specified path.

### Parameters

- **path** (string)  – The path to remove

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/wp-admin/",
    "steps": [
        {
            "step": "rmdir",
            "path": "/wordpress/wp-admin"
        }
    ]
}
```

```
import { rmdir } from '@wp-playground/blueprints';

rmdir(
    playground,
    {
        "step": "rmdir",
        "path": "/wordpress/wp-admin"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi8iLCJzdGVwcyI6W3sic3RlcCI6InJtZGlyIiwicGF0aCI6Ii93b3JkcHJlc3Mvd3AtYWRtaW4ifV19)

---

## runPHP[​](#RunPHPStep "Direct link to RunPHPStep")[​](#runPHP "Direct link to runPHP")

Runs PHP code.  
When running WordPress functions, the `code` key must first load [`wp-load.php`](https://github.com/WordPress/WordPress/blob/master/wp-load.php) and start with `"<?php require_once '/wordpress/wp-load.php'; "`.

### Parameters

- **code** – The PHP code to run.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "runPHP",
            "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>"
        }
    ]
}
```

```
import { runPHP } from '@wp-playground/blueprints';

runPHP(
    playground,
    {
        "step": "runPHP",
        "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InJ1blBIUCIsImNvZGUiOiI8P3BocCByZXF1aXJlX29uY2UgJy93b3JkcHJlc3Mvd3AtbG9hZC5waHAnOyB3cF9pbnNlcnRfcG9zdChhcnJheSgncG9zdF90aXRsZScgPT4gJ3dwLWxvYWQucGhwIHJlcXVpcmVkIGZvciBXUCBmdW5jdGlvbmFsaXR5JywgJ3Bvc3Rfc3RhdHVzJyA9PiAncHVibGlzaCcpKTsgPz4ifV19)

---

## runPHPWithOptions[​](#RunPHPWithOptionsStep "Direct link to RunPHPWithOptionsStep")[​](#runPHPWithOptions "Direct link to runPHPWithOptions")

Runs PHP code.  
When running WordPress functions, the `code` key must first load [`wp-load.php`](https://github.com/WordPress/WordPress/blob/master/wp-load.php) and start with `"<?php require_once '/wordpress/wp-load.php'; "`.

### Parameters

- **options** (PHPRunOptions)  – Run options (See  
  /wordpress-playground/api/universal/interface/PHPRunOptions/))

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "runPHPWithOptions",
            "options": {
                "code": "<?php require_once '/wordpress/wp-load.php'; update_option('blogname', file_get_contents('php://input'));?>",
                "body": "Site Name Modified by runPHPWithOptions"
            }
        }
    ]
}
```

```
import { runPHPWithOptions } from '@wp-playground/blueprints';

runPHPWithOptions(
    playground,
    {
        "step": "runPHPWithOptions",
        "options": {
            "code": "<?php require_once '/wordpress/wp-load.php'; update_option('blogname', file_get_contents('php://input'));?>",
            "body": "Site Name Modified by runPHPWithOptions"
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InJ1blBIUFdpdGhPcHRpb25zIiwib3B0aW9ucyI6eyJjb2RlIjoiPD9waHAgcmVxdWlyZV9vbmNlICcvd29yZHByZXNzL3dwLWxvYWQucGhwJzsgdXBkYXRlX29wdGlvbignYmxvZ25hbWUnLCBmaWxlX2dldF9jb250ZW50cygncGhwOi8vaW5wdXQnKSk7Pz4iLCJib2R5IjoiU2l0ZSBOYW1lIE1vZGlmaWVkIGJ5IHJ1blBIUFdpdGhPcHRpb25zIn19XX0=)

---

## runSql[​](#RunSqlStep "Direct link to RunSqlStep")[​](#runSql "Direct link to runSql")

Run one or more SQL queries.  
This step uses WP\_MySQL\_Naive\_Query\_Stream to parse and execute SQL queries using  
streaming semantics. It supports multiline queries, comments, and queries  
separated by semicolons. Each query is executed using `$wpdb`. This step assumes  
a presence of the `sqlite-database-integration` plugin that ships the required  
query tokenizer classes.

### Parameters

- **sql** (ResourceType)  – The SQL to run. Each non-empty line must contain a valid SQL query.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "runSql",
            "sql": {
                "resource": "literal",
                "name": "schema.sql",
                "contents": "DELETE FROM wp_posts"
            }
        }
    ]
}
```

```
import { runSql } from '@wp-playground/blueprints';

runSql(
    playground,
    {
        "step": "runSql",
        "sql": await fetchMyFile()
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InJ1blNxbCIsInNxbCI6eyJyZXNvdXJjZSI6ImxpdGVyYWwiLCJuYW1lIjoic2NoZW1hLnNxbCIsImNvbnRlbnRzIjoiREVMRVRFIEZST00gd3BfcG9zdHMifX1dfQ==)

---

## setSiteLanguage[​](#SetSiteLanguageStep "Direct link to SetSiteLanguageStep")[​](#setSiteLanguage "Direct link to setSiteLanguage")

Sets the site language and download translations.

### Parameters

- **language** (string)  – The language to set, e.g. ‘en\_US’

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "setSiteLanguage",
            "language": "en_US"
        }
    ]
}
```

```
import { setSiteLanguage } from '@wp-playground/blueprints';

setSiteLanguage(
    playground,
    {
        "step": "setSiteLanguage",
        "language": "en_US"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InNldFNpdGVMYW5ndWFnZSIsImxhbmd1YWdlIjoiZW5fVVMifV19)

---

## setSiteOptions[​](#SetSiteOptionsStep "Direct link to SetSiteOptionsStep")[​](#setSiteOptions "Direct link to setSiteOptions")

Sets site options. This is equivalent to calling [`update_option`](https://developer.wordpress.org/reference/functions/update_option/) for each  
option in the [`options`](https://developer.wordpress.org/apis/options/#available-options-by-category) object.

### Parameters

- **options** (Record)  – The options to set on the site.

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "setSiteOptions",
            "options": {
                "blogname": "My Blog",
                "blogdescription": "A great blog"
            }
        }
    ]
}
```

```
import { setSiteOptions } from '@wp-playground/blueprints';

setSiteOptions(
    playground,
    {
        "step": "setSiteOptions",
        "options": {
            "blogname": "My Blog",
            "blogdescription": "A great blog"
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InNldFNpdGVPcHRpb25zIiwib3B0aW9ucyI6eyJibG9nbmFtZSI6Ik15IEJsb2ciLCJibG9nZGVzY3JpcHRpb24iOiJBIGdyZWF0IGJsb2cifX1dfQ==)

---

## unzip[​](#UnzipStep "Direct link to UnzipStep")[​](#unzip "Direct link to unzip")

Unzip a zip file.

### Parameters

- **extractToPath** (string)  – The path to extract the zip file to
- **zipFile** (ResourceType)  – The zip file to extract
- **zipPath** (string)  – The path of the zip file to extract

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "unzip",
            "zipFile": {
                "resource": "vfs",
                "path": "/wordpress/data.zip"
            },
            "extractToPath": "/wordpress"
        }
    ]
}
```

```
import { unzip } from '@wp-playground/blueprints';

unzip(
    playground,
    {
        "step": "unzip",
        "zipFile": await fetchMyFile(),
        "extractToPath": "/wordpress"
    },
    progress
)
```

---

## updateUserMeta[​](#UpdateUserMetaStep "Direct link to UpdateUserMetaStep")[​](#updateUserMeta "Direct link to updateUserMeta")

Updates user meta. This is equivalent to calling [`update_user_meta`](https://developer.wordpress.org/reference/functions/update_user_meta/) for each  
meta value in the `meta` object.

### Parameters

- **meta** (Record)  – An object of user meta values to set, e.g. { “first\_name”: “John” }
- **userId** (number)  – User ID

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "updateUserMeta",
            "meta": {
                "first_name": "John",
                "last_name": "Doe"
            },
            "userId": 1
        }
    ]
}
```

```
import { updateUserMeta } from '@wp-playground/blueprints';

updateUserMeta(
    playground,
    {
        "step": "updateUserMeta",
        "meta": {
            "first_name": "John",
            "last_name": "Doe"
        },
        "userId": 1
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6InVwZGF0ZVVzZXJNZXRhIiwibWV0YSI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9LCJ1c2VySWQiOjF9XX0=)

---

## wp-cli[​](#WPCLIStep "Direct link to WPCLIStep")[​](#wp-cli "Direct link to wp-cli")

Runs PHP code using [WP-CLI](https://developer.wordpress.org/cli/commands/).

### Parameters

- **command** – The WP CLI command to run.
- **wpCliPath** (string)  – wp-cli.phar path

### Example

- Blueprint API
- Function API

```
{
    "steps": [
        {
            "step": "wp-cli",
            "command": "wp post create --post_title='Test post' --post_excerpt='Some content'"
        }
    ]
}
```

```
import { wpCLI } from '@wp-playground/blueprints';

wpCLI(
    playground,
    {
        "step": "wp-cli",
        "command": "wp post create --post_title='Test post' --post_excerpt='Some content'"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6IndwLWNsaSIsImNvbW1hbmQiOiJ3cCBwb3N0IGNyZWF0ZSAtLXBvc3RfdGl0bGU9J1Rlc3QgcG9zdCcgLS1wb3N0X2V4Y2VycHQ9J1NvbWUgY29udGVudCcifV19)

---

## writeFile[​](#WriteFileStep "Direct link to WriteFileStep")[​](#writeFile "Direct link to writeFile")

Writes data to a file at the specified path.

### Parameters

- **data** – The data to write
- **path** (string)  – The path of the file to write to

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/test.php",
    "steps": [
        {
            "step": "writeFile",
            "path": "/wordpress/test.php",
            "data": "<?php echo 'Hello World!'; ?>"
        }
    ]
}
```

```
import { writeFile } from '@wp-playground/blueprints';

writeFile(
    playground,
    {
        "step": "writeFile",
        "path": "/wordpress/test.php",
        "data": "<?php echo 'Hello World!'; ?>"
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii90ZXN0LnBocCIsInN0ZXBzIjpbeyJzdGVwIjoid3JpdGVGaWxlIiwicGF0aCI6Ii93b3JkcHJlc3MvdGVzdC5waHAiLCJkYXRhIjoiPD9waHAgZWNobyAnSGVsbG8gV29ybGQhJzsgPz4ifV19)

---

## writeFiles[​](#WriteFilesStep "Direct link to WriteFilesStep")[​](#writeFiles "Direct link to writeFiles")

Writes multiple files to a specified directory in the Playground  
filesystem.

```
my-plugin/
├── index.php
└── public/
    └── style.css
```

### Parameters

- **filesTree** (DirectoryResource)  – The ‘filesTree’ defines the directory structure, supporting ‘literal:directory’ or  
  ‘git:directory’ types. The ‘name’ represents the root directory, while ‘files’ is an object  
  where keys are file paths, and values contain either file content as a string or nested objects  
  for subdirectories.
- **writeToPath** (string)  – The path of the file to write to

### Example

- Blueprint API
- Function API

```
{
    "landingPage": "/test.php",
    "steps": [
        {
            "step": "writeFiles",
            "writeToPath": "/wordpress/wp-content/plugins/my-plugin",
            "filesTree": {
                "name": "my-plugin",
                "files": {
                    "index.php": "<?php echo '<a>Hello World!</a>'; ?>",
                    "public": {
                        "style.css": "a { color: red; }"
                    }
                }
            }
        }
    ]
}
```

```
import { writeFiles } from '@wp-playground/blueprints';

writeFiles(
    playground,
    {
        "step": "writeFiles",
        "writeToPath": "/wordpress/wp-content/plugins/my-plugin",
        "filesTree": {
            "name": "my-plugin",
            "files": {
                "index.php": "<?php echo '<a>Hello World!</a>'; ?>",
                "public": {
                    "style.css": "a { color: red; }"
                }
            }
        }
    },
    progress
)
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii90ZXN0LnBocCIsInN0ZXBzIjpbeyJzdGVwIjoid3JpdGVGaWxlcyIsIndyaXRlVG9QYXRoIjoiL3dvcmRwcmVzcy93cC1jb250ZW50L3BsdWdpbnMvbXktcGx1Z2luIiwiZmlsZXNUcmVlIjp7Im5hbWUiOiJteS1wbHVnaW4iLCJmaWxlcyI6eyJpbmRleC5waHAiOiI8P3BocCBlY2hvICc8YT5IZWxsbyBXb3JsZCE8L2E+JzsgPz4iLCJwdWJsaWMiOnsic3R5bGUuY3NzIjoiYSB7IGNvbG9yOiByZWQ7IH0ifX19fV19)

# Troubleshoot and debug Blueprints

Blueprint errors usually point to one of three places:

- The Blueprint JSON is invalid.
- Playground could not fetch the Blueprint or one of its resources.
- A Blueprint step ran, but WordPress, PHP, WP-CLI, or a plugin failed.

Start with the exact error name shown by Playground, then use the matching
section below.

## Quick checklist

- Paste the Blueprint into the [Blueprints editor](https://playground.wordpress.net/builder/builder.html) to validate the JSON schema.
- If the Blueprint is loaded from a URL, open that URL directly in a private browser window and confirm it downloads valid JSON or a Blueprint ZIP bundle.
- If a step fails, note the step number in `BlueprintStepExecutionError`. The failed step is usually the item at that position after Blueprint shorthands have been expanded.
- Open browser developer tools and check the Console and Network tabs for download, CORS, PHP, or plugin activation details.
- For plugin/theme activation failures, check the Playground **Logs** panel or the browser console for PHP warnings and fatal errors.

## InvalidBlueprintError

`InvalidBlueprintError` means the Blueprint does not match the
[Blueprint data format](https://developer.wordpress.org/playground/blueprints/data-format). The error output usually
contains paths such as `/steps/2/pluginData` or `/preferredVersions`.

### Unexpected property `activate`

`activate` belongs inside `options`, not directly on the step or inside
`pluginData`.

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "wordpress.org/plugins",
        "slug": "woocommerce"
    },
    "options": {
        "activate": true
    }
}
```

### Unexpected property `plugins` in `preferredVersions`

`preferredVersions` only accepts `php` and `wp`. Install plugins with the
top-level `plugins` shorthand or with an explicit `installPlugin` step.

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "plugins": ["sqlite-database-integration"]
}
```

### Missing `slug`, `url`, `path`, or `files`

The resource object is incomplete or uses the wrong shape. Common fixes:

- WordPress.org plugin: `{ "resource": "wordpress.org/plugins", "slug": "akismet" }`
- ZIP URL: `{ "resource": "url", "url": "https://example.com/plugin.zip" }`
- Git directory: `{ "resource": "git:directory", "url": "https://github.com/org/repo", "ref": "trunk", "refType": "branch" }`

See [Resources References](https://developer.wordpress.org/playground/blueprints/steps/resources) for all supported
resource shapes.

### Mixed plugin install properties

Use `pluginData` for `installPlugin`. Do not provide both `pluginData` and
older examples or custom objects such as `pluginZipFile`.
The WordPress.org plugin resource also needs a separate `slug`:

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "wordpress.org/plugins",
        "slug": "woocommerce"
    }
}
```

Do not write `"resource": "wordpress.org/plugins/woocommerce"`.

## BlueprintFetchError

`BlueprintFetchError` means Playground could not load the file passed to
`?blueprint-url=`.
Check that the URL:

- Is public and does not require cookies, login, a temporary session, or a VPN.
- Returns HTTP 200 when opened directly.
- Serves valid JSON or a ZIP bundle with `blueprint.json` inside it.
- Sends `Access-Control-Allow-Origin: *` or another header that allows
  `https://playground.wordpress.net`.
- Uses a raw file URL, not a repository HTML page.

For GitHub, use `raw.githubusercontent.com` URLs instead of `github.com/.../blob/...`.
For GitLab, use the raw file URL instead of a `/-/blob/` page.

```
# Good
https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/welcome/blueprint.json

# Not a raw JSON response
https://github.com/WordPress/blueprints/blob/trunk/blueprints/welcome/blueprint.json
```

Temporary tunnel URLs, local development URLs, and draft release assets often
fail because the browser cannot reach them or because they do not allow
cross-origin requests. Move the Blueprint to a public host with CORS enabled.

### Blueprint file is neither a valid JSON nor a ZIP file

This means Playground received a response, but the response was not a Blueprint.
The URL may have returned an HTML page, 404 page, repository file viewer, proxy
warning, login page, or corrupted ZIP.
Open the URL directly and check that:

- JSON URLs return valid Blueprint JSON.
- ZIP bundle URLs download a real ZIP archive.
- Blueprint bundles contain `blueprint.json` at the root of the ZIP.
- The response is not a small HTML or text error page.

### URIError: URI malformed

`URIError: URI malformed` usually points to a broken encoded Blueprint fragment
in the URL, not to a failed Blueprint step. Check for invalid `%` escapes,
double-encoded fragments, or raw JSON pasted after `#`. Rebuild the link from
the original Blueprint and encode it once, or use Base64. See
[Encoded Blueprint fragments](https://developer.wordpress.org/playground/blueprints/using-blueprints).

## ResourceDownloadError

`ResourceDownloadError` means the Blueprint loaded, but a step could not download
a resource such as a plugin ZIP, theme ZIP, WXR file, or imported site archive.
Confirm the resource URL:

- Downloads the actual file, not an HTML page, redirect warning, or expired artifact.
- Is public and does not require authentication.
- Allows cross-origin requests.
- Is the direct file URL. Some release pages and CI artifact pages are human pages, not direct downloads.
- Still exists. Temporary links and CI artifacts can expire.

For source code in a Git repository, prefer a
[`git:directory` resource](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference).
Use a `url` resource for built ZIP artifacts that are already publicly
downloadable.

## BlueprintStepExecutionError

`BlueprintStepExecutionError` means a specific step failed after the Blueprint
started running. The message includes a step number:

```
BlueprintStepExecutionError: Error when executing the blueprint step #4
```

Use that number to inspect the matching step. If your Blueprint uses shorthands
such as `plugins`, `login`, `siteOptions`, or `constants`, Playground expands
them into steps before running the Blueprint. Use explicit `steps` when the
order matters.
URL query parameters such as `?plugin=...`, `?theme=...`, `?php=...`,
`?wp=...`, and `?networking=yes` also create an implicit Blueprint. Errors from
those URLs are still Blueprint execution errors, and the generated steps affect
the reported step number.

## PHP.run() failed with exit code 255

Exit code `255` usually means PHP hit a fatal error. Look for the first
`Fatal error`, `Uncaught`, or `TypeError` line in the output. The large HTML
error page around it is usually WordPress’s generic critical error screen.
To make the output more useful while debugging, enable WordPress debug constants
near the beginning of the Blueprint:

```
{
    "step": "defineWpConfigConsts",
    "consts": {
        "WP_DEBUG": true,
        "WP_DEBUG_LOG": true,
        "WP_DEBUG_DISPLAY": true,
        "WP_DISABLE_FATAL_ERROR_HANDLER": true
    }
}
```

Then rerun the Blueprint and check the Playground **Logs** panel or browser
console.

## PHP.run() failed with exit code 1

Exit code `1` often appears when WP-CLI or WordPress returns an application
error. Read the `Stderr` section first. It usually names the unsupported
argument, missing resource, or command-specific failure.
Some WP-CLI commands behave differently in Playground because WordPress runs in
WebAssembly with SQLite. Keep commands small and test them individually before
adding a long chain to a Blueprint.

## Undefined constant `ABSPATH`

This usually happens in a `runPHP` step that calls WordPress APIs without first
loading WordPress.
Add `wp-load.php` before any WordPress function, constant, option, or plugin API:

```
{
    "step": "runPHP",
    "code": "<?php require '/wordpress/wp-load.php'; update_option('blogname', 'Demo site');"
}
```

## Plugin could not be activated

Plugin activation errors usually come from the plugin itself, not from the
Blueprint runner. Common causes:

- The plugin requires a newer PHP version or WordPress version.
- The plugin has a fatal error on activation.
- The plugin depends on another plugin that is not installed or activated.
- The plugin performs a redirect or prints unexpected output during activation.
- The plugin ZIP extracts to a folder or main file name different from the path the step is activating.

If the error says the current PHP or WordPress version does not meet minimum
requirements, set `preferredVersions`:

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    }
}
```

### WordPress exited with exit code 0

Activation can fail even when WordPress exits with code `0`. This usually means
WordPress returned an activation error response rather than a PHP process crash.
When the message says `Inspect the "debug" logs`, check the Playground **Logs**
panel, browser console, or CLI output.
Look for PHP warnings, redirects or output printed during activation, missing
dependency plugins, or plugin minimum PHP/WordPress requirements.

### Current PHP or WordPress version does not meet minimum requirements

Version mismatch errors often include text like:

```
Current PHP version (7.4.33) does not meet minimum requirements. The plugin requires PHP 8.0.
```

or:

```
Current WordPress version (6.9.4) does not meet minimum requirements. The plugin requires WordPress 7.0.
```

Set `preferredVersions` to a compatible PHP and WordPress version, or use a
plugin/theme release that supports the versions available in Playground.
If the error is:

```
Failed to download WordPress 6.9.0 (HTTP 404)
```

the requested WordPress build is not available. Use `latest`, a supported
released version, or a supported beta/nightly value.
If the error says `Plugin file does not exist`, inspect the installed folder
name. For ZIP URLs with unusual folder names, set `targetFolderName`:

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "url",
        "url": "https://example.com/my-plugin.zip"
    },
    "options": {
        "activate": true,
        "targetFolderName": "my-plugin"
    }
}
```

If the plugin has dependencies, install and activate those dependencies first
with explicit steps.

## Theme could not be activated

Theme activation failures usually mean the theme folder name is wrong, the
theme ZIP extracted to an unexpected directory, or the theme code caused a
WordPress/PHP error.
Use `installTheme` with `options.activate` when installing a theme:

```
{
    "step": "installTheme",
    "themeData": {
        "resource": "wordpress.org/themes",
        "slug": "twentytwentyfour"
    },
    "options": {
        "activate": true
    }
}
```

If you use a standalone `activateTheme` step, pass the folder name inside
`wp-content/themes`, not a full URL or ZIP filename.

## Could not write to a file

Errors like this mean the parent directory does not exist:

```
Could not write to "/wordpress/wp-content/plugins/example/index.php":
There is no such file or directory OR the parent directory does not exist.
```

Create the directory first with `mkdir`, or use `writeFiles` with a
`literal:directory` resource.

```
[
    {
        "step": "mkdir",
        "path": "/wordpress/wp-content/plugins/example"
    },
    {
        "step": "writeFile",
        "path": "/wordpress/wp-content/plugins/example/index.php",
        "data": "<?php /* Plugin Name: Example */"
    }
]
```

## Could not unzip file

This usually means the file is not a valid ZIP archive. The URL may have
returned an HTML page, an error response, a login page, or a truncated file.
If the output says `Could not unzip file. Error code: 19`, verify the download
is a ZIP archive. A small file size often means the server returned an HTML
error page instead of the archive.
Open the URL directly and confirm the browser downloads a ZIP. If you are using
a GitHub or CI artifact, use a direct-download URL and make sure the release or
artifact is public.

## WP-CLI command pitfalls

The `wp-cli` step runs WP-CLI inside Playground. It is useful for setup tasks,
but not every command or shell feature behaves like a local terminal.
Common fixes:

- Use the step name `"wp-cli"`, not `"wpcli"` or `"cli"`.
- Keep commands focused. Prefer multiple `wp-cli` steps over one complex shell command.
- Avoid shell substitutions such as `$(...)` in shared Blueprints. Use `runPHP` for logic that needs WordPress APIs.
- Check parameter names against the WP-CLI command you are using. For example, command-specific parameters may differ between `wp post list`, `wp post delete`, and plugin-provided commands.
- If a plugin-provided WP-CLI command fails with a plugin stack trace, the fix usually belongs in that plugin or in the input data passed to the command.
- If a command fails with `unknown --post_type parameter` or `unknown --format parameter`, check whether the flags belong to a different command in the pipeline.
- If a plugin command fails with `Unsupported argument type passed to WP_CLI::error_to_string(): 'NULL'`, confirm the plugin is active, the imported data exists, and the command input points to a valid resource.

## WP-CLI: Error establishing a database connection on mounted sites

When using `wp-cli` with a mounted Playground site, for example via
`--mount-before-install`, you might encounter an “Error establishing a database
connection.” This happens because WordPress Playground loads the SQLite database
integration plugin from its internal files by default, not from the mounted
directory.
Add the SQLite integration plugin to the mounted WordPress site explicitly:

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "plugins": ["sqlite-database-integration"],
    "steps": [
        {
            "step": "login",
            "username": "admin"
        }
    ]
}
```

Then run the Blueprint with the mounted site:

```
mkdir wordpress
npx @wp-playground/cli server --mount-before-install=wordpress:/wordpress --blueprint=./blueprint.json
```

## Debugging tools

### Blueprints editor

Use the in-browser [Blueprints editor](https://playground.wordpress.net/builder/builder.html)
to build, validate, and preview Blueprints.

**Caution**

The editor is under development and the embedded Playground sometimes fails to
load. To get around it, refresh the page.

### Filesystem and database inspection

Some Blueprint steps, such as [`writeFile`](https://developer.wordpress.org/playground/blueprints/steps),
alter the internal filesystem. Others, such as
[`runSql`](https://developer.wordpress.org/playground/blueprints/steps), alter the database.
To inspect the final state, use **Files**, **Database**, and **Logs** from the Dock.
Use **Files** to confirm the Blueprint created, moved, or edited the expected files.
![The Files pane showing a selected WordPress file and its contents](https://i0.wp.com/developer.wordpress.org/files/2026/07/files.webp?fit=1440%2C900&ssl=1)
Use **Database** to inspect tables and records changed by SQL or WordPress steps.
![The Database pane showing database inspection tools](https://i0.wp.com/developer.wordpress.org/files/2026/07/database.webp?fit=1440%2C900&ssl=1)
You can also inspect a Playground instance from the browser console through
`window.playground`:

```
await playground.isDir('/wordpress/wp-content/plugins');
await playground.listFiles('/wordpress/wp-content/plugins');
```

See the full [PlaygroundClient API](https://developer.wordpress.org/playground/api/client/interface/PlaygroundClient).

### Browser console and network requests

Open browser developer tools to check JavaScript errors, PHP debug logs, and
failed network requests. In Chrome, Firefox, and Edge, press
`Ctrl + Shift + I` on Windows/Linux or `Cmd + Option + I` on macOS.

**Safari**

If you have not enabled the Develop menu, go to **Safari > Settings… >
Advanced** and check **Show features for web developers**.

### Custom error logging

You can write your own messages with `error_log()` in a
[`runPHP` step](https://developer.wordpress.org/playground/blueprints/steps), then check the Playground
**Logs** panel or the browser console.
![The PHP error log pane showing PHP log output](https://i0.wp.com/developer.wordpress.org/files/2026/07/logs.webp?fit=1440%2C900&ssl=1)

When you download your Playground as a ZIP through **Export → Download as .zip**, the archive also includes `debug.log`.

## Ask for help

If you need help, [open an issue](https://github.com/WordPress/wordpress-playground/issues)
and include:

- The Blueprint JSON or the public Blueprint URL.
- The exact error message.
- The failing step number, if shown.
- Browser, operating system, and whether you used the website, JavaScript API, or CLI.
- Relevant console, network, or CLI output.

# Blueprints 101 – A crash course

Welcome to a Blueprints crash course, where you’ll find everything you need to know about Blueprints: what they are, how to create them, and how to use them effectively.

1. [What are Blueprints, and what can you do with them?](https://developer.wordpress.org/playground/blueprints/tutorial/what-are-blueprints)
2. [How to load and run Blueprints](https://developer.wordpress.org/playground/blueprints/tutorial/how-to-load-run)
3. [Build your first Blueprint](https://developer.wordpress.org/playground/blueprints/tutorial/build-your-first)

If you encounter any issues while following this tutorial, refer to the [Troubleshoot and debug Blueprints](https://developer.wordpress.org/playground/blueprints/troubleshoot-and-debug) section for tips and tools to help you solve them.

# Using Blueprints

You can use Blueprints in one of the following ways:

- Open **New → Blueprint gallery** in the Playground Dock and choose an example.
- Open **New → Blueprint URL** and enter a public Blueprint JSON or ZIP bundle URL.
- Open **New → Write a Blueprint** to author a Blueprint in the browser.
- Open **Blueprint** to inspect or edit the current Playground’s Blueprint.
- Pass a Blueprint as a URL fragment.
- Load a Blueprint from a URL using the `blueprint-url` parameter.
- Use Blueprint bundles (ZIP files or directories).
- Use the JavaScript API.

![The New Playground pane with the Blueprint gallery selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground.webp?fit=1440%2C900&ssl=1)

## URL Fragment

The easiest way to start using Blueprints is to paste one into the URL “fragment” on WordPress Playground website, e.g. `https://playground.wordpress.net/#{"preferredVersions...`.
For example, to create a Playground with specific versions of WordPress and PHP you would use the following Blueprint:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.5"
    }
}
```

And then you would go to
`https://playground.wordpress.net/#{"preferredVersions":{"php":"8.3","wp":"6.5"}}`.

In Javascript, you can get a compact version of any blueprint JSON with [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
Example:

```
const blueprintJson = `{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.5"
    }
}`;
const minifiedBlueprintJson = JSON.stringify(JSON.parse(blueprintJson)); // {"preferredVersions":{"php":"8.3","wp":"6.5"}}
const encodedBlueprint = encodeURIComponent(minifiedBlueprintJson);
const playgroundUrl = `https://playground.wordpress.net/#${encodedBlueprint}`;
```

You won’t have to paste links to follow along. We’ll use code examples with a “Try it out” button that will automatically run the examples for you:

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJwcmVmZXJyZWRWZXJzaW9ucyI6eyJwaHAiOiI4LjMiLCJ3cCI6IjYuNSJ9fQ==)

### Encoded Blueprint fragments

When you create Playground links from JavaScript or automation tools, encode the minified JSON once with `encodeURIComponent()` and append it after `#`:

```
const blueprint = {
    $schema: 'https://playground.wordpress.net/blueprint-schema.json',
    preferredVersions: {
        php: '8.3',
        wp: '6.5',
    },
};
const playgroundUrl = `https://playground.wordpress.net/#${encodeURIComponent(JSON.stringify(blueprint))}`;
```

Playground also supports Base64-encoded Blueprints. Base64 is useful when a platform modifies JSON fragments or when you want a compact, copyable link. For example, that’s the above Blueprint in Base64 format: `eyIkc2NoZW1hIjogImh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L2JsdWVwcmludC1zY2hlbWEuanNvbiIsInByZWZlcnJlZFZlcnNpb25zIjogeyJwaHAiOiAiNy40Iiwid3AiOiAiNi41In19`.
To run it, go to https://playground.wordpress.net/#eyIkc2NoZW1hIjogImh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L2JsdWVwcmludC1zY2hlbWEuanNvbiIsInByZWZlcnJlZFZlcnNpb25zIjogeyJwaHAiOiAiNy40Iiwid3AiOiAiNi41In19

#### URIError: URI malformed

If a Playground link fails with `URIError: URI malformed`, the encoded
Blueprint fragment is usually malformed. Common causes include an invalid `%`
escape, a fragment that was encoded twice, or JSON pasted into the URL without
encoding.
Rebuild the link from the original Blueprint object and encode it once:

```
const playgroundUrl = `https://playground.wordpress.net/#${encodeURIComponent(JSON.stringify(blueprint))}`;
```

If another tool changes URL fragments, use a Base64-encoded Blueprint instead.

In JavaScript, You can get any blueprint JSON in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64#javascript_support) with global function `btoa()`.

Example:

```
const blueprintJson = `{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.5"
    }
}`;
const minifiedBlueprintJson = btoa(blueprintJson); // eyIkc2NoZW1hIjogImh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L2JsdWVwcmludC1zY2hlbWEuanNvbiIsInByZWZlcnJlZFZlcnNpb25zIjogeyJwaHAiOiAiNy40Iiwid3AiOiAiNi41In19
```

### Load Blueprint from a URL

When your Blueprint gets too wieldy, you can load it via the `?blueprint-url` query parameter in the URL, like this:
<https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/adamziel/blueprints/trunk/blueprints/latest-gutenberg/blueprint.json>
Note that the Blueprint must be publicly accessible and served with [the correct `Access-Control-Allow-Origin` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin):

```
Access-Control-Allow-Origin: *
```

When a Blueprint URL fails with `BlueprintFetchError`, check these details:

- The URL must return a JSON file or a Blueprint ZIP bundle, not an HTML page.
- GitHub URLs should use `raw.githubusercontent.com`, not `github.com/.../blob/...`.
- GitLab URLs should use the raw file URL, not a `/-/blob/` page.
- The file must be reachable without login, cookies, VPN access, or a temporary browser session.
- Draft releases, expired CI artifacts, and temporary tunnel URLs can stop working even if the Blueprint was valid earlier.
- If you host the file yourself, configure CORS so `https://playground.wordpress.net` can fetch it.

#### Blueprint Bundles

The `?blueprint-url` parameter now also supports Blueprint bundles in ZIP format. A Blueprint bundle is a ZIP file that contains a `blueprint.json` file at the root level, along with any additional resources referenced by the Blueprint.
For example, you can load a Blueprint bundle like this:
<https://playground.wordpress.net/?blueprint-url=https://example.com/my-blueprint-bundle.zip>
When using a Blueprint bundle, you can reference bundled resources using the `bundled` resource type:

```
{
    "landingPage": "/my-file.txt",
    "steps": [
        {
            "step": "writeFile",
            "path": "/wordpress/my-file.txt",
            "data": {
                "resource": "bundled",
                "path": "/bundled-text-file.txt"
            }
        }
    ]
}
```

For more information on Blueprint bundles, see the [Blueprint Bundles](https://developer.wordpress.org/playground/blueprints/bundles) documentation.

## JavaScript API

You can also use Blueprints with the JavaScript API using the `startPlaygroundWeb()` function from the `@wp-playground/client` package. Here’s a small, self-contained example you can run on JSFiddle or CodePen:

```
<iframe id="wp-playground" style="width: 1200px; height: 800px"></iframe>
<script type="module">

    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp-playground'),
        remoteUrl: `https://playground.wordpress.net/remote.html`,
        blueprint: {
            landingPage: '/wp-admin/',
            preferredVersions: {
                php: '8.3',
                wp: 'latest',
            },
            steps: [
                {
                    step: 'login',
                    username: 'admin',
                    password: 'password',
                },
                {
                    step: 'installPlugin',
                    pluginData: {
                        resource: 'wordpress.org/plugins',
                        slug: 'friends',
                    },
                },
            ],
        },
    });

    const response = await client.run({
        // wp-load.php is only required if you want to interact with WordPress.
        code: '<?php require_once "/wordpress/wp-load.php"; $posts = get_posts(); echo "Post Title: " . $posts[0]->post_title;',
    });
    console.log(response.text);
</script>
```

# JSON API and Function API

Blueprints are defined in JSON format, but the underlying implementation uses JavaScript functions to execute the steps. While JSON is the most convenient way of interacting with Blueprints, you can also use the underlying functions directly.

JSON is merely a wrapper around the functions. Whether you use the JSON steps or the exported functions, you’ll have to provide the same parameters (except for the step name):

You can use Blueprints both with the web and the node.js versions of WordPress Playground.

**Blueprints version 2**

The team is exploring ways to transition Blueprints from a TypeScript library to a PHP library. This would allow people to run Blueprints in any WordPress environments: Playground, a hosted site, or a local setup.

The proposed [new specification](https://github.com/WordPress/blueprints-library/issues/6) is discussed on a separate [GitHub repository](https://github.com/WordPress/blueprints-library/), and you’re more than welcome to join (there or on the [#playground](https://wordpress.slack.com/archives/C04EWKGDJ0K) Slack channel) and help shape the next generation of Playground.

## Differences between JSON and Function APIs

There are two main differences between the JSON and Function APIs:

1. Blueprints handle the progress bar and error reporting for you. The function API requires you to handle these yourself.
2. The function API requires importing the API client library while Blueprints may be just pasted into the URL fragment.

Check the [Use the same structure for Blueprint JSON definitions and step handlers](https://github.com/WordPress/wordpress-playground/pull/215) issue at [wordpress-playground](https://github.com/WordPress/wordpress-playground) repo for more detailed info about this topic

# Resources References

“Resource References” allow you use external files in Blueprints

Blueprint steps such as [`installPlugin`](https://developer.wordpress.org/playground/blueprints/steps) or [`installTheme`](https://developer.wordpress.org/playground/blueprints/steps) require a location of the plugin or theme to be installed.

That location can be defined as [a `URL` resource](#urlreference) of the `.zip` file containing the theme or plugin. It can also be defined as a [`wordpress.org/plugins`](#corepluginreference) or [`wordpress.org/themes`](#corethemereference) resource for those plugins/themes published in the official WordPress directories.

The following resource references are available:

### URLReference

The `URLReference` resource is used to reference files that are stored on a remote server. The `URLReference` resource is defined as follows:

```
type URLReference = {
    resource: 'url';
    url: string;
};
```

To use the `URLReference` resource, you need to provide the URL of the file. For example, to reference a file named “index.html” that is stored on a remote server, you can create a `URLReference` as follows:

```
{
    "resource": "url",
    "url": "https://example.com/index.html"
}
```

The `url` resource works with Blueprint steps such as [`installPlugin`](https://developer.wordpress.org/playground/blueprints/steps) or  
[`installTheme`](https://developer.wordpress.org/playground/blueprints/steps).  
These steps require a `ResourceType` to define the location of the plugin or the theme to install.

With a `"resource": "url"` we can define the location of a `.zip` containing the plugin/theme. Use this for built ZIP artifacts hosted on a publicly accessible URL that does not require authentication, such as a release asset. CI artifact direct-download URLs can work, but they are often short-lived or restricted.

For source code stored in a Git repository, prefer [`git:directory`](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference). It can fetch a repository subdirectory from a branch, tag, or commit without requiring a ZIP archive.

Before using a `url` resource, verify that the URL:

- Downloads the file directly. It must not return an HTML page, redirect warning, login page, repository file viewer, or proxy error page.
- Is available without cookies, authentication, a VPN, or a temporary browser session.
- Sends CORS headers that allow Playground to fetch it.
- Points to the expected file type. `installPlugin` and `installTheme` need a plugin or theme ZIP archive unless you use another resource type.
- Will remain available. Temporary tunnel URLs, draft release assets, and short-lived CI artifacts can expire.
- Is a real ZIP archive when the step expects a ZIP. Very small downloads often mean the server returned an HTML error page instead of the archive.

For GitHub source code, do not point `url` at a repository page or a generated  
ZIP from a branch when you can use `git:directory`. Use `url` for built ZIP  
artifacts and `git:directory` for source directories.

### GitDirectoryReference

The `GitDirectoryReference` resource is used to reference a directory inside a Git repository. This is useful when a plugin or theme lives in a subfolder of a repo, or when you want to install from a specific branch, tag, or commit.

```
type GitDirectoryReference = {
    resource: 'git:directory';
    url: string; // Repository URL (https://, ssh git@..., etc.)
    path?: string; // Optional subdirectory inside the repository
    ref?: string; // Branch, tag, or commit SHA (defaults to HEAD)
    refType?: 'branch' | 'tag' | 'commit'; // Hint for resolving the ref
    '.git'?: boolean; // Experimental: include a .git directory with fetched metadata
};
```

**Example:**

```
{
    "step": "installPlugin",
    "pluginData": {
        "resource": "git:directory",
        "url": "https://github.com/WordPress/block-development-examples",
        "ref": "HEAD",
        "path": "plugins/data-basics-59c8f8"
    },
    "options": {
        "activate": true,
        "targetFolderName": "data-basics"
    }
}
```

**Notes:**

- When using a branch or tag name for `ref`, you must specify `refType` (e.g. `"refType": "branch"`). Without it, only `HEAD` is reliably resolved.
- Playground automatically detects providers like GitHub and GitLab.
- Repository URLs may include or omit a trailing `.git` suffix. Extra trailing slashes are ignored.
- It handles CORS-proxied fetches and sparse checkouts, so you can use URLs that point to specific subdirectories or branches.
- This resource can be used with steps like [`installPlugin`](https://developer.wordpress.org/playground/blueprints/steps) and [`installTheme`](https://developer.wordpress.org/playground/blueprints/steps).
- Set `".git": true` to include a `.git` folder containing packfiles and refs so Git-aware tooling can detect the checkout. This currently mirrors a shallow clone of the selected ref.
- The folder name is derived from the URL by default (e.g. `https-github-com-WordPress-block-development-examples-HEAD-at-plugins-data-basics-59c8f8`). Use `options.targetFolderName` in the step to override it, as shown in the example above.

### CoreThemeReference

The *CoreThemeReference* resource is used to reference WordPress core themes. The *CoreThemeReference* resource is defined as follows:

```
type CoreThemeReference = {
    resource: 'wordpress.org/themes';
    slug: string;
    version?: string;
};
```

To use the *CoreThemeReference* resource, you need to provide the slug of the theme. For example, to reference the “Twenty Twenty-One” theme, you can create a *CoreThemeReference* as follows:

```
{
    "resource": "wordpress.org/themes",
    "slug": "twentytwentyone"
}
```

### CorePluginReference

The *CorePluginReference* resource is used to reference WordPress core plugins. The *CorePluginReference* resource is defined as follows:

```
type CorePluginReference = {
    resource: 'wordpress.org/plugins';
    slug: string;
    version?: string;
};
```

To use the *CorePluginReference* resource, you need to provide the slug of the plugin. For example, to reference the “Akismet” plugin, you can create a *CorePluginReference* as follows:

```
{
    "resource": "wordpress.org/plugins",
    "slug": "akismet"
}
```

### VFSReference

The *VFSReference* resource is used to reference files that are stored in a virtual file system (VFS). The VFS is a file system that is stored in memory and can be used to store files that are not part of the file system of the operating system. The *VFSReference* resource is defined as follows:

```
type VFSReference = {
    resource: 'vfs';
    path: string;
};
```

To use the *VFSReference* resource, you need to provide the path to the file in the VFS. For example, to reference a file named “index.html” that is stored in the root of the VFS, you can create a *VFSReference* as follows:

```
{
    "resource": "vfs",
    "path": "/index.html"
}
```

### LiteralReference

The *LiteralReference* resource is used to reference files that are stored as literals in the code. The *LiteralReference* resource is defined as follows:

```
type LiteralReference = {
    resource: 'literal';
    name: string;
    contents: string | Uint8Array;
};
```

To use the *LiteralReference* resource, you need to provide the name of the file and its contents. For example, to reference a file named “index.html” that contains the text “Hello, World!”, you can create a *LiteralReference* as follows:

```
{
    "resource": "literal",
    "name": "index.html",
    "contents": "Hello, World!"
}
```

### BundledReference

The `BundledReference` resource is used to reference files that are bundled with the Blueprint itself. This is particularly useful for creating self-contained Blueprint bundles that include all necessary resources. The `BundledReference` resource is defined as follows:

```
type BundledReference = {
    resource: 'bundled';
    path: string;
};
```

To use the `BundledReference` resource, you need to provide the relative path to the file within the bundle. For example, to reference a file named “plugin.php” that is bundled with the Blueprint, you can create a `BundledReference` as follows:

```
{
    "resource": "bundled",
    "path": "plugin.php"
}
```

Blueprint bundles can be distributed in various formats, including:

- ZIP files with a top-level `blueprint.json` file
- Directories containing a `blueprint.json` file and related resources
- Remote URLs where the Blueprint and its resources are hosted together

For more information on Blueprint bundles, see the [Blueprint Bundles](https://developer.wordpress.org/playground/blueprints/bundles) documentation.

# Shorthands

You can specify some `steps` using a `shorthand` syntax. The following `steps` are currently supported:

### `login`

Use

```
    "login": true,
```

Or

```
{
    "step": "login",
    "username": "admin",
    "password": "password"
}
```

### `plugins`

(replaces the `installPlugin` step)

Use

```
    "plugins": [
        "hello-dolly",
        "https://raw.githubusercontent.com/adamziel/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
    ]
```

Or

```
[
    {
        "step": "installPlugin",
        "pluginData": {
            "resource": "wordpress.org/plugins",
            "slug": "hello-dolly"
        }
    },
    {
        "step": "installPlugin",
        "pluginData": {
            "resource": "url",
            "url": "https://raw.githubusercontent.com/adamziel/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
        }
    }
]
```

### `siteOptions`

Use

```
    "siteOptions": {
        "blogname": "My first Blueprint"
    }
```

Or

```
    "step": "setSiteOptions",
    "options": {
        "blogname": "My first Blueprint"
    }
```

### `defineWpConfigConsts`

(`constants` only)

Use

```
{
    "step": "defineWpConfigConsts",
    "consts": {
        "WP_DISABLE_FATAL_ERROR_HANDLER": true,
        "WP_DEBUG": true,
        "WP_DEBUG_DISPLAY": true
    }
}
```

Or

```
    {
        "step": "defineWpConfigConsts",
        "consts": {
            "WP_DISABLE_FATAL_ERROR_HANDLER": true
        }
    },
    {
        "step": "defineWpConfigConsts",
        "consts": {
            "WP_DEBUG": true
        }
    },
    {
        "step": "defineWpConfigConsts",
        "consts": {
            "WP_DEBUG_DISPLAY": true
        }
    }
```

---

The `shorthand` syntax and the `step` syntax correspond to each other. Every `step` specified with the `shorthand` syntax is added to the top of the `steps` array in arbitrary order.

**Which should you choose?**

- Use `shorthands` when **brevity** is your main concern.
- Use explicit `steps` when you need more control over the **execution order**.

# Build your first Blueprint

Let’s build an elementary Blueprint that

1. Creates a new WordPress site
2. Sets the site title to “My first Blueprint”
3. Installs the *Adventurer* theme
4. Installs the *Hello Dolly* plugin from the WordPress plugin directory
5. Installs a custom plugin
6. Changes the site content

## 1. Create a new WordPress site

Let’s start by creating a `blueprint.json` file with the following contents:

```
{}
```

It may seem like nothing is happening, but this Blueprint already spins up a WordPress site with the latest major version.
[Run Blueprint](https://playground.wordpress.net/#{})

**Autocomplete**

If you use an IDE, like VS Code or PHPStorm, you can use the [Blueprint JSON Schema](https://playground.wordpress.net/blueprint-schema.json) for an autocompleted Blueprint development experience. Add the following line at the top of your `blueprint.json` file:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json"
}
```

Here’s what it looks like in VS Code:
![Autocompletion visualized](https://i0.wp.com/developer.wordpress.org/files/2026/07/schema-autocompletion.webp?fit=1632%2C968&ssl=1)

## 2. Set the site title to “My first Blueprint”

Blueprints consist of a series of [steps](https://developer.wordpress.org/playground/blueprints/steps) that define how to build a WordPress site. Before you write the first step, declare an empty list of steps:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "steps": []
}
```

This Blueprint isn’t very exciting—it creates the same default site as the empty Blueprint above. Let’s do something about it!
WordPress stores the site title in the `blogname` option. Add your first step and set that option to “My first Blueprint”:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "steps": [
        {
            "step": "setSiteOptions",
            "options": {
                "blogname": "My first Blueprint"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#https://playground.wordpress.net/#eyIkc2NoZW1hIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLndvcmRwcmVzcy5uZXQvYmx1ZXByaW50LXNjaGVtYS5qc29uIiwic3RlcHMiOlt7InN0ZXAiOiJzZXRTaXRlT3B0aW9ucyIsIm9wdGlvbnMiOnsiYmxvZ25hbWUiOiJNeSBmaXJzdCBCbHVlcHJpbnQifX1dfQ==)
The [`setSiteOptions` step](https://developer.wordpress.org/playground/blueprints/steps#SetSiteOptionsStep) specifies the site options in the WordPress database. The `options` object contains the key-value pairs to set. In this case, you changed the value of the `blogname` key to “My first Blueprint”. You can read more about all available steps in the [Blueprint Steps API Reference](https://developer.wordpress.org/playground/blueprints/steps).

### Shorthands

You can specify some steps using a shorthand syntax. For example, you could write the `setSiteOptions` step like this:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "siteOptions": {
        "blogname": "My first Blueprint"
    }
}
```

The shorthand syntax and the step syntax correspond with each other. Every step specified with the shorthand syntax is automatically added at the beginning of the `steps` array in an arbitrary order. Which should you choose? Use shorthands when brevity is your main concern, use steps when you need more control over the order of execution.

## 3. Install the *Adventurer* theme

Adventurer is an open-source theme [available in the WordPress theme directory](https://wordpress.org/themes/adventurer/). Let’s install it using the [`installTheme` step](https://developer.wordpress.org/playground/blueprints/steps#InstallThemeStep):

```
{
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#eyIkc2NoZW1hIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLndvcmRwcmVzcy5uZXQvYmx1ZXByaW50LXNjaGVtYS5qc29uIiwib3B0aW9ucyI6eyJibG9nbmFtZSI6Ik15IGZpcnN0IEJsdWVwcmludCJ9LCJzdGVwcyI6W3sic3RlcCI6Imluc3RhbGxUaGVtZSIsInRoZW1lWmlwRmlsZSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvdGhlbWVzIiwic2x1ZyI6ImFkdmVudHVyZXIifX1dfQ==)
The site should now look like the screenshot below:
![Site with the adventurer theme](https://i0.wp.com/developer.wordpress.org/files/2026/07/installed-adventurer-theme.webp?fit=2388%2C2000&ssl=1)

### Resources

The `themeData` defines a [resource](https://developer.wordpress.org/playground/blueprints/steps/resources) and references an external file required to complete the step. Playground supports different types of resources, including

- `url`,
- `wordpress.org/themes`,
- `wordpress.org/plugins`,
- `vfs`(virtual file system), or
- `literal`.

The example uses the `wordpress.org/themes` resource, which requires a `slug` identical to the one used in WordPress theme directory:
In this case, `https://wordpress.org/themes/<slug>/` becomes `https://wordpress.org/themes/adventurer/`.

Learn more about the supported resources in the [Blueprint Resources API Reference](https://developer.wordpress.org/playground/blueprints/steps/resources/).

## 4. Install the *Hello Dolly* plugin

A classic WordPress plugin that displays random lyrics from the song “Hello, Dolly!” in the admin dashboard. Let’s install it using the [`installPlugin` step](https://developer.wordpress.org/playground/blueprints/steps#InstallPluginStep):

```
{
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "hello-dolly"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#eyJzaXRlT3B0aW9ucyI6eyJibG9nbmFtZSI6Ik15IGZpcnN0IEJsdWVwcmludCJ9LCJzdGVwcyI6W3sic3RlcCI6Imluc3RhbGxUaGVtZSIsInRoZW1lWmlwRmlsZSI6eyJyZXNvdXJjZSI6IndvcmRwcmVzcy5vcmcvdGhlbWVzIiwic2x1ZyI6ImFkdmVudHVyZXIifX0seyJzdGVwIjoiaW5zdGFsbFBsdWdpbiIsInBsdWdpblppcEZpbGUiOnsicmVzb3VyY2UiOiJ3b3JkcHJlc3Mub3JnL3BsdWdpbnMiLCJzbHVnIjoiaGVsbG8tZG9sbHkifX1dfQ==)
The Hello Dolly plugin is now installed and activated.
Like the `themeData`, the `pluginData` defines a reference to an external file required for the step. The example uses the `wordpress.org/plugins` resource to install the plugin with the matching `slug` from the WordPress plugin directory.

## 5. Install a custom plugin

Let’s install a custom WordPress plugin that adds a message to the admin dashboard:

```
<?php
/*
Plugin Name: "Hello" on the Dashboard
Description: A custom plugin to showcase WordPress Blueprints
Version: 1.0
Author: WordPress Contributors
*/

function my_custom_plugin() {
    echo '<h1>Hello from My Custom Plugin!</h1>';
}

add_action('admin_notices', 'my_custom_plugin');
```

You can use the [installPlugin](https://developer.wordpress.org/playground/blueprints/steps#InstallPluginStep), but that requires creating a ZIP file. Let’s start with something different to see if the plugin works:

1. Create a `wp-content/plugins/hello-from-the-dashboard` directory using the [`mkdir` step](https://developer.wordpress.org/playground/blueprints/steps#MkdirStep).
2. Write a `plugin.php` file using the [`writeFile` step](https://developer.wordpress.org/playground/blueprints/steps#WriteFileStep).
3. Activate the plugin using the [`activatePlugin` step](https://developer.wordpress.org/playground/blueprints/steps#ActivatePluginStep).

Here’s what that looks like in a Blueprint:

```
{
    // ...
    "steps": [
        // ...
        {
            "step": "mkdir",
            "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard/plugin.php",
            "data": "<?php\n/*\nPlugin Name: \"Hello\" on the Dashboard\nDescription: A custom plugin to showcase WordPress Blueprints\nVersion: 1.0\nAuthor: WordPress Contributors\n*/\n\nfunction my_custom_plugin() {\n    echo '<h1>Hello from My Custom Plugin!</h1>';\n}\n\nadd_action('admin_notices', 'my_custom_plugin');"
        },
        {
            "step": "activatePlugin",
            "pluginPath": "hello-from-the-dashboard/plugin.php"
        }
    ]
}
```

The last thing to do is log the user in as an admin. You can do that with a shorthand of the [`login` step](https://developer.wordpress.org/playground/blueprints/steps#LoginStep):

```
{
    "login": true,
    "steps": {
        // ...
    }
}
```

Here’s the complete Blueprint:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "login": true,
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "hello-dolly"
            }
        },
        {
            "step": "mkdir",
            "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard/plugin.php",
            "data": "<?php\n/*\nPlugin Name: \"Hello\" on the Dashboard\nDescription: A custom plugin to showcase WordPress Blueprints\nVersion: 1.0\nAuthor: WordPress Contributors\n*/\n\nfunction my_custom_plugin() {\n    echo '<h1>Hello from My Custom Plugin!</h1>';\n}\n\nadd_action('admin_notices', 'my_custom_plugin');"
        },
        {
            "step": "activatePlugin",
            "pluginPath": "hello-from-the-dashboard/plugin.php"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#eyJsb2dpbiI6dHJ1ZSwic2l0ZU9wdGlvbnMiOnsiYmxvZ25hbWUiOiJNeSBmaXJzdCBCbHVlcHJpbnQifSwic3RlcHMiOlt7InN0ZXAiOiJpbnN0YWxsVGhlbWUiLCJ0aGVtZVppcEZpbGUiOnsicmVzb3VyY2UiOiJ3b3JkcHJlc3Mub3JnL3RoZW1lcyIsInNsdWciOiJhZHZlbnR1cmVyIn19LHsic3RlcCI6Imluc3RhbGxQbHVnaW4iLCJwbHVnaW5aaXBGaWxlIjp7InJlc291cmNlIjoid29yZHByZXNzLm9yZy9wbHVnaW5zIiwic2x1ZyI6ImhlbGxvLWRvbGx5In19LHsic3RlcCI6Im1rZGlyIiwicGF0aCI6Ii93b3JkcHJlc3Mvd3AtY29udGVudC9wbHVnaW5zL2hlbGxvLW9uLXRoZS1kYXNoYm9hcmQifSx7InN0ZXAiOiJ3cml0ZUZpbGUiLCJwYXRoIjoiL3dvcmRwcmVzcy93cC1jb250ZW50L3BsdWdpbnMvaGVsbG8tb24tdGhlLWRhc2hib2FyZC9wbHVnaW4ucGhwIiwiZGF0YSI6Ijw/cGhwXG4vKlxuUGx1Z2luIE5hbWU6IFwiSGVsbG9cIiBvbiB0aGUgRGFzaGJvYXJkXG5EZXNjcmlwdGlvbjogQSBjdXN0b20gcGx1Z2luIHRvIHNob3djYXNlIFdvcmRQcmVzcyBCbHVlcHJpbnRzXG5WZXJzaW9uOiAxLjBcbkF1dGhvcjogV29yZFByZXNzIENvbnRyaWJ1dG9yc1xuKi9cblxuZnVuY3Rpb24gbXlfY3VzdG9tX3BsdWdpbigpIHtcbiAgICBlY2hvICc8aDE+SGVsbG8gZnJvbSBNeSBDdXN0b20gUGx1Z2luITwvaDE+Jztcbn1cblxuYWRkX2FjdGlvbignYWRtaW5fbm90aWNlcycsICdteV9jdXN0b21fcGx1Z2luJyk7In0seyJzdGVwIjoiYWN0aXZhdGVQbHVnaW4iLCJwbHVnaW5QYXRoIjoiaGVsbG8tb24tdGhlLWRhc2hib2FyZC9wbHVnaW4ucGhwIn1dfQ==)
That’s what it looks like when you navigate to the dashboard:
![Site with the custom plugin](https://i0.wp.com/developer.wordpress.org/files/2026/07/installed-custom-plugin.webp?fit=2272%2C1934&ssl=1)

### Create a plugin and zip it

Encoding PHP files as `JSON` can be useful for quick testing, but it’s inconvenient and difficult to read. Instead, create a file with the plugin code, compress it, and use the `ZIP` file as the `resource` in the [`installPlugin` step](https://developer.wordpress.org/playground/blueprints/steps#InstallPluginStep) to install it (the path in the `URL` should match the one in your GitHub repository):

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "login": true,
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "hello-dolly"
            }
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
            }
        }
    ]
}
```

You can shorten that Blueprint even more using the shorthand syntax:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "login": true,
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "plugins": ["hello-dolly", "https://raw.githubusercontent.com/wordpress/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"],
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#eyIkc2NoZW1hIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLndvcmRwcmVzcy5uZXQvYmx1ZXByaW50LXNjaGVtYS5qc29uIiwibG9naW4iOnRydWUsInNpdGVPcHRpb25zIjp7ImJsb2duYW1lIjoiTXkgZmlyc3QgQmx1ZXByaW50In0sInBsdWdpbnMiOlsiaGVsbG8tZG9sbHkiLCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYWRhbXppZWwvYmx1ZXByaW50cy90cnVuay9kb2NzL2hlbGxvLW9uLXRoZS1kYXNoYm9hcmQuemlwIl0sInN0ZXBzIjpbeyJzdGVwIjoiaW5zdGFsbFRoZW1lIiwidGhlbWVaaXBGaWxlIjp7InJlc291cmNlIjoid29yZHByZXNzLm9yZy90aGVtZXMiLCJzbHVnIjoiYWR2ZW50dXJlciJ9fV19)

## 6. Change the site content

Finally, let’s delete the default content of the site and import a new one from a WordPress export file (WXR).

### Delete the old content

There isn’t a Blueprint step to delete the default content, but you can do that with a snippet of PHP code:

```
<?php
require '/wordpress/wp-load.php';

// Delete all posts and pages
$posts = get_posts(array(
    'numberposts' => -1,
    'post_type' => array('post', 'page'),
    'post_status' => 'any'
));

foreach ($posts as $post) {
    wp_delete_post($post->ID, true);
}
```

To run that code during the site setup, use the [`runPHP` step](https://developer.wordpress.org/playground/blueprints/steps#RunPHPStep):

```
{
    // ...
    "steps": [
        // ...
        {
            "step": "runPHP",
            "code": "<?php\nrequire '/wordpress/wp-load.php';\n\n$posts = get_posts(array(\n    'numberposts' => -1,\n    'post_type' => array('post', 'page'),\n    'post_status' => 'any'\n));\n\nforeach ($posts as $post) {\n    wp_delete_post($post->ID, true);\n}"
        }
    ]
}
```

### Import the new content

Let’s use the [`importWxr` step](https://developer.wordpress.org/playground/blueprints/steps#ImportWXRStep) to import a WordPress export (`WXR`) file that helps test WordPress themes. The file is available in the [WordPress/theme-test-data](https://github.com/WordPress/theme-test-data) repository, and you can access it via its `raw.githubusercontent.com` address: <https://raw.githubusercontent.com/WordPress/theme-test-data/master/themeunittestdata.wordpress.xml>.
Here’s what the final Blueprint looks like:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "login": true,
    "siteOptions": {
        "blogname": "My first Blueprint"
    },
    "plugins": ["hello-dolly", "https://raw.githubusercontent.com/wordpress/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"],
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "adventurer"
            }
        },
        {
            "step": "runPHP",
            "code": "<?php\nrequire '/wordpress/wp-load.php';\n\n$posts = get_posts(array(\n    'numberposts' => -1,\n    'post_type' => array('post', 'page'),\n    'post_status' => 'any'\n));\n\nforeach ($posts as $post) {\n    wp_delete_post($post->ID, true);\n}"
        },
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/WordPress/theme-test-data/master/themeunittestdata.wordpress.xml"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/#eyIkc2NoZW1hIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLndvcmRwcmVzcy5uZXQvYmx1ZXByaW50LXNjaGVtYS5qc29uIiwibG9naW4iOnRydWUsInNpdGVPcHRpb25zIjp7ImJsb2duYW1lIjoiTXkgZmlyc3QgQmx1ZXByaW50In0sInBsdWdpbnMiOlsiaGVsbG8tZG9sbHkiLCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYWRhbXppZWwvYmx1ZXByaW50cy90cnVuay9kb2NzL2Fzc2V0cy9oZWxsby1mcm9tLXRoZS1kYXNoYm9hcmQuemlwIl0sInN0ZXBzIjpbeyJzdGVwIjoiaW5zdGFsbFRoZW1lIiwidGhlbWVaaXBGaWxlIjp7InJlc291cmNlIjoid29yZHByZXNzLm9yZy90aGVtZXMiLCJzbHVnIjoiYWR2ZW50dXJlciJ9fSx7InN0ZXAiOiJydW5QSFAiLCJjb2RlIjoiPD9waHBcbnJlcXVpcmUgJy93b3JkcHJlc3Mvd3AtbG9hZC5waHAnO1xuXG4kcG9zdHMgPSBnZXRfcG9zdHMoYXJyYXkoXG4gICAgJ251bWJlcnBvc3RzJyA9PiAtMSxcbiAgICAncG9zdF90eXBlJyA9PiBhcnJheSgncG9zdCcsICdwYWdlJyksXG4gICAgJ3Bvc3Rfc3RhdHVzJyA9PiAnYW55J1xuKSk7XG5cbmZvcmVhY2ggKCRwb3N0cyBhcyAkcG9zdCkge1xuICAgIHdwX2RlbGV0ZV9wb3N0KCRwb3N0LT5JRCwgdHJ1ZSk7XG59In0seyJzdGVwIjoiaW1wb3J0V3hyIiwiZmlsZSI6eyJyZXNvdXJjZSI6InVybCIsInVybCI6Imh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9Xb3JkUHJlc3MvdGhlbWUtdGVzdC1kYXRhL21hc3Rlci90aGVtZXVuaXR0ZXN0ZGF0YS53b3JkcHJlc3MueG1sIn19XX0=)
And that’s it. Congratulations on creating your first Blueprint! 🥳

# How to load and run Blueprints

## Playground Dock

The easiest way to run a Blueprint from the website is through the Dock:

- Open **New → Blueprint gallery** to choose an example.
- Open **New → Blueprint URL** to run a public Blueprint JSON or ZIP bundle URL.
- Open **New → Write a Blueprint** to write a Blueprint in the browser.
- Open **Blueprint** to inspect or edit the current Playground’s Blueprint.

See [Using Blueprints](https://developer.wordpress.org/playground/blueprints/using-blueprints) for the full list of UI paths.
![The New Playground pane with the Blueprint gallery selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground.webp?fit=1440%2C900&ssl=1)

## URL fragment

For links and automation, you can paste a Blueprint into the URL “fragment” of a WordPress Playground website. Just add a `#` after the `.net/`.
Let’s say you want to create a Playground with specific versions of WordPress and PHP using the following Blueprint:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "preferredVersions": {
        "php": "8.3",
        "wp": "5.9"
    }
}
```

To run it, go to `https://playground.wordpress.net/#{"preferredVersions": {"php":"8.3", "wp":"5.9"}}`. You can also use the button below:
[Run Blueprint](https://playground.wordpress.net/#{"preferredVersions":{"php":"8.3","wp":"5.9"}})
Use this method to run the example code in the next chapter, [**Build your first Blueprint**](https://developer.wordpress.org/playground/blueprints/tutorial/build-your-first).

### Encoded Blueprint fragments

When you build a Playground link from JavaScript or an automation tool, encode the Blueprint JSON once with `encodeURIComponent(JSON.stringify(blueprint))` and append it after `#`.
Playground also supports [Base64-encoded Blueprints](https://www.base64encode.org), which are useful when a platform modifies JSON fragments or when you want a compact, copyable link. For example, that’s the above Blueprint in Base64 format: `eyJwcmVmZXJyZWRWZXJzaW9ucyI6IHsicGhwIjoiNy40IiwgIndwIjoiNS45In19`.
To run it, go to <https://playground.wordpress.net/#eyJwcmVmZXJyZWRWZXJzaW9ucyI6IHsicGhwIjoiNy40IiwgIndwIjoiNS45In19>

### Load Blueprint from a URL

When your Blueprint gets too wieldy, you can load it via the `?blueprint-url` query parameter in the URL, like this:
<https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/latest-gutenberg/blueprint.json>
Note that the Blueprint must be publicly accessible and served with [the correct `Access-Control-Allow-Origin` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin):

```
Access-Control-Allow-Origin: *
```

# What are Blueprints, and what can you do with them?

With WordPress Playground you can create a whole website, including plugins, themes, content (posts, pages, taxonomy, and comments), settings (site name, users, permalinks, and more), etc. They allow you to generate a WooCommerce store complete with products, a magazine populated with articles, a corporate blog with multiple users, and more.

Blueprints are `JSON` files that you can use to configure Playground instances.

Blueprints support advanced use cases, like file system and database manipulation, and give you fine-grained control over the instance you create. The WordPress Test Team has been using Playground in [the 6.5 beta release cycle](https://wordpress.org/news/2024/03/wordpress-6-5-release-candidate-2/), creating a Blueprint that loads the latest version, several testing plugins, and dummy data.

## A simple example

A Blueprint might look something like this:

```
{
    "plugins": ["akismet", "gutenberg"],
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "twentynineteen"
            }
        }
    ],
    "siteOptions": {
        "blogname": "My Blog",
        "blogdescription": "Just another WordPress site"
    },
    "constants": {
        "WP_DEBUG": true
    }
}
```

The Blueprint above installs the *Akismet* and *Gutenberg* plugins and the *Twenty Nineteen* theme, sets the site name and description, and enables the WordPress debugging mode.

## The benefits of Blueprints

Blueprints are an invaluable tool for building WordPress sites via Playground

- **Flexibility**: developers can make granular adjustments to the build process.
- **Consistency**: ensure that every new site starts with the same configuration.
- **Lightweight**: small text files that are easy to store and transfer.
- **Transparency**: A Blueprint includes all the commands needed to build a snapshot of a WordPress site. You can read through it and understand how the site is built.
- **Productivity**: reduces the time-consuming process of manually setting up a new WordPress site. Instead of installing and configuring themes and plugins for each new project, apply a Blueprint and set everything in one process.
- **Up-to-date dependencies**: fetch the latest version of WordPress, a particular plugin, or a theme. Your snapshot is always up to date with the latest features and security fixes.
- **Collaboration**: the `JSON` files are easy to review in tools like GitHub. Share Blueprints with your team or the WordPress community. Allowing others to use your well-configured setup.
- **Experimentation and Learning**: For those new to WordPress or looking to experiment with different configurations, Blueprints provide a safe and easy way to try new setups without “breaking” a live site.
- **WordPress.org integration**: offer a [demo of your plugin](https://developer.wordpress.org/plugins/wordpress-org/previews-and-blueprints/) in the WordPress plugin directory, or a preview in a [Theme Trac ticket](https://meta.trac.wordpress.org/ticket/7382).
- **Spinning a development environment**: A new developer in the team could download the Blueprint, run a hypothetical `wp up` command, and get a fresh developer environments—loaded with everything they need. The entire CI/CD process can reuse the same Blueprint.

**More Resources**

Visit these links to learn more about the (endless) possibilities of Blueprints:

- [Introduction to WordPress Playground](https://developer.wordpress.org/news/2024/04/05/introduction-to-playground-running-wordpress-in-the-browser/)
- Embed a pre-configured WordPress site in your website using the [WordPress Playground Block](https://wordpress.org/plugins/interactive-code-block/).
- [Blueprints examples](https://developer.wordpress.org/playground/blueprints/examples)
- [Demos and apps built with Blueprints](https://developer.wordpress.org/playground/links-and-resources#apps-built-with-wordpress-playground)

# APIs overview

## WordPress Playground APIs overview

WordPress Playground exposes a few APIs that you can use to interact with the Playground:

### Query API

Basic operations can be done by adjusting the URL, for example here’s how you can preinstall a coblocks plugin:

<https://playground.wordpress.net/?plugin=coblocks>

Or a theme:

<https://playground.wordpress.net/?theme=pendant>

This is called [Query API](https://developer.wordpress.org/playground/developers/apis/query-api/) and you can learn more about it [here](https://developer.wordpress.org/playground/developers/apis/query-api/). Once you have a URL that you like, you can embed it in your website using an iframe:

```
<iframe style="width: 800px; height: 500px;" src="https://playground.wordpress.net/?plugin=coblocks"></iframe>
```

Check the [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) section for more info.

### Blueprints

If you need more control over your Playground, you can use JSON Blueprints. For example, here’s how to create a post and install a plugin:

```
{
    "steps": [
        {
            "step": "login"
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "friends"
            }
        },
        {
            "step": "runPHP",
            "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1)); ?>"
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJzdGVwcyI6W3sic3RlcCI6ImxvZ2luIn0seyJzdGVwIjoiaW5zdGFsbFBsdWdpbiIsInBsdWdpbkRhdGEiOnsicmVzb3VyY2UiOiJ3b3JkcHJlc3Mub3JnL3BsdWdpbnMiLCJzbHVnIjoiZnJpZW5kcyJ9fSx7InN0ZXAiOiJydW5QSFAiLCJjb2RlIjoiPD9waHAgcmVxdWlyZV9vbmNlICcvd29yZHByZXNzL3dwLWxvYWQucGhwJzsgd3BfaW5zZXJ0X3Bvc3QoYXJyYXkoJ3Bvc3RfdGl0bGUnID0+ICdQb3N0IHRpdGxlJywgJ3Bvc3RfY29udGVudCcgPT4gJ1Bvc3QgY29udGVudCcsICdwb3N0X3N0YXR1cycgPT4gJ3B1Ymxpc2gnLCAncG9zdF9hdXRob3InID0+IDEpKTsgPz4ifV19)

Blueprints play a significant role in WordPress Playground, so they have their own dedicated documentation hub. Learn more about JSON Blueprints at the [Blueprints Docs Hub](https://developer.wordpress.org/playground/blueprints).

### Sites API

The Playground website at [playground.wordpress.net](https://playground.wordpress.net) exposes a `window.playgroundSites` object for managing the sites shown in the Dock’s **Playgrounds** pane — listing them, creating new ones, persisting temporary sites to OPFS or local disk, switching between them, and changing PHP version or networking.

```
await playgroundSites.createNewTemporarySite('demo', { phpVersion: '8.4' });
await playgroundSites.saveInBrowser('My demo site');
const client = playgroundSites.getClient();
```

Check the [Sites API](https://developer.wordpress.org/playground/developers/apis/sites-api) section for more info.

### JavaScript API

The `@wp-playground/client` package provides a JavaScript API you can use to fully control your Playground instance. Here’s a simple example of what you can do:

```
<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
<script type="module">
    // Use unpkg for convenience

    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp'),
        remoteUrl: `https://playground.wordpress.net/remote.html`,
    });
    // Let's wait until Playground is fully loaded
    await client.isReady();
</script>
```

Check the [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/) section for more info.

## Playground APIs Concepts

WordPress Playground in the browser is all about links and iframes. Regardless of which API you choose, you will use it in one of the following ways:

### Link to the Playground site

You can customize WordPress Playground by modifying the https://playground.wordpress.net/ link. You can, for example, create a post, request a specific plugin, or run any PHP code.

To prepare such a link, use either the [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) (easy) or the [JSON Blueprints API](https://developer.wordpress.org/playground/blueprints) (medium).

Once it’s ready, simply post it on your site. It makes a great “Try it yourself” button in a tutorial, for example.

#### Embed in an `<iframe>`

WordPress Playground can be embedded in your app using an `<iframe>`:

```
<iframe src="https://playground.wordpress.net/"></iframe>
```

To customize that Playground instance, you can:

- Load it from special link prepared using the [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) (easy) or the [JSON Blueprints API](https://developer.wordpress.org/playground/blueprints) (medium).
- Control it using the [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/).

The JavaScript API gives you the most control, but it is also the least convenient option as it requires loading the Playground Client library.

**Careful with the demo site**

The site at <https://playground.wordpress.net> is there to support the community, but there are no guarantees it will continue to work if the traffic grows significantly.

If you need certain availability, you should [host your own WordPress Playground](https://developer.wordpress.org/playground/developers/architecture/host-your-own-playground).

### Browser APIs

The following Playground APIs are available in the browser:

- [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) enable basic operations using only query parameters
- [Blueprints API](https://developer.wordpress.org/playground/blueprints) give you a great degree of control with a simple JSON file
- [Sites API](https://developer.wordpress.org/playground/developers/apis/sites-api) lets scripts running on playground.wordpress.net manage saved sites
- [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api) give you full control via a JavaScript client from an npm package

### In Node.js

The following Playground APIs are available in Node.js:

- [JSON Blueprints API](https://developer.wordpress.org/playground/blueprints)
- [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/)

These APIs are very similar to their web counterparts, but, unsurprisingly, they are not based or links or iframes.

# Architecture overview

WordPress Playground consists of the following high-level components:

- [WordPress](https://developer.wordpress.org/playground/developers/architecture/wordpress)
- [WebAssembly PHP](https://developer.wordpress.org/playground/developers/architecture/wasm-php-overview)
- [Browser bindings](https://developer.wordpress.org/playground/developers/architecture/browser-concepts)
- [PHP.wasm packages](https://developer.wordpress.org/playground/developers/architecture/php-wasm-packages) for the shared
  API, platform bindings, and version-specific binaries
- [Public API](https://developer.wordpress.org/playground/developers/apis/)

Visit each section to learn more about the specific parts of the architecture.

## Tooling

### NX: building packages and projects

WordPress Playground uses [NX](https://nx.dev/), a build system designed for monorepos.
The dependencies between Playground packages and projects [are too complex](https://github.com/WordPress/wordpress-playground/pull/151) for a bundler like Webpack, and NX handles this complexity much better:
![Dependency graph](https://i0.wp.com/developer.wordpress.org/files/2026/07/dependencies-scaled.webp?fit=3200%2C742&ssl=1)
To learn more, head over to the [NX developer docs](https://nx.dev/getting-started/intro).

### Lerna: publishing packages and projects

WordPress Playground includes several NPM packages, a VS Code extension, WordPress plugins, a web app, and other GitHub releases, all managed across two monorepos: the main [wordpress-playground](https://github.com/WordPress/wordpress-playground) and [Playground Tools](https://github.com/WordPress/playground-tools/).
We use [Lerna](https://lerna.js.org) to build, manage, and publish all JavaScript/TypeScript packages. Lerna handles everything simultaneously: it increments the version number, sets a new tag, and publishes the modified packages to `npm`.
The published packages share the same version number, so when updating a single package, Lerna bumps the version number of all dependent packages.

# Quick Start Guide for Developers

WordPress Playground was created as a programmable tool. Below you’ll find a few examples of what you can do with it. Each discussed API is described in detail in the [APIs section](https://developer.wordpress.org/playground/developers/apis/):

## Embed WordPress on your website

Playground can be embedded on your website using the HTML `<iframe>` tag as follows:

```
<iframe src="https://playground.wordpress.net/"></iframe>
```

Every visitor will get their own private WordPress instance for free. You can then customize it using one of the [Playground APIs](https://developer.wordpress.org/playground/developers/apis/).

**Careful with the demo site**

The site at <https://playground.wordpress.net> is there to support the community, but there are no guarantees it will continue to work if the traffic grows significantly.

If you need certain availability, you should [host your own WordPress Playground](https://developer.wordpress.org/playground/developers/architecture/host-your-own-playground).

## Control the embedded website

WordPress Playground provides three APIs you can use to control the iframed website. All the examples in this section are built using one of these:

- [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) enable basic operations using only query parameters
- [Blueprints API](https://developer.wordpress.org/playground/blueprints) give you a great degree of control with a simple JSON file
- [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api) give you full control via a JavaScript client from an npm package

Learn more about each of these APIs in the [APIs overview section](https://developer.wordpress.org/playground/developers/apis/).

## Showcase a plugin or theme from WordPress directory

You can install plugins and themes from the WordPress directory with only URL parameters. This iframe preinstalls the `coblocks` and `friends` plugins and the `pendant` theme.  
This is called [Query API](https://developer.wordpress.org/playground/developers/apis/query-api/) and you can learn more about it [here](https://developer.wordpress.org/playground/developers/apis/query-api/).

```
<iframe src="https://playground.wordpress.net/?plugin=coblocks"></iframe>
```

## Showcase any plugin or theme

What if your plugin is not in the WordPress directory?

You can still showcase it on Playground by using [JSON Blueprints](https://developer.wordpress.org/playground/blueprints). For example, this Blueprint would download and install a plugin and a theme from your website and also import some starter content:

```
{
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "url",
                "url": "https://your-site.com/your-plugin.zip"
            }
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "url",
                "url": "https://your-site.com/your-theme.zip"
            }
        },
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://your-site.com/starter-content.wxr"
            }
        }
    ]
}
```

See [getting started with Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started) to learn more.

## Preview pull requests from your repository

You can preview repository code two ways: directly with `git:directory`, or by pointing to a `.zip` from your CI pipeline. Here’s the `git:directory` approach using [Blueprints](https://developer.wordpress.org/playground/blueprints):

```
{
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "git:directory",
                "url": "https://github.com/my-user/my-repo",
                "ref": "refs/pull/1/head",
                "refType": "refname"
            },
            "options": {
                "activate": true
            },
            "progress": {
                "caption": "Installing plugin from my-user/my-repo PR #1"
            }
        }
    ]
}
```

In the code above, it will install a plugin from a repository located at the `url`, and the reference to find the branch is `refType`; in this case, it will use `refname`, but it can also use `branch`, `tag`, and `commit`.

You can automate this process using the [GitHub Action to generate preview links](https://developer.wordpress.org/playground/guides/github-action-pr-preview), which will help streamline the process.

Loading a `.zip` file is another alternative for previewing your project. See the [live example of Gutenberg PR previewer](https://playground.wordpress.net/gutenberg.html).

To use Playground as a PR previewer, you need:

- A CI pipeline that bundles your plugin or theme
- Public access to the generated `.zip` file

Those zip bundles aren’t any different from regular WordPress Plugins, which means you can install them in Playground using the [JSON Blueprints](https://developer.wordpress.org/playground/blueprints) API. Once you expose an endpoint like https://your-site.com/pull-request-1234.zip, the following Blueprint will do the rest:

```
{
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "url",
                "url": "https://your-site.com/pull-request-1234.zip"
            }
        }
    ]
}
```

The official Playground demo uses this technique to preview pull requests from the Gutenberg repository:

```
{
    "landingPage": "/wp-admin/plugins.php?test=42test",
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        },
        {
            "step": "mkdir",
            "path": "/wordpress/pr"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/pr/pr.zip",
            "data": {
                "resource": "url",
                "url": "/plugin-proxy.php?org=WordPress&repo=gutenberg&workflow=Build%20Gutenberg%20Plugin%20Zip&artifact=gutenberg-plugin&pr=60819",
                "caption": "Downloading Gutenberg PR 47739"
            },
            "progress": {
                "weight": 2,
                "caption": "Applying Gutenberg PR 47739"
            }
        },
        {
            "step": "unzip",
            "zipPath": "/wordpress/pr/pr.zip",
            "extractToPath": "/wordpress/pr"
        },
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "vfs",
                "path": "/wordpress/pr/gutenberg.zip"
            }
        }
    ]
}
```

[Try it out!](https://playground.wordpress.net/?mode=seamless#eyJsYW5kaW5nUGFnZSI6Ii93cC1hZG1pbi9wbHVnaW5zLnBocD90ZXN0PTQydGVzdCIsInN0ZXBzIjpbeyJzdGVwIjoibG9naW4iLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9LHsic3RlcCI6Im1rZGlyIiwicGF0aCI6Ii93b3JkcHJlc3MvcHIifSx7InN0ZXAiOiJ3cml0ZUZpbGUiLCJwYXRoIjoiL3dvcmRwcmVzcy9wci9wci56aXAiLCJkYXRhIjp7InJlc291cmNlIjoidXJsIiwidXJsIjoiL3BsdWdpbi1wcm94eS5waHA/b3JnPVdvcmRQcmVzcyZyZXBvPWd1dGVuYmVyZyZ3b3JrZmxvdz1CdWlsZCUyMEd1dGVuYmVyZyUyMFBsdWdpbiUyMFppcCZhcnRpZmFjdD1ndXRlbmJlcmctcGx1Z2luJnByPTYwODE5IiwiY2FwdGlvbiI6IkRvd25sb2FkaW5nIEd1dGVuYmVyZyBQUiA0NzczOSJ9LCJwcm9ncmVzcyI6eyJ3ZWlnaHQiOjIsImNhcHRpb24iOiJBcHBseWluZyBHdXRlbmJlcmcgUFIgNDc3MzkifX0seyJzdGVwIjoidW56aXAiLCJ6aXBQYXRoIjoiL3dvcmRwcmVzcy9wci9wci56aXAiLCJleHRyYWN0VG9QYXRoIjoiL3dvcmRwcmVzcy9wciJ9LHsic3RlcCI6Imluc3RhbGxQbHVnaW4iLCJwbHVnaW5EYXRhIjp7InJlc291cmNlIjoidmZzIiwicGF0aCI6Ii93b3JkcHJlc3MvcHIvZ3V0ZW5iZXJnLnppcCJ9fV19)

### Preview WordPress Core and Gutenberg Branches or PRs

You can preview specific pull requests from WordPress Core and Gutenberg repositories using Query API parameters. Gutenberg branches also have an alternative to preview them with the parameter `gutenberg-branch`. This is useful for testing the latest trunk changes or specific feature branches without creating a PR.

- Preview a specific WordPress Core PR: `https://playground.wordpress.net/?core-pr=9500`
- Preview a specific Gutenberg PR: `https://playground.wordpress.net/?gutenberg-pr=73010`
- Preview the Gutenberg trunk branch: `https://playground.wordpress.net/?gutenberg-branch=trunk`

## Build a compatibility testing environment

Test your plugin across PHP and WordPress versions by configuring them in Playground. This helps you verify compatibility before release.

With the Query API, you’d simply add the `php` and `wp` query parameters to the URL:

```
<iframe src="https://playground.wordpress.net/?php=8.3&wp=6.1"></iframe>
```

With JSON Blueprints, you’d use the `preferredVersions` property:

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "6.1"
    }
}
```

## Run PHP code in the browser

The JavaScript API provides the `run()` method which you can use to run PHP code in the browser:

```
<iframe id="wp"></iframe>
<script type="module">
    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp'),
        remoteUrl: 'https://playground.wordpress.net/remote.html',
    });
    await client.isReady;
    await client.run({
        code: `<?php
        require("/wordpress/wp-load.php");

        update_option("blogname", "Playground is really cool!");
        echo "Site title updated!";
        `,
    });
    client.goTo('/');
</script>
```

Combine that with a code editor like Monaco or CodeMirror, and you’ll get live code snippets like in [this article](https://adamadam.blog/2023/02/16/how-to-modify-html-in-a-php-wordpress-plugin-using-the-new-tag-processor-api/)!

# Limitations

WordPress Playground is under active development and has some limitations you should keep in mind when running it and developing with it.
You can track the status of these issues on the [Playground Project board](https://github.com/orgs/WordPress/projects/180).

## In the browser

### Browser storage and recovery

Playground runs WordPress in the browser. New Playgrounds are autosaved when
browser storage and saving are available, and they appear in **Your
Playgrounds**. Playground keeps up to five recent autosaves. After five exist,
creating another deletes the oldest one. Autosaves are recovery points, not
long-term backups. Store an autosave permanently or export a ZIP when you want
to keep it.
Use these storage modes deliberately:

- **Autosaved**: stored in browser storage and retained only while it is one of up to five recent autosaves.
- **Saved**: stored permanently in browser storage or saved to a local directory.
- **Temporary**: created with `?storage=temp` or when saving is unavailable. It is discarded when the tab closes or the browser page refreshes.

The Playground **Refresh page** button reloads the WordPress page inside the current Playground. Browser refresh (Cmd+R or F5) reloads the whole Playground app. A stored or autosaved Playground can recover after that reload, but a temporary Playground cannot.
![The Dock controls for refreshing WordPress, opening storage choices, and exporting the Playground](https://i0.wp.com/developer.wordpress.org/files/2026/07/persistence-controls.webp?fit=1440%2C700&ssl=1)
Browser storage still belongs to the browser. Storage pressure, private browsing, profile changes, or clearing site data can remove it. Export a ZIP when you need a portable backup.
![The Your Playgrounds pane with the current Playground](https://i0.wp.com/developer.wordpress.org/files/2026/07/your-playgrounds.webp?fit=1440%2C900&ssl=1)

### Browser support

WordPress Playground is designed to work across all major desktop and mobile browsers. This includes:

- **Desktop browsers**: Chrome, Firefox, Safari, Edge, and other Chromium-based browsers
- **Mobile browsers**: Safari (iOS), Chrome (Android), and other mobile browser variants

Playground leverages modern web technologies and should function consistently across these browser environments. However, some advanced features may have varying levels of support depending on the specific browser and its version.

### Performance expectations

Loading times vary based on what Playground needs to set up:

| Scenario | Typical Load Time |
| --- | --- |
| Fresh WordPress (no plugins) | 5-10 seconds |
| With small plugins | 10-20 seconds |
| With large plugins (e.g., WooCommerce) | 30-60 seconds |
| On mobile devices | 1.5-2x slower than desktop |

![Playground performance graph](https://i0.wp.com/developer.wordpress.org/files/2026/07/playground-performance-graph.webp?fit=1024%2C314&ssl=1)
**Factors that affect performance:**

- **Plugin size**: Large plugins take longer to install at runtime
- **Network speed**: WASM files are 15-30MB
- **Device memory**: Low-memory devices may experience slowdowns
- **Browser**: Chrome/Edge perform best; Safari slightly slower

> **Note:** Opera Mini support is not currently confirmed.

## When developing with Playground

### Iframe quirks

Playground renders WordPress in an [`iframe`](https://developer.wordpress.org/playground/developers/architecture/browser-iframe-rendering) so clicking links with `target="_top"` will reload the page you’re working on.
Also, JavaScript popups originating in the `iframe` may not always display.

### Run WordPress PHP functions

Playground supports running PHP code in Blueprints using the [`runPHP` step](https://developer.wordpress.org/playground/blueprints/steps#RunPHPStep). To run WordPress-specific PHP functions, you’d need to first require [wp-load.php](https://github.com/WordPress/WordPress/blob/master/wp-load.php):

```
{
    "step": "runPHP",
    "code": "<?php require_once('wordpress/wp-load.php'); OTHER_CODE ?>"
}
```

### Using WP-CLI

You can execute `wp-cli` commands via the Blueprints [`wp-cli`](https://developer.wordpress.org/playground/blueprints/steps#WPCLIStep) step. However, since Playground runs in the browser, it doesn’t support the [full array](https://developer.wordpress.org/cli/commands/) of available commands. While there is no definite list of supported commands, experimenting in [the online demo](https://playground.wordpress.net/demos/wp-cli.html) will help you assess what’s possible.

# Local Development

## Local WordPress Development with Playground

Playground offers various development environments to streamline setting up and managing WordPress sites.

For a quick start, use a [public Playground web instance](https://developer.wordpress.org/playground/web-instance) at https://playground.wordpress.net/. Alternatively, you can [host your own WordPress Playground](https://developer.wordpress.org/playground/developers/architecture/host-your-own-playground).

Playground also provides tools for local WordPress development, prioritizing ease of installation and usability:

- **[@wp-playground/cli](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli):** A command-line tool to quickly launch a WordPress site from your terminal.
- **[Visual Studio Code Extension](https://developer.wordpress.org/playground/developers/local-development/vscode-extension):** Integrates WordPress development directly into VS Code for a seamless experience within the popular editor.

For those needing more control, Playground offers tools for Node.js:

- **[WordPress Playground in Node.js](https://developer.wordpress.org/playground/developers/local-development/php-wasm-node):** For low-level control over the WebAssembly PHP build, explore the [`@php-wasm/node` package](https://npmjs.org/@php-wasm/node), which includes the PHP WebAssembly runtime.

# Debugging with Xdebug in WordPress Playground

Xdebug is a debugging extension for PHP that lets you set breakpoints, inspect variables, and step through your code. WordPress Playground includes Xdebug in its WebAssembly-compiled PHP, so you can debug WordPress code running directly in your browser or IDE.

## Why Xdebug matters for PHP WASM

Debugging PHP code in WebAssembly is different from debugging traditional PHP. Without Xdebug, you’re limited to `var_dump()` and `error_log()` statements. Xdebug gives you a proper debugger with breakpoints, variable inspection, and call stack navigation—the same tools you’d use when debugging PHP on a regular server.

## XDebug on WordPress Playground

For a quick start, check the [getting started with Xdebug guide](https://developer.wordpress.org/playground/developers/xdebug/xdebug-getting-started)

You’ll learn to debug:

- Form processing logic
- Input validation
- WordPress hooks and filters

## Two debugging approaches

WordPress Playground supports two ways to debug with Xdebug:

**Chrome DevTools**: Debug directly in your browser without any IDE setup. Great for quick debugging sessions or when you want to see everything in one place.

**IDE integration**: Use VSCode or PhpStorm with full IDE features, including code navigation, project-wide search, and advanced breakpoint conditions. Better for complex debugging scenarios.

## What you’ll need

- Node.js installed
- Chrome or Chromium browser (for DevTools debugging)
- Visual Studio Code or PhpStorm (for IDE debugging, optional)
- Basic familiarity with WordPress plugin development

**Next**: [Getting Started with Xdebug →](https://developer.wordpress.org/playground/developers/xdebug/xdebug-getting-started)

# JavaScript API

WordPress Playground comes with a JavaScript API client that grants you full control over your WordPress.

**API here doesn’t mean “REST API”**

WordPress Playground is a browser-based application.  
The term API here refers to a set of functions you can  
call inside JavaScript. This is **not** a network-based REST API.

## Quick start

To use the JavaScript API, you’ll need:

- An `<iframe>` element
- The `@wp-playground/client` package (from npm or a CDN)

Here’s the shortest example of how to use the JavaScript API in a HTML page:

```
<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
<script type="module">
    // Use unpkg for convenience

    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp'),
        remoteUrl: `https://playground.wordpress.net/remote.html`,
    });
    // Let's wait until Playground is fully loaded
    await client.isReady();
</script>
```

**/remote.html is a special URL**

`/remote.html` is a special URL that loads the Playground  
API endpoint instead of the demo app with the browser UI. Read more about the difference between `/` and `/remote.html` and [on this page](https://developer.wordpress.org/playground/developers/apis/javascript-api/-html-vs-remote-html).

## Controlling the website

Now that you have a `client` object, you can use it to control the website inside the iframe. There are three ways to do that:

- [Playground API Client](https://developer.wordpress.org/playground/developers/apis/javascript-api/playground-api-client)
- [Blueprint JSON](https://developer.wordpress.org/playground/developers/apis/javascript-api/blueprint-json)
- [Blueprint functions](https://developer.wordpress.org/playground/developers/apis/javascript-api/blueprint-functions)

## Debugging and testing

For quick testing and debugging, the JavaScript API client is exposed as `window.playground` by both `index.html` and `remote.html`.

```
> await playground.listFiles("/")
(6) ['tmp', 'home', 'dev', 'proc', 'internal', 'wordpress']
```

Note that in `index.html`, `playground` is a Proxy object and you won’t get any autocompletion from the browser. In `remote.html`,  
however, `playground` is a class instance and you will benefit from browser’s autocompletion.

# Query API

WordPress Playground exposes a simple API that you can use to configure the Playground in the browser.

It works by passing configuration options as query parameters to the Playground URL. For example, to install the pendant theme, you would use the following URL:

```
https://playground.wordpress.net/?theme=pendant
```

You can go ahead and try it out. The Playground will automatically install the theme and log you in as an admin. You may even embed this URL in your website using an `<iframe>` tag:

```
<iframe src="https://playground.wordpress.net/?theme=pendant"></iframe>
```

## Available options

| Option | Default Value | Description |
| --- | --- | --- |
| `php` | `8.5` | Loads the specified PHP version. Accepts `7.4`, `8.0`, `8.1`, `8.2`, `8.3`, `8.4`, `8.5`, `latest`, or `next`. Use `next` to preview the next PHP version from the php-src development branch; it is currently supported by the web runtime only. |
| `php-extension` |  | Loads a custom PHP.wasm extension manifest before PHP starts. Accepts absolute, root-relative, or page-relative HTTP(S) URLs. Can be used multiple times. |
| `wp` | `latest` | Loads the specified WordPress version. Accepts the last three major WordPress versions. As of June 1, 2024, that’s `6.3`, `6.4`, or `6.5`. You can also use the generic values `latest`, `nightly`, or `beta`. |
| `blueprint-url` |  | The URL of the Blueprint that will be used to configure this Playground instance. |
| `networking` | `yes` | Enables or disables the networking support for Playground. Accepts `yes` or `no`. |
| `plugin` |  | Installs the specified plugin. Use the plugin name from the WordPress Plugins Directory URL. For example, if the URL is `` https://wordpress.org/plugins/wp-lazy-loading/`, the plugin name would be ``wp-lazy-loading`. You can pre-install multiple plugins by saying`plugin=coblocks&plugin=wp-lazy-loading&…`. Installing a plugin automatically logs the user in as an admin. More than one plugin could be installed, just repeating the`plugin` attribute on the URL. |
| `theme` |  | Installs the specified theme. Use the theme name from the WordPress Themes Directory URL. For example, if the URL is `` https://wordpress.org/themes/disco/`, the theme name would be ``disco`. Installing a theme automatically logs the user in as an admin. Multiples themes could be installed just repeating the`theme` attribute on the URL. |
| `url` | `/wp-admin/` | Load the specified initial WordPress page in this Playground instance. |
| `mode` | `browser-full-screen` | Determines how the WordPress instance is displayed. Either wrapped in a browser UI or full width as a seamless experience. Accepts `browser-full-screen`, or `seamless`. |
| `lazy` |  | Defer loading the Playground assets until someone clicks on the “Run” button. Does not accept any values. If `lazy` is added as a URL parameter, loading will be deferred. |
| `login` | `yes` | Log the user in as an admin. Accepts `yes` or `no`. |
| `multisite` | `no` | Enables the WordPress multisite mode. Accepts `yes` or `no`. |
| `import-site` |  | Imports site files and database from a ZIP file specified by a URL. |
| `import-wxr` |  | Imports site content from a WXR file specified by a URL. It uses the WordPress Importer plugin, so the default admin user must be logged in. |
| `site-slug` |  | Selects which site to load from browser storage. If the specified site does not exist, Playground creates a new browser-saved site with the specified slug unless temporary storage is requested. |
| `storage` |  | Controls the storage lifecycle. Use `storage=temp` to create a genuinely temporary Playground that is discarded when the page is refreshed or closed. Without it, Playground may autosave the new Playground when browser storage and saving are available. |
| `language` | `en_US` | Sets the locale for the WordPress instance. This must be used in combination with `networking=yes` otherwise WordPress won’t be able to download translations. |
| `core-pr` |  | Installs a specific https://github.com/WordPress/wordpress-develop core PR. Accepts the PR number. For example, `core-pr=6883`. |
| `gutenberg-pr` |  | Installs a specific https://github.com/WordPress/gutenberg PR. Accepts the PR number. For example, `gutenberg-pr=65337`. |
| `gutenberg-branch` |  | Installs a specific branch from https://github.com/WordPress/gutenberg. Accepts the branch name. For example, `gutenberg-branch=trunk`. |
| `page-title` |  | Customizes the browser tab title. Useful for identifying different Playground instances when working with multiple tabs. The parameter is preserved when navigating between sites. |
| `can-save` |  | Controls whether the Playground shell offers automatic browser saving. Use `can-save=no` to start a temporary Playground and hide the Dock save status/persistence prompt. |
| `mcp-port` | `7999` | Sets the WebSocket port used by the MCP bridge to communicate with the MCP server. For example, `mcp-port=8080`. |
| `overlay` |  | Opens a Playground tool on page load. Supports `new` for the Dock’s **New** pane. For example, `?overlay=new`. `blueprints` is kept as a compatibility alias that opens the same **New** pane, so `?overlay=blueprints` still works. The parameter is removed from the URL when the pane is closed. |

For example, the following code embeds a Playground with a preinstalled Gutenberg plugin and opens the post editor:

```
<iframe src="https://playground.wordpress.net/?plugin=gutenberg&url=/wp-admin/post-new.php&mode=seamless"> </iframe>
```

## Loading PHP.wasm extensions

Use `php-extension` to load an external PHP.wasm extension manifest before PHP  
starts. This is useful for demos that need native extension performance, such  
as the SQLite Database Integration plugin’s `wp_mysql_parser` extension. You do  
not need custom JavaScript for this — the Query API turns each parameter into a  
runtime extension request before Playground boots.

```
https://playground.wordpress.net/?php=8.5&php-extension=https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json
```

Use `/latest/manifest.json` for a quick demo. Use a pinned manifest from the  
extension release page when you need a stable URL for tests or documentation.

You can repeat `php-extension` to load multiple manifests:

```
https://playground.wordpress.net/?php-extension=https://example.com/one/manifest.json&php-extension=https://example.com/two/manifest.json
```

The parameter accepts absolute, root-relative, and page-relative manifest URLs:

```
?php-extension=https://cdn.example.com/wp_mysql_parser/manifest.json
?php-extension=/extensions/wp_mysql_parser/manifest.json
?php-extension=./extensions/wp_mysql_parser/manifest.json
```

Relative values are resolved against the current Playground page URL. The  
manifest URL must use HTTP(S) once resolved. `file:` URLs are rejected in the  
browser. For local CLI workflows, use  
the [`--php-extension` flag](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli#loading-phpwasm-extensions)  
instead.

**CORS policy**

To import files from a URL, such as a site zip package or PHP extension  
manifest and `.so` artifact, they must be served with the  
`Access-Control-Allow-Origin` header set. For reference, see:  
[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers).

## GitHub Export Options

The following additional query parameters may be used to pre-configure the GitHub export form:

- `gh-ensure-auth`: If set to `yes`, Playground will display a modal to ensure the  
  user is authenticated with GitHub before proceeding.
- `ghexport-repo-url`: The URL of the GitHub repository to export to.
- `ghexport-pr-action`: The action to take when exporting (create or update).
- `ghexport-playground-root`: The root directory in the Playground to export from.
- `ghexport-repo-root`: The root directory in the repository to export to.
- `ghexport-content-type`: The content type of the export (plugin, theme, wp-content, custom-paths).
- `ghexport-plugin`: Plugin path. When the content type is `plugin`, pre-select the plugin to export.
- `ghexport-theme`: Theme directory name. When the content type is `theme`, pre-select the theme to export.
- `ghexport-path`: A path relative to `ghexport-playground-root`. Can be provided multiple times. When the  
  content type is `custom-paths`, it pre-populates the list of paths to export.
- `ghexport-commit-message`: The commit message to use when exporting.
- `ghexport-allow-include-zip`: Whether to offer an option to include a zip file in the GitHub  
  export (yes, no). Optional. Defaults to `yes`.

# Sites API

The Sites API is a JavaScript API exposed by [playground.wordpress.net](https://playground.wordpress.net). It manages the WordPress sites shown in the Dock’s **Playgrounds** pane: recent autosaves, permanently stored browser Playgrounds, local-directory Playgrounds, and temporary Playgrounds.

The API is reached through the `window.playgroundSites` global. `window.playgroundSites` is not assigned during the first moments of page load. Wait for it to appear, then call [`isReady()`](#isready) before making API calls that need the active Playground client.

The Sites API ships with the Playground website application, not with the `@wp-playground/client` library. It is exposed on the top-level page (`/`), not on `/remote.html`. If you embed Playground via `startPlaygroundWeb` into your own page, your iframe won’t expose `window.playgroundSites` to the parent — use the [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/) for direct control instead.

## Quick start

Open [playground.wordpress.net](https://playground.wordpress.net), wait for WordPress to load, then run this in DevTools:

```
await playgroundSites.isReady();

playgroundSites.list();
// → [{ slug: 'calm-sunny-park', name: 'Calm Sunny Park', storage: 'opfs', persistence: 'autosave', isActive: true }]

await playgroundSites.keep(undefined, 'My demo site');
// The active autosave is now a permanently saved browser Playground.

const client = playgroundSites.getClient();
await client.listFiles('/wordpress');
```

A fresh visit normally creates an autosaved browser Playground when browser storage and saving are available. Use `?storage=temp` or [`createNewTemporarySite()`](#createnewtemporarysiteslug-settings) when you need a Playground that is discarded on page refresh or tab close.

## Storage and persistence

Every site has a storage backend. Stored sites also have a persistence lifecycle.

| Field | Value | Meaning |
| --- | --- | --- |
| `storage` | `temporary` | In-memory only. It is discarded on browser refresh or tab close. |
| `storage` | `opfs` | Stored in the browser’s Origin Private File System. |
| `storage` | `local-fs` | Stored in a local directory through the File System Access API. |
| `persistence` | `autosave` | A stored recovery point. Playground keeps the five most recent autosaves. |
| `persistence` | `explicit` | A stored Playground kept permanently until the user or API deletes it. |

Temporary Playgrounds do not report `persistence` because there is no stored lifecycle to preserve.

## Creating and switching sites

### `createNewTemporarySite(slug?, settings?)`

Creates a new temporary site and switches to it. Resolves with the new site’s slug once it has booted.

```
createNewTemporarySite(
    slug?: string,
    settings?: {
        phpVersion?: string; // e.g. '8.4'
        wpVersion?: string; // e.g. '6.8', 'latest', 'nightly', 'beta'
        networking?: boolean;
        language?: string; // e.g. 'pl_PL'
        multisite?: boolean;
    }
): Promise<string>;
```

Each setting corresponds to an equivalent [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) parameter of the same name, with `phpVersion` mapped to `php` and `wpVersion` mapped to `wp`. Other Query API options like `plugin`, `theme`, or `blueprint-url` are not accepted here.

### `createNewSavedSite(slug?, settings?, options?)`

Creates a new browser-stored site and switches to it. The site starts as an explicit save unless `options.persistence` is `autosave`.

```
createNewSavedSite(
    slug?: string,
    settings?: {
        phpVersion?: string;
        wpVersion?: string;
        networking?: boolean;
        language?: string;
        multisite?: boolean;
    },
    options?: {
        persistence?: 'autosave' | 'explicit';
        updateUrl?: boolean;
        excludeFromPruning?: string[];
    }
): Promise<string>;
```

Autosaved sites created through this method participate in the same last-five autosave pruning as autosaves created by the UI.

### `setActiveSite(slug, options?)`

Switches to a different site and boots it. Resolves when the site is ready or rejects if it fails to boot. Resolves immediately if the site is already active.

```
setActiveSite(
    siteSlug: string,
    options?: { updateUrl?: boolean }
): Promise<void>;
```

## Reading site state

### `list()`

Returns every known site, including the active one.

```
list(): Array<{
    slug: string;
    name: string;
    storage: 'temporary' | 'opfs' | 'local-fs';
    persistence?: 'autosave' | 'explicit';
    isActive: boolean;
}>;
```

### `getClient()`

Returns the [`PlaygroundClient`](https://developer.wordpress.org/playground/developers/apis/javascript-api/playground-api-client) for the active site, or `undefined` if the site exists but has not finished booting yet. Throws if no site is selected at all, which only happens very early in page load.

```
getClient(): PlaygroundClient | undefined;
```

### `isReady()`

Resolves once the active site is fully booted and its `PlaygroundClient` is ready for API calls. Rejects if the site fails to boot.

For scripts that run before the page has finished loading, wait for `window.playgroundSites` to appear first:

```
await new Promise((resolve) => {
    const id = setInterval(() => {
        if (window.playgroundSites) {
            clearInterval(id);
            resolve();
        }
    }, 50);
});
await window.playgroundSites.isReady();
```

```
isReady(): Promise<void>;
```

## Persisting and keeping sites

These methods are safe to call on already-stored sites unless noted otherwise.

### `autosaveTemporarySite(slug?, options?)`

Persists a temporary Playground into browser storage as an autosave. It returns the site’s slug and storage backend. If the site is already stored, it returns the current storage without copying files.

```
autosaveTemporarySite(
    slug?: string,
    options?: {
        updateUrl?: boolean;
        excludeFromPruning?: string[];
    }
): Promise<{ slug: string; storage: 'opfs' | 'local-fs' }>;
```

### `keep(slug?, name?)`

Marks an autosaved Playground as an explicit saved Playground. This changes lifecycle metadata only. It does not copy files, change storage backends, or reboot the site.

```
keep(slug?: string, name?: string): Promise<void>;
```

### `saveInBrowser(name?)`

Persists the active temporary site to OPFS. When called on an autosaved site, it keeps that site as an explicit saved Playground.

```
saveInBrowser(
    name?: string
): Promise<{ slug: string; storage: 'opfs' | 'local-fs' }>;
```

### `saveToLocalFileSystem(name?, localFsHandle?)`

Persists the active temporary or autosaved site to a local directory. Prompts the user to pick a directory when `localFsHandle` is omitted. Requires the File System Access API, so availability depends on the browser and platform.

```
saveToLocalFileSystem(
    name?: string,
    localFsHandle?: FileSystemDirectoryHandle
): Promise<{ slug: string; storage: 'opfs' | 'local-fs' }>;
```

## Modifying a stored site

These methods throw when called on a temporary site.

### `rename(newName, slug?)`

Renames the active site, or the site identified by `slug`.

```
rename(newName: string, slug?: string): Promise<void>;
```

### `setPhpVersion(version)`

Changes the PHP version of the active stored site and reboots it. To pick a PHP version for a fresh site, pass `phpVersion` to `createNewTemporarySite()` or `createNewSavedSite()` instead.

```
setPhpVersion(version: string): Promise<void>;
```

`version` accepts any value from the [`AllPHPVersion`](https://github.com/WordPress/wordpress-playground/blob/trunk/packages/php-wasm/universal/src/lib/supported-php-versions.ts) union. The supported list evolves as PHP releases come and go — see the linked source for the current set.

### `setNetworking(enabled)`

Enables or disables outbound network access for the active stored site and reboots it.

```
setNetworking(enabled: boolean): Promise<void>;
```

### `updateRuntimeSettings(settings)`

Applies the runtime settings that can change on an existing stored Playground: PHP version and networking. If the settings already match, Playground reloads the current WordPress page.

```
updateRuntimeSettings(settings: {
    phpVersion: string;
    networking: boolean;
}): Promise<void>;
```

WordPress version, language, and multisite are installation settings. Create a fresh Playground when those need to change.

### `delete(slug)`

Deletes a stored site and its persisted data. Throws if the site does not exist or is temporary.

```
delete(siteSlug: string): Promise<void>;
```

## Next steps

- [JavaScript API](https://developer.wordpress.org/playground/developers/apis/javascript-api/playground-api-client) — full reference for the `PlaygroundClient` returned by `getClient()`
- [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) — the URL parameters that mirror new-site settings
- [Blueprints](https://developer.wordpress.org/playground/blueprints) — declarative site setup that runs when a site is created

# Blueprints Functions and the API Client

Every Blueprint step you can declare in the JSON object also provides a handler function that can be used directly.

For example:

```
const client = await startPlaygroundWeb({
    iframe: document.getElementById('wp'),
    remoteUrl: `https://playground.wordpress.net/remote.html`,
});
await client.isReady();

await login(client, {
    username: 'admin',
    password: 'password',
});

await installPlugin(client, {
    // Resources can only be used with JSON Blueprints.
    // If you use functions, you must provide the resolved
    // file.
    pluginData: await fetch(pluginUrl),
});
```

For more information and live examples visit the [Blueprints Steps page](https://developer.wordpress.org/playground/blueprints/steps).

# Blueprints JSON and the API Client

The Playground API client can be initialized with a [JSON Blueprint](https://developer.wordpress.org/playground/blueprints). This is a convenient way of preconfiguring it in any way you like without worrying about progress bars and fetching remote files:

```
const client = await startPlaygroundWeb({
    iframe: document.getElementById('wp'),
    remoteUrl: `https://playground.wordpress.net/remote.html`,
    blueprint: {
        preferredVersions: {
            wp: '6.3',
            php: '8.3',
        },
        steps: [
            { step: 'login' },
            {
                step: 'installPlugin',
                pluginData: {
                    resource: 'wordpress.org/plugins',
                    slug: 'gutenberg',
                },
            },
        ],
    },
});
await client.isReady();
```

Running a JSON Blueprint is only possible during the initialization of the API client.

If this is sufficient for your needs, read more about [JSON Blueprints](https://developer.wordpress.org/playground/blueprints).

If you need to work with an already initialized client, you should look into [Blueprint functions](https://developer.wordpress.org/playground/developers/apis/javascript-api/blueprint-functions).

# Building PHP extensions

Use `@php-wasm/compile-extension` when you have a `phpize` extension source  
directory and want a distributable PHP.wasm `.so` plus a manifest that  
`loadNodeRuntime()` and `loadWebRuntime()` can load before PHP starts.

The source directory must contain a normal PHP extension build recipe, usually  
`config.m4` and the C or C++ files referenced from it.

```
npx @php-wasm/compile-extension \
    --source ./wp-mysql-parser \
    --name wp_mysql_parser \
    --php-versions 8.4 \
    --out ./dist/wp_mysql_parser
```

Docker is required. The helper reuses the PHP.wasm compile image, builds a  
matching PHP source tree, and runs `phpize`, `emconfigure`, and `emmake` with  
the side-module flags expected by PHP.wasm.

Custom extensions are built for JSPI runtimes. The helper does not build  
Asyncify side modules.

## Output

The output directory contains one JSPI artifact for each PHP version plus  
`manifest.json`:

```
dist/wp_mysql_parser/
|-- manifest.json
`-- wp_mysql_parser-php8.4-jspi.so
```

The manifest matches the `PHPExtensionManifest` shape from  
`@php-wasm/universal`. It records the extension name, the artifact matrix,  
and a `sourcePath` for each artifact relative to the manifest URL:

```
{
    "name": "wp_mysql_parser",
    "version": "0.1.0",
    "artifacts": [
        {
            "phpVersion": "8.4",
            "sourcePath": "wp_mysql_parser-php8.4-jspi.so"
        }
    ]
}
```

Host the whole output directory from the same static location. Relative  
artifact paths are resolved from the manifest URL.

### Sidecar files

Pass `--extra-files <hostDir>:<vfsRoot>` to stage data directories, web UI  
assets, ICU data, or anything else the extension needs at runtime. The host  
directory is copied next to the manifest, and each entry is recorded under  
`extraFiles.nodes` with a `vfsPath` relative to `vfsRoot` and a `sourcePath`  
relative to the manifest URL:

```
npx @php-wasm/compile-extension \
    --source ./spx-src \
    --name spx \
    --php-versions 8.2 \
    --extra-files ./web-ui:/internal/shared/spx \
    --out ./dist/spx
```

Empty directories are recorded as `type: "directory"` nodes so the loader  
creates them before PHP starts. Multiple `--extra-files` entries are  
allowed, but they must agree on `vfsRoot` — the manifest format stores a  
single root per group.

## Loading in Node.js

Pass the manifest to the startup-time `extensions` option:

```
const php = new PHP(
    await loadNodeRuntime('8.4', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: './dist/wp_mysql_parser/manifest.json',
                },
            },
        ],
    })
);
```

Node.js accepts local paths, `file:` URLs, and HTTP(S) URLs for `manifestUrl`.  
The loader selects the artifact whose `phpVersion` matches the runtime,  
stages the `.so`, copies any `extraFiles` declared in the manifest, writes a  
generated `.ini` file, and starts PHP with the extension scan directory  
configured.

## Loading in the browser

In the browser, host the output directory and pass an absolute URL:

```
const php = new PHP(
    await loadWebRuntime('8.4', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: new URL('/extensions/wp_mysql_parser/manifest.json', location.href),
                },
            },
        ],
    })
);
```

Serve the `.so` artifacts with a static-file server that permits cross-origin  
requests from the page that creates the runtime.

## Direct artifacts

Use a manifest when you publish a matrix. If the caller already picked the  
artifact, use `format: 'url'` instead:

```
await loadWebRuntime('8.4', {
    extensions: [
        {
            source: {
                format: 'url',
                name: 'wp_mysql_parser',
                url: new URL('https://cdn.example.com/wp_mysql_parser-php8.4-jspi.so'),
            },
        },
    ],
});
```

## Compatibility

Build every PHP version you plan to support. A `.so` built for PHP 8.4 cannot  
be loaded into PHP 8.3. Custom extension artifacts are JSPI-only and must be  
loaded by a JSPI runtime.

The helper supports PHP `7.4` and `8.0` through `8.5`. Pass the matrix as a  
comma-separated list:

```
npx @php-wasm/compile-extension \
    --source ./wp-mysql-parser \
    --name wp_mysql_parser \
    --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \
    --out ./dist/wp_mysql_parser
```

Extension loading is startup-only. Declare custom extensions in the  
`extensions` option before the runtime is created.

## Running the helper in CI

The helper is published as `@php-wasm/compile-extension` on npm and only  
needs Docker and Node. A typical GitHub Actions job:

```
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
  with:
    node-version: '24'

- name: Build the extension matrix
  run: |
    npx --yes @php-wasm/compile-extension \
      --source ./my-extension \
      --name my_extension \
      --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \
      --out ./dist/my-extension
```

When you build the matrix in GitHub Actions, set `strategy.max-parallel: 1`  
on the WASM job. Parallel Docker builds on hosted runners frequently hit  
apt-mirror flakes during the base image build.

For native dependencies, see  
[PHP extension dependencies](https://developer.wordpress.org/playground/developers/apis/javascript-api/php-extension-dependencies).

# `remote.html` vs `index.html`

[playground.wordpress.net](https://playground.wordpress.net/) exposes two distinct APIs through two separate HTML files: `remote.html` and `index.html`. Here’s an overview of their functions and differences:

- `index.html` uses WordPress Playground API client to control the “endpoint” that is `remote.html`.
- The [Query API](../query-api/) is exclusively provided by `index.html`, independent of the WordPress Playground JavaScript API.
- The [JavaScript API](../javascript-api/) is exclusively provided by `remote.html`. Only that file can be used as an “endpoint” for the `PlaygroundClient` class.

Here’s a bit more about each of these files:

## Remote.html

`remote.html` runs and renders WordPress and also exposes an API for developers to control it. Importantly, `remote.html` does not render any UI elements, such as browser UI or version switchers. It’s just WordPress. The primary functions of `remote.html` are:

- Loading the suitable version of php.wasm, the WebAssembly build of PHP.
- Loading the correct version of WordPress for user interaction.
- Initiating PHP in a WebWorker and registering a ServiceWorker for HTTP requests.
- Listening to the `message` event from the parent window and executing the appropriate code command.

That last part is how the public API works. The parent window (`index.html`) sends a message to the `iframe` (`remote.html`) with a command and arguments, and the `iframe` then executes that command and sends the result back with another message.

Sending messages is cumbersome, so the `PlaygroundClient` class provides an object-oriented API that handles the messages internally.

For quick testing and debugging, `remote.html` also exposes the JavaScript API client as `window.playground`. You can use it from your devtools as follows:

```
> await playground.listFiles("/")
(6) ['tmp', 'home', 'dev', 'proc', 'internal', 'wordpress']
```

`playground` is a class instance in this context, and you will benefit from browser’s autocompletion.

## Index.html

`index.html` is an independent app built around `remote.html` using the WordPress Playground API client.

It renders the browser UI, version selectors, and renders WordPress by embedding `remote.html` via an `iframe`. UI features, such as an address bar or a version selector, are implemented by communicating with `remote.html` using `PlaygroundClient`.

`index.html` monitors the query parameters it receives and triggers the appropriate `PlaygroundClient` methods. For instance, `?plugin=coblocks` triggers `installPluginsFromDirectory( client, ['coblocks'] )`. This mechanism forms the basis of the Query API.

For quick testing and debugging, `index.html` also exposes the JavaScript API client as `window.playground`. You can use it from your devtools as follows:

```
> await playground.listFiles("/")
(6) ['tmp', 'home', 'dev', 'proc', 'internal', 'wordpress']
```

Note that `playground` is a Proxy object in this context and you won’t get any autocompletion from the browser.

# Mount data

## Mount a directory from the browser

You can mount a directory from the browser to Playground using the `window.showDirectoryPicker` API. Check the [Browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#browser_compatibility) before using this API.

```
window.showDirectoryPicker().then(function (directoryHandle) {
    window.parent.postMessage({
        type: 'mount-directory-handle',
        directoryHandle,
        mountpoint: '/wordpress/wp-content/uploads/markdown/',
    });
});
```

## Mount Browser’s OPFS Storage

You can mount OPFS storage available within the browser as well. Under the hood, we sync the memory filesystem to OPFS at the end of every PHP request served. It’s advisable to delay mounting of OPFS after boot as shown below, so that WordPress installation doesn’t trigger a sync of over 3000 files slowing down the boot process.

```
const hasWordPressSiteInOPFS = false; // roll your logic to track this
const blueprint = {
    preferredVersions: {
        php: '8.4',
        wp: 'latest',
    },
    features: {
        networking: true,
    },
    login: true,
    steps: [], // add steps
};

try {
    const mountDescriptor: MountDescriptor = {
        device: {
            type: 'opfs',
            path: `my-unique-prefix/my-site`,
        },
        mountpoint: '/wordpress',
        initialSyncDirection: hasWordPressSiteInOPFS ? 'opfs-to-memfs' : 'memfs-to-opfs',
    };

    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp'),
        remoteUrl: 'https://playground.wordpress.net/remote.html',
        blueprint: blueprint,
        shouldInstallWordPress: !hasWordPressSiteInOPFS,
        mounts: hasWordPressSiteInOPFS ? [mountDescriptor] : [],
    });

    if (!hasWordPressSiteInOPFS) {
        await client.mountOpfs(mountDescriptor);
    }

    await client.isReady();
    return client;
} catch (error) {
    // handle error here
}
```

For persistence guarantees, check [Storage quotes and eviction criterias](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria).

# PHP extension dependencies

Custom PHP.wasm extensions can only link WebAssembly code built with the same  
Emscripten toolchain and JSPI ABI as the PHP runtime. Native host libraries from  
`/usr/lib`, Homebrew, apt, or npm packages cannot be linked into the final  
`.so`.

Build dependencies as static WebAssembly archives and pass their headers and  
archives to `@php-wasm/compile-extension`.

## Playground-built dependencies

Some libraries already have recipes in `packages/php-wasm/compile`. Build the  
JSPI target and pass the mounted path inside the helper container:

```
make -C packages/php-wasm/compile libz_jspi

npx @php-wasm/compile-extension \
    --source ./zlib-probe \
    --name zlib_probe \
    --php-versions 8.4 \
    --extra-cflags "-I/php-wasm-compile/libz/jspi/dist/root/lib/include" \
    --extra-ldflags "/php-wasm-compile/libz/jspi/dist/root/lib/lib/libz.a"
```

Use the `jspi` archive for custom extension builds.

## Vendored dependencies

If the dependency is not built by Playground, vendor the source under your  
extension directory and build it with Emscripten before the extension is linked.  
Paths from `--source` are available under `/build` inside the helper container:

```
npx @php-wasm/compile-extension \
    --source ./external-lib-probe \
    --name external_lib_probe \
    --php-versions 8.4 \
    --extra-cflags "-I/build/vendor/string-score/install/include" \
    --extra-ldflags "/build/vendor/string-score/install/lib/libstring_score.a"
```

`--extra-cflags` is visible during `./configure`. `--extra-ldflags` is applied  
to the final side-module link so dependency archives do not break Autoconf’s  
compiler smoke tests.

## Prebuilt static archives

Use `--extra-ldflags` to link prebuilt static archives such as a Rust  
`staticlib`. The helper detects `.a` entries in `--extra-ldflags` and  
force-links them into the final side module with `--whole-archive`.

```
npx @php-wasm/compile-extension \
    --source ./my-rust-extension \
    --name my_rust_extension \
    --php-versions 8.4 \
    --extra-cflags "-I/build/include" \
    --extra-ldflags "/build/target/wasm32-unknown-emscripten/release/libmy_rust_extension.a"
```

The extension still needs a small `phpize` wrapper. A common Rust pattern is:

- `config.m4` declares the PHP extension and builds a tiny C shim with  
  `PHP_NEW_EXTENSION`.
- `_shim.c` defines the PHP module entry and calls exported Rust functions over  
  C ABI.
- `--extra-ldflags` points at the Rust `staticlib` archive under `/build`.

Do not use `PHP_ADD_LIBRARY_WITH_PATH` for a sibling `.a` archive in  
`config.m4`. PHP’s libtool setup can look for a matching `.so`, fail to link  
the archive into the side module, and still leave a build artifact behind. Pass  
static archives through `--extra-ldflags` instead.

## CMake dependencies

Build CMake dependencies as static archives with Emscripten and store the  
install tree under the extension source directory:

```
source /root/emsdk/emsdk_env.sh

emcmake cmake \
    -S vendor/libfoo \
    -B vendor/libfoo/build \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX="$PWD/vendor/libfoo/install" \
    -DBUILD_SHARED_LIBS=OFF

emmake cmake --build vendor/libfoo/build --target install

npx @php-wasm/compile-extension \
    --source . \
    --name my_extension \
    --php-versions 8.4 \
    --extra-cflags "-I/build/vendor/libfoo/install/include" \
    --extra-ldflags "/build/vendor/libfoo/install/lib/libfoo.a"
```

## Makefile dependencies

Force Makefile dependencies to use Emscripten tools:

```
source /root/emsdk/emsdk_env.sh

emmake make -C vendor/libfoo \
    CC=emcc \
    CXX=em++ \
    AR=emar \
    RANLIB=emranlib \
    PREFIX="$PWD/vendor/libfoo/install" \
    install

npx @php-wasm/compile-extension \
    --source . \
    --name my_extension \
    --php-versions 8.4 \
    --extra-cflags "-I/build/vendor/libfoo/install/include" \
    --extra-ldflags "/build/vendor/libfoo/install/lib/libfoo.a"
```

The final PHP extension still needs a `phpize` build recipe. If the extension  
itself is CMake-only or Makefile-only, add a thin `config.m4` wrapper that  
builds the PHP extension and treats the CMake or Make output as dependency  
code.

## Rust build systems

The helper image includes a host `php` CLI because `ext-php-rs` and similar  
build systems often shell out to `php` for version detection. The host CLI is  
only for build scripts. The compiled extension must still link against the  
PHP.wasm headers and side-module ABI.

Rust extensions that use `bindgen` need the same target, sysroot, and Zend  
defines as PHP.wasm. The helper exports `BINDGEN_EXTRA_CLANG_ARGS` with:

```
--target=wasm32-unknown-emscripten \
--sysroot=$EMSDK_SYSROOT \
-DZEND_ENABLE_ZVAL_LONG64 \
-D__x86_64__
```

If you build the Rust archive outside the helper container, set the same value  
before running `cargo`.

Rust `cc-rs` build scripts also need position-independent objects for side  
module linking. The helper exports target-specific compiler variables including  
`CFLAGS_wasm32_unknown_emscripten=-fPIC` and  
`CXXFLAGS_wasm32_unknown_emscripten=-fPIC`. Without `-fPIC`, the final link can  
fail with an error like:

```
R_WASM_MEMORY_ADDR_SLEB cannot be used against symbol ...; recompile with -fPIC
```

Rust’s precompiled `std` for `wasm32-unknown-emscripten` is built with  
unwinding support. That imports a `__cpp_exception` WebAssembly tag that  
PHP.wasm does not export. Build Rust static libraries with `panic=abort` and a  
nightly rebuilt standard library:

```
RUSTFLAGS="-C panic=abort" cargo +nightly build \
    --release \
    --target wasm32-unknown-emscripten \
    -Zbuild-std=std,panic_abort
```

Link the resulting `lib*.a` with `--extra-ldflags`.

### PHP version support for `ext-php-rs`

`ext-php-rs` `0.15` depends on PHP 8 Zend APIs and does not compile against  
PHP `7.4` headers. Rust extensions built on top of `ext-php-rs` `0.15` should  
restrict the PHP matrix to `8.0` through `8.5`:

```
npx @php-wasm/compile-extension \
    --source ./my-rust-extension \
    --name my_rust_extension \
    --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \
    --extra-ldflags "/build/target/wasm32-unknown-emscripten/release/libmy_rust_extension.a"
```

The helper itself still supports PHP `7.4`. The limitation is in the Rust  
binding layer, not in the WebAssembly side-module ABI. Hand-written Rust  
extensions that bind Zend directly through `bindgen` can target PHP `7.4` if  
the C ABI shim does the same.

`ext-php-rs` also generates bindings for one PHP version at a time. When you  
build the same Rust crate for several PHP versions, rebuild the staticlib for  
each PHP version with that version’s Zend API number visible to the build  
script, and link the matching archive into each `--php-versions` lane.

## Troubleshooting

`configure: error: ... not found`

The dependency headers or libraries are not visible inside the container. Use  
paths under `/build` for files copied from `--source`, or  
`/php-wasm-compile/<dependency>/<mode>/dist/root/lib` for Playground-built  
dependencies.

`undefined symbol` when loading the extension

The extension references a function that is not exported by the PHP main module  
or was not linked from a WebAssembly dependency archive. Add the dependency  
archive to `--extra-ldflags`, or rebuild the main PHP.wasm runtime if the  
symbol must come from PHP core.

`WebAssembly.LinkError` or startup crashes

Check that the extension loads in a JSPI runtime. The custom extension helper  
does not build Asyncify artifacts.

`wasm-ld: unknown file type` or `file not recognized`

One of the linked libraries is a native host library. Rebuild that dependency  
with Emscripten and link the resulting `.a` file.

`R_WASM_MEMORY_ADDR_SLEB cannot be used against symbol`

A C or C++ object in a static archive was not compiled as position-independent  
code. Rebuild it with `-fPIC`, or make sure Rust `cc-rs` sees  
`CFLAGS_wasm32_unknown_emscripten=-fPIC`.

`__cpp_exception` is undefined when loading a Rust extension

The Rust archive was built against an unwinding `std`. Rebuild it with  
`RUSTFLAGS="-C panic=abort"` and  
`cargo +nightly build -Zbuild-std=std,panic_abort`.

`bad export type for 'stdin'` or another C runtime global

The side module pulled in a dependency object that expects a mutable C runtime  
global the main PHP.wasm module does not export. Rebuild the dependency with  
the unused feature disabled, link a smaller archive that excludes that object,  
or move the dependency into the main PHP.wasm build so the global is provided  
by the runtime.

`phpize` cannot find headers

The helper image builds and installs a minimal matching PHP source tree before  
running `phpize`. If an extension includes headers from optional PHP  
extensions, copy or generate those headers in the Docker layer or include them  
in the extension source.

# Loading PHP extensions

PHP reads extension declarations while it starts. In PHP.wasm, that means  
extensions must be declared before `loadNodeRuntime()` or `loadWebRuntime()`  
creates the runtime.

Use the `extensions` array for both bundled extensions and external `.so`  
artifacts.

Use external PHP.wasm extensions when the code that must be fast or native-like  
is too hot for PHP userland, but you still want to ship it with a Playground  
demo, CLI workflow, or embedded app. The same manifest can be loaded by the  
JavaScript API, the Playground web Query API, and Playground CLI.

External extensions are JSPI side modules. If the browser or Node.js build does  
not expose JSPI, PHP.wasm rejects the extension request instead of starting  
with a partially loaded runtime.

## Bundled extensions

`@php-wasm/node` ships `intl`, `xdebug`, `redis`, and `memcached`:

```
const php = new PHP(
    await loadNodeRuntime('8.4', {
        extensions: ['intl', 'redis', 'memcached', { name: 'xdebug', options: { ideKey: 'PLAYGROUND' } }],
    })
);
```

`@php-wasm/web` currently ships `intl`:

```
const php = new PHP(
    await loadWebRuntime('8.4', {
        extensions: ['intl'],
    })
);
```

The old `withIntl`, `withXdebug`, `withRedis`, and `withMemcached` options are  
still accepted where they already existed. New code should use `extensions`  
because it also supports external extensions.

## External extensions

An external extension needs a WebAssembly `.so` built for the same PHP version  
as the PHP.wasm JSPI runtime. Publish the artifact with a manifest:

```
{
    "name": "wp_mysql_parser",
    "version": "0.1.0",
    "artifacts": [
        {
            "phpVersion": "8.4",
            "sourcePath": "wp_mysql_parser-php8.4-jspi.so"
        }
    ]
}
```

`sourcePath` may be an absolute URL or a path relative to the manifest URL. If  
the manifest lives at  
`` https://cdn.example.com/extensions/wp_mysql_parser/manifest.json`, the ``sourcePath`above resolves to`https://cdn.example.com/extensions/wp\_mysql\_parser/wp\_mysql\_parser-php8.4-jspi.so`.

If the extension ships sidecar assets (UI files, data, empty directories), list  
them under `extraFiles`. Each `node` declares its final VFS path as  
`vfsRoot + vfsPath`:

```
{
    "name": "spx",
    "artifacts": [
        {
            "phpVersion": "8.4",
            "sourcePath": "spx-php8.4-jspi.so",
            "extraFiles": {
                "vfsRoot": "/internal/shared/spx",
                "nodes": [
                    { "vfsPath": "data", "type": "directory" },
                    { "vfsPath": "ui/index.html", "sourcePath": "ui/index.html" }
                ]
            }
        }
    ]
}
```

In Node.js, `manifestUrl` may be a local path, a `file:` URL, or an HTTP(S)  
URL. Relative local paths are resolved from the current working directory:

```
const php = new PHP(
    await loadNodeRuntime('8.4', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: './dist/wp_mysql_parser/manifest.json',
                },
            },
        ],
    })
);
```

Node.js applies the same local-path support to direct artifact URLs and inline  
manifest `baseUrl` values.

External extensions are only supported when JSPI is available. Asyncify support  
is limited to the bundled extensions shipped with the PHP.wasm packages, such  
as `intl`, `xdebug`, `redis`, and `memcached`.

In the browser, pass an absolute URL or construct one with the base URL you want:

```
const php = new PHP(
    await loadWebRuntime('8.4', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: new URL('/extensions/wp_mysql_parser/manifest.json', location.href),
                },
            },
        ],
    })
);
```

If you already have the manifest object in memory, pass `baseUrl` so relative  
artifact files can still be resolved:

```
await loadNodeRuntime('8.4', {
    extensions: [
        {
            source: {
                format: 'manifest',
                manifest,
                baseUrl: 'https://cdn.example.com/extensions/wp_mysql_parser/',
            },
        },
    ],
});
```

If you already have the `.so` bytes, skip the manifest:

```
await loadNodeRuntime('8.4', {
    extensions: [
        {
            name: 'wp_mysql_parser',
            source: {
                format: 'so',
                bytes,
            },
        },
    ],
});
```

Use a direct artifact URL when the caller, not a manifest, chooses the artifact:

```
await loadWebRuntime('8.4', {
    extensions: [
        {
            source: {
                format: 'url',
                name: 'wp_mysql_parser',
                url: new URL('https://cdn.example.com/wp_mysql_parser-php8.4-jspi.so'),
            },
        },
    ],
});
```

## Try published PHP.wasm extensions

Two projects publish ready-to-use manifests you can inspect before building  
your own:

- [SQLite Database Integration native extension](https://wordpress.github.io/sqlite-database-integration/native-extension/)  
  publishes `wp_mysql_parser`, a native lexer/parser used by the SQLite  
  Database Integration plugin.
- [PHP Toolkit Native APIs WASM releases](https://wordpress.github.io/php-toolkit/wp_native_apis-wasm-extension/)  
  publishes `wp_native_apis`, an experimental extension for WordPress native  
  APIs.

To try a published extension in Playground web, pass its manifest URL with the  
`php-extension` Query API parameter. This loads the extension before PHP starts,  
without writing any JavaScript:

```
https://playground.wordpress.net/?php=8.5&php-extension=https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json
```

Use `/latest/manifest.json` for a quick demo. Use one of the pinned manifest  
URLs from the extension release page for reproducible demos, tests, and  
documentation links.

Repeat the parameter to load more than one extension:

```
https://playground.wordpress.net/?php=8.5&php-extension=https://example.com/one/manifest.json&php-extension=https://example.com/two/manifest.json
```

The Query API accepts absolute, root-relative, and page-relative manifest URLs.  
After resolution, the manifest URL must use HTTP(S). For example, if your  
Playground page is served from `https://example.com/tools/`, these are valid:

```
?php-extension=https://cdn.example.com/wp_mysql_parser/manifest.json
?php-extension=/extensions/wp_mysql_parser/manifest.json
?php-extension=./extensions/wp_mysql_parser/manifest.json
```

The same manifest-loading path is documented in the  
[Query API reference](https://developer.wordpress.org/playground/developers/apis/query-api).

The same manifest works in Playground CLI:

```
npx @wp-playground/cli@latest server \
    --php=8.5 \
    --php-extension=https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json \
    --blueprint=https://wordpress.github.io/sqlite-database-integration/blueprint.json
```

When you embed Playground with `@wp-playground/client`, pass the same manifest  
through the `extensions` option:

```
<iframe id="wp"></iframe>
<script type="module">

    const client = await startPlaygroundWeb({
        iframe: document.getElementById('wp'),
        remoteUrl: 'https://playground.wordpress.net/remote.html',
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: 'https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json',
                },
            },
        ],
    });

    await client.isReady();
</script>
```

Use the lower-level runtime API when you are building directly on  
`@php-wasm/node` or `@php-wasm/web`:

```
const php = new PHP(
    await loadNodeRuntime('8.5', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: 'https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json',
                },
            },
        ],
    })
);
```

## Build your own extension

`@php-wasm/compile-extension` turns a normal `phpize` extension directory into  
JSPI `.so` artifacts and a loadable manifest:

```
npx @php-wasm/compile-extension \
    --source ./wp-mysql-parser \
    --name wp_mysql_parser \
    --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \
    --out ./dist/wp_mysql_parser
```

Host the whole output directory from one static location. The manifest lets the  
runtime select the artifact that matches the active PHP version and load it  
before PHP starts.

## Startup files

PHP loads extensions from `.ini` files it reads during startup. PHP.wasm builds  
those files from the extension request before the runtime starts:

```
extension=/internal/shared/extensions/my_extension.so
my_extension.option=value
```

`loadWithIniDirective` chooses the first line of that generated `.ini` file.  
Regular PHP extensions use `extension`. Zend extensions such as Xdebug use  
`zend_extension`:

```
zend_extension=/internal/shared/extensions/xdebug.so
xdebug.mode=debug,develop
```

The remaining startup options describe what PHP.wasm writes before PHP starts:

- `iniEntries`: extra lines in the generated extension `.ini` file.
- `extraFiles`: sidecar files staged in the PHP virtual filesystem.
- `env`: environment variables set before PHP starts.
- `extensionDir`: the virtual directory for the `.so` and generated `.ini`  
  files.

A Zend extension needs `zend_extension` and usually several `.ini` entries:

```
await loadNodeRuntime('8.4', {
    extensions: [
        {
            name: 'my_debugger',
            loadWithIniDirective: 'zend_extension',
            iniEntries: {
                'my_debugger.mode': 'debug',
                'my_debugger.start_with_request': 'yes',
            },
            source: { format: 'so', bytes },
        },
    ],
});
```

An extension with data files can stage them before PHP starts and point the  
extension at their virtual path. `extraFiles` keys are absolute VFS paths:

```
await loadNodeRuntime('8.4', {
    extensions: [
        {
            name: 'my_text_extension',
            source: { format: 'so', bytes },
            env: {
                MY_TEXT_DATA: '/internal/shared/my_text_extension',
            },
            extraFiles: {
                files: {
                    '/internal/shared/my_text_extension/data.bin': dataBytes,
                },
            },
        },
    ],
});
```

## Limits

Extension loading is startup-only. PHP.wasm writes a generated `.ini` file for  
each extension, stages the `.so` and any sidecar files, updates  
`PHP_INI_SCAN_DIR`, and then starts PHP. Loading an extension into an already  
running PHP runtime with `dl()` is not supported by this API.

Legacy PHP builds do not support extension loading. The runtime will reject  
extension requests for those PHP versions instead of starting with a partially  
loaded configuration.

# Playground API Client

The `PlaygroundClient` object implements the `UniversalPHP` interface. All the methods from that interface are also available in Node.js and same-process PHP instances (Playground runs PHP in a web worker).

Broadly speaking, you can use the client to perform three types of operations:

- Running PHP code
- Customizing `PHP.ini`
- Managing files and directories

## Running PHP code

The two methods you can use to run PHP code are:

- [`run()`](#the-run-method) – runs PHP code and returns the output
- [`request()`](#the-request-method) – makes an HTTP request to the website

In Node.js, you can also use the [`cli()`](#the-cli-method) method to run PHP in a CLI mode.

### The `run()` method

### The `request()` method

## Customizing `PHP.ini`

The API client also allows you to change the `php.ini` file:

```
await setPhpIniEntries(client, {
    display_errors: 'On',
    error_reporting: 'E_ALL',
});
```

## Managing files and directories

The `client` object provides you with a low-level API for managing files and directories in the PHP filesystem:

```
await client.mkdirTree('/wordpress/test');
// Create a new PHP file
await client.writeFile(
    '/wordpress/test/index.php',
    `<?php
     echo "Hello, world!<br/>";
     // List all the files in current directory
     print_r(glob(__DIR__ . '/*'));
  `
);
// Create files named 1, 2, and 3
await client.writeFile('/wordpress/test/1', '');
await client.writeFile('/wordpress/test/2', '');
await client.writeFile('/wordpress/test/3', '');
// Remove the file named 1
await client.unlink('/wordpress/test/1');
// Navigate to our PHP file
await client.goTo('/test/index.php');
```

For a complete list of these methods, refer to the `PlaygroundClient` interface.

## Sending messages to JavaScript

You can pass messages from PHP to JavaScript using the `post_message_to_js()` function. It accepts one argument:

- `$data` (string) – Data to pass to JavaScript.

For example, here’s how you would send a message with a JSON-encoded post ID and title:

```
const php = new PHP(await loadNodeRuntime('8.3'));

php.onMessage(
    // The data is always passed as a string
    function (data: string) {
        // Let's decode and log the data:
        console.log(JSON.parse(data));
    }
);

// Now that we have a listener in place, let's
// dispatch a message:
await php.runStream({
    code: `<?php
        post_message_to_js(
            json_encode([
                'post_id' => '15',
                'post_title' => 'This is a blog post!'
            ])
        );
    `,
});

// You will see the following output in the console:
// { post_id: '15', post_title: 'This is a blog post!' }
```

## The `cli()` method

In Node.js, you also have access to the `cli()` method that runs PHP in a CLI mode:

```
// Run PHP in a CLI mode
client.cli(['-r', 'echo "Hello, world!";']);
// Outputs "Hello, world!"
```

Once `cli()` method finishes running, the PHP instance is no longer usable and should be discarded. This is because PHP internally cleans up all the resources and calls `exit()`.

# Running PHP apps in the browser with ServiceWorkers and Worker Threads

On a high level, WordPress Playground works in web browsers as follows:

- The `index.html` file on playground.wordpress.net loads the `remote.html` file via an `<iframe src="/remote.html">`.
- `remote.html` starts a Worker Thread and a ServiceWorker and sends back the download progress information.
- The Worker Thread starts PHP and populates the filesystem with a WordPress patched to run on SQLite.
- The ServiceWorker starts intercepting all HTTP requests and forwarding them to the Worker Thread.
- `remote.html` creates an `<iframe src="/index.php">`, and the Service Worker forwards the `index.php` request to the Worker Thread where the WordPress homepage is rendered.

Visually, it looks like this:
![Architecture overview](https://i0.wp.com/developer.wordpress.org/files/2026/07/architecture-overview.webp?fit=2608%2C2201&ssl=1)

## High-level ideas

The [`@php-wasm/web`](https://github.com/WordPress/wordpress-playground/blob/trunk/packages/php-wasm/web/) is built on top of the following ideas:

- [**Browser tab orchestrates everything**](https://developer.wordpress.org/playground/developers/architecture/browser-tab-orchestrates-execution) – The browser tab is the main program. Closing or reloading it means destroying the entire execution environment.
- [**Iframe-based rendering**](https://developer.wordpress.org/playground/developers/architecture/browser-iframe-rendering) – Every response produced by the PHP server must be rendered in an iframe to avoid reloading the browser tab when the user clicks on a link.
- [**PHP Worker Thread**](https://developer.wordpress.org/playground/developers/architecture/browser-php-worker-threads) – The PHP server is slow and must run in a web worker, otherwise handling requests freezes the website UI.
- [**Service Worker routing**](https://developer.wordpress.org/playground/developers/architecture/browser-service-workers) – All HTTP requests originating in that iframe must be intercepted by a Service worker and passed on to the PHP worker thread for rendering.

# Cross-process communication

`@php-wasm/web` uses the [Comlink](https://github.com/GoogleChromeLabs/comlink) library to turns the one-way `postMessage` available in JavaScript into a two-way communication channel.

If `postMessage` sounds unfamiliar, it’s what JavaScript threads use to communicate. Please review the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) before continuing.

By default, `postMessage` does not offer any request/response mechanics. You may send messages to another thread and you may independently receive messages from it, but you can’t send a message and await a response to that specific message.

To quote the [Comlink](https://github.com/GoogleChromeLabs/comlink) library documentation:

**main.js**

```
async function init() {
    const worker = new Worker('worker.js');
    // WebWorkers use `postMessage` and therefore work with Comlink.
    const obj = Comlink.wrap(worker);
    alert(`Counter: ${await obj.counter}`);
    await obj.inc();
    alert(`Counter: ${await obj.counter}`);
}
init();
```

**worker.js**

```
importScripts('https://unpkg.com/comlink/dist/umd/comlink.js');

const obj = {
    counter: 0,
    inc() {
        this.counter++;
    },
};

Comlink.expose(obj);
```

# Iframe-based rendering

To avoid page reloads, all the `PHPRequestHandler` responses must be rendered in an iframe. Remember, the entire setup only lives as long as the main `index.html`. We want to avoid reloading the main app at all costs.

In our app example above, `index.php` renders the following HTML:

```
<a href="page.php">Go to page.php</a>
```

Imagine our `index.html` rendered it in a `<div>` instead of an `<iframe>`. As soon as you click on that link, the browser will try to navigate from `index.html` to `page.php`. However, `index.html` runs the entire PHP app, including the Worker Thread, the PHPRequestHandler, and the traffic control connecting them to the Service Worker. Navigating away from it would destroy the app.

Now, consider an iframe with the same link in it:

```
<iframe srcdoc='<a href="page.php">Go to page.php</a>'></iframe>
```

This time, click the link in the browser to load `page.php` **inside the iframe**. The top-level `index.html`, where the PHP application runs, remains unaffected. That’s why iframes are crucial for the `@php-wasm/web` setup.

**Crash reports**

Playground doesn’t collect crash reports automatically. Instead, it prompts users to submit a crash report when an instance fails to run in the browser.

The report includes a log, description, and a URL, and users can modify it before submitting it.

The [Logger API](https://github.com/WordPress/wordpress-playground/blob/trunk/packages/playground/website/public/logger.php) handles it from there. This simple REST API validates the data and sends it to the **Making WordPress** [#playground-logs Slack channel](https://wordpress.slack.com/archives/C06Q5DCKZ3L).

## Iframes caveats

- `target="_top"` isn’t handled yet, so clicking links with `target="_top"` will reload the page you’re working on.
- JavaScript popups originating in the `iframe` may not always display.

# PHP Worker Threads

PHP is always ran in a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) to ensure the PHP runtime doesn’t slow down the user interface of the main website.

Imagine the following code:

```
<button onclick="for(let i=0;i<100000000;i++>) {}">Freeze the page</button>
<input type="text" />
```

As soon as you click that button the browser will freeze and you won’t be able to type in the input. That’s just how browsers work. Whether it’s a for loop or a PHP server, running intensive tasks slows down the user interface.

### Initiating web workers

Web workers are separate programs that can process heavy tasks outside of the main application. They must be initiated by the main JavaScript program living in the browser tab. Here’s how:

```
const phpClient = consumeAPI<PHPClient>(
    spawnPHPWorkerThread(
        '/worker-thread.js' // Valid Worker script URL
    )
);
await phpClient.isReady();
await phpClient.run({ code: `<?php echo "Hello from the thread!";` });
```

### Controlling web workers

Exchanging messages is the only way to control web workers. The main application has no access to functions or variables inside of a web worker. It can only send and receive messages using `worker.postMessage` and `worker.onmessage = function(msg) { }`.

This can be tedious, which is why Playground provides a convenient [consumeAPI](https://developer.wordpress.org/playground/api/universal/function/consumeAPI) function that abstracts the message exchange and exposes specific functions from the web worker. This is why we can call `phpClient.run` in the example above.

# Scopes

Scopes keep your app working when you open it in two different browser tabs.

The Service Worker passes the intercepted HTTP requests to the PHPRequestHandler for rendering. Technically, it sends a message through a [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) which then gets delivered to every browser tab where the application is open. This is undesirable, slow, and leads to unexpected behaviors.

Unfortunately, the Service Worker cannot directly communicate with the relevant Worker Thread – see [PR #31](https://github.com/WordPress/wordpress-playground/pull/31) and [issue #9](https://github.com/WordPress/wordpress-playground/issues/9) for more details.

Scopes enable each browser tab to:

- Brand the outgoing HTTP requests with a unique tab id
- Ignore any `BroadcastChannel` messages with a different id

Technically, a scope is a string included in the `PHPRequestHandler.absoluteUrl`. For example:

- In an **unscoped app**, `/index.php` would be available at `http://localhost:8778/wp-login.php`
- In an **scoped app**, `/index.php` would be available at `http://localhost:8778/scope:96253/wp-login.php`

The service worker is aware of this concept and will attach the `/scope:` found in the request URL to the related `BroadcastChannel` communication.

A worker thread initiated with a scoped `absoluteUrl` is said to be **scoped**:

```
import {
    PHP,
    setURLScope,
    exposeAPI,
    parseWorkerStartupOptions,
} from '@php-wasm/web';

// Don't use the absoluteURL directly:
const absoluteURL = 'http://127.0.0.1'

// Instead, set the scope first:
const scope = Math.random().toFixed(16)
const scopedURL = setURLScope(absoluteURL, scope).toString()

const { phpVersion } = parseWorkerStartupOptions<{ phpVersion?: string }>();
const php = await PHP.load('8.0', {
    requestHandler: {
        documentRoot: '/',
        absoluteUrl: scopedSiteUrl
    }
});

// Expose the API to app.ts:
const [setApiReady, ] = exposeAPI( php );
setApiReady();
```

# Service Workers

[A Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) is used to handle the HTTP traffic using the in-browser [`PHPRequestHandler`](https://developer.wordpress.org/playground/developers/architecture/browser-concepts).
Imagine your PHP script renders the following page [in the iframe viewport](https://developer.wordpress.org/playground/developers/architecture/browser-iframe-rendering):

```
<html>
    <head>
        <title>John's Website</title>
    </head>
    <body>
        <a href="/">Homepage</a>
        <a href="/blog">Blog</a>
        <a href="/contact">Contact</a>
    </body>
</html>
```

When the user clicks, say the `Blog` link, the browser would normally send a HTTP request to the remote server to fetch the `/blog` page and then display it instead of the current iframe contents. However, our app isn’t running on the remote server. The browser would just display a 404 page.
Enter [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) – a tool to intercept the HTTP requests and handle them inside the browser:
![Service worker data flow](https://i0.wp.com/developer.wordpress.org/files/2026/07/workers-diagram.webp?fit=2608%2C2201&ssl=1)

### Service Worker setup

The main application living in `/index.html` is responsible for registering the service worker.
Here’s the minimal setup:
**/app.js:**

```
function main() {
    await registerServiceWorker(
        phpClient,
        "default", // PHP instance scope
        "/sw.js",  // Must point to a valid Service Worker implementation.
        "1"        // Service worker version, used for reloading the script.
    );

}
```

You will also need a separate `/service-worker.js` file that actually intercepts and routes the HTTP requests. Here’s what a minimal implementation looks like:
**/service-worker.js**:

```
// Intercepts all HTTP traffic on the current domain and
// passes it to the Worker Thread.
initializeServiceWorker();
```

# Browser tab orchestrates the execution

The main `index.html` ties the entire application together. It starts all the concurrent processes and displays the PHP responses. The app only lives as long as the main `index.html`.
Keep this point in mind as you read through the rest of the docs. At this point it may seem obvious, by the lines may get blurry later on. This package runs code outside of the browser tab using Web Workers, Service Workers, and, in the future, Shared Workers. Some of these workers may keep running even after the browser tab with `index.html` is closed.

## Boot sequence

Here’s what a boot sequence for a minimal app looks like:
![The boot sequence](https://i0.wp.com/developer.wordpress.org/files/2026/07/boot-sequence.webp?fit=1462%2C1421&ssl=1)
The main app initiates the Iframe, the Service Worker, and the Worker Thread. Note how the main app doesn’t use the PHP stack directly – it’s all handled in the Worker Thread.
Here’s what that boot sequence looks like in code:
**/index.html**:

```
<script src="/app.ts"></script>
<iframe id="my-app"></iframe>
```

**/app.ts**:

```
const workerUrl = '/worker-thread.js';

export async function startApp() {
    const phpClient = consumeAPI<PlaygroundWorkerEndpoint>(
        await spawnPHPWorkerThread(
            workerUrl, // Valid Worker script URL
            {
                wpVersion: 'latest',
                phpVersion: '8.3', // Startup options
            }
        )
    );

    // Await the two-way communication channel
    await phpClient.isReady();

    // Must point to a valid Service Worker script:
    await registerServiceWorker(
        phpClient,
        'default', // PHP instance scope, keep reading to learn more.
        '/sw.js', // Valid Service Worker script URL.
        '1' // Service worker version, used for reloading the script.
    );

    // Create a few PHP files to browse:
    await workerThread.writeFile('/index.php', '<a href="page.php">Go to page.php</a>');
    await workerThread.writeFile('/page.php', '<?php echo "Hello from PHP!"; ?>');

    // Navigate to index.php:
    document.getElementById('my-app').src = playground.pathToInternalUrl('/index.php');
}
startApp();
```

Keep reading to learn how all these pieces fit together.

### Data flow

Here’s what happens whenever the iframe issues a same-domain request:
![The data flow](https://i0.wp.com/developer.wordpress.org/files/2026/07/data-flow-scaled.webp?fit=3200%2C1320&ssl=1)
A step-by-step breakdown:

1. The request is intercepted by the Service Worker
2. The Service Worker passes it to the Worker Thread
3. The Worker Thread calls `PHP.request` to convert that request to a response
4. The Worker Thread passes the response to the Service Worker
5. The Service Worker provides the browser with a response

At this point, if the request was triggered by user clicking on a link, the browser will render PHPRequestHandler’s response inside the iframe.

# Bundling WordPress for the browser

The [web bundler Dockerfile](https://github.com/WordPress/wordpress-playground/blob/trunk/src/wordpress-playground/wordpress/Dockerfile) turns a vanilla WordPress into a browser-optimized one:

- Makes WordPress run on SQLite using the [official drop-in plugin](https://github.com/WordPress/sqlite-database-integration) as MySQL is unsupported in the browser.
- Reduces the WordPress website size from about 70MB to about 10MB, or 5MB compressed.
- Runs the WordPress installation wizard.
- Bundles WordPress as a minified zip file.

Build a new bundle with `nx bundle-wordpress playground-wordpress-builds --wp-version=<version>`, e.g.:

```
nx bundle-wordpress playground-wordpress-builds --wp-version=6.1
```

The bundler outputs:

- `packages/playground/wordpress-builds/public/wp-6.1.zip` – zipped WordPress files
- `packages/playground/wordpress-builds/public/wp-6.1/` – a directory with static assets for the specified WordPress versions

Consult [the web bundler Dockerfile](https://github.com/WordPress/wordpress-playground/blob/trunk/src/wordpress-playground/wordpress/Dockerfile) for more details (like the list of supported WordPress versions) and modify it to customize the default WordPress installation.

# Asyncify and JSPI: Stack Switching in PHP WebAssembly

[Asyncify](https://emscripten.org/docs/porting/asyncify.html) lets synchronous C or C++ code interact with asynchronous JavaScript. Technically, it saves the entire C call stack before yielding control back to JavaScript, and then restores it when the asynchronous call is finished. This is called **stack switching**.
Networking support in the WebAssembly PHP build is implemented using Asyncify. When PHP makes a network request, it yields control back to JavaScript, which makes the request, and then resumes PHP when the response is ready. It works well enough that PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

## Asyncify crashes

Stack switching requires wrapping all C functions that may be found at a call stack at a time of making an asynchronous call. Blanket-wrapping of every single C function adds a **significant** overhead, which is why we maintain a list of specific function names:
https://github.com/WordPress/wordpress-playground/blob/15a660940ee9b4a332965ba2a987f6fda0c159b1/packages/php-wasm/compile/Dockerfile#L624-L632
Unfortunately, missing even a single item from that list results in a WebAssembly crash whenever that function is a part of the call stack when an asynchronous call is made. It looks like this:
![A screenshot of an asyncify error in the terminal](https://i0.wp.com/developer.wordpress.org/files/2026/07/asyncify-error.webp?fit=2116%2C1404&ssl=1)
Asyncify can auto-list all the required C functions when built without `ASYNCIFY_ONLY`, but that auto-detection is overeager and ends up listing about 70,000 C functions which increases the startup time to 4.5s. That’s why we maintain the list manually.
If you are interested in more details, [see GitHub issue 251](https://github.com/WordPress/wordpress-playground/issues/251).

## Fixing Asyncify crashes

[Pull Request 253](https://github.com/WordPress/wordpress-playground/pull/253) adds a `fix-asyncify` command that runs a specialized test suite and automatically adds any identified missing C functions to the `ASYNCIFY_ONLY` list.
If you run into a crash like the one above, you can fix it by:

1. Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that.
2. Adding a test case that triggers a crash to `packages/php-wasm/node/src/test/php-asyncify.spec.ts`
3. Running: `npm run fix-asyncify`
4. Committing the test case, the updated `Dockerfile`, and the rebuilt `PHP.wasm`

## JSPI: The Modern Alternative to Asyncify

The [JavaScript Promise Integration (JSPI)](https://v8.dev/blog/jspi) API handles stack switching natively in V8, eliminating the need for Asyncify’s function wrapping. WordPress Playground now ships JSPI builds alongside Asyncify builds for all PHP versions (7.4–8.5).
**Current status:**

- The Playground CLI **auto-detects JSPI support** and enables it automatically — no manual flags needed
- Node.js 23+ supports JSPI natively; Node.js 22 requires the `--experimental-wasm-jspi` flag (handled automatically by the CLI)
- Node.js 24+ is expected to have JSPI unflagged
- Browser support varies: JSPI is available in Chrome/Chromium-based browsers behind flags

## Binary Size Optimization with MAIN\_MODULE=2

Both Asyncify and JSPI builds are compiled with Emscripten’s `MAIN_MODULE=2` flag, which performs dead code elimination on exported symbols. Only symbols that dynamic extensions actually need are exported.
**Impact:**

- Total binary size reduced by **122 MB** (13.7%)
- `.wasm` files reduced by **109 MB** (16%)
- JavaScript glue code reduced by **14.5 MB** (63%)

This optimization applies across all PHP versions (7.4–8.5) for both Node.js and Web targets. The exported symbol list is centrally managed in the Dockerfile, with conditional exports for specific extensions (e.g., `__c_longjmp` for Xdebug, `_wasm_recv` for Memcached).

# Compiling PHP

The build pipeline lives in a [`Dockerfile`](https://github.com/WordPress/wordpress-playground/blob/trunk/src/packages/php-wasm/compile/Dockerfile). It was originally forked from [seanmorris/php-wasm](https://github.com/seanmorris/php-wasm)

In broad strokes, that `Dockerfile`:

- Installs all the necessary linux packages (like `build-essential`)
- Downloads PHP and the required libraries, e.g. `sqlite3`.
- Applies a few patches.
- Compiles everything using [Emscripten](https://emscripten.org/), a drop-in replacement for the C compiler.
- Compiles `php_wasm.c` – a convenient API for JavaScript.
- Outputs a `php.wasm` file and one or more JavaScript loaders, depending on the configuration.
- Transforms the Emscripten’s default `php.js` output into an ESM module with additional features.

To find out more about each step, refer directly to the [Dockerfile](https://github.com/WordPress/wordpress-playground/blob/trunk/src/packages/php-wasm/compile/Dockerfile).

### Building

To build all PHP versions, run `nx recompile-php:all php-wasm-web` (or `php-wasm-node`) in the repository root. You’ll find the output files in `packages/php-wasm/php-web/public`. To build a specific version, run `nx recompile-php:all php-wasm-node --PHP_VERSION=8.0 --WITH_JSPI=yes` (and repeat with `--WITH_JSPI=no`).

### PHP next builds

Playground can also run the next PHP version from the php-src development branch in the web runtime. These builds are published separately from the main repository because the generated WebAssembly files are large and change often.

The nightly refresh workflow builds the php-src development branch, writes the web artifacts to the gitignored `packages/playground/website/public/php-next/` directory, and publishes the result to the `php-next-builds` branch. Website deploys and the local dev server sync that branch before serving `?php=next`.

To refresh the local copy manually, run:

```
npm run sync:php-next
```

To rebuild the web artifacts locally from the php-src development branch, run:

```
npm run recompile:php:web:next
```

`php=next` currently ships web main modules only. Matching extension side modules and Playground CLI support are separate follow-up work.

### PHP extensions

PHP is built with several extensions listed in the [`Dockerfile`](https://github.com/WordPress/wordpress-playground/blob/trunk/src/packages/php-wasm/compile/Dockerfile).

Some extensions, like `zip`, can be turned on or off during the build. Others, like `sqlite3`, are hardcoded.

If you need to turn off one of the hardcoded extensions, feel free to open an issue in this repo. Better yet, this project needs contributors. You are more than welcome to open a PR and author the change you need.

PHP.wasm can also load dynamic `.so` extensions before PHP starts. Built-in  
dynamic extensions such as `intl`, `xdebug`, `redis`, and `memcached` are  
distributed with the Node package, and external extensions can be supplied with  
a manifest that selects the artifact matching the active PHP version and async  
mode. See [Loading PHP extensions](https://developer.wordpress.org/playground/developers/apis/javascript-api/php-extensions)  
for the runtime API.

### C API exposed to JavaScript

The C API exposed to JavaScript lives in the [`php_wasm.c`](https://github.com/WordPress/wordpress-playground/blob/trunk/src/packages/php-wasm/compile/build-assets/php_wasm.c) file. The most important functions are:

- `void phpwasm_init()` – It creates a new PHP context and must be called before running any PHP code.
- `int phpwasm_run(char *code)` – Runs a PHP script and writes the output to /tmp/stdout and /tmp/stderr. Returns the exit code.
- `void phpwasm_refresh()` – Destroy the current PHP context and starts a new one. Call it after running one PHP script and before running another.

Refer to the inline documentation in [`php_wasm.c`](https://github.com/WordPress/wordpress-playground/blob/trunk/src/packages/php-wasm/compile/build-assets/php_wasm.c) to learn more.

### Build configuration

The build is configurable via the [Docker `--build-arg` feature](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg). You can set them up through the `build.js` script, just run this command to get the usage message:

```
nx recompile-php php-wasm-web
```

**Supported build options:**

- `PHP_VERSION` – The PHP version to build, default: `8.0.24`. This value must point to an existing branch of the https://github.com/php/php-src.git repository when prefixed with `PHP-`. For example, `7.4.0` is valid because the branch `PHP-7.4.0` exists, but just `7` is invalid because there’s no branch `PHP-7`. The PHP versions that are known to work are `7.4.*` and `8.0.*`. Others likely work as well but they haven’t been tried.
- `EMSCRIPTEN_ENVIRONMENT` – `web` or `node`, default: `web`. The platform to build for. When building for `web`, two JavaScript loaders will be created: `php-web.js` and `php-webworker.js`. When building for Node.js, only one loader called `php-node.js` will be created.
- `WITH_LIBXML` – `yes` or `no`, default: `no`. Whether to build with `libxml2` and the `dom`, `xml`, and `simplexml` PHP extensions (`DOMDocument`, `SimpleXML`, ..).
- `WITH_LIBZIP` – `yes` or `no`, default: `yes`. Whether to build with `zlib`, `libzip`, and the `zip` PHP extension (`ZipArchive`).
- `WITH_NODEFS` – `yes` or `no`, default: `no`. Whether to include [the Emscripten’s NODEFS JavaScript library](https://emscripten.org/docs/api_reference/Filesystem-API.html#filesystem-api-nodefs). It’s useful for loading files and mounting directories from the local filesystem when running php.wasm from Node.js.

# PHP Filesystem

The PHP module has its own filesystem separate from your computer’s filesystem. It is provided by [Emscripten’s FS library](https://emscripten.org/docs/api_reference/Filesystem-API.html) and the default APIs is low-level and cumbersome to use. The `PHP` JavaScript class shipped with WordPress Playground wraps it with a more convenient higher-level API.

In general, WordPress Playground uses an in-memory virtual filesystem.

However, in Node.js, you can also mount a real directory from the host filesystem into the PHP filesystem.

Here’s how to interact with the filesystem in WordPress Playground:

```
// Recursively create a /var/www directory
php.mkdirTree('/var/www');

console.log(php.fileExists('/var/www/file.txt'));
// false

php.writeFile('/var/www/file.txt', 'Hello from the filesystem!');

console.log(php.fileExists('/var/www/file.txt'));
// true

console.log(php.readFile('/var/www/file.txt'));
// "Hello from the filesystem!

// Delete the file:
php.unlink('/var/www/file.txt');
```

For more details consult the BasePHP class directly – it has some great documentation strings.

# PHP.js JavaScript module

### PHP.js JavaScript module

The `php.js` file generated by the [WebAssembly PHP build pipeline](https://developer.wordpress.org/playground/developers/architecture/wasm-php-compiling) is **not** a vanilla Emscripten module. Instead, it’s an ESM module that wraps the regular Emscripten output and adds some extra functionality.

Here’s the API it exposes:

```
// php.wasm size in bytes:
export const dependenciesTotalSize = 5644199;

// php.wasm filename:
export const dependencyFilename = 'php.wasm';

// Run Emscripten's generated module:
export default function (jsEnv, emscriptenModuleArgs) {}
```

The generated JavaScript module is not meant for direct use. Instead, it can be consumed through the `PHP` class:

```
// In Node.js:
const php = new PHP(await loadNodeRuntime('8.0'));

// On the web:
const php = new PHP(await loadWebRuntime('8.0'));
```

Both platform loaders return a runtime ID accepted by the `PHP` class from  
`@php-wasm/universal`, which provides the shared API across environments. See  
[PHP.wasm packages](https://developer.wordpress.org/playground/developers/architecture/php-wasm-packages) for how the  
shared API, platform adapters, and version-specific binaries fit together.

### Loading the PHP runtime

The `loadPHPRuntime()` function handles the PHP initialization pipeline. In  
particular, it:

- Instantiates the Emscripten PHP module
- Wires it together with the data dependencies and loads them
- Ensures it all happens in the correct order
- Waits until the entire loading sequence is finished

# WebAssembly PHP

WordPress Playground build [the PHP interpreter](https://github.com/php/php-src) to WebAssembly using [Emscripten](https://emscripten.org/docs/porting/networking.html) and a [dedicated pipeline](https://github.com/WordPress/wordpress-playground/blob/0d451c33936a8db5b7a158fa8aad288c19370a7d/packages/php-wasm/compile/Dockerfile).
![Building C programs to WebAssembly](https://i0.wp.com/developer.wordpress.org/files/2026/07/c-programs-general-scaled.webp?fit=3200%2C2069&ssl=1)
Building PHP to WebAssembly is very similar to building vanilla PHP. The wasm build required [adjusting a function signature here](https://github.com/WordPress/wordpress-playground/blob/0d451c33936a8db5b7a158fa8aad288c19370a7d/packages/php-wasm/compile/build-assets/php7.1.patch#L8-L9), [forcing a config variable there](https://github.com/WordPress/wordpress-playground/blob/0d451c33936a8db5b7a158fa8aad288c19370a7d/packages/php-wasm/compile/Dockerfile#L495), and applying [a few small patches](https://github.com/WordPress/wordpress-playground/tree/0d451c33936a8db5b7a158fa8aad288c19370a7d/packages/php-wasm/compile/build-assets), but there’s relatively few adjustments involved.
![Building PHP to WebAssembly](https://i0.wp.com/developer.wordpress.org/files/2026/07/c-programs-php-scaled.webp?fit=3200%2C2069&ssl=1)
However, vanilla PHP builds aren’t very useful in the browser. As a server software, PHP doesn’t have a JavaScript API to pass the request body, upload files, or populate the `php://stdin` stream. WordPress Playground had to build one from scratch. The WebAssembly binary comes with a [dedicated PHP API module](https://github.com/WordPress/wordpress-playground/blob/0d451c33936a8db5b7a158fa8aad288c19370a7d/packages/php-wasm/compile/build-assets/php_wasm.c) written in C and a [JavaScript PHP class](https://github.com/WordPress/wordpress-playground/blob/da38192af57a95699d8731c855b82ac0222df61b/packages/php-wasm/common/src/lib/php.ts) that exposes methods like writeFile() or run().
Because every PHP version is just a static .wasm file, the PHP version switcher is actually pretty boring. It simply tells the browser to download, for example, `php_7_3.wasm` instead of, say, `php_8_2.wasm`.
![Building different versions of PHP to WebAssembly](https://i0.wp.com/developer.wordpress.org/files/2026/07/c-programs-php-versions-scaled.webp?fit=3200%2C2069&ssl=1)

### Networking support varies between platforms

When it comes to networking, WebAssembly programs are limited to calling JavaScript APIs. It is a safety feature, but also presents a challenge. How do you support low-level, synchronous networking code used by PHP with the high-level asynchronous APIs available in JavaScript?
In Node.js, the answer involves a WebSocket to TCP socket proxy, [Asyncify](https://emscripten.org/docs/porting/asyncify.html), and patching deep PHP internals like php\_select. It’s complex, but there’s a reward. The Node.js-targeted PHP build can request web APIs, install composer packages, and even connect to a MySQL server.
In the browser, networking is supported in two ways:

- A fast transport for `wp_safe_remote_get` to translate them into `fetch()` calls.
- A slower transport for all other network calls that [parses the TLS transmission](https://github.com/WordPress/wordpress-playground/pull/1926) initiated by PHP and translates it to a `fetch()` call.

# How does it work?

## Database is supported via MySQL\<->SQLite translation layer

WordPress requires MySQL. However, there isn’t a WebAssembly version of MySQL you could run in the browser. WordPress Playground therefore ships PHP with the [native SQLite driver](https://www.php.net/manual/en/ref.pdo-sqlite.php) and leans on SQLite.

But how can WordPress run on a different database?

Behind the scenes, the official [SQLite Database Integration](https://github.com/WordPress/sqlite-database-integration) plugin intercepts all MySQL queries and rewrites them in SQLite dialect. The 2.0 release ships [a new WordPress Playground-informed translation layer](https://github.com/WordPress/sqlite-database-integration/pull/9) that allows WordPress on SQLite to pass 99% of the WordPress unit test suite.

# WordPress support

WordPress, as a PHP application, can run on PHP WebAssembly. However, there are a few caveats.

## SQLite

First, WordPress requires MySQL. However, there isn’t a WebAssembly version of MySQL you could run in the browser. WordPress Playground, therefore, ships PHP with the [native SQLite driver](https://www.php.net/manual/en/ref.pdo-sqlite.php) and leans on SQLite.

But how can WordPress run on a different database?

Behind the scenes, the official [SQLite Database Integration](https://github.com/WordPress/sqlite-database-integration) plugin intercepts all MySQL queries and rewrites them in SQLite dialect. The x.0 release ships [a new WordPress Playground-informed translation layer](https://github.com/WordPress/sqlite-database-integration/pull/9) that allows WordPress on SQLite to pass 99% of the WordPress unit test suite.

## WordPress in the browser

You can use any WordPress build in the browser. For convenience and to reduce the data transfer size, WordPress Playground ships a few [minified WordPress releases](https://github.com/WordPress/wordpress-playground/tree/trunk/packages/playground/wordpress-builds) that you can use in the browser.

## WordPress in Node.js

In Node.js, you’ll typically want to [mount WordPress](https://developer.wordpress.org/playground/developers/architecture/wasm-php-filesystem) from a disk directory.

# Using WordPress Playground in Node.js

As a WebAssembly project, you can also use WordPress Playground in Node.js.

If you need direct control over the underlying WebAssembly PHP runtime, take a  
look at the [@php-wasm/node package](https://npmjs.org/@php-wasm/node). It  
provides the Node.js loader and runtime integrations used by WordPress  
Playground tools. The compiled binaries are published in version-specific  
packages such as `@php-wasm/node-8-4`.

See [PHP.wasm packages](https://developer.wordpress.org/playground/developers/architecture/php-wasm-packages) to learn  
how `@php-wasm/universal`, the Node.js and web adapters, and the version-specific  
packages fit together. That page also explains the lower-level, single-version  
setup with a smaller dependency footprint.

Consult the [complete list](https://developer.wordpress.org/playground/api/node) of Classes, Functions, Interfaces, and Type Aliases.

## WebAssembly PHP for Node.js

Together, `@php-wasm/node` and a version-specific package provide the compiled  
PHP runtime and JavaScript API optimized for Node.js. PHP starts with an  
in-memory filesystem; use the Node.js filesystem helpers to mount host paths.  
The runtime can access the network if you plug in a custom WebSocket-to-TCP  
proxy.

### Basic usage

```
const php = new PHP(await loadNodeRuntime('8.3'));
const output = await php.runStream({
    code: '<?php phpinfo(); ?>',
});
console.log(await output.stdoutText);
```

### Load one PHP version directly

If installation size matters and you only need the shared, low-level PHP API,  
you can omit `@php-wasm/node` and install one Node.js build instead:

```
npm install @php-wasm/universal @php-wasm/node-8-4
```

This approach bypasses Node.js-specific setup such as networking, file locking,  
and extension loading. See  
[Load one PHP version directly](https://developer.wordpress.org/playground/developers/architecture/php-wasm-packages#load-one-php-version-directly)  
for the complete example and tradeoffs.

### Loading PHP extensions

Use the `extensions` loader option to enable optional extensions before PHP  
starts:

```
const php = new PHP(
    await loadNodeRuntime('8.4', {
        extensions: ['intl', 'redis', 'memcached', { name: 'xdebug', options: { ideKey: 'PLAYGROUND' } }],
    })
);
```

The same array can load external JSPI `.so` artifacts from a manifest:

```
const php = new PHP(
    await loadNodeRuntime('8.4', {
        extensions: [
            {
                source: {
                    format: 'manifest',
                    manifestUrl: './dist/wp_mysql_parser/manifest.json',
                },
            },
        ],
    })
);
```

External extensions require JSPI. Asyncify support is limited to the bundled  
extensions shipped with the PHP.wasm packages.

See [Loading PHP extensions](https://developer.wordpress.org/playground/developers/apis/javascript-api/php-extensions)  
for manifest format, browser usage, sidecar files, and compatibility notes.

## Use cases

Run PHP inside Node.js without a native PHP install. Allow developer to produce the following solutions:

- CI/CD jobs and developer tooling.
- Support education and WordPress workflows: Power interactive tutorials, sandboxes, and coding challenges.
- Generate content and prototype server behavior.
- Render HTML using PHP templates, and quickly stand up mock API endpoints to simulate requests.

## Practical demos

We will list some examples using the PHP-WASM package.

### Demo 1: File system operations

Execute PHP scripts that interact with the file system:

```
const php = new PHP(await loadNodeRuntime('8.3'));

// Create directory structure
php.mkdir('/app/data');

// Write configuration file
await php.writeFile(
    '/app/config.json',
    JSON.stringify({
        app: 'MyApp',
        version: '1.0.0',
        debug: true,
    })
);

// Create and run PHP script that reads the config
await php.writeFile(
    '/app/index.php',
    `<?php
$config = json_decode(file_get_contents('/app/config.json'), true);
echo "Application: " . $config['app'] . "\\n";
echo "Version: " . $config['version'] . "\\n";
echo "Debug Mode: " . ($config['debug'] ? 'ON' : 'OFF') . "\\n";

// List all files
echo "\\nFiles in /app:\\n";
foreach (scandir('/app') as $file) {
    if ($file !== '.' && $file !== '..') {
        echo "  - $file\\n";
    }
}
?>`
);

const result = await php.runStream({ scriptPath: '/app/index.php' });
console.log(await result.stdoutText);
```

### Demo 2: SQLite database operations

Use PHP’s SQLite extension for data storage:

```
const php = new PHP(await loadNodeRuntime('8.3'));

// Create directory for database
php.mkdir('/data');

// Create database, insert data, and query
const result = await php.runStream({
    code: `<?php
// Create/connect to SQLite database
$db = new SQLite3('/data/app.db');

// Create table
$db->exec('CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)');

// Insert sample data
$stmt = $db->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$users = [
    ['Alice Johnson', 'alice@example.com'],
    ['Bob Smith', 'bob@example.com'],
    ['Charlie Davis', 'charlie@example.com']
];

foreach ($users as $user) {
    $stmt->bindValue(1, $user[0]);
    $stmt->bindValue(2, $user[1]);
    $stmt->execute();
}

// Query data
echo "All Users:\\n";
echo str_repeat('-', 50) . "\\n";
$results = $db->query('SELECT * FROM users ORDER BY name');
while ($row = $results->fetchArray(SQLITE3_ASSOC)) {
    echo "ID: {$row['id']} | {$row['name']} ({$row['email']})\\n";
}

$db->close();
?>`,
});

console.log(await result.stdoutText);

// Database file persists in the virtual file system
const dbExists = await php.fileExists('/data/app.db');
console.log('\nDatabase persisted:', dbExists);
```

### Demo 3: Processing uploaded files (ZIP archives)

Process ZIP files using PHP’s Libzip extension:

```
const php = new PHP(await loadNodeRuntime('8.3'));

// Create sample files
php.mkdir('/uploads');
await php.writeFile('/uploads/readme.txt', 'This is a sample text file');
await php.writeFile('/uploads/data.json', JSON.stringify({ name: 'Test', version: '1.0' }));

// Create, process, and extract ZIP archive
const result = await php.runStream({
    code: `<?php
// Create ZIP archive
$zip = new ZipArchive();
$zip->open('/uploads/archive.zip', ZipArchive::CREATE);
$zip->addFromString('readme.txt', file_get_contents('/uploads/readme.txt'));
$zip->addFromString('data.json', file_get_contents('/uploads/data.json'));
$zip->addFromString('info.txt', 'Created with PHP WASM');
$zip->close();

echo "ZIP archive created successfully\\n\\n";

// Read and display archive contents
$zip->open('/uploads/archive.zip');
echo "Archive Contents:\\n";
echo str_repeat('=', 50) . "\\n";

for ($i = 0; $i < $zip->numFiles; $i++) {
    $stat = $zip->statIndex($i);
    $size = round($stat['size'] / 1024, 2);
    echo sprintf("%-40s %10s KB\\n", $stat['name'], $size);
}

// Extract files
$zip->extractTo('/uploads/extracted/');
$zip->close();

echo "\\nExtracted successfully to /uploads/extracted/\\n";

// List extracted files
echo "\\nExtracted Files:\\n";
$files = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator('/uploads/extracted/')
);
foreach ($files as $file) {
    if ($file->isFile()) {
        echo "  " . $file->getPathname() . "\\n";
    }
}
?>`,
});

console.log(await result.stdoutText);
```

### Demo 4: HTTP request/response pattern

Simulate web server behavior with request handlers:

```
const php = new PHP(await loadNodeRuntime('8.3'));

// Set up a simple API endpoint
await php.mkdir('/www/api');
await php.writeFile(
    '/www/api/users.php',
    `<?php
header('Content-Type: application/json');

// Parse request
$method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true);

// Simple routing
switch ($method) {
    case 'GET':
        echo json_encode([
            'users' => [
                ['id' => 1, 'name' => 'John Doe'],
                ['id' => 2, 'name' => 'Jane Smith']
            ]
        ]);
        break;

    case 'POST':
        $name = $input['name'] ?? 'Unknown';
        echo json_encode([
            'success' => true,
            'user' => [
                'id' => 3,
                'name' => $name
            ],
            'message' => "User $name created"
        ]);
        break;

    default:
        http_response_code(405);
        echo json_encode(['error' => 'Method not allowed']);
}
?>`
);

// Make GET request
const getResponse = await php.runStream({
    scriptPath: '/www/api/users.php',
    env: {
        REQUEST_METHOD: 'GET',
        SERVER_NAME: 'localhost',
        SERVER_PORT: '80',
    },
});
console.log('GET Response:', await getResponse.stdoutText);

// Make POST request
const postResponse = await php.runStream({
    scriptPath: '/www/api/users.php',
    env: {
        REQUEST_METHOD: 'POST',
        SERVER_NAME: 'localhost',
        SERVER_PORT: '80',
    },
    body: JSON.stringify({ name: 'Alice Wonder' }),
});
console.log('\\nPOST Response:', await postResponse.stdoutText);
```

### Demo 5: Template rendering engine

Use PHP as a templating engine for dynamic content:

```
const php = new PHP(await loadNodeRuntime('8.3'));

// Create templates directory
php.mkdir('/templates');

// Create template
await php.writeFile(
    '/templates/email.php',
    `<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; }
        .header { background: #4CAF50; color: white; padding: 20px; }
        .content { padding: 20px; }
        .footer { background: #f1f1f1; padding: 10px; text-align: center; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Welcome, <?= htmlspecialchars($name) ?>!</h1>
    </div>
    <div class="content">
        <p>Thank you for registering with <?= $appName ?>.</p>
        <p>Your account details:</p>
        <ul>
            <li><strong>Email:</strong> <?= htmlspecialchars($email) ?></li>
            <li><strong>Member Since:</strong> <?= date('F j, Y', $timestamp) ?></li>
        </ul>
        <p>You now have access to the following features:</p>
        <ul>
            <?php foreach ($features as $feature): ?>
                <li><?= htmlspecialchars($feature) ?></li>
            <?php endforeach; ?>
        </ul>
    </div>
    <div class="footer">
        <p>&copy; <?= date('Y') ?> <?= $appName ?>. All rights reserved.</p>
    </div>
</body>
</html>`
);

// Render template with data
const templateData = {
    name: 'Priya Sharma',
    email: 'priya@example.com',
    appName: 'MyAwesomeApp',
    timestamp: Math.floor(Date.now() / 1000),
    features: ['Dashboard Access', 'API Integration', 'Premium Support', 'Custom Branding'],
};

// Pass data to template via environment variables or files
await php.writeFile('/template-data.json', JSON.stringify(templateData));

const result = await php.runStream({
    code: `<?php
    $data = json_decode(file_get_contents('/template-data.json'), true);
    extract($data);
    include '/templates/email.php';
  ?>`,
});

console.log(await result.stdoutText);
// Now you have rendered HTML that can be sent via email or saved
```

### Demo 6: Real-time code execution and streaming

Process PHP output as it’s generated:

```
const php = new PHP(await loadNodeRuntime('8.3'));

await php.writeFile(
    '/stream-demo.php',
    `<?php
// Simulate long-running process
echo "Starting process...\\n";
flush();

for ($i = 1; $i <= 10; $i++) {
    echo "Processing item $i/10...\\n";
    flush();
    usleep(100000); // Sleep 100ms
}

echo "Process complete!\\n";
?>`
);

// Run PHP script
const streamedResponse = await php.runStream({
    scriptPath: '/stream-demo.php',
});

streamedResponse.stdout.pipeTo(
    new WritableStream({
        write(chunk) {
            console.log(chunk);
        },
    })
);
```

## Integration patterns

### Pattern 1: Express.js middleware

Integrate PHP processing into an Express.js application:

```
const app = express();
const php = new PHP(await loadNodeRuntime('8.3'));

// PHP execution middleware
app.use('/php', async (req, res, next) => {
    try {
        const phpScript = req.query.script || 'index.php';
        const result = await php.runStream({
            scriptPath: `/www/${phpScript}`,
            env: {
                REQUEST_METHOD: req.method,
                QUERY_STRING: new URLSearchParams(
                    req.query as Record<string, string>
                ).toString(),
                REQUEST_URI: req.url,
            },
        });

        res.send(await result.stdoutText);
    } catch (error) {
        next(error);
    }
});

app.listen(3000, () => {
    console.log('Server with PHP support running on port 3000');
});
```

### Pattern 2: Automated testing

Create automated tests for PHP code:

```
describe('PHP Functions', () => {
    let php: PHP;

    beforeAll(async () => {
        php = new PHP(await loadNodeRuntime('8.3'));
    });

    it('should calculate sum correctly', async () => {
        const result = await php.run({
            code: `<?php
        function sum($a, $b) {
          return $a + $b;
        }
        echo sum(5, 3);
      ?>`,
        });

        expect(result.text).toBe('8');
    });

    it('should handle JSON operations', async () => {
        const input = { name: 'Test', value: 42 };
        const result = await php.run({
            code: `<?php
        $input = json_decode('${JSON.stringify(input)}', true);
        $output = [
          'received' => $input,
          'doubled' => $input['value'] * 2
        ];
        echo json_encode($output);
      ?>`,
        });

        const output = JSON.parse(result.text);
        expect(output.doubled).toBe(84);
    });
});
```

### Pattern 3: Build tool integration

Use in build scripts with other Node.js tools:

```
async function generateDocumentation() {
    const php = new PHP(await loadNodeRuntime('8.3'));

    // Create output directory
    php.mkdir('/output');

    // Generate documentation
    const result = await php.runStream({
        code: `<?php
echo "Generating documentation...\\n";

$summary = "# Generated Documentation\\n\\n";
$summary .= "Generated at: " . date('Y-m-d H:i:s') . "\\n\\n";

file_put_contents('/output/summary.md', $summary);
echo "Documentation generated successfully!\\n";
?>`,
    });

    console.log(await result.stdoutText);

    // Extract generated docs back to Node.js file system
    await fs.mkdir('./docs', { recursive: true });
    const summaryContent = await php.readFileAsText('/output/summary.md');
    await fs.writeFile('./docs/summary.md', summaryContent);

    console.log('Documentation saved to ./docs/summary.md');
}

generateDocumentation().catch(console.error);
```

## Advanced features

### Working with environment variables

```
const php = new PHP(await loadNodeRuntime('8.3'));

const result = await php.runStream({
    code: '<?php echo getenv("CUSTOM_VAR"); ?>',
    env: {
        CUSTOM_VAR: 'Hello from Node.js!',
    },
});

console.log(await result.stdoutText);
```

### Error handling

```
const php = new PHP(await loadNodeRuntime('8.3'));

try {
    const result = await php.runStream({
        code: '<?php trigger_error("Test error", E_USER_ERROR); ?>',
    });

    const stdout = await result.stdoutText;
    const stderr = await result.stderrText;

    console.log('stdout:', stdout);
    console.log('stderr:', stderr);

    if (stderr) {
        console.error('PHP produced errors:', stderr);
    }
} catch (error: any) {
    console.error('JavaScript Error:', error.message);
}
```

## Performance considerations

- **Reuse PHP instances**: Creating a new PHP instance is expensive. Reuse the same instance when possible.
- **Batch operations**: Group multiple file operations together rather than running separate scripts.
- **Memory management**: Large files may impact performance. Consider streaming for big datasets.
- **Caching**: Cache compiled PHP scripts and frequently accessed data.

# VS Code extension

Start a zero-setup development environment using the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground), and develop your plugin or theme locally without installing Apache or MySQL.

Key Features:

- **Integrated Development**: Develop WordPress sites directly within VS Code.
- **Ease of Use**: Simplifies the development workflow with integrated tools.

**Documentation**

The VS Code extension is maintained in a different GitHub repository, [Playground Tools](https://github.com/WordPress/playground-tools/). You can find the latest documentation in the [dedicated README file](https://github.com/WordPress/playground-tools/blob/trunk/packages/vscode-extension/README.md).

## Installation and Usage:

1. **Install the Extension**: Search for “WordPress Playground” in the VS Code extensions marketplace and install it.
2. **Setup**: Follow the setup instructions provided in the extension to configure your development environment.
3. **Develop and Debug**: Use the integrated tools to develop and debug your WordPress site.

The extension ships with a portable WebAssembly version of PHP and sets up WordPress to use SQLite. Once installed, all you have to do is click the **Start WordPress Server** button in VS Code:

# Playground CLI

[@wp-playground/cli](https://www.npmjs.com/package/@wp-playground/cli) is a command-line tool that simplifies the WordPress development and testing flow.
Playground CLI supports auto-mounting a directory with a plugin, theme, or WordPress installation. But if you need flexibility, the CLI supports mounting commands to personalize your local environment.
**Key features:**

- **Quick Setup**: Set up a local WordPress environment in seconds.
- **Flexibility**: Allows for configuration to adapt to different scenarios.
- **Simple Environment**: No extra configuration, just a compatible Node version, and you are ready to use it.

The Playground CLI includes two main commands for running WordPress locally:

- **`start`** (Simplified): Auto-detects your project type, persists sites between sessions, and opens a browser automatically.
- **`server`** (Advanced): Provides full manual control over configuration. Best for custom setups, CI/CD pipelines, or when you need fine-grained control.

## Requirements

The Playground CLI requires Node.js 20.18 or higher, which is the recommended Long-Term Support (LTS) version. You can download it from the [Node.js website](https://nodejs.org/en/download).

## Quickstart

To run the Playground CLI, open a command line and use one of the following commands:

### Using `start` (Simplified)

The `start` command is the easiest way to get started. It automatically detects your project type, persists your site, and opens the browser:

```
npx @wp-playground/cli@latest start
```

When run inside a plugin or theme directory, `start` automatically mounts your project:

```
cd my-plugin
npx @wp-playground/cli@latest start
```

**Key differences from `server`:**

- Auto-login is enabled by default
- Opens browser automatically
- Auto-mounts the project by default

## Migrating from wp-now

The deprecated `@wp-now/wp-now` package maps most directly to the `start`
command:

| wp-now | Playground CLI |
| --- | --- |
| `npx @wp-now/wp-now start` | `npx @wp-playground/cli@latest start` |
| `npx @wp-now/wp-now start --path=./plugin` | `cd ./plugin && npx @wp-playground/cli@latest start` |
| `npx @wp-now/wp-now start --wp=6.8 --php=8.3` | `npx @wp-playground/cli@latest start --wp=6.8 --php=8.3` |
| `npx @wp-now/wp-now start --blueprint=./blueprint.json` | `npx @wp-playground/cli@latest start --blueprint=./blueprint.json` |
| `npx @wp-now/wp-now start --skip-browser` | `npx @wp-playground/cli@latest start --skip-browser` |
| `npx @wp-now/wp-now start --reset` | `npx @wp-playground/cli@latest start --reset` |

The main workflow change is where the saved site lives:

- With `wp-now`, `--path=./plugin` picked the project and the saved site.
- With Playground CLI, `start` saves the site for the current directory. For
  the closest match, `cd` into the project first, then run `start`.
- When Playground CLI creates WordPress for you, it keeps the WordPress files
  in `~/.wordpress-playground/sites/<path-hash>/`.
- If you run it on a full WordPress directory, or mount a directory at
  `/wordpress`, that directory is the WordPress site. Changes are written
  there.
- `start --path=./plugin` still mounts that folder, but it does not make
  `./plugin` the saved site. The saved site still belongs to the directory
  where you ran the command.

Use `start` for the familiar wp-now-style flow. Use `server` only when you want
to spell out mounts, storage, or automation yourself.

### Using `server` (Advanced)

The `server` command provides full control over configuration:

```
npx @wp-playground/cli@latest server
```

![Playground CLI in Action](https://i0.wp.com/developer.wordpress.org/files/2026/07/npx-wp-playground-server.gif?fit=915%2C588&ssl=1)
**Automatic site persistence:** When the `start` command manages the WordPress
root directory, it keeps your WordPress site persistent across sessions. Your
files and database are stored in `~/.wordpress-playground/sites/<path-hash>/`,
where `<path-hash>` is derived from the command’s current working directory. If
you start from a full WordPress installation, or explicitly mount `/wordpress`,
that mounted directory becomes the persistent store instead.
This is useful when:

- You want a clean WordPress installation
- Testing fresh installation scenarios
- Your site data became corrupted or inconsistent

The `--reset` flag works only with `start`. For `server`, manually delete the persisted site directory at `~/.wordpress-playground/sites/<path-hash>/`.

### Choosing a WordPress and PHP Version

By default, the CLI loads the latest stable version of WordPress and PHP 8.3 due to its improved performance. To specify your preferred versions, you can use the flag `--wp=<version>` and `--php=<version>`:

```
npx @wp-playground/cli@latest server --wp=6.8 --php=8.4
```

### Loading Blueprints

One way to take your Playground CLI development experience to the next level is to integrate with [Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started/). For those unfamiliar with this technology, it allows developers to configure the initial state for their WordPress Playground instances.
Using the `--blueprint=<blueprint-address>` flag, developers can run a Playground with a custom initial state. We’ll use the example below to do this.
**(my-blueprint.json)**

```
{
  "landingPage": "/wp-admin/options-general.php?page=akismet-key-config",
  "login": true,
  "plugins": [
    "hello-dolly",
    "https://raw.githubusercontent.com/adamziel/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
  ]
}
```

CLI command loading a blueprint:

```
npx @wp-playground/cli@latest server --blueprint=my-blueprint.json
```

### Mounting folders manually

Some projects have a specific structure that requires a custom configuration; for example, your repository contains all the files in the `/wp-content/` folder. So in this scenario, you can specify to the Playground CLI that it will mount your project from that folder using the `--mount` flag.

```
npx @wp-playground/cli@latest server --mount=.:/wordpress/wp-content/plugins/MY-PLUGIN-DIRECTORY
```

### Mounting before WordPress installation

Consider mounting your WordPress project files before the WordPress installation begins. This approach is beneficial if you want to override the Playground boot process, as it can help connect Playground with `WP-CLI`. The `--mount-before-install` flag supports this process.

```
npx @wp-playground/cli@latest server --mount-before-install=.:/wordpress/
```

On Windows, the path format `/host/path:/vfs/path` can cause issues. To resolve this, use the flags `--mount-dir` and `--mount-dir-before-install`. These flags let you specify host and virtual file system paths in an alternative format: `"/host/path"` `"/vfs/path"`.

### Understanding Data Persistence and SQLite Location in `server` mode

By default, Playground CLI stores WordPress files and the SQLite database in **temporary directories on your operating system**:

```
<OS-TEMP-DIR>/playground-<random-id>/
├── wordpress/          # WordPress installation
├── internal/          # Playground runtime config
└── tmp/              # Temporary PHP files
```

**Finding Your Temp Directory:**
The actual location depends on your OS (these are examples or common possibilities):

- **macOS/Linux**: May be under `/tmp/` or `/private/var/folders/` (varies by system)
- **Windows**: `C:\Users\<username>\AppData\Local\Temp\`

To see the exact temp directory path being used, run the CLI with the `--verbosity=debug` flag:

```
npx @wp-playground/cli@latest server --verbosity=debug
```

This will output something like:

```
Native temp dir for VFS root:
/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC
Mount before WP install: /home ->
/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/home
Mount before WP install: /tmp ->
/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/tmp
Mount before WP install: /wordpress ->
/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/wordpress
```

**Where is the SQLite Database Stored?**
The database location depends on what you mount:

- **Auto-mounting wp-content or full WordPress**:
  - Database: `<your-local-project>/wp-content/database/.ht.sqlite`
  - ✅ **Persisted locally** in your project folder
- **Auto-mounting plugin/theme only**:
  - Database: `<OS-TEMP-DIR>/playground-<id>/wordpress/wp-content/database/.ht.sqlite`
  - ⚠️ **Lost when server stops** (temp directories are cleaned up)
- **Custom mounts**: Database location follows your mount configuration

**Automatic Cleanup:**
Playground CLI automatically removes temp directories that are:

- Older than 2 days
- No longer associated with a running process

**Recommendation:** To persist both your code and database when developing plugins or themes, mount the entire `wp-content` directory instead of just the plugin/theme folder.
**Example: Mounting wp-content for persistence**

```
# Mount your entire wp-content directory
cd my-wordpress-project
npx @wp-playground/cli@latest server --mount=./wp-content:/wordpress/wp-content
```

### Data Persistence in `start` mode

When `start` manages the WordPress root directory, Playground CLI
**automatically persists** your WordPress site in a dedicated directory:

```
~/.wordpress-playground/sites/<path-hash>/
├── wordpress/          # WordPress installation
├── internal/          # Playground runtime config
└── tmp/              # Temporary PHP files
```

The `<path-hash>` is derived from the command’s current working directory.
This ensures isolation between different projects when you run `start` from each
project directory.

#### Persistence behavior

- **Default (no WordPress root mount)**: WordPress files and database persist in `~/.wordpress-playground/sites/<path-hash>/`. Changes survive between CLI restarts.
- **Full WordPress directory or explicit `/wordpress` mount**: Automatic persistence in `~/.wordpress-playground/sites/<path-hash>/` is skipped. The mounted WordPress directory is the persistent store.

The database location depends on your configuration:

- **Default (automatic persistence)**:
  - Database: `~/.wordpress-playground/sites/<path-hash>/wordpress/wp-content/database/.ht.sqlite`
  - **Persisted automatically** between sessions
- **Full WordPress directory or explicit `/wordpress` mount**:
  - Database: Follows the mounted WordPress directory
  - **Persisted in that mounted directory**

#### Resetting a persisted site

To start fresh, use the `--reset` flag with the `start` command:

```
npx @wp-playground/cli@latest start --reset
```

## Command and Arguments

Playground CLI is simple, configurable, and unopinionated. You can set it up according
to your unique WordPress setup. With the Playground CLI, you can use the following top-level commands:

- **`start`**: (Simplified) Starts a local WordPress server with automatic project detection, site persistence, and browser opening.
- **`server`**: (Advanced) Starts a local WordPress server with full manual control over configuration.
- **`run-blueprint`**: Executes a Blueprint file without starting a web server.
- **`build-snapshot`**: Builds a ZIP snapshot of a WordPress site based on a Blueprint.

The `start` command supports these common optional arguments. Run
`npx @wp-playground/cli@latest start --help` for the full list:

- `--path=<path>`: Path to the project directory. Defaults to the current working directory.
- `--wp=<version>`: WordPress version to use. Defaults to the latest.
- `--php=<version>`: PHP version to use. Defaults to PHP 8.3.
- `--port=<port>`: The port number for the server to listen on. Defaults to 9400 when available.
- `--blueprint=<path>`: The path to a JSON Blueprint file to execute.
- `--login`: Automatically log the user in as an administrator. Defaults to true.
- `--skip-browser`: Do not open the site in your default browser.
- `--reset`: Delete the stored site and start fresh. Defaults to false.
- `--no-auto-mount`: Disable automatic project detection.

The `server` command supports these common optional arguments. Run `npx @wp-playground/cli@latest server --help` for the full list:

- `--port=<port>`: The port number for the server to listen on. Defaults to 9400.
- `--version`: Show version number.
- `--site-url=<url>`: Site URL to use for WordPress. Defaults to `http://127.0.0.1:{port}`.
- `--wp=<version>`: The version of WordPress to use. Defaults to the latest.
- `--php=<version>`: PHP version to use. Choices: `8.5`, `8.4`, `8.3`, `8.2`, `8.1`, `8.0`, `7.4`. Defaults to `8.3`.
- `--auto-mount[=<path>]`: Automatically mount a directory. If no path is provided, mounts the current working directory. You can mount a WordPress directory, a plugin directory, a theme directory, a wp-content directory, or any directory containing PHP and HTML files.
- `--mount=<mapping>`: Manually mount a directory (can be used multiple times). Format: `"/host/path:/vfs/path"`.
- `--mount-before-install`: Mount a directory to the PHP runtime before WordPress installation (can be used multiple times). Format: `"/host/path:/vfs/path"`.
- `--mount-dir`: Mount a directory to the PHP runtime (can be used multiple times). Format: `"/host/path"` `"/vfs/path"`.
- `--mount-dir-before-install`: Mount a directory before WordPress installation (can be used multiple times). Format: `"/host/path"` `"/vfs/path"`
- `--blueprint=<path>`: The path to a JSON Blueprint file to execute.
- `--blueprint-may-read-adjacent-files`: Consent flag: Allow “bundled” resources in a local blueprint to read files in the same directory as the blueprint file.
- `--login`: Automatically log the user in as an administrator.
- `--wordpress-install-mode <mode>`: Control how Playground prepares WordPress before booting. Defaults to `download-and-install`. Other options: `install-from-existing-files` (install using files you’ve mounted), `install-from-existing-files-if-needed` (skip setup when an existing site is detected), and `do-not-attempt-installing` (never download or install WordPress).
- `--skip-sqlite-setup`: Do not set up the SQLite database integration.
- `--verbosity=<level>`: Output logs and progress messages. Choices: `quiet`, `normal`, `debug`. Defaults to `normal`.
- `--debug`: Print the PHP error log if an error occurs during boot.
- `--follow-symlinks`: Allow Playground to follow symlinks by automatically mounting symlinked directories and files encountered in mounted directories.
- `--internal-cookie-store`: Enable internal cookie handling. When enabled, Playground will manage cookies internally using an HttpCookieStore that persists cookies across requests. When disabled, cookies are handled externally (e.g., by a browser in Node.js environments). Defaults to false.
- `--phpmyadmin[=<path>]`: Install phpMyAdmin for database management. The phpMyAdmin URL will be printed after boot. Optionally specify a custom URL path (default: `/phpmyadmin`).
- `--xdebug`: Enable Xdebug. Defaults to false.
- `--php-extension=<manifest>`: Load a PHP.wasm extension manifest before PHP starts. Accepts local paths, `file:` URLs, and HTTP(S) URLs. Can be used multiple times.
- `--experimental-devtools`: Enable experimental browser development tools. Defaults to false.
- `--experimental-unsafe-ide-integration=<ide>`: Set up the Xdebug integration on VS Code (`vscode`) and PhpStorm (`phpstorm`).
- `--workers=<n|auto>`: Number of request-handling worker threads. Pass a positive integer, or `auto` to use one worker per CPU core (minus one). Defaults to `min(6, cpus-1)`. Useful for multi-client workloads (e.g. parallel e2e suites) that need more than 6 in-flight requests.
- `--experimental-multi-worker=<number>`: Deprecated. Use `--workers=<n|auto>` instead. The value of this flag is ignored.

### Loading PHP.wasm extensions

Playground CLI can load external PHP.wasm extensions before PHP starts. Pass a
manifest produced by `@php-wasm/compile-extension`, or a published manifest such
as the SQLite Database Integration native parser:

```
npx @wp-playground/cli@latest server \
    --php=8.5 \
    --php-extension=https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json \
    --blueprint=https://wordpress.github.io/sqlite-database-integration/blueprint.json
```

For local development, point the flag at a local manifest:

```
npx @wp-playground/cli@latest server \
    --php=8.5 \
    --php-extension=./dist/wp_mysql_parser/manifest.json
```

`--php-extension` is repeatable. The manifest selects the `.so` artifact
matching the active PHP version and can also stage sidecar files, `.ini`
settings, and environment variables before startup.
External extensions are JSPI side modules. If your Node.js build does not expose
JSPI, the CLI rejects the extension request during startup. Use a Node.js build
with JSPI support when testing custom extensions locally.
See [Loading PHP extensions](https://developer.wordpress.org/playground/developers/apis/javascript-api/php-extensions)
and
[Building PHP extensions](https://developer.wordpress.org/playground/developers/apis/javascript-api/build-php-extensions)
for manifest format, compiler helpers, and dependency notes.

With the flag `--follow-symlinks`, the following symlinks will expose files outside mounted directories to Playground and could be a security risk.

## Need some help with the CLI?

With the Playground CLI, you can use the `--help` flag to get the full list of available commands and arguments.

```
npx @wp-playground/cli@latest --help
```

## Programmatic usage

The Playground CLI can also be controlled programmatically from JavaScript/TypeScript
using the `runCLI` function. See the [Programmatic Usage guide](https://developer.wordpress.org/playground/guides/programmatic-playground-cli)
for details on automation and testing.

# Getting Started with Xdebug

This guide shows you how to enable Xdebug in WordPress Playground and start debugging your code.

## PHP WASM CLI vs Playground CLI

First, Xdebug is present in two different CLIs:

- **`@php-wasm/cli`**: Run standalone PHP scripts. Use this when debugging PHP code without needing a WordPress environment.
- **`@wp-playground/cli`**: Run a full WordPress installation. Useful for debugging WordPress plugins, themes, or core functionality.

For this guide, we’ll use `@wp-playground/cli`. If you’re not familiar with the tool, we recommend reading the [`@wp-playground/cli` guide](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli), but the same process can also be applied to debugging PHP applications with `@php-wasm/cli`.

## Quick start with `npx`

The fastest way to get started is using npx, which doesn’t require installation:

```
npx @wp-playground/cli@latest server --xdebug
```

This starts WordPress on `http://127.0.0.1:9400` with Xdebug enabled. Now you can connect a debugger.

Only one project can be debugged at a time.

## Starting with IDE integration

Similar to the process with DevTools, let’s use the same plugin code from before to debug with VS Code, and add the `--experimental-unsafe-ide-integration=vscode` flag. This flag will optimize the setup process for VS Code. If you’re working with PhpStorm, add the `--experimental-unsafe-ide-integration=phpstorm` flag.

This flag is marked as `unsafe` because it edits the IDE config files to set Xdebug path mappings and web server details. **CAUTION:** If there are bugs, this feature may cause your IDE configuration files to break. Please consider backing up your IDE configs before using this feature.

To debug in VS Code, you’ll need the following prerequisites:

1. An extension to add PHP profiling support, for example, [PHP Debug](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) or another debugger extension to your preference.
2. Have a `.vscode/` folder.
3. Enable breakpoints in your IDE. Some IDEs come with this feature disabled, so be aware of this detail.

If everything is ready, you run the command:

```
npx @wp-playground/cli@latest server --xdebug --experimental-unsafe-ide-integration=vscode --auto-mount
```

If you don’t have a `.vscode/launch.json` file, the terminal will create a file similar to this:

```
{
    "configurations": [
        {
            "name": "WP Playground CLI - Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "/": "${workspaceFolder}/.playground-xdebug-root",
                "/wordpress/wp-content/plugins/test-xdebug": "${workspaceFolder}/"
            }
        }
    ]
}
```

Now, you can go to your code, add the breakpoints, start the debugging session named by your IDE, and happy testing.
![Xdebug in action on VS Code](https://i0.wp.com/developer.wordpress.org/files/2026/08/xdebug-in-action-on-vscode.webp?fit=2940%2C2044&ssl=1)

## Early Access: DevTools debugger

Playground CLI ships an **unstable, highly exploratory** workflow that enables debugging PHP programs using Chrome DevTools.
To try it, use the `--experimental-devtools` flag:

```
npx @wp-playground/cli@latest server --xdebug --experimental-devtools
```

The terminal will display a URL to connect to Chrome DevTools:

```
Starting a PHP server...
Setting up WordPress latest
Resolved WordPress release URL: https://downloads.w.org/release/wordpress-6.8.3.zip
Fetching SQLite integration plugin...
Booting WordPress...
WordPress is running on http://127.0.0.1:9400 with 1 worker(s)
Starting XDebug Bridge...
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229

Chrome connected! Initializing Xdebug receiver...
XDebug receiver running on port 9003
Running a PHP script with Xdebug enabled...
```

By clicking on the provided URL, for example, `devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229`, you can access DevTools connected to your application, with the ability to inspect all files of a WordPress instance.
![Chrome Devtools integrated with Xdebug](https://i0.wp.com/developer.wordpress.org/files/2026/08/playground-xdebug-on-devtools-scaled.webp?fit=3200%2C2075&ssl=1)
For a more practical example, let’s debug a plugin that has the following code:

```
<?php
/**
 * Plugin Name: Simple Admin Message
 * Description: Displays a simple message in the WordPress admin
 * Version: 1.0
 * Author: Playground Team
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

// Display admin notice
function sam_display_admin_message() {
    $message = 'Hello! This is a simple admin message.';
    ?>
    <div class="notice notice-info is-dismissible">
        <p><?php _e($message, 'simple-admin-message'); ?></p>
    </div>
    <?php
}
add_action('admin_notices', 'sam_display_admin_message');
```

In the folder where the plugin is located, let’s run the command in our terminal:

```
npx @wp-playground/cli@latest server --xdebug --experimental-devtools --auto-mount
```

The Playground CLI(`@wp-playground/cli`) will automatically detect the plugin folder and mount it. Opening the project in your browser and DevTools, you’ll be able to add breakpoints in your plugin’s code and test it line by line.
![Chrome Devtools integrated with Xdebug](https://i0.wp.com/developer.wordpress.org/files/2026/08/playground-cli-running-xdebug-on-devtools.webp?fit=3104%2C2024&ssl=1)
This feature is in experimental mode. Until it’s finished, we’ll need your feedback. Please connect with us in the [#playground Slack channel](https://wordpress.slack.com/archives/C04EWKGDJ0K) and share your thoughts.

# About WordPress Playground

## What is WordPress Playground?

**WordPress Playground is the platform that lets you run WordPress instantly on any device without a host**. It allows you to experiment and learn about WordPress without affecting your live website. It’s a virtual sandbox where you can play around with different features, designs, and settings in a safe and controlled environment.

WordPress Playground is your place to build, test, and launch:

- [Build](https://developer.wordpress.org/playground/about/build): WordPress Playground can help you to build products with WordPress. Use it from where you work best, whether that’s in the browser, Node.js, mobile apps, VS Code, or elsewhere.
- [Test](https://developer.wordpress.org/playground/about/test): Upgrade your QA process with WordPress Playground. Quickly test your plugins or themes, experiment in a private sandbox, and create PRs from your WP Playground instance to any repo.
- [Launch](https://developer.wordpress.org/playground/about/launch): Use WordPress Playground to showcase your product, let users try it live, or launch it in the App Store with zero lead time.

## Why WordPress Playground?

### Try themes and plugins on the fly

With WordPress Playground, you can explore any [theme](https://developer.wordpress.org/themes/getting-started/what-is-a-theme/). You can choose from a wide range of themes and see how they look on your site. You can also modify the colors, fonts, layouts, and other visual elements to create a unique design.

In addition to themes, you can experiment with plugins too. With WordPress Playground, you can install and test different plugins to see how they work and what they can do for your site. This allows you to explore and understand the capabilities of WordPress without worrying about breaking anything.

### Create content on the go

Another great feature of WordPress Playground is the ability to create and edit content. You can write blog posts, create pages, and add media like images and videos to your site. This helps you understand how to organize and structure your content effectively.

The content you create is limited to the Playground on your device and disappears once you leave it, so you are free to explore and play without risking breaking any actual site.

But hey! You can also connect your Playground instance to a GitHub repo and create a PR to persist those changes.

### It’s super safe

Overall, WordPress Playground provides a risk-free environment for beginners to learn and get hands-on experience with WordPress. It helps you to gain confidence and knowledge before making changes to your live website.

Check the [guides section](https://developer.wordpress.org/playground/guides) to learn more about how to leverage WordPress Playground to test your themes and plugins and create content on the fly.

## How does WordPress Playground work?

When you first start using WordPress Playground, you’ll be provided with a separate space where you can create and customise your own WordPress website. This space is completely isolated from your actual website.

### Streamed, not served.

The WordPress you see when you open Playground in your browser is a WordPress that should function like any WordPress, with [a few limitations](https://developer.wordpress.org/playground/developers/limitations) and the important exception that it’s not a permanent server with an internet address which will limit connections to some third-party services (automation, sharing, analysis, email, backups, etc.) in a persistent way.

The loading screen and progress bar you see on Playground includes both the streaming of those foundational technologies to your browser and configuration steps from [WordPress Blueprints](https://developer.wordpress.org/playground/blueprints) (see [examples](https://developer.wordpress.org/playground/blueprints/examples)), so that a full server, WordPress software, Theme & Plugin solutions and configuration instructions can be streamed over-the-wire.

## What makes Playground different from running WordPress on a web server or local desktop app?

Web applications like WordPress have long relied on server technologies [to run logic](https://developer.wordpress.org/playground/developers/architecture/wasm-php-overview) and [store data](https://developer.wordpress.org/playground/developers/architecture/wordpress#sqlite).

Using those technologies has meant either running a web server connected to the internet or using those technologies in a desktop service or app (sometimes called a “WordPress local environment”) that either leans on a virtual server with the technologies installed or the underlying technologies on the current device.

Playground is a novel way to stream server technologies—including WordPress (and WP-CLI)—as files that can then run in the browser.

# Guides

In this section we present a selection of guides that will help you to both work with, and to better understand, a variety of topics related to [WordPress Playground](https://developer.wordpress.org/playground/).

## [PHP code snippets and embeds](https://developer.wordpress.org/playground/guides/php-code-snippets)

Embed editable, runnable PHP and WordPress examples in any web page with the `<php-snippet>` web component. The guide covers custom Blueprints, expected output, pure-PHP snippets, runtime sharing, and the standalone PHP Playground.

## [Run PHP frameworks in Playground](https://developer.wordpress.org/playground/guides/php-frameworks)

Use Playground as a generic browser-based PHP runtime. This guide shows how to skip the WordPress download, load a bundled Symfony app with a Blueprint, and run it from a `<php-snippet>`.

## [WordPress Playground for Everyone](https://developer.wordpress.org/playground/guides/playground-for-everyone)

Think Playground is only for developers? Think again. This guide shows how WordPress Playground helps beginners, site owners, and everyday users experiment safely — no technical expertise required.

## [How to ship a real WordPress site in a native iOS app via Playground?](https://developer.wordpress.org/playground/guides/wordpress-native-ios-app)

Check “Blocknotes”, the first app to run WordPress natively on iOS via WordPress Playground. It showcases the potential for seamless mobile web integration using WebAssembly and the WordPress block editor.

## [Providing content for your demo with WordPress Playground](https://developer.wordpress.org/playground/guides/providing-content-for-your-demo)

To provide a good demo of your theme or plugin via Playground, you may want to load it with default content that highlights the features of your product. Check this guide to learn how to do so.

## [Importing content into WordPress with Blueprints](https://developer.wordpress.org/playground/guides/import-content-with-blueprints)

Compare XML exports, generated PHP content, and ZIP snapshots, including their trade-offs and test results for importing the same dataset into fresh WordPress instances.

## [WordPress Playground for Theme Developers](https://developer.wordpress.org/playground/guides/for-theme-developers)

This guide will show you the essential settings to fully create a theme demo using WordPress Playground and how you can leverage it during the building stage.

## [WordPress Playground for Plugin Developers](https://developer.wordpress.org/playground/guides/for-plugin-developers)

This guide will show you the basic settings to showcase your plugin using WordPress Playground and how to use it while developing your plugin.

## [Adding PR Preview Buttons with GitHub Actions](https://developer.wordpress.org/playground/guides/github-action-pr-preview)

Learn how to automatically add one-click preview buttons to your pull requests. When someone opens a PR on your plugin or theme repository, they get an instant link to test the changes in a fully configured WordPress instance running in the browser.

## [Using the WordPress Playground Agent Skill](https://developer.wordpress.org/playground/guides/agent-skill-wp-playground)

Automate WordPress Playground workflows with Claude Code. Learn how to install the wp-playground agent skill and use it for local testing, Blueprint execution, snapshot building, version switching, and debugging.

## [Programmatic Usage of Playground CLI](https://developer.wordpress.org/playground/guides/programmatic-playground-cli)

Learn how to use the `runCLI` function to control WordPress Playground programmatically from JavaScript/TypeScript for automation, end-to-end testing, and CI/CD pipelines.

## [Running PHPUnit with the Playground CLI](https://developer.wordpress.org/playground/guides/phpunit-testing)

Run PHPUnit tests for WordPress plugins and themes using the Playground CLI — no database required, clean environment on every run.

## [E2E Testing with Playwright and WordPress Playground](https://developer.wordpress.org/playground/guides/e2e-testing-with-playwright)

Set up automated end-to-end tests for your WordPress plugins and themes using Playwright and the WordPress Playground CLI.

# Links and Resources

There’s a set of redirections in place to make it easier the access to some of the tools related to Playground:

- [\*\*https://playground.wordpress.net/\*\*](<https://playground.wordpress.net/>) → Playground instance
- [https://playground.wordpress.net\*\*/docs\*\*](<https://playground.wordpress.net/docs>) → Playground Docs
- [https://playground.wordpress.net\*\*/builder\*\*](<https://playground.wordpress.net/builder>) → Playground Blueprints Builder
- [https://playground.wordpress.net\*\*/wordpress\*\*](<https://playground.wordpress.net/wordpress>) → Playground PR viewer for WordPress
- [https://playground.wordpress.net\*\*/gutenberg\*\*](<https://playground.wordpress.net/gutenberg>) → Playground PR viewer for Gutenberg

## Frequently sought links

- [Demo](https://playground.wordpress.net/)
- [GitHub Repository](https://github.com/WordPress/wordpress-playground)
- [Documentation](https://wordpress.github.io/wordpress-playground/)
- [Playground tools Repository](https://github.com/WordPress/playground-tools)
- [Awesome WordPress Playground](https://github.com/akirk/awesome-wordpress-playground) – a community-curated list of resources, tools, and examples

## Apps built with WordPress Playground

- [Official demo](https://playground.wordpress.net/) and the [showcase](https://developer.wordpress.org/playground) app – install a theme, try out a plugin, create a few pages, export what you’ve built
- [@wp-playground/cli](https://www.npmjs.com/package/@wp-playground/cli) – a CLI tool for instant WordPress dev environment
- [WordPress Playground for VS Code](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground)
- Live Translations: [App](https://translate.wordpress.org/projects/wp-plugins/friends/dev/pl/default/playground/), [announcement](https://make.wordpress.org/polyglots/2023/04/19/wp-translation-playground/), [more details](https://make.wordpress.org/polyglots/2023/05/08/translate-live-updates-to-the-translation-playground/)
- [Interactive code block](https://wordpress.org/plugins/interactive-code-block/) which powers the [HTML Tag Processor tutorial](https://adamadam.blog/2023/02/16/how-to-modify-html-in-a-php-wordpress-plugin-using-the-new-tag-processor-api/) and the [Playground JS API tutorial](https://wordpress.github.io/wordpress-playground/developers/apis/javascript-api/)
- [Gutenberg Pull Request previewer](https://playground.wordpress.net/gutenberg.html)
- [GraphQL REPL](https://www.wpgraphql.com/2023/06/15/announcing-the-wpgraphql-repl)
- [Blocknotes](https://twitter.com/adamzielin/status/1669478239771799552) – the first ever iOS app running WordPress on your phone
- [Plugin demos on wp.org](https://gist.github.com/adamziel/0fe3ffc1fb5202a907a87d055ee37135) – a user script that adds a “demo” tab to plugin pages on WordPress.org
- [WordPress Pull Request previewer](https://playground.wordpress.net/wordpress.html)
- [Synchronization between two Playgrounds](https://playground.wordpress.net/demos/sync.html)
- [Time Travel](https://playground.wordpress.net/demos/time-traveling.html)
- [WP-CLI](https://playground.wordpress.net/demos/wp-cli.html)
- [PHP implementation of Blueprints](https://playground.wordpress.net/demos/php-blueprints.html)

## Reading materials

- [Build in-browser WordPress experiences with WordPress Playground and WebAssembly](https://web.dev/wordpress-playground/)
- [WordPress Playground on developer.wordpress.org](https://developer.wordpress.org/playground)
- [In-Browser WordPress Tech Demos: WordPress Development with WordPress Playground](https://make.wordpress.org/core/2023/04/13/in-browser-wordpress-tech-demos-wordpress-development-with-wordpress-playground/)
- [Initial announcement on make.wordpress.org](https://make.wordpress.org/core/2022/09/23/client-side-webassembly-wordpress-with-no-server/)
- [Hackernews discussion](https://news.ycombinator.com/item?id=32960560)

## Videos

- Developer Hours Videos:
  - [Americas Region (May 23,2023)](https://wordpress.tv/2023/05/23/developer-hours-wordpress-playground-americas/)
  - [APAC/EMEA Region (May 24,2023)](https://wordpress.tv/2023/05/24/developer-hours-wordpress-playground-apac-emea/)
  - [Creating WordPress Playground Blueprints for Testing and Demos (May 28, 2024)](https://wordpress.tv/2024/05/28/developer-hours-creating-wordpress-playground-blueprints-for-testing-and-demos/) by Birgit Pauli-Haack & Nick Diego
  - [Developer Hours: Everything you need to know about WordPress Playground (Dec 17, 2024)](https://wordpress.tv/2024/12/17/developer-hours-everything-you-need-to-know-about-wordpress-playground/) by Nick Diego & Ryan Welcher
- [Playground at State of the Word](https://youtu.be/VeigCZuxnfY?t=2912)
- [Playground at WCEU 2023](https://www.youtube.com/watch?v=e-CwouzTGp4&t=26946s)
- [Watch “WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress”](https://www.youtube.com/watch?v=dN_LaenY8bI) by Anne McCarthy
- [How to start using WordPress Playground](https://wordpress.tv/2023/06/26/how-to-start-using-wordpress-playground/) by Adam Zielinski
- [WordPress Playground, present and future applications](https://wordpress.tv/2023/10/15/wordpress-playground-present-and-future-applications/) by Antonio Sejas
- [WordPress Playground: The Famous « 5 Second » Install](https://wordpress.tv/2023/11/21/wordpress-playground-the-famous-5-second-install/) by Dave Kellam
- [Testing WP Playground with wp-now](https://wordpress.tv/2024/04/04/testing-wp-playground-with-wp-now/) by Jonathan Bossenger
- [Translate Live: in-context translation using WordPress Playground](https://wordpress.tv/2024/04/10/translate-live-in-context-translation-using-wordpress-playground/) by Alex Kirk
- [Testing WP Playground in VS Code](https://wordpress.tv/2024/04/25/testing-wp-playground-in-vs-code/) by Jonathan Bossenger
- [Submitting an enhancement to the WordPress Playground Block](https://wordpress.tv/2024/09/06/submitting-an-enhancement-to-the-wordpress-playground-block/) by Jonathan Bossenger
- [WordPress Playground for developers](https://wordpress.tv/2024/12/16/wordpress-playground-for-developers/) by Berislav Grgicak and Jonathan Bossenger
- [WordPress Playground Block code editor theme support](https://wordpress.tv/2024/10/05/wordpress-playground-block-code-editor-theme-support/) by Jonathan Bossenger
- [WordPress Playground – use WordPress without a server at WCEU 2024](https://wordpress.tv/2024/07/03/wordpress-playground-use-wordpress-without-a-server/) by Adam Zielinski
- [Code, Test, Repeat: Accelerating Development with WordPress Playground at WordCamp Larissa 2024](https://wordpress.tv/2024/12/13/code-test-repeat-accelerating-development-with-wordpress-playground/) by Uros Tasic
- [Liberating data with WordPress Playground in a Browser Extension at WordCamp Netherlands 2024](https://wordpress.tv/2024/12/24/liberating-data-with-wordpress-playground-in-a-browser-extension/) by Alex Kirk
- [Beyond the Playground: WordPress as a Tool and Product Builder at WCUS 2024](https://wordpress.tv/2024/10/10/beyond-the-playground-wordpress-as-a-tool-and-product-builder/) by Dennis Snell
- [Create a demo with Playground at WC Asia 2025](https://wordpress.tv/2025/04/30/create-a-demo-with-playground/) by Birgit Pauli-Haack
- [Dissecting WordPress Playground at WordCamp Nepal 2025](https://wordpress.tv/2025/04/30/dissecting-wordpress-playground/) by Sakar Upadhyaya Khatiwada
- [Building Automated Test with WordPress Playground at WCEU 2025](https://wordpress.tv/2025/06/07/building-automated-tests-with-wordpress-playground/)by Berislav Grgicak
- [From Zero to Demo: Mastering WordPress Playground Blueprints at WCEU 2025](https://wordpress.tv/2025/06/07/from-zero-to-demo-mastering-wordpress-playground-blueprints/) by Birgit Pauli-Haack
- [Playground at WordCamp Gliwice (in Polish)](https://www.youtube.com/watch?v=AUHklF9GdL8&list=PLiCne9CeL82_hGuJOAJlsc84WxVDSH-c9&index=4) by Adam Zielinski
- [Adam Zielinski: WordPress bez serwera – poznaj WordPress Playground i nowy paradygmat aplikacji (in Polish)](https://wordpress.tv/2023/05/30/adam-zielinski-wordpress-bez-serwera-poznaj-wordpress-playground-i-nowy-paradygmat-aplikacji/) by Adam Zielinski
- [WordPress bez instalacji, komputera, i internetu: Poznaj WordPress Playground (in Polish)](https://wordpress.tv/2023/10/12/wordpress-bez-instalacji-komputera-i-internetu-poznaj-wordpress-playground/) by Adam Zielinski
- [WordPress Playground at WordCamp Wrocław 2024 (in Polish)](https://wordpress.tv/2024/12/02/wordpress-playground-przelom-w-wordpressie-2/) by Adam Zielinski
- [WordPress Playground at WordCamp Gdynia 2025 (in Polish)](https://wordpress.tv/2025/04/21/wordpress-playground/) by Magdalena Paciorek
- [Discovering Playground, the demo tool(in Spanish)](https://wordpress.tv/2024/08/09/descubriendo-playground-la-herramienta-para-hacer-demos/) by Alex Cuadra
- [WordPress Playground: Complete and functional WordPress installation(in Spanish)](https://wordpress.tv/2024/02/07/wordpress-playground-instalacion-completa-y-funcional-de-wordpress/) by Fernando García Rebolledo
- [Playground: A throwaway WordPress within your browser at WordCamp Madrid 2025(in Spanish)](https://wordpress.tv/2025/03/09/playground-un-wordpress-de-usar-y-tirar-dentro-de-tu-navegador/) by Álvaro Gómez Velasco
- [Use WordPress with just a browser! WordPress Playground Tutorial: Basic usage of Playground (in Japanese)](https://www.youtube.com/watch?v=6s_B0WvJauU) by Shimomura Tomoki
- [WordPress Playground: How to use Blueprints](https://www.youtube.com/watch?v=Vcao6uXguWg) by Shimomura Tomoki
- [Streamlined Block Theme Development: Using WordPress Playground and GitHub for No-Code Version Control of Site Editor Changes](https://wordpress.tv/2025/09/30/streamlined-block-theme-development-using-wordpress-playground-and-github-for-no-code-version-contr/) by Birgit Pauli-Haack
- [Playground, la mejor herramienta jamás inventada para enseñar WordPress (in Spanish)](https://wordpress.tv/2025/10/05/playground-la-mejor-herramienta-jamas-inventada-para-ensenar-wordpress/) by Nilo Vélez
- [Testing Faster Than a Red Bull Pit Stop: WordPress Playground and WooCommerce Blueprints](https://wordpress.tv/2025/09/30/testing-faster-than-a-red-bull-pit-stop-wordpress-playground-and-woocommerce-blueprints/) by Daniel Dudzic
- [Is WordPress playground only for developers?](https://wordpress.tv/2025/10/25/is-wordpress-playground-only-for-developers/) by Fellyph Cintra
- [How to test the next WordPress release with WordPress Playground](https://wordpress.tv/2025/11/13/how-to-test-the-next-wordpress-release-with-wordpress-playground/) by Fellyph cintra
- [Running WordPress directly from the JavaScript code with runCLI](https://wordpress.tv/2025/10/22/running-wordpress-directly-from-the-javascript-code-with-runcli/) by Fellyph Cintra
- [WordPress Playground: The Path to Test Automation](https://wordpress.tv/2025/11/24/wordpress-playground-the-path-to-test-automation/) by Fellyph Cintra
- [Previewing GitHub branches with WordPress Playground](https://wordpress.tv/2025/12/09/previewing-github-branches-with-wordpress-playground/) by Fellyph Cintra
- [Introduction to WordPress Playground landing page](https://wordpress.tv/2025/12/09/introduction-to-wordpress-playground-landing-page/) by Fellyph Cintra
- [Using WordPress Playground to work with AI agents](https://wordpress.tv/2025/12/09/using-wordpress-playground-to-work-with-ai-agents/) by Fellyph Cintra
- [WordPress Playground Blueprints: De Cero a Pro (in Spanish)](https://wordpress.tv/2026/01/28/wordpress-playground-blueprints-de-cero-a-pro/) by Fellyph Cintra
- [Getting Started with WordPress Blueprints (in Bengali)](https://wordpress.tv/2026/02/09/getting-started-with-wordpress-blueprints/) by Monzur Alam
- [WordPress Playground + AI: Building Autonomous Testing Pipelines](https://wordpress.tv/2026/04/21/wordpress-playground-ai-building-autonomous-testing-pipelines/) by Fellyph Cintra
- [What’s new in WordPress Playground?](https://wordpress.tv/2026/06/05/whats-new-in-wordpress-playground/) by Fellyph Cintra

# Start using WordPress Playground in 5 minutes

WordPress Playground can help you with any of the following:
This page will guide you through each of these. Oh, and if you’re a visual learner – here’s a video. Some interface details in the video predate the Dock; follow the written steps below for the current UI.

## Start a new WordPress site

Open the [official demo on playground.wordpress.net](https://playground.wordpress.net/) to start WordPress in your browser.
You can create pages, upload plugins, install themes, import content, and do most things you would do on a regular WordPress site.
When browser storage is available, new Playgrounds are autosaved. You can find
up to five recent autosaves in **Your Playgrounds** from the Dock. If you need a
site that is discarded on refresh, open Playground with `?storage=temp`.

**WordPress Playground is private**

The Playground runs locally in your browser. It does not upload your site
unless you choose an action such as **Export to GitHub**. Once you’re finished,
you can store the Playground permanently, export it as a ZIP, or start over
from **New Playground**.

## Try a block, a theme, or a plugin

You can upload any plugin or theme you want in [/wp-admin/](https://playground.wordpress.net/?url=/wp-admin/).
To save a few clicks, you can preinstall plugins or themes from the WordPress plugin directory by adding a `plugin` or `theme` parameter to the URL. For example, to install the coblocks plugin, you can use this URL:
https://playground.wordpress.net/?plugin=coblocks
Or this URL to preinstall the `pendant` theme:
https://playground.wordpress.net/?theme=pendant
In case you would like to install multiple themes and plugins, it is possible to repeat the `theme` or `plugin` parameters:
https://playground.wordpress.net/?theme=pendant&theme=acai
You can also mix and match these parameters and even add multiple plugins:
https://playground.wordpress.net/?plugin=coblocks&plugin=friends&theme=pendant
This is called [Query API](https://developer.wordpress.org/playground/developers/apis/query-api/) and you can learn more about it [here](https://developer.wordpress.org/playground/developers/apis/query-api/).

## Store a Playground in browser storage

Click the **Autosaved** or **Unsaved** status in the Dock to open **Store
permanently**, then choose **Save in browser storage**.
![The Store permanently pane with browser storage selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/store-permanently-browser-1.webp?fit=1440%2C900&ssl=1)
A saved browser Playground appears in **Your Playgrounds**. Autosaves also
appear there, but Playground keeps up to five recent autosaves. Store a
Playground permanently when you want to keep it beyond the autosave lifecycle.
Browser storage still belongs to the browser. Export a ZIP when you need a file you can move, archive, or restore later.

## Export a portable ZIP

Open **Export** from the Dock and use **Download as .zip**.
![The Export pane with ZIP, setup link, and GitHub options](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-export-playground-1.webp?fit=1440%2C900&ssl=1)
The exported file contains the current files, database, plugins, themes, uploads, and edits. You can restore it in Playground or host it on a server that supports PHP and SQLite.
The SQLite database file is included at `wp-content/database/.ht.sqlite`. Files starting with a dot are hidden by default on most operating systems, so you may need to enable hidden files in your file manager.

## Restore a ZIP

Open **New Playground** from the Dock, choose **Import zip**, and select the ZIP file.
![The New Playground pane with Import zip selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground-import-zip.webp?fit=1440%2C900&ssl=1)
This restores the files and database from the ZIP into a new Playground.

## Use a specific WordPress or PHP version

Open **Site Settings** from the Dock to choose WordPress, PHP, language, multisite, and networking options.
![The Site Settings pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-site-settings-1.webp?fit=1440%2C900&ssl=1)

**Test your plugin or theme**

Compatibility testing with so many WordPress and PHP versions was always a pain. WordPress Playground makes this process effortless – use it to your advantage!

You can also use the `wp` and `php` [query parameters](https://developer.wordpress.org/playground/developers/apis/query-api) to open Playground with the right versions already loaded:

- https://playground.wordpress.net/?wp=6.5
- https://playground.wordpress.net/?php=8.3
- https://playground.wordpress.net/?php=8.2&wp=6.2
- https://playground.wordpress.net/?php=next

This is called [Query API](https://developer.wordpress.org/playground/developers/apis/query-api/) and you can learn more about it [here](https://developer.wordpress.org/playground/developers/apis/query-api/).
Use `php=next` to preview the next PHP version built from the php-src development branch. For example, see the [PHP 8.6 feature preview](https://playground.wordpress.net/php-8-6.html).
To learn more about preparing content for demos, see the [providing content for your demo guide](https://developer.wordpress.org/playground/guides/providing-content-for-your-demo).

**Major versions only**

You can specify major versions like `wp=6.2` or `php=8.1` and expect the most recent release in that line. You cannot, however, request older minor versions so neither `wp=6.1.2` nor `php=7.4.9` will work. Generic aliases like `latest` and `next` are exceptions.

## Import a WXR file

You can import a WordPress export file by uploading a WXR file in [/wp-admin/](https://playground.wordpress.net/?url=/wp-admin/import.php).
You can also use [JSON Blueprints](https://developer.wordpress.org/playground/blueprints). See [getting started with Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started) to learn more.
This is different from restoring a Playground ZIP. A WXR file imports WordPress content into an existing site. A Playground ZIP restores files and the database into a new Playground.

## Build apps with WordPress Playground

WordPress Playground is programmable, which means you can [build WordPress apps](https://developer.wordpress.org/playground/developers/build-your-first-app), set up plugin demos, and even use it as a zero-setup [local development environment](https://developer.wordpress.org/playground/developers/local-development/).
To learn more about developing with WordPress Playground, check out the [development quick start](https://developer.wordpress.org/playground/developers/build-your-first-app) section.

# Troubleshooting WordPress Playground

This page covers errors from the Playground website itself, saved Playgrounds,  
browser storage, and WordPress boot. For Blueprint-specific errors, see  
[Troubleshoot and debug Blueprints](https://developer.wordpress.org/playground/blueprints/troubleshoot-and-debug).

## Playground looks broken

Try these first:

- Use the Playground **Refresh page** button instead of refreshing the browser tab. Browser refresh (Cmd+R or F5) starts the whole Playground app again.
- Open the same URL in a private window to rule out saved-site or browser-storage state.
- Disable browser extensions that block JavaScript, WebAssembly, storage, workers, or network requests.
- Check browser developer tools for Console and Network errors.
- If the URL includes `?site-slug=...`, try removing that query parameter to start a new autosaved Playground. Use `?storage=temp` when you need a truly temporary site.

## A clean site says the MySQL extension is missing

You may see a WordPress error page like this:

```
Your PHP installation appears to be missing the MySQL extension which is required by WordPress.
```

In Playground, this usually means WordPress did not load the SQLite integration  
that lets WordPress run without MySQL. Playground runs WordPress in WebAssembly  
and uses SQLite instead of a MySQL server.

Try these steps:

- Start a fresh Playground at https://playground.wordpress.net/ to confirm the public site can boot.
- If the URL includes a saved site, remove `?site-slug=...` and load a new Playground. Add `?storage=temp` only when you need an ephemeral site.
- If this happened after importing a ZIP, confirm the import did not include a custom `wp-content/db.php` that overrides Playground’s SQLite setup.
- If this happened in the CLI, do not use `--skip-sqlite-setup` unless you provide your own database integration.
- If this happened with a Blueprint, see the [Blueprint troubleshooting page](https://developer.wordpress.org/playground/blueprints/troubleshoot-and-debug).

If you are writing a Blueprint and need to add the SQLite integration plugin,  
`plugins` goes at the top level:

```
{
    "preferredVersions": {
        "php": "8.3",
        "wp": "latest"
    },
    "plugins": ["sqlite-database-integration"],
    "steps": [
        {
            "step": "login",
            "username": "admin"
        }
    ]
}
```

## Error connecting to the SQLite database

This means Playground loaded the SQLite integration, but WordPress still could  
not connect to the database.

Common causes:

- A saved Playground’s browser storage is stale or incomplete.
- An imported site ZIP contains an incompatible database file or database drop-in.
- A mounted local directory is missing files that WordPress needs.
- Browser storage was cleared, evicted, or blocked.

Recommended recovery:

1. Start a fresh Playground without `site-slug`.
2. If the fresh site works, the issue is tied to the saved site or imported archive.
3. Export accessible files from the broken saved site using **Files**, **Export**, or a local directory copy, if available.
4. Re-import the site into a new Playground, or rebuild it from its Blueprint.

## NotAllowedError

`NotAllowedError` usually means the browser blocked an operation that requires  
user permission or a supported browser context. In Playground, this often  
relates to saved sites or local directory access.

You may see this exact message:

```
The request is not allowed by the user agent or the platform in the current context.
```

Try:

- Open Playground in a normal top-level browser tab, not inside a restricted iframe.
- Reopen the site from **Your Playgrounds** in the Dock.
- If the site used a local directory, Playground cannot currently re-select the directory after its saved permission expires. The files remain on disk; use them directly or recreate the Playground from an exported ZIP or Blueprint, if available.
- Confirm the browser supports the file or storage API being used. Chrome and Edge generally have the broadest local directory support.
- Check whether private browsing mode, enterprise policy, or browser settings block storage access.

## NoModificationAllowedError

`NoModificationAllowedError` means the browser or filesystem refused a write.  
This can happen when a saved local directory became read-only, permission was  
lost, or browser storage is unavailable.

You may see this exact message:

```
An attempt was made to write to a file or directory which could not be modified due to the state of the underlying filesystem.
```

Try:

- If the Playground still opens, use **Export → Download as .zip** before starting over.
- Check that the target folder still exists and is writable.
- Avoid system-protected folders or synced folders that temporarily lock files.
- Start a fresh Playground with `?storage=temp` if you only need a temporary test site.
- Use [Playground CLI](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) for local development that needs reliable filesystem persistence.

## Saved Playground cannot reload

Saved Playgrounds are stored in browser storage or in a local directory you  
selected. They are not hosted on a remote server.

If a saved Playground cannot reload:

- Confirm you are using the same browser and browser profile where it was saved.
- Check whether browser data was cleared or storage was disabled.
- If the site was saved to a local directory, confirm the directory still exists and has not moved.
- If the URL includes `?site-slug=...`, remove it to start a fresh autosaved Playground.
- Recreate the saved site from its original Blueprint or import ZIP if storage was lost.

## Browser storage and persistence

New Playgrounds are autosaved when browser storage and saving are available. Open **Your Playgrounds** in the Dock to recover recent autosaves or saved Playgrounds. Playground keeps up to five recent autosaves.

Click the **Autosaved** or **Unsaved** status in the Dock to open the storage  
choices. Select **Save in browser storage**, or select **Save in a local  
directory** and choose a directory dedicated to that Playground before  
clicking **Save**. Matching files in the directory may be overwritten. Use  
**Export** to download a portable ZIP. A browser refresh can recover a stored  
or autosaved Playground, but `?storage=temp` creates a temporary Playground  
that is discarded on refresh or tab close.

For longer-running local development, prefer the [Playground CLI](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli), which persists site files on disk.

See [the Dock guide](https://developer.wordpress.org/playground/web-instance) for the current Dock destinations and export workflow.

## When to start fresh

Start a fresh Playground when:

- You only need to test whether the public Playground site is working.
- The URL points to a saved `site-slug` that no longer loads.
- You are debugging whether an error comes from Playground itself or from a plugin, theme, Blueprint, or imported site.
- Browser storage or local directory access is suspected to be broken.

Use this URL for a clean site:

```
https://playground.wordpress.net/
```

## Report a Playground issue

If the problem reproduces on a fresh Playground, please  
[open an issue](https://github.com/WordPress/wordpress-playground/issues) and  
include:

- The full Playground URL.
- The browser and operating system.
- Whether you used a saved site, imported ZIP, Blueprint, local directory, or CLI.
- The exact error name and message.
- Console and Network details from browser developer tools.

# WordPress Playground web instance

<https://playground.wordpress.net/> runs
WordPress in your browser without a server. The page opens a Playground, shows
the WordPress site, and keeps the site tools in the **Dock**.
![The Playground web instance with the Dock visible at the bottom of the page](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-overview.webp?fit=1440%2C900&ssl=1)
The Dock has an address field, a save status, layout controls, and destinations for creating, storing, inspecting, and exporting Playgrounds.

## Customize Playground

The Dock includes these destinations:

- **New**: Start from the Blueprint gallery, a public Blueprint URL, a new
  Blueprint, a pull request preview, a GitHub repository, or an imported `.zip`
  file.
- **Playgrounds**: Switch between recent and saved Playgrounds.
- **Blueprint**: View, edit, export, and run the current Blueprint.
- **Site Settings**: Configure WordPress version, PHP version, language,
  networking, and multisite.
- **Database**: Inspect or download the SQLite database and open database tools.
- **Files**: Browse and edit files in the WordPress filesystem.
- **Logs**: Inspect PHP errors, warnings, and notices.
- **Export**: Download a `.zip`, copy the original setup link, or export selected
  files to a GitHub pull request.

## Navigate inside WordPress

Use the Dock address field to open a path inside the current WordPress site.
For example, enter `/wp-admin/` to open the dashboard or
`/wp-admin/plugins.php` to open the Plugins screen. **Refresh page** reloads
the current WordPress path.
You can also use the [Query Params API](https://developer.wordpress.org/playground/developers/apis/query-api/) to open Playground with a specific setup, such as a WordPress version, PHP version, plugin, theme, or Blueprint.

## Understand the save status

The status next to the address field tells you how the current Playground is stored:

- **Autosaved** means the Playground is stored in this browser and can be recovered from **Your Playgrounds**. Playground keeps up to five recent autosaves.
- **Saved** means the Playground was stored permanently in browser storage or saved to a local directory.
- **Unsaved** means the Playground has not been saved. Temporary Playgrounds, including `?storage=temp`, are lost when the tab is closed or refreshed.

Click **Autosaved** or **Unsaved** to open **Store permanently**.
![The Store permanently pane with browser storage selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/store-permanently-browser-1.webp?fit=1440%2C900&ssl=1)
Store permanently can keep an autosaved Playground in browser storage so autosave pruning no longer removes it. In browsers that support the File System Access API, it can also save the Playground to a local directory.
Browser storage still belongs to the browser. The browser may remove stored data when storage pressure or privacy settings require it. Export a ZIP when you need a portable backup.

## Start a Playground

Open **New Playground** from the Dock by clicking **New**. The pane contains
**Blueprint gallery**, **From a URL**, **Write a Blueprint**, **Preview a PR**,
**From GitHub**, and **Import zip**.
![The New Playground pane with the Blueprint gallery selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground-1.webp?fit=1440%2C900&ssl=1)
The Blueprint gallery starts with **Vanilla WordPress**, which creates a clean
WordPress install. **From a URL** opens a public Blueprint URL. **Write a
Blueprint** opens an editor for a new Blueprint. **Import zip** restores a ZIP
exported from Playground.
![The New Playground pane with Import zip selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground-import-zip.webp?fit=1440%2C900&ssl=1)

## Return to recent and saved Playgrounds

Open **Your Playgrounds** from the Dock by clicking **Playgrounds**. It lists the current Playground, recent autosaves, and Playgrounds you saved permanently.
![The Your Playgrounds pane with the current Playground](https://i0.wp.com/developer.wordpress.org/files/2026/07/your-playgrounds-1.webp?fit=1440%2C900&ssl=1)
Autosaved Playgrounds are recovery points. Playground retains up to five recent
autosaves. Use **Store permanently** to keep one as a saved Playground.

## Change site settings

Open **Site Settings** to change runtime and WordPress setup options.
![The Site Settings pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-site-settings-1.webp?fit=1440%2C900&ssl=1)
PHP version and networking can be applied to an existing stored Playground. WordPress version, language, and multisite change the WordPress installation itself, so they require a fresh Playground.
Running an edited Blueprint keeps stored and autosaved Playgrounds. It discards a temporary Playground because the new run starts from a fresh setup.

## Inspect the current Blueprint

Open **Blueprint** to view and edit the Blueprint for the current Playground.
![The Blueprint editor pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-current-blueprint.webp?fit=1440%2C900&ssl=1)
The editor can run the edited Blueprint in a new Playground. For a stored or autosaved Playground, the original Playground remains available in **Your Playgrounds**.

## Inspect files, database, and logs

Open **Files** to browse and edit the current Playground files.
![The Files pane with a WordPress file selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/files-1.webp?fit=1440%2C900&ssl=1)
Open **Database** to use database tools or download the SQLite database.
![The Database pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/database-1.webp?fit=1440%2C900&ssl=1)
Open **Logs** to inspect PHP errors, warnings, and notices.
![The PHP error log pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/logs-1.webp?fit=1440%2C900&ssl=1)

## Export and share

Open **Export** to download or share the current Playground.
![The Export pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-export-playground-1.webp?fit=1440%2C900&ssl=1)
**Download as .zip** exports the current files, database, plugins, themes, uploads, and edits. The ZIP can be restored later with **New → Import zip**.
**Copy original setup link** copies a link that recreates only the original
setup. It does not include edits made after the Playground started.
**Export to GitHub** can create a pull request with selected files from the current Playground.

## Change the Dock layout

The Dock can be shown as a floating panel or full-width bar. Use **Full width** to switch layouts.

| Floating | Full width |
| --- | --- |
| The default floating Dock | The full-width Dock layout |

Use **Hide tools** to collapse the Dock to its address field and save status.
Use **Show tools** to reopen the tool row.
![The Playground with Dock tools hidden](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-hidden-tools.webp?fit=1440%2C900&ssl=1)
You can drag the floating Dock on desktop. Drag it past the left or right edge
to fold it into a corner launcher, then click the launcher to restore the Dock.
![The Dock folded into the corner launcher](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-corner-launcher.webp?fit=1440%2C900&ssl=1)
On narrow screens, the Dock uses a full-width mobile layout.
![The Dock on a mobile viewport](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-mobile.webp?fit=390%2C844&ssl=1)

The site at <https://playground.wordpress.net> is there to support the community, but there are no guarantees it will continue to work if the traffic grows significantly.

If you need certain availability, you should [host your own WordPress Playground](https://developer.wordpress.org/playground/developers/architecture/host-your-own-playground).

# Build

WordPress Playground can help you to create and learn WordPress quickly, even on mobile with no signal. You can use Playground where you work best, whether that’s in the browser, Node.js, mobile apps, VS Code, or elsewhere.

## Setting up a local WordPress environment quickly

You can seamlessly integrate Playground into your development workflow to launch a local WordPress environment quickly for testing your code. You can do this directly [from the terminal](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) or [your preferred IDE.](https://developer.wordpress.org/playground/developers/local-development/vscode-extension)

## Save changes done on a Block Theme and create GitHub Pull Requests

You can connect your Playground instance to a GitHub repository and create a Pull Request with the changes you’ve made through the WordPress UI, leveraging the [Create Block Theme](https://wordpress.org/plugins/create-block-theme/) plugin.
With this workflow, you could build a block theme completely in your browser and save your changes to GitHub, or you could improve/fix an existing one.
Some more examples of this workflow:

- [Developer Hours: Creating WordPress Playground Blueprints for Testing and Demos](https://www.youtube.com/watch?v=gKrij8V3nK0&t=2488s)
- [Recap Hallway Hangout: Theme Building with Playground, Create-block-theme plugin, and GitHub](https://make.wordpress.org/core/2024/06/25/recap-hallway-hangout-theme-building-with-playground-create-block-theme-plugin-and-github/)

## Synchronize your Playground with a local folder and create GitHub Pull Requests

In the Dock, click the **Autosaved** or **Unsaved** save status, select **Save
in a local directory**, click **Choose…**, and select a directory dedicated
to this Playground. After granting write access, click **Save**. Playground
copies the current site into the selected directory and overwrites files with
matching names; it does not import an existing site from that directory.
![The Store permanently pane with local-directory storage selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/store-permanently-local-directory.webp?fit=1440%2C900&ssl=1)
Local-directory storage uses the File System Access API, so availability depends on browser and platform support for choosing and writing to directories. Chromium-based desktop browsers usually support it. Browsers without that capability can still use browser storage and ZIP export. See [Browser support](https://developer.wordpress.org/playground/developers/limitations#browser-support) for the broader compatibility model.
Files changed in Playground are written to the selected directory. Files changed on disk are not pulled into the running Playground automatically. For a local-directory Playground, open the **Saved** status menu in the Dock and choose **Reload files from disk** when you want Playground to read the current files from the directory.
With this workflow, you can create GitHub PRs directly from changes made in your local directory.
See here a little demo of this workflow in action:

## Integrate with other APIs to create new tools.

Playground can be combined with different APIs to create amazing tools. The possibilities are endless.
You can [use WordPress Playground in Node.js](https://developer.wordpress.org/playground/developers/local-development/php-wasm-node) to create new tools. The [@php-wasm/node package](https://npmjs.org/@php-wasm/node), which ships the PHP WebAssembly runtime, is the package used for <https://playground.wordpress.net/>, for example.
Another interesting app built on top of Playground is **Translate Live** (see [example](https://translate.wordpress.org/projects/wp-plugins/friends/dev/de/default/playground/)) which, in combination with OpenAI provides a WordPress translations tool “in place” where translations can be seen and modified in their real context (see example). Read more about this tool at [Translate Live: Updates to the Translation Playground](https://make.wordpress.org/polyglots/2023/05/08/translate-live-updates-to-the-translation-playground/)

## Work offline and as a native app

When you first visit [playground.wordpress.net](https://playground.wordpress.net/), your browser automatically caches all the necessary files to use Playground. From that point on, you can access [playground.wordpress.net](https://playground.wordpress.net/), even without an internet connection, ensuring you can continue working on your projects without interruptions.
You can also install Playground on your device as a Progressive Web App (PWA) to launch the Playground directly from your home screen—just like a native app.
Read [Introducing Offline Mode and PWA Support for WordPress Playground](https://make.wordpress.org/playground/2024/08/05/offline-mode-and-pwa-support/) for more info.

## Embed a WordPress site in non-web environments

The [How to ship a real WordPress site in a native iOS app via Playground?](../guides/wordpress-native-ios-app) guide shows how we can leverage Playground to wrap a WordPress site into an IOS app.

# Launch

Reach your clients or customers faster. Showcase your product, let users try it live, or launch it in the App Store with zero lead time.

## Embed interactive product demos on websites.

Leverage [blueprints’](https://developer.wordpress.org/playground/blueprints) potential to create interactive demos of your plugins or themes. For example, you can provide a link to your users/clients to showcase how your custom plugin integrates with an adapted theme, demonstrating their combined functionality and appearance.

Read more about this at [How to use WordPress Playground for interactive demos](https://developer.wordpress.org/news/2024/04/25/how-to-use-wordpress-playground-for-interactive-demos/)

Get inspiration about the type of interactive demos you can create at the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md)

The [Blueprints builder](https://playground.wordpress.net/builder/builder.html) tool allows you edit your blueprint online and run it directly in a Playground instance.

Another handy tool to create a blueprint is the [WordPress Playground Step Library](https://akirk.github.io/playground-step-library/#) tool that provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also [create your own steps](https://github.com/akirk/playground-step-library/#contributing)!

## Embed demos in posts and pages with the WordPress Playground block

This [WordPress Playground block](https://wordpress.org/plugins/interactive-code-block/) allows you to embed WordPress Playground in your posts and pages. You can also include an interactive code editor to demonstrate and teach your readers how WordPress plugins are built.

With this block you have a straightforward and effective way to create live WordPress environments that can be embedded within your blog posts.

For any issues or questions about the WordPress Playground Block, please open a GitHub issue in the [playground-tools](https://github.com/WordPress/playground-tools) repository.

## Put a native app running WordPress in the App Store.

Check the [How to ship a real WordPress site in a native iOS app via Playground?](../guides/wordpress-native-ios-app) guide for info on this use case

# Test

Upgrade your QA process with the ability to review progress in your browser in a single click. When you’re ready, push updates instantly.

## Test any theme or plugin

With Playground, you can test any plugin or theme. Use the [Query API](https://developer.wordpress.org/playground/developers/apis/query-api) to quickly load any plugin or theme published in wordpress.org [plugins](https://wordpress.org/plugins) and [themes](https://wordpress.org/themes/) directories.

For example, the following link will load the [“pendant” theme](https://wordpress.org/themes/pendant/) and the [“gutenberg” plugin](https://wordpress.org/plugins/gutenberg/) on a Playground instance:

<https://playground.wordpress.net/?theme=pendant&plugin=gutenberg>

But you can also test [more elaborate configurations using blueprints](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md), for example testing a plugin’s code from a gist (see [blueprint](https://github.com/wordpress/blueprints/blob/trunk/blueprints/install-plugin-from-gist/blueprint.json) and [live demo](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-plugin-from-gist/blueprint.json))

## Live preview pull requests

Testing pull requests is one of the most exciting use cases for the Playground project. With Playground, you can enable a Live preview link on each Pull Request of a WordPress-related project in GitHub so that developers can see in action the effects of code in that Pull Request. Read more about this at [Preview WordPress Core Pull Requests with Playground](https://wptavern.com/preview-wordpress-core-pull-requests-with-playground#:~:text=Previewing%20WordPress%20Pull%20Requests%20requires,testing%20and%20team%20workflows%20difficult.).

There are some public implementations of this use case such as [WordPress Core PR previewer](https://playground.wordpress.net/wordpress.html) and [Gutenberg PR previewer](https://playground.wordpress.net/gutenberg.html). Users can input the PR number or URL to be redirected to a WordPress instance, powered by Playground, where the changes from the PR are applied.

You can add automated PR preview buttons to your own plugin or theme repository using the WordPress Playground PR Preview GitHub Action. When someone opens a pull request, the action automatically adds a button that launches a configured WordPress instance with the changes ready to test. For detailed setup instructions and advanced configurations, see the [Adding PR Preview Buttons with GitHub Actions](https://developer.wordpress.org/playground/guides/github-action-pr-preview) guide.

## Clone your site and experiment in a private sandbox.

With the [Sandbox Site powered by Playground](https://wordpress.org/plugins/playground/) plugin you can create a private WordPress Playground copy of your site to test plugins safely or do any other experiments on your site’s replica without uploading any data to the cloud and without affecting the original site.

## Test different WordPress and PHP versions.

With Playground, you can quickly test any major WordPress or PHP version by *customizing its settings* or using a custom blueprint with the `preferredVersions` property.

For example, you can always test the latest development version of WordPress, also called [Beta Nightly](https://wordpress.org/download/beta-nightly/), from this link: <https://playground.wordpress.net/?wp=nightly>

During the Beta period of any WordPress release, you can also test the latest WordPress Beta or RC release with theme test data and debugging plugins (see [blueprint](https://github.com/WordPress/blueprints/blob/trunk/blueprints/beta-rc/blueprint.json) and [live demo).](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/beta-rc/blueprint.json)

You can also load any [theme, plugin](https://developer.wordpress.org/playground/developers/apis/query-api), or [configuration](https://developer.wordpress.org/playground/blueprints) in any of the available WordPress and PHP versions to check how they work in that environment.

The [WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress](https://www.youtube.com/watch?v=dN_LaenY8bI) provides a great overview of the testing possibilities with Playground.

# Code contributions

Like all WordPress projects, Playground uses GitHub to manage code and track issues. The main repository is at <https://github.com/WordPress/wordpress-playground> and the Playground Tools repository is at <https://github.com/WordPress/playground-tools/>.

**Contribute to Playground Tools**

This guide includes links to the main repository, but all the steps and options apply for both. If you’re interested in the plugins or [local development](https://developer.wordpress.org/playground/developers/local-development/) tools—start there.

Browse [the list of open issues](https://github.com/wordpress/wordpress-playground/issues) to find what to work on. The [`Good First Issue`](https://github.com/wordpress/wordpress-playground/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) label is a recommended starting point for first-time contributors.

Be sure to review the following resources before you begin:

- [Coding principles](https://developer.wordpress.org/playground/contributing/coding-standards)
- [Architecture](https://developer.wordpress.org/playground/developers/architecture)
- [Vision and Philosophy](https://github.com/WordPress/wordpress-playground/issues/472)
- [WordPress Playground Roadmap](https://github.com/WordPress/wordpress-playground/issues/525)

## Contribute Pull Requests

[Fork the Playground repository](https://github.com/WordPress/wordpress-playground/fork) and clone it to your local machine. To do that, copy and paste these commands into your terminal:

```
git clone -b trunk --single-branch --depth 1 --recurse-submodules

# replace `YOUR-GITHUB-USERNAME` with your GitHub username:
git@github.com:YOUR-GITHUB-USERNAME/wordpress-playground.git
cd wordpress-playground
npm install
```

Create a branch, make changes, and test it locally by running the following command:

```
npm run dev
```

Playground will open in a new browser tab and refresh automatically with each change.

**Troubleshooting: File watcher limit on Linux**

On Linux, you might see an error like `ENOSPC: System limit for number of file watchers reached` when running `npm run dev`. This happens because the Playground repository has more files than the default system limit allows to watch.

To fix this, first check your current limit:

```
cat /proc/sys/fs/inotify/max_user_watches
```

If it’s around 65,536 or lower, increase it by running:

```
sudo sysctl fs.inotify.max_user_watches=131070
sudo sysctl -p
```

Then try `npm run dev` again. This is a common issue on Debian, Ubuntu, and other Linux distributions.

When your’e ready, commit the changes and submit a Pull Request.

**Formatting**

We handle code formatting and linting automatically. Relax, type away, and let the machines do the work.

### Running a local Multisite

WordPress Multisite has a few [restrictions when run locally](https://developer.wordpress.org/advanced-administration/multisite/prepare-network/#restrictions). If you plan to test a Multisite network using Playground’s `enableMultisite` step, make sure you either change Playground CLI’s default port or set a local test domain running via HTTPS.

To change Playground CLI’s default port to the one supported by WordPress  
Multisite, run it using the `--port=80` flag:

```
npx @wp-playground/cli@latest start --port=80
```

There are a few ways to set up a local test domain, including editing your `hosts` file. If you’re unsure how to do that, we suggest installing [Laravel Valet](https://laravel.com/docs/11.x/valet) and then running the following command:

```
valet proxy playground.test http://127.0.0.1:5400 --secure
```

Your dev server is now available on https://playground.test.

## Debugging

### Use VS Code and Chrome

If you’re using VS Code and have Chrome installed, you can debug Playground in the code editor:

- Open the project folder in VS Code.
- Select Run > Start Debugging from the main menu or press `F5`/`fn`+`F5`.

### Debugging PHP

Playground logs PHP errors in the browser console after every PHP request.

# Coding principles

## Error messages

A good error message informs the user of the following steps to take. Any ambiguity in errors thrown by Playground [Public APIs](https://developer.wordpress.org/playground/developers/apis/) will prompt the developers to open issues.

Consider a network error, for example—can we infer the type of error and display a relevant message summarizing the next steps?

- **Network error**: “Your internet connection twitched. Try to reload the page.
- **404**: “Could not find the file”.
- **403**: “The server blocked access to the file”.
- **CORS**: clarify it’s a browser security feature and add a link to a detailed explanation (on MDN or another reliable source). Suggest the user move their file somewhere else, like `raw.githubusercontent.com`, and link to a resource explaining how to set up CORS headers on their servers.

We handle code formatting and linting automatically. Relax, type away, and let the machines do the work.

## Public API

Playground aims to keep the narrowest possible API scope.

Public APIs are easy to add and hard to remove. It only takes one PR to introduce a new API, but it may take a thousand to remove it, especially if other projects have already consumed it.

- Don’t expose unnecessary functions, classes, constants, or other components.

## Blueprints

[Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started) are the primary way to interact with Playground. These JSON files describe a set of steps that Playground executes in order.

### Guidelines

Blueprint steps should be **concise and focused**. They should do one thing and do it well.

- If you need to create a new step, try refactoring an existing one first.
- If that’s not enough, ensure the new step delivers a new capability. Don’t replicate the functionality of existing steps.
- Assume the step would be called more than once.
- Assume it would run in a specific order.
- Add unit tests to verify that.

Blueprints should be **intuitive and straightforward**.

- Don’t require arguments that can be optional.
- Use plain argument. For example, `slug` instead of `path`.
- Define constants in virtual JSON files—don’t modify PHP files.
- Define a TypeScript type for the Blueprint. That’s how Playground generates its JSON schema.
- Write a function to handle a Blueprint step. Accept the argument of the type you defined.
- Provide a usage example in the doc string. It’s automatically reflected in the docs.

# Playground Contributor Badge

This page provides detailed information on the Playground Contributor Badge and the process of requesting it on your [WordPress.org](https://wordpress.org) profile.

---

## Getting Started

Any contribution to the WordPress Playground project is highly valued. The Playground team recognizes contributions across several key areas:

- **Playground Code:** Making code changes and reviewing the core project.
- **Playground UI:** Improving the user interface of the web experience.
- **Documentation:** Writing, updating, and reviewing documentation.
- **Translation:** Translating any part of the project.
- **Blueprints Gallery:** Creating new blueprints or improving existing ones.

## Playground Contributor Badge

To get a Playground Contributor Badge, you must have made at least one eligible contribution from the list above. The team may choose to award the badge for other contributions or a combination of the above at the team’s discretion.
![Playground Contributor Badge](https://i0.wp.com/developer.wordpress.org/files/2026/07/playground-contributor-badge.webp?fit=620%2C108&ssl=1)

## Playground Team Badge

If you are currently a contributor and have been actively involved in the Playground project for the past **twelve months**, you are eligible for a **Playground Team Badge**.
![Playground Contributor Badge](https://i0.wp.com/developer.wordpress.org/files/2026/07/playground-team-badge.webp?fit=620%2C108&ssl=1)

## Requesting a Profile Badge

If you meet the criteria, you can request a badge. Please include links to resources (such as GitHub pull requests, issues, or translated strings) that show you have met the criteria. Send a request at the links bellow:

- [Contributor Badge](https://profiles.wordpress.org/associations/playground-contributor/)
- [Team Badge](https://profiles.wordpress.org/associations/playground-team/)

### Request form

![Playground Contributor Badge](https://i0.wp.com/developer.wordpress.org/files/2026/07/request-contributor-badge.webp?fit=2850%2C1442&ssl=1)
To access the request, the user should be logged in with their WordPress.org account and open the Request Membership tab, after submitting the required information. A Playground Team Representative will confirm your contribution and assign the badge. The team will perform a weekly review of contributions and award badges at that time. Updates on new badges awarded will be posted during the Playground Team meeting.

# Contributor Day Table Leadership Guide

This guide helps table leads prepare for and manage a WordPress Playground contributor table at WordCamp events.

## Before Contributor Day

### Pre-Work Checklist

- **Curate “Good First Issues”**: Review and update the [good first issues list](https://github.com/WordPress/wordpress-playground/labels/good%20first%20issue) on GitHub. These should be straightforward tasks that new contributors can complete independently. If you find a bug that is not on the list but could be part of it, contact the playground team at the Slack channel.
- **Coordinate with the Playground Team**: Confirm if Playground team members are available online to provide remote support during the event, especially for flagship WordCamps. Due to timezone differences, align in advance at the #playground channel to check their availability.
- **Connect with Local Contributors**: Identify regular contributors in the region attending the event. Check on the #playground Slack Channel if an active community member is participating in the contributor day. This is an opportunity to gather feedback and strengthen community connections.
- **Check the Playground Repository**: If you never contribute with the WordPress Playground Repository, you should get familiar with this a good section at the documentation that can guide you to understand the project is [Developers > Architecture](https://developer.wordpress.org/playground/developers/architecture) it will contain information how the project is organized. If you have any questions, please get in touch with the team at the Playground Slack channel.

## Starting the Day

### Setup and Onboarding

1. **Create Your Agenda**: Prepare a flexible checklist of key activities while allowing for organic collaboration. Share it in the project documentation if helpful.
2. **Guide Contributors to Slack**: Direct everyone to the [`#playground` channel on WordPress Slack](https://wordpress.slack.com/archives/C04EWKGDJ0K). This centralizes communication and enables asynchronous collaboration with late arrivals.
3. **Post a Welcome Message**: Share an initial message in the Slack channel announcing your presence (in-person or online) and welcoming contributions from everyone.
4. **Share Essential Links**: Post these resources in the `#playground` channel:

   - [WordPress Playground Web Instance](https://playground.wordpress.net/)
   - [Playground Documentation](https://wordpress.github.io/wordpress-playground/)
   - [Playground Step Library](https://akirk.github.io/playground-step-library/)
   - [GitHub Repository](https://github.com/WordPress/wordpress-playground)
   - [Contributor Day guide](https://developer.wordpress.org/playground/contributing/contributor-day/)
5. **Introduce the GitHub Repository**: Provide a brief walkthrough of the repository structure, highlighting different packages and their purposes for first-time contributors.

## During the Day

### Managing Contributions

**Encourage Different Contribution Types**:

Check the contributors’ levels, try to understand based on their level how they can contribute to the project in the short window of a contributor day. Ask if the participants need help and redirect them to the related documentation page. Also, encourage them to ask questions at the [#playground Slack channel](https://wordpress.slack.com/archives/C04EWKGDJ0K). Here are some suggestions for ways of contributing:

- Documentation improvements and translations.
- Carefully crafted issues describing problems with actionable solutions.
- Blueprint creation and plugin demos at the WordPress plugin repository.
- Testing and product feedback.

**Foster Collaboration**: Look for cross-table opportunities. For example, contributors at the [Polyglots/Translation table](https://make.wordpress.org/polyglots/) might translate Playground documentation, or the [Core Test team](https://make.wordpress.org/test/) could provide valuable Playground feedback.

**Collect Feedback**: Ask contributors about their experience and note improvement suggestions. Report this in the [#playground Slack Channel](https://wordpress.slack.com/archives/C04EWKGDJ0K) if possible.

## After the Event

### Follow-Up and Support

1. **Review Pull Requests**: List PRs created during the day and assess their completion likelihood. Most contributions have a short momentum window—engagement within the first two weeks is critical.
2. **Set Clear Expectations**: For incomplete PRs, follow this approach:

   - After one month of inactivity, leave a comment asking if the author plans to complete the work.
   - If no response after two more weeks, inform them that the PR may be taken over by another contributor or closed.
3. **Stay Active on Slack**: Continue supporting new contributors through the `#playground` channel, answering questions and helping them become regular contributors.
4. **Reflect and Improve**: Review collected feedback and your experience to refine this guide for future events. Feel free to submit a Pull Request to this guide!

## Getting Help

- **During the Event**: Connect with contributors at the Playground table.
- **Ongoing Support**: Use the [`#playground` Slack channel](https://wordpress.slack.com/archives/C04EWKGDJ0K).
- **Report Issues**: Submit to the [WordPress Playground GitHub repository](https://github.com/WordPress/wordpress-playground/issues/new).

For more information on contributing to WordPress Playground, see the [Contributor Day guide](https://developer.wordpress.org/playground/contributing/contributor-day).

# WordCamp Contributor Day

WordCamp Contributor Day is an event where the WordPress community comes together to contribute to the WordPress project. This guide focuses on how you can contribute to the WordPress Playground project or how the Playground can assist you in contributing to WordPress Core.

## Who Can Contribute?

Some events will have a dedicated table for the project. The WordPress Playground contributor tables welcome all kinds of contributions, not just from developers. Whether you are a writer, coder, tester, plugin or theme developer, marketer, site owner, or any other type of user, you are encouraged to contribute.

We value diverse contributions across various areas, including community building, testing, documentation, and design.

## How to Contribute to the Playground Project

This section outlines how you can contribute directly to the WordPress Playground project and its associated tools:

- **Documentation:** Enhance our documentation by improving existing content, developing new guides, or translating materials into different languages.
- **Blueprints:** Create plugin demos for plugins at the WordPress Plugin repository, or develop new Blueprints to enrich our project documentation.
- **Testing the Playground Environment:** Engage in testing the WordPress Playground project itself. You can do this by carefully crafting new issues that describe problems you encounter and suggesting actionable solutions. Test our WordPress web instance (the playground.wordpress.net site), or explore the various applications powered by Playground. Test these tools, observe their functionality, and provide detailed feedback.
- **Product Feedback:** Your insights are invaluable for improving the Playground experience. This includes general feedback on the web instance, the application, and any server-side tools.

All feedback, including reported issues and test results, can be submitted through our GitHub repository.

### Follow-up and Continued Engagement

While many tasks are completed during the event, your contribution journey doesn’t have to end there. You are welcome to continue working on your issues or pull requests after Contributor Day. We anticipate ongoing activity from contributors who take on tasks beyond the event. Please note that if a pull request shows no activity for one month, it may be considered abandoned and subsequently closed.

### Getting Help and Staying Engaged

During Contributor Day, you can find direct assistance and interact with us at the dedicated Playground table. For continuous support and community interaction, you can connect with us on the `#playground` channel on WordPress Slack or via GitHub.

## How to use Playground at Contributor Day

Now we are going to cover how the Playground can assist you during the Contributor Day. The [WordPress Playground VS Code extension](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground) and [@wp-playground/cli](https://www.npmjs.com/package/@wp-playground/cli) streamline the process of setting up a local WordPress environment. WordPress Playground powers both—no Docker, MySQL, or Apache required.

Keep reading to learn how to use these tools for [local development](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) when contributing to WordPress. Please note that the extension and the NPM package are under development, and not all [Make WordPress teams](https://make.wordpress.org/) are fully supported.

## Getting Started

### VS Code Playground extension

The [Visual Studio Code Playground extension](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground) is a friendly zero-setup development environment.

1. Open VS Code and navigate to the **Extensions** tab (**View > Extensions**).
2. In the search bar, type *WordPress Playground* and click **Install**.
3. To interact with Playground, click the new icon in the **Activity Bar** and hit the **Start WordPress Server** button.
4. A new tab will open in your browser within seconds.

### @wp-playground/cli NPM package

[`@wp-playground/cli`](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) is a CLI tool that allows you to spin up a WordPress site with a single command. No Docker, MySQL, or Apache are required.

#### Prerequisites

`@wp-playground/cli` requires Node.js 20.18 or newer and NPM. If you haven’t yet, [download and install](https://nodejs.org/en/download) both before you begin.

Depending on the Make WordPress team you contribute to, you may need a different Node.js version than the one you have installed. You can use Node Version Manager (NVM) to switch between versions. [Find the installation guide here](https://github.com/nvm-sh/nvm#installing-and-updating).

#### Running `@wp-playground/cli`

You don’t have to install `@wp-playground/cli` on your device to use it. Navigate to your plugin or theme directory and start `@wp-playground/cli` with the following commands:

```
cd my-plugin-or-theme-directory
npx @wp-playground/cli@latest server --auto-mount
```

## Ideas for contributors

### Create a Gutenberg Pull Request (PR)

1. Fork the [Gutenberg repository](https://github.com/WordPress/gutenberg) in your GitHub account.
2. Then, clone the forked repository to download the files.
3. Install the necessary dependencies and build the code in development mode.

```
git clone git@github.com:WordPress/gutenberg.git
cd gutenberg
npm install
npm run dev
```

If you’re unsure about the steps listed above, visit the official [Gutenberg Project Contributor Guide](https://developer.wordpress.org/block-editor/contributors/). Note that in this case, `@wp-playground/cli` replaces `wp-env`.

Open a new terminal terminal tab, navigate to the Gutenberg directory, and start WordPress using `@wp-playground/cli`:

```
cd gutenberg
npx @wp-playground/cli@latest server --auto-mount
```

When you’re ready, commit and push your changes to your forked repository on GitHub and open a Pull Request on the Gutenberg repository.

### Test a Gutenberg PR

1. To test other Gutenberg PRs, checkout the branch associated with it.
2. Pull the latest changes to ensure your local copy is up to date.
3. Next, install the necessary dependencies, ensuring your testing environment matches the latest changes.
4. Finally, build the code in development mode.

```
# copy the branch-name from GitHub #
git checkout branch-name
git pull
npm install
npm run dev

# In a different terminal inside the Gutenberg directory *
npx @wp-playground/cli@latest server --auto-mount
```

#### Test a Gutenberg PR with Playground in the browser

You don’t need a [local development environment](https://developer.wordpress.org/playground/developers/local-development/) to test Gutenberg PRs—use Playground to do it directly in the browser.

1. Copy the ID of the PR you’d like to test (pick one from the [list of open Pull Requests](https://github.com/WordPress/gutenberg/pulls)).
2. Open Playground’s [Gutenberg PR Previewer](https://playground.wordpress.net/gutenberg.html) and paste the ID you copied.
3. Once you click **Go**, Playground will verify the PR is valid and open a new tab with the relevant PR, allowing you to review the proposed changes.

## Translate WordPress Plugins with Playground in the browser

You can translate supported WordPress Plugins by loading the plugin you want to translate and use Inline Translation. If the plugin developers have added the option, you’ll find the **Translate Live** link on the top right toolbar of the translation view. You can read more about this exciting new option on [this Polyglots blog post](https://make.wordpress.org/polyglots/2023/05/08/translate-live-updates-to-the-translation-playground/).

## Get help and contribute to WordPress Playground

Have a question or an idea for a new feature? Found a bug? Something’s not working as expected? We’re here to help:

- During Contributor Day, you can reach us at the **Playground table**.
- Open an issue on the [WordPress Playground GitHub repository](https://github.com/WordPress/wordpress-playground/issues/new). If your focus is the VS Code extension, NPM package, or the plugins, open an issue on the [Playground Tools repository](https://github.com/WordPress/playground-tools/issues/new).
- Share your feedback on the [**#playground** Slack channel](https://wordpress.slack.com/archives/C04EWKGDJ0K).

# Documentation contributions

[WordPress Playground’s documentation site](https://developer.wordpress.org/playground/) is maintained by volunteers like you, who’d love your help.

All documentation-related issues are labeled [`[Type] Documentation`](https://github.com/WordPress/wordpress-playground/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%5BType%5D%20Documentation%22) or [`[Type] Developer Documentation`](https://github.com/WordPress/wordpress-playground/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%5BType%5D%20Developer%20Documentation%22) in the [WordPress/wordpress-playground](https://github.com/WordPress/wordpress-playground) repository. Browse the list of open issues to find one you’d like to work on. Alternatively, if you believe something is missing from the current documentation, open an issue to discuss your suggestion.

## How can I contribute?

You can contribute by [opening an issue in the project repository](https://github.com/WordPress/wordpress-playground/issues/new) and describing what you’d like to add or change.

If you feel up to it, write the content in the issue description, and the project contributors will take care of the rest.

Would you like to see the documentation in your language? Check the [Translation section](https://developer.wordpress.org/playground/contributing/translations).

### Forking the repo, edit files locally and opening Pull Requests

If you are familiar with markdown, you can [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) the `wordpress-playground` repo and propose changes and new documentation pages by submitting a Pull Request.

The process of creating a branch to open new PRs with translated pages on the [WordPress/wordpress-playground](https://github.com/WordPress/wordpress-playground) repository is the same as contributing to other WordPress repositories such as Gutenberg:

> [Git Workflow](https://developer.wordpress.org/block-editor/contributors/code/git-workflow/)

The documentation files (`.md` files) are stored in Playground’s GitHub repository, [under `/packages/docs/site/docs`](https://github.com/WordPress/wordpress-playground/tree/trunk/packages/docs/site/docs) for English and [`/packages/docs/site/i18n`](https://github.com/WordPress/wordpress-playground/tree/trunk/packages/docs/site/i18n) for other languages.

### Edit in the browser

If logged in GitHub, you can also edit existing files (or add new ones) and submit a PR directly from the GitHub UI:

1. Find the page you’d like to edit or the directory of the chapter you’d like to add a new page to.
2. Click the **Add Files** button to add a new file, or click on an existing file and then click the pencil icon to edit it.
3. GitHub will ask you to fork the repository and create a new branch with your changes.
4. An editor will open where you can make the changes.
5. When you’re done, click the **Commit Changes** button and submit a Pull Request.

That’s it! You’ve just contributed to the WordPress Playground documentation.

This approach means you don’t need to clone the repository, set up a local development environment, or run any commands.

The downside is that you won’t be able to preview your changes. Keep reading to learn how to review your changes before submitting a Pull Request.

### Local preview

Clone the repository and navigate to the directory on your device. Now run the following commands:

```
npm install
npm run build:docs
npm run dev:docs
```

The documentation site opens in a new browser tab and refreshes automatically with each change. Continue to edit the relevant file in your code editor and test the changes in real-time.

# Releasing packages

Playground publishes its packages to npm using automated CI workflows. This page explains how the release process works and what you need to know when adding new packages.

## Automated releases

The npm packages are published automatically every Monday via GitHub Actions, or manually by maintainers using the workflow dispatch. The workflow bumps versions using [Lerna](https://lerna.js.org/), tags the release, and publishes all public packages to npm.
The CI authenticates with npm using [OpenID Connect (OIDC) trusted publishing](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions). This is more secure than using long-lived npm tokens because it generates short-lived credentials for each workflow run and ties package provenance directly to the GitHub repository.

## Adding a new package

When you add a new npm package to the monorepo, the automated release workflow won’t be able to publish it on the first run. This is an npm security feature: OIDC trusted publishing only works for packages that already exist and have been configured to trust the GitHub repository.
Here’s what you need to do:

### 1. Publish the package manually

First, authenticate with npm on your local machine:

```
npm login
```

Then publish the package for the first time:

```
cd packages/your-new-package
npm publish --access public
```

This creates the package on the npm registry under your account.

### 2. Configure trusted publishing

After the initial publish, go to the package’s settings on npmjs.com and set up OIDC trusted publishing:

1. Navigate to your package on [npmjs.com](https://www.npmjs.com)
2. Go to **Settings** → **Configure Trusted Publishers**
3. Add a new trusted publisher with these settings. All of them are case-sensitive!
   - **Repository owner**: `WordPress`
   - **Repository name**: `wordpress-playground`
   - **Workflow filename**: `publish-npm-packages.yml`
   - **Environment**: `npm`

![Setting up OIDC trusted publishing on npm](https://i0.wp.com/developer.wordpress.org/files/2026/07/php-wasm-node-oidc.webp?fit=3016%2C1468&ssl=1)

### 3. Transfer ownership (if needed)

If you published under your personal account, transfer the package to the `@aspect` organization or ensure the appropriate team has publish access.
Once configured, subsequent releases will work automatically through the CI workflow.

## Why OIDC can’t publish new packages

npm’s OIDC implementation requires the package to already exist before a trusted publisher can be configured. This is a chicken-and-egg situation by design—it prevents someone from hijacking a package name through a GitHub workflow before the legitimate owner can claim it.
The manual first publish establishes ownership, and trusted publishing then provides secure, token-free authentication for all future releases.

# Contributions to translations

Help make WordPress Playground accessible to a global audience by translating its documentation. This guide provides everything you need to know to get started. Contributing translations follows the same workflow as any other documentation change. You can either fork the [WordPress/wordpress-playground](https://github.com/WordPress/wordpress-playground) repository and create a pull request (PR) with your changes or edit pages directly using the GitHub UI.

For a detailed guide on the contribution workflow (forking, creating PRs, etc.), please see our [documentation contribution guide](https://developer.wordpress.org/playground/contributing/documentation#how-can-i-contribute)

## How Translations Work

Playground’s documentation site is built with Docusaurus, which handles the internationalization (i18n) features.

To learn more about how Docusaurus manages translations, see the [Internationalization section](https://docusaurus.io/docs/i18n/introduction) of the official Docusaurus documentation.

### Configuration

Available languages are defined in the `packages/docs/site/docusaurus.config.js` file. For example:

```
i18n: {
  defaultLocale: 'en',
  path: 'i18n',
  locales: ['en', 'fr'],
  localeConfigs: {
    en: {
        label: 'English',
        path: 'en',
    },
    fr: {
        label: 'French',
        path: 'fr',
    },
  },
}
```

### File Structure

All translated documentation pages are located within the `packages/docs/site/i18n/` directory, organized by language code.
For a language to work correctly, its file structure must mirror the original English documentation found in `packages/docs/site/docs`.
For example, the Spanish (es) translation for `docs/main/intro.md` must be placed at:
packages`/docs/site/i18n/es/docusaurus-plugin-content-docs/current/main/intro.md`.
If a translated file does not exist for a specific language, Docusaurus will automatically fall back to the English version of that page.

### Generating Translation Files

When adding a new language, you can generate the necessary JSON files for UI strings (like button labels and navigation items) by running the following command from the `packages/docs/site` directory:

```
npm run write-translations -- --locale <LANGUAGE_CODE>
```

With the proper i18n `docusaurus.config.js` configuration and files under `i18n` when running `npm run build:docs` from the root of the project, specific folders under `dist` for each language will be created.

## Testing Translations Locally

To preview your changes for an existing language:

1. Modify or add a translated file in the appropriate language directory, such as `packages/docs/site/i18n/es/docusaurus-plugin-content-docs/current/`.
2. From the `/packages/docs/site` directory, run the local development server for your target language. For example, to test Spanish (es):

```
npm run dev -- --locale es
```

## The Language Switcher

The language switcher is a dropdown menu that allows users to select their preferred language.
![Documentation Language Switcher](https://i0.wp.com/developer.wordpress.org/files/2026/07/language-switcher-docs-scaled.webp?fit=3200%2C2067&ssl=1)

### Making a language publicly available on the Language Switcher

We recommend only adding a language to the switcher when a significant portion of the documentation has been translated. This avoids a poor user experience where switching to a new language results in seeing mostly untranslated English content.
As a guideline, a language should be made publicly available in the switcher only when the entire “Documentation” hub is translated, including these key sections:

- [Quick Start Guide](https://wordpress.github.io/wordpress-playground/quick-start-guide)
- [Playground web instance](https://wordpress.github.io/wordpress-playground/web-instance)
- [About Playground](https://wordpress.github.io/wordpress-playground/about)
- [Guides](https://wordpress.github.io/wordpress-playground/guides)
- [Contributing](https://wordpress.github.io/wordpress-playground/contributing)
- [Links and Resources](https://wordpress.github.io/wordpress-playground/links-and-resources)

All languages are available once the i18n setup for a language is complete and the correct file structure is in place under `i18n`.

- https://wordpress.github.io/wordpress-playground/
- https://wordpress.github.io/wordpress-playground/es/
- https://wordpress.github.io/wordpress-playground/fr/

Assuming the `fr` language is the first language with the Documentation hub pages (Quick Start Guide, Playground web instance, About Playground, Guides,… ) completely translated to French, the `docusaurus.config.js` should look like this in that branch so `npm run build:docs` properly generate the `fr` subsite and only displays the french language in the `localeDropdown` language switcher.

```
  {
    "i18n": {
      "defaultLocale": "en",
      "path": "i18n",
      "locales": [
        "en",
        "fr"
      ],
      "localeConfigs": {
        "en": {
          "label": "English",
          "path": "en"
        },
        "fr": {
          "label": "French",
          "path": "fr"
        }
      }
    }
  },
  {
    "type": "localeDropdown",
    "position": "right"
  }
```

## Translation Workflow

Follow these steps to translate a page:

1. **Check for an Existing Translation Issue**: First, [search the repository issues](https://github.com/WordPress/wordpress-playground/issues?q=is%3Aissue%20state%3Aopen%20%5Btranslation%5D%20progress) to see if a tracking issue for your desired language already exists. If it does, comment on the issue to claim the page(s) you would like to translate.
2. **Create a New Translation Issue**: If no issue exists, please create a new one to track the translation progress for the language. You can model it after issue [#2202](https://github.com/WordPress/wordpress-playground/issues/2202) and use the markdown checklist below to track progress.
3. **Translate the File**:

- Check if you have the latest version of the documentation
- Copy the original .md file from `packages/docs/site/docs/...` to the corresponding path in the language directory (e.g., `packages/docs/site/i18n/<LANGUAGE_CODE>/...`). It is crucial to replicate the original file structure.
- Translate the content of the new file, keeping the original content commented out `<!-- English Content -->`.
- The assets are listed at `packages/docs/site/static/img/` only place assets inside the translation folder when it requires localized content.
- Once the translations are ready, check if the docs build script is running properly `npm run build:docs`.

1. **Create a pull request with your changes**

- Add a prefix to the title `[i18n]` to help to identify the translations
- Describe the pages that you translated
- Request a review at `#playground` or `#polyglots` at `wordpress.slack.com`

We highly recommend submitting pull requests with a small number of translated pages. This approach simplifies the review process and allows for a more gradual and manageable integration of your work.

### Translation Tracking Template

You can use the following markdown in your tracking issue:

```
## Remaining translation pages

<details open>
<summary><h3>Main</h3></summary>

- about
  - [ ] build.md
  - [ ] index.md
  - [ ] launch.md
  - [ ] test.md
- contributing
  - [ ] code.md
  - [ ] coding-standards.md
  - [ ] contributor-badge.md
  - [ ] contributor-day.md
  - [ ] contributor-day-table-lead.md
  - [ ] documentation.md
  - [ ] index.md
  - [ ] releases.md
  - [ ] translations.md
- guides
  - [ ] for-plugin-developers.md
  - [ ] for-theme-developers.md
  - [ ] github-action-pr-preview.md
  - [ ] index.md
  - [ ] providing-content-for-your-demo.md
  - [ ] wordpress-native-ios-app.md
- [ ] changelog.md
- [ ] intro.md
- [ ] quick-start-guide.md
- [ ] resources.md
- [ ] web-instance.md

</details>

<details open>
<summary><h3>Blueprints</h3></summary>

- [ ] 01-index.md
- [ ] 02-using-blueprints.md
- [ ] 03-data-format.md
- [ ] 04-resources.md
- [ ] 05-steps.md
- [ ] 05-steps-shorthands.md
- [ ] 06-bundles.md
- [ ] 07-json-api-and-function-api.md
- [ ] 08-examples.md
- [ ] 09-troubleshoot-and-debug-blueprints.md
- [ ] intro.md
- tutorial
  - [ ] 01-what-are-blueprints-what-you-can-do-with-them.md
  - [ ] 02-how-to-load-run-blueprints.md
  - [ ] 03-build-your-first-blueprint.md
  - [ ] index.md

</details>

<details open>
<summary><h3>Developers</h3></summary>

- 03-build-an-app
  - [ ] 01-index.md
- 05-local-development
  - [ ] 01-wp-now.md
  - [ ] 02-vscode-extension.md
  - [ ] 03-php-wasm-node.md
  - [ ] 04-wp-playground-cli.md
  - [ ] intro.md
- 06-apis
  - [ ] 01-index.md
  - javascript-api
    - [ ] 01-index.md
    - [ ] 02-index-html-vs-remote-html.md
    - [ ] 03-playground-api-client.md
    - [ ] 04-blueprint-json-in-api-client.md
    - [ ] 05-blueprint-functions-in-api-client.md
    - [ ] 06-mount-data.md
  - query-api
    - [ ] 01-index.md
- 07-xdebug
  - [ ] 01-introduction.md
  - [ ] 02-getting-started.md
- 23-architecture
  - [ ] 01-index.md
  - [ ] 02-wasm-php-overview.md
  - [ ] 03-wasm-php-compiling.md
  - [ ] 04-wasm-php-javascript-module.md
  - [ ] 05-wasm-php-filesystem.md
  - [ ] 07-wasm-asyncify.md
  - [ ] 08-browser-concepts.md
  - [ ] 09-browser-tab-orchestrates-execution.md
  - [ ] 10-browser-iframe-rendering.md
  - [ ] 11-browser-php-worker-threads.md
  - [ ] 12-browser-service-workers.md
  - [ ] 13-browser-scopes.md
  - [ ] 14-browser-cross-process-communication.md
  - [ ] 15-wordpress.md
  - [ ] 16-wordpress-database.md
  - [ ] 17-browser-wordpress.md
  - [ ] 18-host-your-own-playground.md
- 24-limitations
  - [ ] 01-index.md
- [ ] intro-devs.md

</details>
```

### Translating with the GitHub Web Interface

If you prefer not to use developer tools, you can easily contribute translations directly on the GitHub website. All you need is a free GitHub account.
This guide will show you how to both update an existing translation and add a brand-new one.

---

#### Updating an Existing Translation

1. **Navigate to the file.** Go to the repository and find the file you want to update. Translation files are located in a folder named after their language code. For example, all French translations are in `packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/`.
2. **Open the editor.** Select the file you wish to edit and click the pencil icon (**Edit this file**) in the upper right corner.
   ![Editing existing translation](https://i0.wp.com/raw.githubusercontent.com/WordPress/wordpress-playground/refs/heads/trunk/packages/docs/site/static/img/contributing/editing-translations.webp?ssl=1)
3. **Fork the repository.** GitHub will automatically prompt you to **Fork this repository**. This creates a personal copy for you to edit safely. Click the button to proceed.
4. **Make your changes.** The editor will open in your browser. Update the text with your improved translations.
5. **Propose your changes.** Once you are finished, scroll to the bottom of the page. Add a brief title and description of your changes (e.g., “Fixing typos in French translation”) and click the **Propose changes** button.
6. **Create a Pull Request.** On the next screen, click the **Create pull request** button. This will submit your changes to the project maintainers for review.

---

#### Adding a New Translation

1. **Determine the correct file path.** The new file’s path and name must mirror the original English file.
   - **English original:** `packages/docs/site/docs/main/contributing/documentation.md`
   - **French translation:** `packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/main/contributing/documentation.md`
2. **Create the new file.** Navigate to the correct language folder (e.g., `/packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/`). Click **Add file** > **Create new file**.
   ![Creating a new translation](https://i0.wp.com/developer.wordpress.org/files/2026/07/adding-file-github-ui.webp?fit=3104%2C2024&ssl=1)
   - **Pro Tip:** In the filename box, you can create new folders by typing the folder name followed by a `/`. For example, typing `main/contributing/documentation.md` will create the `main` and `contributing` folders automatically.
3. **Fork the repository.** Just like before, GitHub will prompt you to **Fork this repository**. Click the button to create your personal copy.
4. **Add the translated content.** The editor will open with an empty file. For the convenience of reviewers, please copy the content from the original English file and paste it into your new file, wrapping it in comment tags. Add your translation below it.

   ```
   <!--
   This is the original English content.
   It helps reviewers understand the context of the translation.
   -->

   Ceci est le contenu traduit en français.
   ```

   ![GitHub UI Editor](https://i0.wp.com/raw.githubusercontent.com/WordPress/wordpress-playground/refs/heads/trunk/packages/docs/site/static/img/contributing/editor-github-ui.webp?ssl=1)
5. **Commit the new file.** When you are done, scroll to the bottom. Add a title for your new file (e.g., “Add French translation for documentation.md”) and click the **Commit new file** button.
6. **Create a Pull Request.** On the next screen, click **Create pull request** to submit your new translation for review.

## Review Process

To simplify the review process, please keep the original English text as a comment directly above the translated content.

```
<!--
👋 Hi! Welcome to WordPress Playground documentation.

Playground is an online tool to experiment and learn about WordPress. This site (Documentation) is where you will find all the information you need to start using Playground.
-->

👋 Olá! Bem vindo a documentação oficial do WordPress Playground.

WordPress Playground é uma ferramenta online onde podes testar e aprender mais sobre o WordPress. Nesta página(Documentação) irá encontrar todas as informações necessárias para começar a trabalhar com o Playground.
```

This practice also helps the maintenance team identify outdated translations. When the original English content is updated, we can search the codebase for the old text (now in comments) and flag the corresponding translation for review.

To find a reviewer fluent in the language of your PR, you can post a request on the [Make WordPress Polyglots blog](https://make.wordpress.org/polyglots/). Be sure to include the locale tag (e.g., #ja for Japanese) to notify the appropriate General Translation Editors (GTEs).
When the PR is merged, the translated version of that page should appear under `https://wordpress.github.io/wordpress-playground/{%LANGUAGE%}`, if you are contributing for the first time request your [Contributor Badge](https://developer.wordpress.org/playground/contributing/contributor-badge).

# E2E Testing with Playwright and WordPress Playground

End-to-end testing verifies that your WordPress plugin or theme works correctly from a user’s perspective — clicking buttons, filling forms, and navigating pages in a real browser. This guide shows how to combine [Playwright](https://playwright.dev/) with the [WordPress Playground CLI](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) to write reliable E2E tests without Docker, databases, or manual setup.

This guide assumes familiarity with WordPress plugin or theme development. For an introduction to using Playground in your development workflow, see [WordPress Playground for Plugin Developers](https://developer.wordpress.org/playground/guides/for-plugin-developers). For Blueprint configuration details, see [Blueprints Getting Started](https://developer.wordpress.org/playground/blueprints/getting-started).

## Prerequisites

- **Node.js 20+** and up
- A WordPress plugin/theme or an entire WordPress site to test
- **Recommended:** enable the `@typescript-eslint/no-floating-promises` ESLint rule to catch missing `await` on async Playwright calls

## Project setup

### Install dependencies

From your plugin or theme root directory:

```
npm init -y
npm install --save-dev @playwright/test @wp-playground/cli
npx playwright install chromium
```

This installs Playwright as the test runner, the Playground CLI for creating WordPress instances, and the Chromium browser for test execution.

### Configure Playwright

Create a `playwright.config.ts` file in your project root:

```
export default defineConfig({
    testDir: './tests/e2e',
    fullyParallel: false,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: 1,
    reporter: 'html',
    timeout: 120_000,
    expect: {
        timeout: 30_000,
    },
    use: {
        screenshot: 'only-on-failure',
        trace: 'on-first-retry',
    },
});
```

WordPress Playground needs more time to start than a typical web app. The 120-second test timeout and 30-second assertion timeout account for WordPress boot time and page loads. Setting `workers: 1` prevents port conflicts when multiple tests share a Playground server.

**Using baseURL with dynamic ports**

By default, Playground will sign the port `9400`. If you want to select a different port, pass `port: [NEW_PORT_NUMBER]` in the `runCLI` options to select a different port:

```
const cli = await runCLI({ command: 'server', port: 9500, blueprint });
```

Then add `baseURL: "http://localhost:9500"` to the `use` section above. Note that `testMatch` defaults to `**/*.spec.ts` — customize it if your test files use a different naming pattern.

The WordPress Playground project uses even longer timeouts (300s test, 60s assertion) for its own tests. Start with the values above and increase if your CI environment is slower.

### First test file

Create `tests/e2e/plugin.spec.ts`:

```
let cli: Awaited<ReturnType<typeof runCLI>>;

test.beforeAll(async () => {
    cli = await runCLI({
        command: 'server',
        blueprint: {
            preferredVersions: { php: '8.3', wp: 'latest' },
            login: true,
        },
    });
});

test.afterAll(async () => {
    await cli?.server?.close();
});

test('WordPress dashboard loads', async ({ page }) => {
    await page.goto(`${cli.serverUrl}/wp-admin/`);
    // WordPress core admin elements lack ARIA roles — CSS selectors are acceptable here
    await expect(page.locator('#wpbody-content')).toBeVisible();
    await expect(page).toHaveTitle(/Dashboard/);
});
```

Run the test:

```
npx playwright test
```

## Choosing locators

Playwright provides several ways to find elements on the page. Prefer locators that reflect how users see the page, falling back to CSS selectors only when necessary.

**Locator priority** (most to least preferred):

1. `page.getByRole()` — buttons, headings, links, form controls
2. `page.getByLabel()` — form inputs with associated labels
3. `page.getByText()` — visible text content
4. `page.getByTestId()` — elements with `data-testid` attributes you add to your plugin
5. `page.locator()` — CSS or XPath selectors as a last resort

### WordPress-specific guidance

In the WordPress admin, some core elements (admin bar, meta boxes) rely on IDs and CSS classes rather than ARIA roles. However, many elements work well with semantic locators. This means:

- **Use semantic locators** for buttons, headings, links, form fields, and admin menu items — WordPress renders standard `<button>`, `<input>`, `<a>`, and `<h1>` elements that `getByRole` and `getByLabel` can find.
- **Use `data-testid`** for your own plugin markup — you control the HTML, so add testable attributes.
- **Use CSS selectors** for WordPress core layout elements like `#wpadminbar` or `#wpbody-content` — these lack ARIA alternatives.

### Same element, three approaches

```
// ✅ Preferred: semantic locator (works because WP renders a real <button>)
await page.getByRole('button', { name: 'Save Changes' }).click();

// ⚠️ Acceptable: test ID you added to your plugin markup
await page.getByTestId('save-settings').click();

// ❌ Avoid: brittle CSS selector tied to WordPress markup
await page.locator('#submit').click();
```

**Generate locators automatically**

Run `npx playwright codegen localhost:9400/wp-admin/` to open a browser and record interactions. Playwright generates locator code as you click, helping you discover which semantic locators work for each element.

## Auto-waiting and web-first assertions

Playwright locators wait automatically for elements to appear, become visible, and become actionable. You do not need manual `waitForSelector` calls in most cases.

### Web-first assertions

Web-first assertions auto-retry until the condition passes or the timeout expires. Always prefer them over manual checks:

```
// ✅ Web-first assertion (auto-retries until visible or timeout)
await expect(page.getByText('Settings saved')).toBeVisible();

// ❌ Manual check (no retry — flaky if the element appears after a delay)
expect(await page.getByText('Settings saved').isVisible()).toBe(true);
```

### Soft assertions

Use `expect.soft()` to check multiple things on one page without stopping at the first failure. All failures appear in the test report:

```
await expect.soft(page.getByLabel('API Key')).toHaveValue('test-key-123');
await expect.soft(page.getByText('Settings saved')).toBeVisible();
await expect.soft(page.getByRole('heading', { level: 1 })).toContainText('Settings');
```

## Writing tests

### Starting a Playground server

The `runCLI` function starts a local Playground server and returns an object with `serverUrl` (the URL string) and `server` (the HTTP server instance). Pass a Blueprint to configure the WordPress instance:

```
const cli = await runCLI({
    command: 'server',
    blueprint: {
        preferredVersions: { php: '8.3', wp: 'latest' },
        login: true,
        steps: [
            {
                step: 'installPlugin',
                pluginData: {
                    resource: 'wordpress.org/plugins',
                    slug: 'woocommerce',
                },
            },
        ],
    },
});
```

#### Server lifecycle: shared vs. per-test

**Shared server (`beforeAll`/`afterAll`)** — one Playground instance serves all tests in a describe block. Faster, but tests can affect each other:

```
test.describe('Plugin settings', () => {
    test.beforeAll(async () => {
        cli = await runCLI({ command: 'server', blueprint });
    });
    test.afterAll(async () => {
        await cli?.server?.close();
    });
    // Tests share the same WordPress instance
});
```

**Per-test server (`beforeEach`/`afterEach`)** — each test gets a fresh instance. Slower, but fully isolated:

```
test.beforeEach(async () => {
    cli = await runCLI({ command: 'server', blueprint });
});
test.afterEach(async () => {
    await cli?.server?.close();
});
```

Use shared servers when tests only read state (checking pages render). Use per-test servers when tests modify state (creating posts, changing settings).

### Using Blueprints as test fixtures

Blueprints define the WordPress state each test scenario needs. Here are common patterns:

#### Installing a plugin from wordpress.org

```
const blueprint = {
    preferredVersions: { php: '8.3', wp: 'latest' },
    login: true,
    steps: [
        {
            step: 'installPlugin',
            pluginData: {
                resource: 'wordpress.org/plugins',
                slug: 'contact-form-7',
            },
        },
    ],
};
```

#### Installing a local plugin

Mount your local plugin directory into the Playground instance:

```
const cli = await runCLI({
    command: 'server',
    mount: {
        './': '/wordpress/wp-content/plugins/my-plugin',
    },
    blueprint: {
        preferredVersions: { php: '8.3', wp: 'latest' },
        login: true,
        steps: [
            {
                step: 'activatePlugin',
                pluginPath: 'my-plugin/my-plugin.php',
            },
        ],
    },
});
```

This maps your current directory to the plugin path inside WordPress, then activates the plugin. Changes to your local files are reflected immediately. The user can set the `autoMount` property to identify plugins and themes, but the `mount` property will provide more control to the user to set different folders in the project.

#### Setting options and creating content

```
const blueprint = {
    login: true,
    steps: [
        {
            step: 'setSiteOptions',
            options: {
                blogname: 'Test Site',
                permalink_structure: '/%postname%/',
            },
        },
        {
            step: 'runPHP',
            code: `<?php
        require '/wordpress/wp-load.php';
        wp_insert_post([
          'post_title' => 'Test Post',
          'post_content' => '<!-- wp:paragraph --><p>Hello World</p><!-- /wp:paragraph -->',
          'post_status' => 'publish',
        ]);
      `,
        },
    ],
};
```

Use the [Playground Step Library](https://akirk.github.io/playground-step-library/) or [Pootle Playground](https://pootleplayground.com/) to prototype your Blueprint configuration visually before adding it to your test code.

### Testing WordPress admin pages

Navigate to admin pages and interact with the WordPress UI:

```
test('plugin settings page saves options', async ({ page }) => {
    await page.goto(`${cli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);

    await page.getByLabel('API Key').fill('test-key-123');
    await page.getByRole('button', { name: 'Save Changes' }).click();

    await expect(page.getByText('Settings saved')).toBeVisible();
    await expect(page.getByLabel('API Key')).toHaveValue('test-key-123');
});
```

#### Handling common admin UI elements

```
// Dismiss WordPress admin notices (WP adds aria-label to dismiss buttons)
await page.getByRole('button', { name: 'Dismiss this notice' }).first().click();

// Wait for admin bar to load — no ARIA role available, use locator
await page.locator('#wpadminbar').waitFor();

// Navigate via admin menu
await page.getByRole('link', { name: 'My Plugin' }).first().click();
```

### Testing the front end

```
test('plugin shortcode renders on front end', async ({ page }) => {
    // Navigate to a page with the shortcode
    await page.goto(`${cli.serverUrl}/?p=2`);

    // Recommend: add data-testid="my-plugin-widget" to your plugin markup
    await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
    await expect(page.getByTestId('my-plugin-widget')).toContainText('Expected content');
    // Or use CSS if you don't control the markup:
    // await expect(page.locator(".my-plugin-widget")).toBeVisible();
});

test('theme displays post correctly', async ({ page }) => {
    await page.goto(`${cli.serverUrl}/test-post/`);

    await expect(page.getByRole('heading', { level: 1 })).toContainText('Test Post');
    await expect(page.getByText('Hello World', { exact: true })).toBeVisible();
});
```

## Page Object Model pattern

The Page Object Model (POM) wraps page interactions into reusable classes. This reduces duplication and makes tests easier to maintain when your plugin UI changes.

```
// tests/e2e/pages/plugin-settings.ts

export class PluginSettingsPage {
    readonly page: Page;
    readonly apiKeyInput: Locator;
    readonly saveButton: Locator;
    readonly successNotice: Locator;

    constructor(page: Page) {
        this.page = page;
        this.apiKeyInput = page.getByLabel('API Key');
        this.saveButton = page.getByRole('button', { name: 'Save Changes' });
        this.successNotice = page.getByText('Settings saved');
    }

    async goto(baseUrl: string) {
        await this.page.goto(`${baseUrl}/wp-admin/options-general.php?page=my-plugin`);
    }

    async setApiKey(key: string) {
        await this.apiKeyInput.fill(key);
        await this.saveButton.click();
    }

    async expectSaved() {
        await expect(this.successNotice).toBeVisible();
    }
}
```

Use the POM in tests:

```
test('save plugin settings', async ({ page }) => {
    const settings = new PluginSettingsPage(page);
    await settings.goto(cli.serverUrl);
    await settings.setApiKey('test-key-123');
    await settings.expectSaved();
});
```

The Playground project uses this pattern with a `WebsitePage` class that provides methods like `goto()`, `wordpress()`, and `getSiteTitle()` — encapsulating navigation and WordPress-specific interactions.

## Testing across PHP and WordPress versions

Parameterized tests cover multiple version combinations without duplicating test code:

```
const versionMatrix = [
    { php: '8.1', wp: '6.5' },
    { php: '8.2', wp: '6.7' },
    { php: '8.3', wp: 'latest' },
];

for (const { php, wp } of versionMatrix) {
    test.describe(`PHP ${php} + WP ${wp}`, () => {
        let versionCli: Awaited<ReturnType<typeof runCLI>>;

        test.beforeAll(async () => {
            versionCli = await runCLI({
                command: 'server',
                blueprint: {
                    preferredVersions: { php, wp },
                    login: true,
                    steps: [
                        {
                            step: 'activatePlugin',
                            pluginPath: 'my-plugin/my-plugin.php',
                        },
                    ],
                },
            });
        });

        test('admin page loads without errors', async ({ page }) => {
            await page.goto(`${versionCli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);
            // WordPress core elements use CSS selectors — no ARIA roles available
            await expect(page.locator('.error')).not.toBeVisible();
            await expect(page.locator('#wpbody-content')).toBeVisible();
        });

        test('front-end output renders', async ({ page }) => {
            await page.goto(versionCli.serverUrl);
            await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
        });

        test.afterAll(async () => {
            await versionCli?.server?.close();
        });
    });
}
```

The `preferredVersions` property in the Blueprint controls which PHP and WordPress versions the Playground instance uses. Supported ranges: PHP 7.4–8.5, WordPress 6.3–6.8+, plus `latest`, `nightly`, and `beta`. For type-safe PHP version values, use the `SupportedPHPVersion` type from `@php-wasm/universal`.

## Running tests in CI/CD

### GitHub Actions

Create `.github/workflows/e2e-tests.yml`:

```
name: E2E Tests

on:
    push:
        branches: [main]
    pull_request:
        branches: [main]

jobs:
    e2e:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4

            - uses: actions/setup-node@v4
              with:
                  node-version: 20
                  cache: 'npm'

            - name: Install dependencies
              run: npm ci

            - name: Cache Playwright browsers
              uses: actions/cache@v4
              id: playwright-cache
              with:
                  path: ~/.cache/ms-playwright
                  key: playwright-${{ hashFiles('package-lock.json') }}

            - name: Install Playwright browsers
              if: steps.playwright-cache.outputs.cache-hit != 'true'
              run: npx playwright install chromium --with-deps

            - name: Run E2E tests
              run: npx playwright test

            - name: Upload test report
              uses: actions/upload-artifact@v4
              if: ${{ !cancelled() }}
              with:
                  name: playwright-report
                  path: playwright-report/
                  retention-days: 30
```

This workflow installs dependencies, downloads Chromium, runs the tests, and uploads the HTML report as an artifact. The `--with-deps` flag installs system libraries Chromium needs on Ubuntu.

**Sharding for faster CI**

Split tests across multiple CI jobs with Playwright’s built-in sharding:

```
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
```

Create three parallel jobs in your workflow matrix, each running a different shard. This reduces total CI time proportionally.

For manual PR testing alongside automated E2E tests, see [Adding PR Preview Buttons with GitHub Actions](https://developer.wordpress.org/playground/guides/github-action-pr-preview).

## Troubleshooting

**Timeout errors** — Increase `timeout` in `playwright.config.ts`. WordPress boot time varies by environment. CI runners often need 120–180 seconds.

**Port conflicts** — Let Playground auto-assign ports. Do not hardcode port numbers in your configuration. The `serverUrl` property returns the correct URL.

**Browser not found** — Run `npx playwright install chromium` to download the browser binary. On CI, add `--with-deps` for system libraries.

**WordPress not loading** — Check your Blueprint syntax against the [Blueprint schema](https://playground.wordpress.net/blueprint-schema.json). Invalid steps fail silently in some cases.

**Tests pass locally but fail in CI** — CI runners have less memory and CPU. Increase timeouts, reduce parallel workers, and ensure `workers: 1` in the config.

## Debugging tests

When a test fails, Playwright provides several tools to investigate:

**Playwright Inspector** — step through tests interactively with a built-in debugger:

```
npx playwright test --debug
```

**Trace viewer** — inspect a timeline of actions, DOM snapshots, and network requests from a failed test. The `trace: "on-first-retry"` setting in the config above captures traces automatically:

```
npx playwright show-trace test-results/plugin-spec-ts/trace.zip
```

**UI mode** — run tests in a visual interface where you can watch, filter, and re-run them:

```
npx playwright test --ui
```

**Screenshot on failure** — the `screenshot: "only-on-failure"` setting in the config saves a screenshot whenever a test fails. Find screenshots in the `test-results/` directory.

Combine `--debug` with a specific test file to focus your investigation: `npx playwright test tests/e2e/settings.spec.ts --debug`

## Next steps

- [WordPress Playground CLI documentation](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) — full CLI reference
- [Playwright documentation](https://playwright.dev/docs/intro) — test writing guide and API reference
- [Blueprints reference](https://developer.wordpress.org/playground/blueprints/steps) — all available Blueprint steps
- [Adding PR Preview Buttons](https://developer.wordpress.org/playground/guides/github-action-pr-preview) — combine automated tests with manual PR previews

# WordPress Playground for Plugin Developers

The WordPress Playground is an innovative tool that allows plugin developers to build, test and showcase their plugins directly in a browser environment.

This guide will show you how to use WordPress Playground to improve your plugin development workflow, create live demos to showcase your plugin, and simplify your plugin testing and review.

Discover how to [Build](https://developer.wordpress.org/playground/about/build), [Test](https://developer.wordpress.org/playground/about/test), and [Launch](https://developer.wordpress.org/playground/about/launch) your products with WordPress Playground in the [About Playground](https://developer.wordpress.org/playground/about) section.

## Launching a Playground instance with a plugin

### Plugin in the WordPress plugins directory

With WordPress Playground, you can quickly launch a WordPress installation with almost any plugin available in the [WordPress Plugins Directory](https://wordpress.org/plugins/) installed and activated. All you need to do is to add the `plugin` [query parameter](https://developer.wordpress.org/playground/developers/apis/query-api) to the [Playground URL](https://playground.wordpress.net) and use the slug of the plugin from the WordPress directory as a value. For example: https://playground.wordpress.net/?plugin=create-block-theme

You can install and activate several plugins via query parameters by repeating the `plugin` parameter for every plugin you want to be installed and activated in the Playground instance. For example: <https://playground.wordpress.net/?plugin=gutenberg&plugin=akismet&plugin=wordpress-seo>.

You can also load any plugin from the WordPress plugins directory by setting the [`installPlugin` step](https://developer.wordpress.org/playground/blueprints/steps#InstallPluginStep) of a [Blueprint](https://developer.wordpress.org/playground/blueprints/getting-started) passed to the Playground instance.

```
{
    "landingPage": "/wp-admin/plugins.php",
    "login": true,
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "wordpress.org/plugins",
                "slug": "gutenberg"
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22landingPage%22:%22/wp-admin/plugins.php%22,%22login%22:true,%22steps%22:[{%22step%22:%22installPlugin%22,%22pluginData%22:{%22resource%22:%22wordpress.org/plugins%22,%22slug%22:%22gutenberg%22}}]})

Blueprints can be passed to a Playground instance [in several ways](https://developer.wordpress.org/playground/blueprints/using-blueprints).

### Plugin in a GitHub repository

A plugin stored in a GitHub repository can also be loaded in a Playground instance via Blueprints.

With the `pluginData` property of the [`installPlugin` blueprint step](https://developer.wordpress.org/playground/blueprints/steps#installPlugin), you can define a [`git:directory` resource](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference) that will build a plugin from the files from a repository in the Playground instance.

Use the [`git:directory` resource](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference) to load plugin source code from a Git repository. It supports branches, tags, commits, and subdirectories without requiring you to create a ZIP archive first. If your plugin needs a Composer, npm, or other build step, publish a built ZIP artifact and install that artifact with a `url` resource instead.

For example, the following `blueprint.json` installs a plugin from a GitHub repository:

```
{
    "landingPage": "/wp-admin/admin.php?page=add-media-from-third-party-service",
    "login": true,
    "steps": [
        {
            "step": "installPlugin",
            "pluginData": {
                "resource": "git:directory",
                "url": "https://github.com/wptrainingteam/devblog-dataviews-plugin",
                "ref": "HEAD",
                "refType": "refname"
            }
        }
    ]
}
```

If your plugin is hosted on GitHub, you can automatically add preview buttons to your pull requests using the Playground PR Preview GitHub Action. This lets reviewers test your changes instantly without any setup. See [Adding PR Preview Buttons with GitHub Actions](https://developer.wordpress.org/playground/guides/github-action-pr-preview) for details.

[Run Blueprint](https://playground.wordpress.net/#{%22landingPage%22:%22/wp-admin/admin.php?page=add-media-from-third-party-service%22,%22login%22:true,%22steps%22:[{%22step%22:%22installPlugin%22,%22pluginData%22:{%22resource%22:%22git:directory%22,%22url%22:%22https://github.com/wptrainingteam/devblog-dataviews-plugin%22,%22ref%22:%22HEAD%22,%22refType%22:%22refname%22}}],%22$schema%22:%22https://playground.wordpress.net/blueprint-schema.json%22,%22meta%22:{%22title%22:%22Empty%20Blueprint%22,%22author%22:%22https://github.com/akirk/playground-step-library%22}})

### Plugin from code in a file or gist in GitHub

By combining the [`writeFile`](https://developer.wordpress.org/playground/blueprints/steps#WriteFileStep) and [`activatePlugin`](https://developer.wordpress.org/playground/blueprints/steps#activatePlugin) steps you can also launch a WP Playground instance with a plugin built on the fly from code stored on a gist or [a file in GitHub](https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/custom-post/books.php):

```
{
    "landingPage": "/wp-admin/plugins.php",
    "login": true,
    "steps": [
        {
            "step": "login"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/plugins/cpt-books.php",
            "data": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/custom-post/books.php"
            }
        },
        {
            "step": "activatePlugin",
            "pluginPath": "cpt-books.php"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22landingPage%22:%22/wp-admin/plugins.php%22,%22login%22:true,%22steps%22:[{%22step%22:%22login%22},{%22step%22:%22writeFile%22,%22path%22:%22/wordpress/wp-content/plugins/cpt-books.php%22,%22data%22:{%22resource%22:%22url%22,%22url%22:%22https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/custom-post/books.php%22}},{%22step%22:%22activatePlugin%22,%22pluginPath%22:%22cpt-books.php%22}]})

The [Install plugin from a gist](https://playground.wordpress.net/builder/builder.html?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-plugin-from-gist/blueprint.json#{%22meta%22:{%22title%22:%22Install%20plugin%20from%20a%20gist%22,%22author%22:%22zieladam%22,%22description%22:%22Install%20and%20activate%20a%20WordPress%20plugin%20from%20a%20.php%20file%20stored%20in%20a%20gist.%22,%22categories%22:[%22plugins%22]},%22landingPage%22:%22/wp-admin/plugins.php%22,%22preferredVersions%22:{%22wp%22:%22beta%22,%22php%22:%228.0%22},%22steps%22:[{%22step%22:%22login%22},{%22step%22:%22writeFile%22,%22path%22:%22/wordpress/wp-content/plugins/0-plugin.php%22,%22data%22:{%22resource%22:%22url%22,%22url%22:%22https://gist.githubusercontent.com/ndiego/456b74b243d86c97cda89264c68cbdee/raw/ff00cf25e6eebe4f5a4eaecff10286f71e65340b/block-hooks-demo.php%22}},{%22step%22:%22activatePlugin%22,%22pluginName%22:%22Block%20Hooks%20Demo%22,%22pluginPath%22:%220-plugin.php%22}]}) example in the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) shows how to load a plugin from code in a gist

## Setting up a demo for your plugin with Blueprints

When providing a link to a WordPress Playground instance with some plugins activated, you may also want to customize the initial setup for that Playground instance using those plugins. With Playground’s [Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started) you can load/activate plugins and configure the Playground instance.

Some useful tools and resources provided by the Playground project to work with blueprints are:

- Check the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.
- The [WordPress Playground Step Library](https://akirk.github.io/playground-step-library/#) tool provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also create your own steps!
- The [Blueprints builder](https://playground.wordpress.net/builder/builder.html) tool allows you edit your blueprint online and run it directly in a Playground instance.

Through properties and [`steps`](https://developer.wordpress.org/playground/blueprints/steps) in the Blueprint, you can configure the Playground instance’s initial setup, providing your plugins with the content and configuration needed for showcasing your plugin’s compelling features and functionality.

A great demo with WordPress Playground might require that you load default content for your plugin and theme, including images and other assets. Check out the [Providing content for your demo](https://developer.wordpress.org/playground/guides/providing-content-for-your-demo) guide to learn more about this.

### `plugins`

If your plugin has dependencies on other plugins you can use the `plugins` shorthand to install yours along with any other needed plugins.

```
{
    "landingPage": "/wp-admin/plugins.php",
    "plugins": ["gutenberg", "sql-buddy", "create-block-theme"],
    "login": true
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22landingPage%22:%22/wp-admin/plugins.php%22,%22plugins%22:[%22gutenberg%22,%22sql-buddy%22,%22create-block-theme%22],%22login%22:true})

### `landingPage`

If your plugin has a settings view or onboarding wizard, you can use the `landingPage` shorthand to automatically redirect to any page in the Playground instance upon loading.

```
{
    "landingPage": "/wp-admin/admin.php?page=my-custom-gutenberg-app",
    "login": true,
    "plugins": ["https://raw.githubusercontent.com/WordPress/block-development-examples/deploy/zips/data-basics-59c8f8.zip"]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22landingPage%22:%22/wp-admin/admin.php?page=my-custom-gutenberg-app%22,%22login%22:true,%22plugins%22:[%22https://raw.githubusercontent.com/WordPress/block-development-examples/deploy/zips/data-basics-59c8f8.zip%22]})

### `writeFile`

With the [`writeFile` step](https://developer.wordpress.org/playground/blueprints/steps#writeFile) you can create any plugin file on the fly, referencing code from a \*.php file stored on a GitHub or Gist.

Here’s an example of a **[plugin that generates Custom Post Types](https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/custom-post/books.php)**, placed in the `mu-plugins` folder to ensure the code runs automatically on load:

```
{
    "landingPage": "/wp-admin/",
    "login": true,
    "steps": [
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/mu-plugins/books.php",
            "data": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/custom-post/books.php"
            }
        }
    ]
}
```

## Plugin Development

### Local plugin development and testing with Playground

From a plugins’ folder in your local development environment, you can quickly load locally a Playground instance with that plugin loaded and activated.

Use the [`@wp-playground/cli` command](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) from your plugin’s root directory using your preferred command line program.

With [Visual Studio Code](https://code.visualstudio.com/) IDE, you can also use the [Visual Studio Code extension](https://developer.wordpress.org/playground/developers/local-development/vscode-extension) while working in the root directory of your plugin.

For example:

```
git clone git@github.com:wptrainingteam/devblog-dataviews-plugin.git
cd devblog-dataviews-plugin
npx @wp-playground/cli server --auto-mount
```

### See your local changes in a Playground instance and directly create PRs in a GitHub repo with your changes

With Google Chrome you can synchronize a Playground instance with your local plugin’s code and your plugin’s GitHub repo. With this connection you can:

- See live (in the Playground instance) your local changes
- Create PRs in the GitHub repo with your changes

Here’s a little demo of this workflow in action:

Check [About Playground > Build > Synchronize your playground instance with a local folder and create GitHub Pull Requests](https://developer.wordpress.org/playground/about/build#synchronize-your-playground-instance-with-a-local-folder-and-create-github-pull-requests) for more info.

# WordPress Playground for Theme Developers

The WordPress Playground is an innovative tool that allows theme developers to build, test, and showcase their themes directly in a browser environment.

This guide will show you how to use WordPress Playground to improve your theme development workflow, create live demos to showcase your theme, and simplify the theme review process.

Discover how to [Build](https://developer.wordpress.org/playground/about/build), [Test](https://developer.wordpress.org/playground/about/test), and [Launch](https://developer.wordpress.org/playground/about/launch) your products with WordPress Playground in the [About Playground](https://developer.wordpress.org/playground/about) section

## Launching a Playground instance with a theme

### Themes in the WordPress themes directory

With WordPress Playground, you can quickly launch a WordPress installation using any theme available in the [WordPress Themes Directory](https://wordpress.org/themes/). Simply pass the `theme` [query parameter](https://developer.wordpress.org/playground/developers/apis/query-api) to the [Playground URL](https://playground.wordpress.net) like this: https://playground.wordpress.net/?theme=disco.

You can also load any theme from the WordPress themes directory by setting the [`installTheme` step](https://developer.wordpress.org/playground/blueprints/steps#InstallThemeStep) of a [Blueprint](https://developer.wordpress.org/playground/blueprints/getting-started) passed to the Playground instance.

```
{
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "twentytwenty"
            },
            "options": {
                "activate": true,
                "importStarterContent": true
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22steps%22:[{%22step%22:%22installTheme%22,%22themeData%22:{%22resource%22:%22wordpress.org/themes%22,%22slug%22:%22twentytwenty%22},%22options%22:{%22activate%22:true,%22importStarterContent%22:true}}]})

### Themes in a GitHub repository

A theme stored in a GitHub repository can also be loaded in a Playground instance with Blueprints.

With the `themeData` property of the [`installTheme` blueprint step](https://developer.wordpress.org/playground/blueprints/steps#InstallThemeStep), you can define a [`git:directory` resource](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference) that will build a theme from the files from a repository in the Playground instance.

Use the [`git:directory` resource](https://developer.wordpress.org/playground/blueprints/steps/resources#gitdirectoryreference) to load theme source code from a Git repository. It supports branches, tags, commits, and subdirectories without requiring you to create a ZIP archive first. If your theme needs a build step, publish a built ZIP artifact and install that artifact with a `url` resource instead.

For example the following `blueprint.json` installs a theme from a GitHub repository:

```
{
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "git:directory",
                "url": "https://github.com/Automattic/themes",
                "ref": "trunk",
                "path": "assembler"
            },
            "options": {
                "activate": true
            }
        }
    ]
}
```

If your theme is hosted on GitHub, you can automatically add preview buttons to your pull requests using the Playground PR Preview GitHub Action. This lets reviewers test your changes instantly without any setup. See [Adding PR Preview Buttons with GitHub Actions](https://developer.wordpress.org/playground/guides/github-action-pr-preview) for details.

[Run Blueprint](https://playground.wordpress.net/#{%22steps%22:[{%22step%22:%22installTheme%22,%22themeData%22:{%22resource%22:%22git:directory%22,%22url%22:%22https://github.com/Automattic/themes%22,%22ref%22:%22trunk%22,%22path%22:%22assembler%22},%22options%22:{%22activate%22:true}}],%22$schema%22:%22https://playground.wordpress.net/blueprint-schema.json%22,%22meta%22:{%22title%22:%22Empty%20Blueprint%22,%22author%22:%22https://github.com/akirk/playground-step-library%22}})

A blueprint can be passed to a Playground instance [in several ways](https://developer.wordpress.org/playground/blueprints/using-blueprints).

## Setting up a demo theme with Blueprints

When providing a link to a WordPress Playground instance with a specific theme activated, you may also want to customize the initial setup for that theme. With Playground’s [Blueprints](https://developer.wordpress.org/playground/blueprints/getting-started) you can load, activate, and configure a theme.

Some useful tools and resources provided by the Playground project to work with blueprints are:

- Check the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.
- The [WordPress Playground Step Library](https://akirk.github.io/playground-step-library/#) tool provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also create your own steps!
- The [Blueprints builder](https://playground.wordpress.net/builder/builder.html) tool allows you edit your blueprint online and run it directly in a Playground instance.

Through properties and [`steps`](https://developer.wordpress.org/playground/blueprints/steps) in the blueprint, you can configure the initial setup of your theme in the Playground instance.

To provide a good demo of your theme via Playground, you may want to load it with default content that highlights the features of your theme. Check out the [Providing content for your demo](https://developer.wordpress.org/playground/guides/providing-content-for-your-demo) guide to learn more about this.

### `resetData`

With the [`resetData`](https://developer.wordpress.org/playground/blueprints/steps#resetData) step, you can remove the default content of a WordPress installation in order to import your own content.

```
"steps": [
    ...,
    {
        "step": "resetData"
    },
    ...
]
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L16)

### `writeFile`

With the [`writeFile`](https://developer.wordpress.org/playground/blueprints/steps#WriteFileStep) step, you can write data to a file at a specified path. You may want to use this step to write custom PHP code in a PHP file inside the `mu-plugins` folder of the Playground WordPress instance, so the code is executed automatically when the WordPress instance is loaded.  
For example, you can add a demo-specific body class that your theme can use for Playground-only styling:

```
"steps": [
    ...,
    {
        "step": "writeFile",
        "path": "/wordpress/wp-content/mu-plugins/demo-body-class.php",
        "data": "<?php add_filter( 'body_class', function ( $classes ) { $classes[] = 'playground-demo'; return $classes; } );"
    },
    ...
]
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L16)

### `updateUserMeta`

With the [`updateUserMeta`](https://developer.wordpress.org/playground/blueprints/steps#updateUserMeta) step, you can update any user metadata. For example, you could update the metadata of the default `admin` user of any WordPress installation:

```
"steps": [
    ...,
    {
        "step": "updateUserMeta",
        "meta": {
            "first_name": "John",
            "last_name": "Doe",
            "admin_color": "modern"
        },
        "userId": 1
    },
    ...
]
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L24)

### `setSiteOptions`

With the [`setSiteOptions`](https://developer.wordpress.org/playground/blueprints/steps#setSiteOptions) step, you can set [site options](https://developer.wordpress.org/apis/options/#available-options-by-category) such as the site name, description, or page to use for posts.

```
"steps": [
    ...,
    {
        "step": "setSiteOptions",
        "options": {
            "blogname": "Rich Tabor",
            "blogdescription": "Multidisciplinary maker specializing in the intersection of product, design and engineering. Making WordPress.",
            "show_on_front": "page",
            "page_on_front": 6,
            "page_for_posts": 2
        }
    },
    ...
]
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L50)

There’s also a [`siteOptions`](https://developer.wordpress.org/playground/blueprints/steps/shorthands#siteoptions) shorthand that can be used instead of the `setSiteOptions` step.

### `plugins`

With the [`plugins`](https://developer.wordpress.org/playground/blueprints/steps/shorthands#plugins) shorthand you can set a list of plugins you want to be installed and activated with your theme in the Playground instance.

```
"plugins": ["todo-list-block", "markdown-comment-block"]
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L60)

You can also use the [`installPlugin`](https://developer.wordpress.org/playground/blueprints/steps#installPlugin) step to install and activate plugins for your Playground instance but the shorthand way is recommended.

### `login`

With the [`login`](https://developer.wordpress.org/playground/blueprints/steps/shorthands#login) shorthand you can launch your Playground instance with the admin user logged in.

```
 "login": true,
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L10)

You can also use the [`login`](https://developer.wordpress.org/playground/blueprints/steps#login) step to launch your Playground instance logged in with any specific user.

The [“Stylish Press”](https://github.com/WordPress/blueprints/tree/trunk/blueprints/stylish-press) and [“Loading, activating, and configuring a theme from a GitHub repository”](https://github.com/WordPress/blueprints/tree/trunk/blueprints/install-activate-setup-theme-from-gh-repo) examples from the [Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md) are great references for loading, activating, importing content, and configuring a block theme on a Playground instance.

## Theme development

### Local theme development and testing with Playground

From the root folder of a block theme’s code, you can quickly load locally a Playground instance with that theme loaded and activated. You can do that by launching, in a theme directory, the [`@wp-playground/cli` command](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) from your preferred command line program or the [Visual Code Studio extension](https://developer.wordpress.org/playground/developers/local-development/vscode-extension) from the [Visual Studio Code](https://code.visualstudio.com/) IDE.

For example:

```
git clone git@github.com:WordPress/community-themes.git
cd community-themes/blue-note
npx @wp-playground/cli server --auto-mount
```

### Design your theme using the WordPress UI and save your changes as Pull Requests

You can connect your Playground instance to a GitHub repository and create a Pull Request with the changes you’ve done through the WordPress UI in the Playground instance, leveraging the [Create Block Theme](https://wordpress.org/plugins/create-block-theme/) plugin. You can also make changes to that theme and export a zip.

Note that you’ll need the [Create Block Theme](https://wordpress.org/plugins/create-block-theme/) plugin installed and activated in the Playground instance in order to use this workflow.

Check [About Playground > Build > Save changes done on a Block Theme and create GitHub Pull Requests](https://developer.wordpress.org/playground/about/build#save-changes-done-on-a-block-theme-and-create-github-pull-requests) for more info.

# Adding PR Preview Buttons with GitHub Actions

The Playground PR Preview action adds a preview button to your pull requests. Clicking the button launches Playground with your plugin or theme installed from the PR branch:
![PR Preview Button](https://i0.wp.com/developer.wordpress.org/files/2026/07/try-it-in-playground.webp?fit=2766%2C1360&ssl=1)
Start with the setup that matches your repository:

| Your repository | Use this setup |
| --- | --- |
| Plugin or theme works directly from the repository checkout | [No build step](#no-build-step) |
| Plugin or theme needs Composer, npm, Vite, or another build command first | [With a build step](#with-a-build-step) |
| Public fork PRs need working previews | [With a build step](#with-a-build-step) |
| Private repository | The default setup is not enough; Playground needs public, unauthenticated URLs. |

If you are not sure, use the no-build setup only when the files committed to the pull request are exactly the files WordPress should run. If CI must generate anything first, use the build-step setup.
For complete configuration options, recipes, and reference tables, see the [action-wp-playground-pr-preview documentation](https://wordpress.github.io/action-wp-playground-pr-preview/).

## How it works

Playground runs WordPress in the browser. Anything Playground installs, such as a plugin ZIP, theme ZIP, or WXR file, must be available at a public URL when someone clicks the preview button.
The action uses two URL strategies:

- `git:directory`: Playground fetches the repository at the pull request ref. Use this for the no-build setup.
- Release assets: CI builds ZIP files and uploads them to a public `ci-artifacts` prerelease. Use this when the preview needs Composer dependencies, npm output, compiled assets, or any other generated files.

**Use the action and reusable workflows in different places**

The direct action, `WordPress/action-wp-playground-pr-preview@v3`, is a regular GitHub Action. Reference it as a step under `jobs.<job_id>.steps[].uses`.

The build-step setup uses reusable workflows, `preview-build.yml@v3` and `preview-publish.yml@v3`. Reference those at the job level under `jobs.<job_id>.uses`.

## No build step

Use this setup when your plugin or theme can run directly from the repository checkout, with no Composer install, npm build, or asset pipeline.
Create `.github/workflows/pr-preview.yml`:

```
name: PR Preview
on:
    pull_request:
        types: [opened, synchronize, reopened, edited]

jobs:
    preview:
        runs-on: ubuntu-latest
        permissions:
            contents: read
            pull-requests: write
        steps:
            - uses: WordPress/action-wp-playground-pr-preview@v3
              with:
                  plugin-path: .
                  github-token: ${{ secrets.GITHUB_TOKEN }}
```

Use `theme-path: .` instead of `plugin-path: .` for a theme:

```
with:
    theme-path: .
    github-token: ${{ secrets.GITHUB_TOKEN }}
```

Use a subdirectory when the plugin or theme is not at the repository root:

```
with:
    plugin-path: plugins/my-plugin
    github-token: ${{ secrets.GITHUB_TOKEN }}
```

You do not need to create `secrets.GITHUB_TOKEN`; GitHub provides it automatically. The `permissions` block gives that token the access this action needs.
Open a pull request. The action updates the PR description with a managed Preview button block. Clicking the button opens Playground with your plugin or theme installed and activated from the pull request ref.

**Fork PR note**

This one-workflow setup is simplest for same-repository PRs. Public fork PRs usually receive a read-only `GITHUB_TOKEN`, so the action may be unable to edit the PR description. If fork contributors need working previews, use the [build-step setup](#with-a-build-step) even when the build command only creates a ZIP.

Do not switch this workflow to `pull_request_target` to run PR code with write permissions.

## With a build step

Use this setup when the preview needs generated files, such as Composer dependencies, npm or Vite bundles, or another build output. It also gives public fork PRs a safe path to working previews.
Create two workflow files:

- A build workflow that runs on `pull_request` with read-only permissions, runs your build command, and uploads ZIP files as a GitHub Actions artifact.
- A publish workflow that runs on `workflow_run` after the build succeeds, uploads those ZIP files to a public release URL, builds a Blueprint, and posts the preview button.

### Build workflow

Create `.github/workflows/pr-preview-build.yml`:

```
name: PR Preview - Build
on:
    pull_request:
        types: [opened, synchronize, reopened, edited]

jobs:
    build:
        uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v3
        with:
            artifacts: my-plugin=build/my-plugin.zip
            node-version: '20'
            build-command: |
                npm ci
                npm run build:plugin-zip
```

In `artifacts: my-plugin=build/my-plugin.zip`, `my-plugin` is the artifact name and `build/my-plugin.zip` is the ZIP file your `build-command` must create. The ZIP should extract to a plugin or theme slug folder, such as `my-plugin/my-plugin.php`, not directly to files at the ZIP root.

### Publish workflow

Create `.github/workflows/pr-preview-publish.yml`:

```
name: PR Preview - Publish
on:
    workflow_run:
        workflows: ['PR Preview - Build']
        types: [completed]

permissions:
    contents: write
    pull-requests: write

jobs:
    publish:
        permissions:
            contents: write
            pull-requests: write
        uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v3
        with:
            kind: plugin
```

Use `kind: theme` for one theme ZIP. For multiple ZIPs or extra setup steps, use a custom `blueprint:` as shown in [Built artifacts with a custom blueprint](#built-artifacts-with-a-custom-blueprint).
Open a pull request. The build workflow runs your build command. After it succeeds, the publish workflow creates or updates the `ci-artifacts` prerelease, uploads the built ZIP, and adds a Preview button to the PR description.

**Why two workflow files?**

GitHub does not allow one workflow to safely run untrusted code from a fork PR and write to releases or PR descriptions. The build workflow runs PR code with read-only permissions. The publish workflow runs later from the default branch with write permissions and never checks out or runs PR code.

## Custom blueprints

Use Blueprints to configure the Playground environment. You can install companion plugins, set WordPress options, import content, pin PHP and WordPress versions, open a specific page, or log in as an admin user.
For a no-build preview, provide a full Blueprint with the direct action:

```
- uses: WordPress/action-wp-playground-pr-preview@v3
  with:
      blueprint: |
          {
            "$schema": "https://playground.wordpress.net/blueprint-schema.json",
            "preferredVersions": { "php": "8.3", "wp": "6.6" },
            "steps": [
              {
                "step": "installPlugin",
                "pluginData": {
                  "resource": "git:directory",
                  "url": "https://github.com/${{ github.repository }}.git",
                  "ref": "${{ github.event.pull_request.head.ref }}",
                  "path": "/"
                },
                "options": { "activate": true }
              },
              {
                "step": "installPlugin",
                "pluginData": {
                  "resource": "wordpress.org/plugins",
                  "slug": "woocommerce"
                },
                "options": { "activate": true }
              },
              { "step": "login", "username": "admin" }
            ]
          }
      github-token: ${{ secrets.GITHUB_TOKEN }}
```

Or reference a hosted Blueprint:

```
- uses: WordPress/action-wp-playground-pr-preview@v3
  with:
      blueprint-url: https://example.com/path/to/blueprint.json
      github-token: ${{ secrets.GITHUB_TOKEN }}
```

Choose the Blueprint input based on where the JSON comes from:

- Use `blueprint` when the direct action or publish workflow can include the full JSON string.
- Use `blueprint-url` with the direct action when the JSON is already hosted at a public URL.
- Use `blueprint-from-build` in `preview-build.yml` together with `blueprint-from-artifact: true` in `preview-publish.yml` when the build workflow generates `blueprint.json` dynamically.

See [Blueprints documentation](https://developer.wordpress.org/playground/blueprints) for all available steps and configuration options.

## Built artifacts with a custom blueprint

Use a custom Blueprint in the publish workflow when a built preview needs more than the default “install and activate” behavior. Common reasons include installing multiple ZIP files, opening a specific admin page, installing PHP extension bundles, or logging in automatically.
Put `artifacts` and `build-command` under `jobs.build.with` in `.github/workflows/pr-preview-build.yml`:

```
jobs:
    build:
        uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v3
        with:
            artifacts: my-plugin=build/my-plugin.zip
            php-version: '8.2'
            build-command: |
                set -euo pipefail
                composer install --no-dev --optimize-autoloader --no-interaction
                mkdir -p build/my-plugin
                rsync -a --delete \
                    --exclude='.git' \
                    --exclude='.github' \
                    --exclude='build' \
                    ./ build/my-plugin/
                ( cd build && zip -qr my-plugin.zip my-plugin )
```

Then put `blueprint` under `jobs.publish.with` in `.github/workflows/pr-preview-publish.yml`:

```
jobs:
    publish:
        permissions:
            contents: write
            pull-requests: write
        uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v3
        with:
            blueprint: |
                {
                  "$schema": "https://playground.wordpress.net/blueprint-schema.json",
                  "landingPage": "/wp-admin/admin.php?page=my-plugin",
                  "steps": [
                    { "step": "login", "username": "admin", "password": "password" },
                    {
                      "step": "installPlugin",
                      "pluginZipFile": {
                        "resource": "url",
                        "url": "{{ARTIFACT_URL:my-plugin}}"
                      },
                      "options": { "activate": true }
                    }
                  ]
                }
```

`{{ARTIFACT_URL:my-plugin}}` is replaced with the public URL of the matching ZIP. The name must match the left side of the corresponding `artifacts` entry.
For a monorepo with multiple plugins or themes, add one `artifacts` entry per ZIP and reference each one with `{{ARTIFACT_URL:<name>}}` in the Blueprint.

## Button placement and templates

By default, the action updates the PR description (`mode: append-to-description`). To post as a comment instead:

```
with:
    plugin-path: .
    mode: comment
    github-token: ${{ secrets.GITHUB_TOKEN }}
```

In the build-step setup, pass `mode: comment` to `preview-publish.yml` instead.
Customize the PR description with `description-template`:

```
with:
    plugin-path: .
    description-template: |
        ### Test this PR in WordPress Playground

        {{PLAYGROUND_BUTTON}}

        **Branch:** {{PR_HEAD_REF}} · **Plugin:** `{{PLUGIN_SLUG}}`
    github-token: ${{ secrets.GITHUB_TOKEN }}
```

Available variables include `{{PLAYGROUND_BUTTON}}`, `{{PLAYGROUND_URL}}`, `{{PR_NUMBER}}`, `{{PR_TITLE}}`, `{{PR_HEAD_REF}}`, `{{PLUGIN_SLUG}}`, and `{{THEME_SLUG}}`.
The action restores the button if it is removed from the PR description. To prevent restoration:

```
with:
    plugin-path: .
    restore-button-if-removed: false
    github-token: ${{ secrets.GITHUB_TOKEN }}
```

## Checklist for using a coding agent

If you ask an LLM or coding agent to add PR previews to a repository, tell it to inspect the repository before choosing a setup.
Suggested prompt:

```
Add WordPress/action-wp-playground-pr-preview@v3 to this repository.
First inspect whether the WordPress plugin or theme can run directly from the repository checkout, or whether CI must build files first.
If no build step is needed, add one pull_request workflow using plugin-path or theme-path.
If a build step is needed, add the two-workflow preview-build.yml / preview-publish.yml setup.
Use the smallest working configuration. Preserve existing CI. Do not use pull_request_target.
After editing, verify that any artifacts path listed in artifacts: is actually created by build-command.
```

When reviewing the result, check that:

- `secrets.GITHUB_TOKEN` is referenced but not created manually.
- Direct action usage appears under `jobs.<job_id>.steps[].uses`.
- Reusable workflow usage appears under `jobs.<job_id>.uses`.
- Build and publish workflows use the same version, for example `@v3`.
- Every `artifacts` entry has the form `name=path/to/file.zip`, and the build command creates that exact ZIP path.
- Plugin and theme ZIPs extract to slug-named folders, not directly to files at the ZIP root.
- The workflow does not use `pull_request_target`.

## Troubleshooting

**`Invalid workflow file` or `jobs.<id>.uses` error:** Check whether you are using the direct action or a reusable workflow. `WordPress/action-wp-playground-pr-preview@v3` belongs under `jobs.<job_id>.steps[].uses`. `WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v3` and `preview-publish.yml@v3` belong under `jobs.<job_id>.uses`.
**The publish workflow run is `startup_failure` with no logs:** The reusable publish workflow needs `contents: write` and `pull-requests: write`. Grant those permissions to the publish job, either with a top-level `permissions:` block that the job inherits or with `jobs.publish.permissions:`. The example above includes both so later workflow edits cannot accidentally narrow the publish job permissions.
**Button not appearing:** The workflow file must exist on the default branch before it runs on PRs. Check the Actions tab for errors and confirm the calling workflow grants `pull-requests: write`.
**`Resource not accessible by integration`:** The workflow calling the action cannot write to the pull request. For same-repository PRs, add `permissions: pull-requests: write`. For public fork PRs, use the two-workflow build-step setup.
**Button appears but preview fails to load or 404s:** For built-artifact workflows, check that the build uploaded the expected ZIP and that the `ci-artifacts` release is a prerelease, not a draft. New v3 setups create a prerelease automatically, but older draft releases may need to be converted once.
**Plugin needs Composer or npm build output and shows up empty:** The workflow is probably using `plugin-path:` directly. That path uses `git:directory`, so Playground receives the repository files without running a build step. Switch to the build-step setup.
**Plugin or theme is missing after install:** Check the ZIP shape. Plugin and theme ZIPs should extract to a slug-named folder, such as `my-plugin/my-plugin.php`, not directly to files at the ZIP root.
**`git diff origin/$GITHUB_BASE_REF...HEAD` fails with “no merge base”:** The default checkout is shallow. Set `fetch-depth: 0` on the build reusable workflow input.
**`plugin-path` or `theme-path` resolves to an empty directory:** The path is relative to the repository root, not to the workflow file. Use `.` for repo-root plugins or themes, or a path like `plugins/my-plugin` for subdirectories.
**Preview fails with `PHP.run()` exit code 255 and no stderr:** This is usually a fatal error during plugin activation, often from missing Composer dependencies. Switch to the build-step setup or check what your `build-command` produces. The build workflow logs `unzip -l` for the final artifact.
More: [action-wp-playground-pr-preview documentation](https://wordpress.github.io/action-wp-playground-pr-preview/)

## Other ways of previewing a git repository

## Examples

- [action-wp-playground-pr-preview examples](https://wordpress.github.io/action-wp-playground-pr-preview/#see-it-live) – Live repositories using the v3 workflows
- [WordPress/blueprints](https://github.com/WordPress/blueprints/pull/155) – Blueprint previews

## Next steps

- Add demo content ([guide](https://developer.wordpress.org/playground/guides/providing-content-for-your-demo))
- Create custom blueprints ([docs](https://developer.wordpress.org/playground/blueprints))

# PHP code snippets and embeds

Use `<php-snippet>` when you want readers to run PHP or WordPress code directly  
from a docs page, tutorial, blog post, or demo. It renders a syntax-highlighted  
code block with a Run button and starts a real Playground runtime only when the  
reader asks for it.

The runtime is shared across matching snippets on the same page, so a tutorial  
can include several runnable examples without starting WordPress over and over.

## Try it

The example below is editable and runnable. It also uses a Blueprint to install  
a small mu-plugin before the snippet runs, so the PHP code can call a helper  
function that did not exist in the default WordPress install.

{
"steps": [
{
"step": "writeFile",
"path": "/wordpress/wp-content/mu-plugins/product-cards.php",
"data": "<?php\nfunction docs\_render\_product\_card( array $product ): string {\n\treturn sprintf(\n\t\t'<article class=\"product-card\"><h3>%s</h3><p>$%0.2f</p></article>',\n\t\tesc\_html( $product['name'] ),\n\t\t$product['price']\n\t);\n}\n"
}
]
}

<?php
require '/wordpress/wp-load.php';
$products = [
[
'name' => 'Canvas Tote',
'price' => 24,
],
[
'name' => 'Coffee & Code Mug',
'price' => 16.5,
],
];
foreach ( $products as $product ) {
echo docs\_render\_product\_card( $product ) . "\n";
}

<article class="product-card"><h3>Canvas Tote</h3><p>$24.00</p></article>
<article class="product-card"><h3>Coffee &amp; Code Mug</h3><p>$16.50</p></article>

Here is the complete embed:

```
<script type="module" src="https://playground.wordpress.net/php-code-snippet.js"></script>

<script id="product-card-blueprint" type="application/json">
    {
        "steps": [
            {
                "step": "writeFile",
                "path": "/wordpress/wp-content/mu-plugins/product-cards.php",
                "data": "<?php\nfunction docs_render_product_card( array $product ): string {\n\treturn sprintf(\n\t\t'<article class=\"product-card\"><h3>%s</h3><p>$%0.2f</p></article>',\n\t\tesc_html( $product['name'] ),\n\t\t$product['price']\n\t);\n}\n"
            }
        ]
    }
</script>

<php-snippet name="product-card.php" blueprint="product-card-blueprint">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';

        $products = [
            [
                'name'  => 'Canvas Tote',
                'price' => 24,
            ],
            [
                'name'  => 'Coffee & Code Mug',
                'price' => 16.5,
            ],
        ];

        foreach ( $products as $product ) {
            echo docs_render_product_card( $product ) . "\n";
        }
    </script>
    <script type="text/expected-output">
        <article class="product-card"><h3>Canvas Tote</h3><p>$24.00</p></article>
        <article class="product-card"><h3>Coffee &amp; Code Mug</h3><p>$16.50</p></article>
    </script>
</php-snippet>
```

Use this pattern when each example should start from the same prepared site:  
helper functions, mu-plugins, options, themes, demo files, or sample content.

## Start with one snippet

For a basic runnable example, add the component script once and place PHP inside  
`<php-snippet>`:

<?php
echo 'Hello from PHP ' . phpversion();

Hello from PHP 8.4.x

```
<script type="module" src="https://playground.wordpress.net/php-code-snippet.js"></script>

<php-snippet name="hello.php">
    <script type="application/x-php">
        <?php
        echo 'Hello from PHP ' . phpversion();
    </script>
    <script type="text/expected-output">
        Hello from PHP 8.4.x
    </script>
</php-snippet>
```

The script itself is small. PHP, WordPress, and the WASM runtime are fetched  
later, after the first Run click. The expected output appears before Run and is  
replaced with the exact PHP version after execution.

## Write PHP safely in HTML

Put inline PHP in a `<script type="application/x-php">` child. Browsers ignore  
script tags with unknown types, which means PHP strings can contain HTML without  
escaping every `<` character.

<?php
require '/wordpress/wp-load.php';
$html = '<img decoding="async" src="hero.jpg" alt="Hero">';
$tags = new WP\_HTML\_Tag\_Processor( $html );
if ( $tags->next\_tag( 'img' ) ) {
$tags->set\_attribute( 'loading', 'lazy' );
}
echo $tags->get\_updated\_html();

<img decoding="async" src="hero.jpg" alt="Hero" loading="lazy">

```
<php-snippet name="html-api.php">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';

        $html = '<img src="hero.jpg" alt="Hero">';
        $tags = new WP_HTML_Tag_Processor( $html );

        if ( $tags->next_tag( 'img' ) ) {
            $tags->set_attribute( 'loading', 'lazy' );
        }

        echo $tags->get_updated_html();
    </script>
    <script type="text/expected-output">
        <img src="hero.jpg" alt="Hero" loading="lazy">
    </script>
</php-snippet>
```

A literal `</script>` still closes the child script element, even when it is  
inside a PHP string. Use JSON-encoded child payloads when the PHP source or  
expected output contains that sequence:

```
<php-snippet name="example.php">
    <script type="application/x-php+json">
        "\u003C?php\necho '\u003C/script\u003E';"
    </script>
    <script type="text/expected-output+json">
        "\u003C/script\u003E"
    </script>
</php-snippet>
```

Both payloads are parsed as JSON and must decode to strings; other JSON values  
are rejected. Encode `<` as `\u003C` so the HTML parser never sees a literal  
`</script>`. `JSON.stringify()` does not escape `<` by itself. The unencoded  
`application/x-php` and `text/expected-output` types remain supported.

Very short snippets can also be written as text, as long as PHP opening tags are  
escaped:

<?php echo 20 + 22;

```
<php-snippet name="sum.php" expected-output="42"> &lt;?php echo 20 + 22; </php-snippet>
```

## Use WordPress APIs

Snippets run in a real WordPress installation by default. Load WordPress with  
`require '/wordpress/wp-load.php'`, then call core APIs as usual.

<?php
require '/wordpress/wp-load.php';
update\_option( 'blogname', 'Snippet Docs' );
echo get\_bloginfo( 'name' );

Snippet Docs

```
<php-snippet name="site-title.php">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';

        update_option( 'blogname', 'Snippet Docs' );
        echo get_bloginfo( 'name' );
    </script>
    <script type="text/expected-output">
        Snippet Docs
    </script>
</php-snippet>
```

If your example is pure PHP and does not need WordPress, use `wp="none"` to skip  
the WordPress download and boot step:

<?php
echo 'WordPress installed: ';
echo file\_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no';

WordPress installed: no

```
<php-snippet name="pure-php.php" wp="none">
    <script type="application/x-php">
        <?php
        echo 'WordPress installed: ';
        echo file_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no';
    </script>
    <script type="text/expected-output">
        WordPress installed: no
    </script>
</php-snippet>
```

### Automatically prepend a hidden PHP script

`auto-prepend-script` mirrors PHP’s  
[`auto_prepend_file`](https://www.php.net/manual/en/ini.core.php#ini.auto-prepend-file)  
`php.ini` directive: the referenced PHP script runs before the snippet’s main  
file on every request. Unlike the PHP directive, the attribute takes the id or  
CSS selector of an inert PHP `<script>` element. Use it when setup code should  
not appear in the editable example:

```
<script id="load-wordpress" type="application/x-php">
    <?php
    require_once '/wordpress/wp-load.php';
</script>

<php-snippet name="site-title.php" auto-prepend-script="#load-wordpress" implicit-php-open-tag>
    <script type="application/x-php">
        echo get_bloginfo( 'name' );
    </script>
</php-snippet>
```

The referenced script must start with `<?php`. It remains outside the visible  
editor and runs before every click, including after the reader edits the  
snippet. Snippets with different auto-prepended scripts can still share one  
runtime.

`implicit-php-open-tag` adds the PHP opening tag omitted from the visible  
source. Playground adds it on the first line of the execution file, so PHP  
errors still point to the displayed line. Diagnostics use a filesystem-safe  
form of the `name` attribute as the filename. Code starting with any PHP  
opening tag (`<?php`, `<?=`, or `<?`) is rejected; remove  
`implicit-php-open-tag` when the source is already a complete PHP file.

Without `implicit-php-open-tag`, snippets using `auto-prepend-script` must  
include their own PHP opening tag. The two attributes are independent, so an  
implicit opening tag also works without an auto-prepended script.

## Edit examples in place

Runnable snippets are editable by default. The edited code is kept only in the  
current page session; refreshing restores the original snippet.

<?php
$numbers = range( 1, 5 );
echo array\_sum( $numbers );

15

```
<php-snippet name="scratch.php">
    <script type="application/x-php">
        <?php
        $numbers = range( 1, 5 );
        echo array_sum( $numbers );
    </script>
    <script type="text/expected-output">
        15
    </script>
</php-snippet>
```

Editable snippets also run with `Ctrl+Enter` or `Cmd+Enter` while the editor is  
focused.

Use `readonly` for runnable examples that should be copied or run as-is:

<?php
echo 'This example can run, but the code is locked.';

This example can run, but the code is locked.

```
<php-snippet name="reference.php" readonly>
    <script type="application/x-php">
        <?php
        echo 'This example can run, but the code is locked.';
    </script>
    <script type="text/expected-output">
        This example can run, but the code is locked.
    </script>
</php-snippet>
```

`editable="false"` works as a compatibility alias for `readonly`.

## Show output before Run

Use expected output when you want the result visible immediately. The placeholder  
is replaced with real runtime output after the reader clicks Run.

<?php
echo '2 + 2 = ' . ( 2 + 2 );

2 + 2 = 4

```
<php-snippet name="precomputed.php">
    <script type="application/x-php">
        <?php
        echo '2 + 2 = ' . ( 2 + 2 );
    </script>
    <script type="text/expected-output">
        2 + 2 = 4
    </script>
</php-snippet>
```

For one-line output, use the `expected-output` attribute:

<?php
echo 'Ready';

```
<php-snippet name="one-line.php" expected-output="Ready">
    <script type="application/x-php">
        <?php
        echo 'Ready';
    </script>
</php-snippet>
```

## Prepare a site with a Blueprint

Use `blueprint` when snippets need setup before the PHP code runs. Put a JSON  
[Blueprint](https://developer.wordpress.org/playground/blueprints/) in the page and point snippets at it by id or CSS  
selector.

{
"steps": [
{
"step": "writeFile",
"path": "/wordpress/wp-content/mu-plugins/helpers.php",
"data": "<?php\nfunction docs\_greet( $name ) {\n\treturn 'Hello, ' . $name;\n}\n"
}
]
}

<?php
require '/wordpress/wp-load.php';
echo docs\_greet( 'Ada' );

Hello, Ada

```
<script id="setup-blueprint" type="application/json">
    {
        "steps": [
            {
                "step": "writeFile",
                "path": "/wordpress/wp-content/mu-plugins/helpers.php",
                "data": "<?php\nfunction docs_greet( $name ) {\n\treturn 'Hello, ' . $name;\n}\n"
            }
        ]
    }
</script>

<php-snippet name="greeting.php" blueprint="setup-blueprint">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';
        echo docs_greet( 'Ada' );
    </script>
    <script type="text/expected-output">
        Hello, Ada
    </script>
</php-snippet>
```

The selector form is useful when generated markup cannot guarantee simple ids:

<?php
require '/wordpress/wp-load.php';
echo docs\_greet( 'Grace' );

Hello, Grace

```
<php-snippet blueprint="#setup-blueprint" name="with-selector.php">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';
        echo docs_greet( 'Grace' );
    </script>
    <script type="text/expected-output">
        Hello, Grace
    </script>
</php-snippet>
```

Prefer `<script type="application/json">` for Blueprints. Its contents are raw  
text, so embedded PHP strings such as `<?php` are safe. A `<template>` can work,  
but its contents are parsed as HTML; if you use one, escape `<` in embedded PHP  
strings as `\u003c`.

## Load PHP from another file

Use `src` when the PHP source should live in a separate file:

```
<php-snippet name="external-example.php" src="/snippets/external-example.php" expected-output="Loaded from an external file"></php-snippet>
```

The URL resolves from the page that contains the snippet. If the file is hosted  
on another origin, serve it with an `Access-Control-Allow-Origin` header that  
allows the documentation page.

`src` loads only the snippet body. Use a Blueprint when you need support files,  
plugins, options, or other setup before the snippet runs. The `expected-output`  
attribute is still useful with `src` when you already know what the external PHP  
file prints.

## Pin PHP or WordPress versions

The default PHP version is `8.4`, and the default WordPress version is `latest`.  
Set `php` or `wp` when the example depends on a specific version.

<?php
enum Status {
case Draft;
case Published;
}
echo Status::Published->name;

Published

```
<php-snippet name="enum.php" php="8.4">
    <script type="application/x-php">
        <?php
        enum Status {
            case Draft;
            case Published;
        }

        echo Status::Published->name;
    </script>
    <script type="text/expected-output">
        Published
    </script>
</php-snippet>
```

<?php
require '/wordpress/wp-load.php';
echo get\_bloginfo( 'version' );

6.8

```
<php-snippet name="wp-version.php" wp="6.8">
    <script type="application/x-php">
        <?php
        require '/wordpress/wp-load.php';
        echo get_bloginfo( 'version' );
    </script>
    <script type="text/expected-output">
        6.8
    </script>
</php-snippet>
```

See the [Query API reference](https://developer.wordpress.org/playground/developers/apis/query-api/#available-options)  
for available PHP and WordPress versions.

## Show code without running it

Set `runnable="false"` for fragments that should be highlighted but not  
executed, such as incomplete examples or code that depends on external services.

<?php
// This fragment is shown for discussion, not execution.
add\_filter( 'the\_content', 'docs\_filter\_content' );

```
<php-snippet name="illustration.php" runnable="false">
    <script type="application/x-php">
        <?php
        // This fragment is shown for discussion, not execution.
        add_filter( 'the_content', 'docs_filter_content' );
    </script>
</php-snippet>
```

## Self-host the runtime

Most pages should use the hosted runtime from `` https://playground.wordpress.net`.  
Set ``playground-origin` when developing Playground itself, testing a self-hosted  
deployment, or pinning examples to infrastructure you control.

```
<php-snippet name="local-runtime.php" playground-origin="http://localhost:5400">
    <script type="application/x-php">
        <?php
        echo phpversion();
    </script>
    <script type="text/expected-output">
        8.4.x
    </script>
</php-snippet>
```

## Runtime sharing

The first Run click on a page:

1. Loads the Playground client.
2. Creates a hidden iframe pointed at `remote.html`.
3. Boots PHP, and WordPress unless the snippet uses `wp="none"`.
4. Runs the snippet code and writes stdout into the output panel.

Later runs reuse an existing runtime when `playground-origin`, `php`, `wp`, and  
the resolved Blueprint JSON all match. This keeps related snippets fast while  
still isolating examples that need different setup.

## Security and CSP

Snippet PHP runs inside the Playground runtime iframe, not in the parent page.  
The parent page still loads the web component script and creates the hidden  
runtime iframe.

If your site has a Content Security Policy, allow:

- The module script from `https://playground.wordpress.net`.
- The hidden iframe from the same origin.
- Network requests for the PHP, WordPress, and Playground runtime assets.

For stricter environments, self-host the snippet script and use  
`playground-origin` to point snippets at your deployment.

## Standalone PHP Playground

Use the standalone PHP Playground when you want a full-page editor, a shareable  
URL, or an iframe instead of inline examples:

> [playground.wordpress.net/php-playground.html](https://playground.wordpress.net/php-playground.html)

You can embed it directly:

```
<iframe src="https://playground.wordpress.net/php-playground.html#eyJjb2RlIjoiPD9waHBcblxuZWNobyBcIkkgYW0gYSBjb2RlIHNuaXBwZXQhXCI7XG4iLCJwaHAiOiI4LjQifQ==" width="100%" height="600"></iframe>
```

The URL fragment is a base64-encoded JSON payload with `code`, `php`, and `wp`  
fields.

## Which embed should you use?

| Use case | Use |
| --- | --- |
| Several runnable examples in one article | `<php-snippet>` |
| A tutorial step where readers should edit code inline | `<php-snippet>` |
| Shared setup across examples | `<php-snippet blueprint="...">` |
| Hidden PHP setup before every run | `<php-snippet auto-prepend-script="...">` |
| PHP source without an opening tag | `<php-snippet implicit-php-open-tag>` |
| A pure PHP language example | `<php-snippet wp="none">` |
| A runnable example that should not be edited | `<php-snippet readonly>` |
| A single full-page editor with a shareable URL | Standalone PHP Playground |
| A complete WordPress site preview | Standard Playground iframe |

## Troubleshooting

If the snippet does not render, check that the module script loaded and that the  
browser supports custom elements.

If Run never finishes, open DevTools and check failed requests for `remote.html`,  
PHP `.wasm` files, WordPress zip files, Blueprint resources, or cross-origin  
`src` files.

If a Blueprint-backed snippet cannot find helper functions, confirm the  
`blueprint` attribute points to the right element and that the JSON is valid.

If output differs from the expected placeholder, trust the runtime output. The  
placeholder is static documentation; Run executes the current code against the  
selected PHP, WordPress, and Blueprint setup.

# Run PHP frameworks in Playground

WordPress Playground is also a browser-based PHP runtime. WordPress is the  
most common app it boots, but a Blueprint can skip the WordPress download,  
write any PHP files into the virtual filesystem, and run a framework such as  
Symfony.

This guide shows the shape of that setup. Use it when you want a shareable demo,  
a docs example, or a quick compatibility check for a PHP app that does not need  
a server, database, Node.js, Sass, or a local Composer install.

## What changes when you skip WordPress

Set `preferredVersions.wp` to `false` in a Blueprint, or `wp="none"` on a  
`<php-snippet>`. Playground still downloads PHP, mounts a writable filesystem,  
runs Blueprint steps, and supports networking when `features.networking` is  
`true`. It just does not download or boot WordPress.

That makes Playground useful for generic PHP examples:

- PHP libraries that need a real filesystem.
- Framework demos that can run behind `public/index.php`.
- Documentation snippets that should execute in the browser.
- Reproducible bug reports for PHP code that is not WordPress-specific.

## Try a Symfony app

The example below uses a Blueprint to download and unzip a bundled Symfony app  
into `/app`. Then a `<php-snippet>` boots the Symfony kernel and renders the  
dashboard route. The app’s Composer dependencies include the WordPress HTML API,  
so the snippet can read the `<h1>` with `WP_HTML_Processor` without installing or  
booting WordPress.

{
"features": {
"networking": true
},
"steps": [
{
"step": "unzip",
"zipFile": {
"resource": "url",
"url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip?v=wp-php-toolkit-html-2026-06-09"
},
"extractToPath": "/app"
}
]
}

<?php
require '/app/symfony-package-radar/vendor/autoload.php';
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
$kernel = new Kernel( 'prod', false );
$request = Request::create( '/' );
$response = $kernel->handle( $request );
$page\_title = get\_first\_h1\_text( $response->getContent() );
echo 'HTTP ' . $response->getStatusCode() . PHP\_EOL;
echo 'Symfony page: ' . $page\_title . PHP\_EOL;
echo 'WordPress installed: ';
echo file\_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no';
$kernel->terminate( $request, $response );
/\*\*
\* The app's Composer dependencies include the WordPress HTML API, so the
\* snippet can read the <h1> with WP\_HTML\_Processor without installing or
\* booting WordPress.
\*/
function get\_first\_h1\_text( string $html ): string {
$processor = WP\_HTML\_Processor::create\_fragment( $html );
if ( ! $processor->next\_tag( 'H1' ) ) {
return 'unknown';
}
$text = '';
while ( $processor->next\_token() ) {
if ( 'H1' === $processor->get\_tag() && $processor->is\_tag\_closer() ) {
break;
}
if ( '#text' === $processor->get\_token\_type() ) {
$text .= $processor->get\_modifiable\_text();
}
}
return trim( $text );
}

HTTP 200
Symfony page: Symfony Playground
WordPress installed: no

Here is the complete embed:

```
<script type="module" src="https://playground.wordpress.net/php-code-snippet.js"></script>

<script id="symfony-blueprint" type="application/json">
{
  "features": {
    "networking": true
  },
  "steps": [
    {
      "step": "unzip",
      "zipFile": {
        "resource": "url",
        "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip?v=wp-php-toolkit-html-2026-06-09"
      },
      "extractToPath": "/app"
    }
  ]
}
</script>

<php-snippet name="run-symfony.php" wp="none" blueprint="symfony-blueprint">
  <script type="application/x-php">
<?php
require '/app/symfony-package-radar/vendor/autoload.php';

use App\Kernel;
use Symfony\Component\HttpFoundation\Request;

$kernel = new Kernel( 'prod', false );
$request = Request::create( '/' );
$response = $kernel->handle( $request );

$page_title = get_first_h1_text( $response->getContent() );

echo 'HTTP ' . $response->getStatusCode() . PHP_EOL;
echo 'Symfony page: ' . $page_title . PHP_EOL;
echo 'WordPress installed: ';
echo file_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no';

$kernel->terminate( $request, $response );

/**
 * The app's Composer dependencies include the WordPress HTML API, so the
 * snippet can read the <h1> with WP_HTML_Processor without installing or
 * booting WordPress.
 */
function get_first_h1_text( string $html ): string {
    $processor = WP_HTML_Processor::create_fragment( $html );
    if ( ! $processor->next_tag( 'H1' ) ) {
        return 'unknown';
    }

    $text = '';
    while ( $processor->next_token() ) {
        if ( 'H1' === $processor->get_tag() && $processor->is_tag_closer() ) {
            break;
        }
        if ( '#text' === $processor->get_token_type() ) {
            $text .= $processor->get_modifiable_text();
        }
    }

    return trim( $text );
}
  </script>
  <script type="text/expected-output">
HTTP 200
Symfony page: Symfony Playground
WordPress installed: no
  </script>
</php-snippet>
```

The same app is also available as a full Playground page:

[Open the Symfony Package Radar demo](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fwordpress.github.io%2Fblueprints%2Fblueprints%2Fsymfony-package-radar%2Fblueprint.json)

## Package the app as a ZIP

For framework demos, prefer a ZIP that already contains `vendor/`. That keeps  
the Playground startup path short and avoids asking every visitor to wait for  
Composer, Git, and package registry downloads. The Symfony demo uses that path to  
bundle both Symfony and a Composer-installed copy of the WordPress HTML API; it  
still does not include a WordPress install.

For snippets or CLI runs, a small Blueprint can install the app into `/app` with  
one step:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/symfony-package-radar/public/index.php",
    "preferredVersions": {
        "php": "8.4",
        "wp": false
    },
    "features": {
        "networking": true
    },
    "steps": [
        {
            "step": "unzip",
            "zipFile": {
                "resource": "bundled",
                "path": "./symfony-package-radar.zip"
            },
            "extractToPath": "/app"
        }
    ]
}
```

Use `bundled` resources when the ZIP ships next to `blueprint.json`, or use a  
`url` resource when the ZIP is hosted separately. See [Blueprint bundles](https://developer.wordpress.org/playground/blueprints/bundles)  
for packaging details.

For a full-page Playground website, use a Blueprint like the gallery demo. It  
adds a tiny router at the Playground document root so the Symfony `public/`  
directory can respond to browser requests.

## Keep the demo browser-friendly

A Playground-hosted framework demo works best when it:

- Does not require a long-running background process.
- Stores generated files under the virtual filesystem.
- Avoids native extensions that are not compiled into PHP.wasm.
- Avoids frontend build steps at runtime.
- Keeps network calls optional or resilient, because browsers may require CORS  
  proxying for third-party services.

Those constraints still leave plenty of room for real framework behavior:  
controllers, routing, dependency injection, templates, forms, HTTP clients, and  
plain PHP libraries all work when their PHP dependencies are available.

# Running PHPUnit with the Playground CLI

The [Playground CLI](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) includes a `php` subcommand that runs PHP scripts directly inside the Playground environment. By mounting your plugin or theme into the Playground filesystem, you can run PHPUnit without a local database. Every run starts with a clean WordPress installation, so tests are fully isolated.

This guide assumes your plugin or theme has PHPUnit installed via Composer (`composer require --dev phpunit/phpunit`). The `vendor/bin/phpunit` path used below assumes a standard Composer setup.

## Running tests

From your plugin or theme directory, run the following command. Replace `themes/THEME_NAME` with the path to your plugin or theme:

```
npx @wp-playground/cli@latest php \
  --auto-mount \
  -- \
  /wordpress/wp-content/themes/THEME_NAME/vendor/bin/phpunit \
  -c /wordpress/wp-content/themes/THEME_NAME/phpunit.xml.dist
```

The `--auto-mount` flag detects whether the current directory is a plugin, theme, or WordPress installation and mounts it at the correct path under `/wordpress/wp-content/`. The `--` separates CLI flags from arguments passed to the PHP interpreter, and you can pass any arguments supported by your PHPUnit configuration.

For a plugin, the path would use `plugins/` instead:

```
npx @wp-playground/cli@latest php \
  --auto-mount \
  -- \
  /wordpress/wp-content/plugins/MY_PLUGIN/vendor/bin/phpunit \
  -c /wordpress/wp-content/plugins/MY_PLUGIN/phpunit.xml.dist
```

You can also use `--mount` to explicitly map a local directory to a path inside the Playground filesystem:

```
npx @wp-playground/cli@latest php \
  --mount=.:/wordpress/wp-content/plugins/MY_PLUGIN \
  -- \
  /wordpress/wp-content/plugins/MY_PLUGIN/vendor/bin/phpunit \
  -c /wordpress/wp-content/plugins/MY_PLUGIN/phpunit.xml.dist
```

## Choosing PHP and WordPress versions

Use the `--php` and `--wp` flags to test against specific versions:

```
npx @wp-playground/cli@latest php \
  --auto-mount \
  --php=8.1 \
  --wp=6.5 \
  -- \
  /wordpress/wp-content/plugins/MY_PLUGIN/vendor/bin/phpunit \
  -c /wordpress/wp-content/plugins/MY_PLUGIN/phpunit.xml.dist
```

Supported PHP versions range from 7.4 to 8.5. For WordPress, you can use a specific version number, `latest`, `nightly`, or `beta`.

## Next steps

- [Playground CLI documentation](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli) — full CLI reference and configuration options
- [E2E Testing with Playwright](https://developer.wordpress.org/playground/guides/e2e-testing-with-playwright) — browser-based end-to-end testing for WordPress plugins and themes

# WordPress Playground for Everyone

**WordPress Playground lets you run WordPress instantly—no server, no setup, no risk.** It works in your browser at [playground.wordpress.net](https://playground.wordpress.net), and developers can also use it via CLI, Node.js, or embedded in their own apps. But you don’t need to be technical to benefit from it.
Watch this quick overview:

## Think of It Like a Car Simulator

A car simulator gives you a steering wheel, pedals, and virtual streets. Practice driving, hit cones, make mistakes — nothing bad happens. No real car gets damaged. Want to try again? Just restart.
WordPress Playground works the same way. It gives you a complete WordPress site to experiment with, but nothing you do affects any real website. Make changes, break things, learn from mistakes — then start fresh whenever you want.
![WordPress Playground Landing Page](https://i0.wp.com/developer.wordpress.org/files/2026/07/wordpress-playground-landing-page.webp?fit=1440%2C971&ssl=1)

## What Can You Do with Playground?

When you visit [playground.wordpress.net](https://playground.wordpress.net), you get a WordPress site running entirely in your browser. You can:

- Install plugins and themes
- Edit pages and create content
- Change WordPress and PHP versions
- Explore features you’ve never tried before

By default, WordPress Playground loads a landing page that introduces Playground. To start with a plain WordPress install, open **New** in the Dock and choose **Vanilla WordPress** from the Blueprint gallery.
![The New Playground pane with Vanilla WordPress first in the Blueprint gallery](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground.webp?fit=1440%2C900&ssl=1)

## If You’re Learning WordPress

Are you new to WordPress or trying to understand features like the Site Editor or the new features of the latest WordPress Release? Playground is your perfect practice space.

### Explore How Pages Are Built

Playground logs you in as an administrator, so you can edit any page. Click **Edit** for editing posts and **Edit Site** to update the website layout in the top toolbar to open the editor.
![Editing WordPress websites](https://i0.wp.com/developer.wordpress.org/files/2026/07/edit-sites-with-playground.webp?fit=1440%2C971&ssl=1)
Want to understand how a page layout was created? Open the **List View** (the three horizontal lines icon) to see every block that makes up the page.
![Site Editor List view](https://i0.wp.com/developer.wordpress.org/files/2026/07/site-editor-list-view.webp?fit=1440%2C971&ssl=1)
You can inspect columns, headings, images, and buttons — and see exactly how they’re arranged. This is a powerful way to learn by example.

#### Explore the Blueprint Library

Open **New** in the Dock to browse the Blueprint gallery. The gallery has examples for art galleries, stores, portfolios, learning environments, and many other starting points.
![The Blueprint gallery in the New Playground pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-new-playground.webp?fit=1440%2C900&ssl=1)

### Try New Features Safely

When the WordPress team releases new features, you can test them in Playground before they affect your real site. Open **Site Settings** from the Dock to choose a WordPress or PHP version.
![The Site Settings pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-site-settings.webp?fit=1440%2C900&ssl=1)

## If You Own a WordPress Site

Running a live website means every change risks breaking something. Playground lets you test before you commit.

### Test Plugins Before Installing

Curious about a new SEO plugin? Want to compare two contact form options? Install them in Playground first:

1. Open [playground.wordpress.net](https://playground.wordpress.net)
2. Go to **Plugins → Add New**
3. Search for and install any plugin
4. Test it thoroughly

Your real site stays untouched while you evaluate whether the plugin fits your needs.
![Installing Plugins](https://i0.wp.com/developer.wordpress.org/files/2026/07/installing-plugins.webp?fit=1440%2C971&ssl=1)

### Preview Theme Changes

Thinking about switching themes? Test your new theme in Playground to see how it handles your content — without disrupting your visitors.

### Compare Options Side by Side

Open multiple browser tabs with different Playground setups. Compare plugin A versus plugin B, or see how your content looks in different themes. Make informed decisions before touching your production site.

**Your Real Site Stays Safe**

Every Playground runs independently in your browser. Nothing syncs to any external server, and nothing affects your live WordPress installation.

## If You Use WordPress Daily

Even experienced WordPress users benefit from a safe testing environment.

### Make Design Experiments

Want to try a different font size? Adjust spacing? Change colors? Load a Playground with the same theme that you are using in production and edit it freely:

1. Open any page in the editor
2. Select a block and modify its settings
3. See the results immediately

If you like what you see, recreate those changes on your real site. If not,
start another Playground. The experiment may remain as a recent autosave until
newer autosaves replace it.

## Yes, You Can Save Your Work

**Playground doesn’t have to be temporary.** You can save your progress and return to it later.

### Save to Your Browser

New Playgrounds are autosaved when browser storage is available. Open **Your Playgrounds** from the Dock to return to recent autosaves. Playground keeps up to five recent autosaves.
![The Your Playgrounds pane with the current Playground](https://i0.wp.com/developer.wordpress.org/files/2026/07/your-playgrounds.webp?fit=1440%2C900&ssl=1)
To keep an autosave permanently, click the **Autosaved** status in the Dock, choose **Save in browser storage**, and save it.
![The Store permanently pane with browser storage selected](https://i0.wp.com/developer.wordpress.org/files/2026/07/store-permanently-browser.webp?fit=1440%2C900&ssl=1)

### Download as a ZIP File

Need to move your work elsewhere? Open **Export** and choose **Download as .zip**. The ZIP contains the current files, database, plugins, themes, uploads, and edits. You can restore it later with **New → Import zip** or host it on a server that supports PHP and SQLite.
![The Export pane](https://i0.wp.com/developer.wordpress.org/files/2026/07/dock-export-playground.webp?fit=1440%2C900&ssl=1)

**Keep a portable backup**

Browser storage belongs to your browser profile and can be cleared or evicted. Export a ZIP when you need a file you can move or archive.

## Next Steps

Now that you know Playground is for everyone, explore further:

- [Quick Start Guide](https://developer.wordpress.org/playground/quick-start-guide) — A 5-minute walkthrough of Playground basics
- [About WordPress Playground](https://developer.wordpress.org/playground/about) — Learn what you can build, test, and launch

# Programmatic Usage of Playground CLI

The Playground CLI can also be controlled programmatically from your JavaScript/TypeScript code using the `runCLI` function. This gives you direct access to all CLI functionalities within your code, which is useful for automating end-to-end tests. The options you pass to `runCLI` map directly to the [CLI flags](https://developer.wordpress.org/playground/developers/local-development/wp-playground-cli#command-and-arguments).

## Running a WordPress instance with a specific version

Using the `runCLI` function, you can specify options like the PHP and WordPress versions. In the example below, we request PHP 8.3, the latest version of WordPress, and to be automatically logged in. All supported arguments are defined in the `RunCLIArgs` type.

```
const cliServer = await runCLI({
  command: 'server',
  php: '8.3',
  wp: 'latest',
  login: true,
});
```

Run the code above using your preferred TypeScript runtime, e.g. `tsx`:

```
npx tsx my-script.ts
```

## Setting a blueprint

You can provide a blueprint in two ways: either as an object literal directly passed to the `blueprint` property, or as a string containing the path to an external `.json` file.

```
const cliServer: RunCLIServer = await runCLI({
  command: 'server',
  wp: 'latest',
  blueprint: {
    steps: [
        {
          "step": "setSiteOptions",
          "options": {
              "blogname": "Blueprint Title",
              "blogdescription": "A great blog description"
          }
        }
    ],
  },
});
```

For full type-safety when defining your blueprint object, you can import and use the `BlueprintDeclaration` type from the `@wp-playground/blueprints` package:

```
const myBlueprint: BlueprintDeclaration = {
  landingPage: "/wp-admin/",
  steps: [
    {
      "step": "installTheme",
      "themeData": {
        "resource": "wordpress.org/themes",
        "slug": "twentytwentyone"
      },
      "options": {
        "activate": true
      }
    }
  ]
};
```

## Mounting a plugin programmatically

You can mount local directories programmatically using `runCLI`. The options `mount` and `mount-before-install` are available. The `hostPath` property expects a path to a directory on your local machine. This path should be relative to where your script is being executed.

```
const cliServer = await runCLI({
  command: 'server',
  login: true,
  'mount-before-install': [
    {
      hostPath: './[my-plugin-local-path]',
      vfsPath: '/wordpress/wp-content/plugins/my-plugin',
    },
  ],
});
```

## Combining mounts with blueprints

You can combine mounting parts of the project with blueprints, for example:

```
const cliServer: RunCLIServer = await runCLI({
    command: 'server',
    php: '8.3',
    wp: 'latest',
    login: true,
    mount: [
        {
            "hostPath": "./plugin/",
            "vfsPath": "/wordpress/wp-content/plugins/playwright-test"
        }
    ],
    blueprint: {
        steps: [
            {
                "step": "activatePlugin",
                "pluginPath": "/wordpress/wp-content/plugins/playwright-test/plugin-playwright.php"
            }
        ]
    }
});
```

## Automated testing

### Integration testing with Vitest

The programmatic API is excellent for automated testing. Here’s a complete example using Vitest:

```
describe('My Plugin Tests', () => {
  const cliServer: RunCLIServer;

  afterEach(async () => {
    if (cliServer) {
      // RunCLIServer exposes Symbol.asyncDispose as its public async cleanup API.
      await cliServer[Symbol.asyncDispose]();
    }
  });

  test('plugin activates successfully', async () => {
    cliServer = await runCLI({
      command: 'server',
      mount: [
        {
          hostPath: './my-plugin',
          vfsPath: '/wordpress/wp-content/plugins/my-plugin'
        }
      ],
      blueprint: {
        steps: [
          {
            step: 'activatePlugin',
            pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
          }
        ]
      }
    });

    const homeUrl = new URL('/', cliServer.serverUrl);
    const response = await fetch(homeUrl);

    expect(response.status).toBe(200);
    const html = await response.text();
    expect(html).toContain('My Plugin');
  });

  test('plugin settings page loads', async () => {
    cliServer = await runCLI({
      command: 'server',
      login: true, // Auto-login as admin
      mount: [
        {
          hostPath: './my-plugin',
          vfsPath: '/wordpress/wp-content/plugins/my-plugin'
        }
      ],
      blueprint: {
        steps: [
          {
            step: 'activatePlugin',
            pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
          }
        ]
      }
    });

    const settingsUrl = new URL(
      '/wp-admin/options-general.php?page=my-plugin',
      cliServer.serverUrl
    );
    const response = await fetch(settingsUrl);

    // Note: A plain `fetch` call does not send the admin session cookie set by `login: true`,
    // so this request is typically redirected to the login page instead of returning 200.
    expect(response.status).toBe(302);
  });
});
```

### Testing with different WordPress/PHP versions

```
test('plugin works with WordPress 6.4 and PHP 8.3', async () => {
  cliServer = await runCLI({
    command: 'server',
    php: '8.3',
    wp: '6.4',
    mount: [
      {
        hostPath: './my-plugin',
        vfsPath: '/wordpress/wp-content/plugins/my-plugin'
      }
    ],
    blueprint: {
      steps: [
        {
          step: 'activatePlugin',
          pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
        }
      ]
    }
  });

  const homeUrl = new URL('/', cliServer.serverUrl);
  const response = await fetch(homeUrl);

  expect(response.status).toBe(200);
});
```

## Advanced configuration

### Skip WordPress and SQLite setup

When you only need to test PHP code without WordPress, you can skip the setup for faster testing:

```
const cliServer = await runCLI({
    command: 'server',
    php: '8.3',
    wordpressInstallMode: 'do-not-attempt-installing',
    skipSqliteSetup: true,
});

// Test PHP version
await cliServer.playground.writeFile(
  '/wordpress/version.php',
  '<?php echo phpversion(); ?>'
);

const versionUrl = new URL('/version.php', cliServer.serverUrl);
const response = await fetch(versionUrl);
const version = await response.text();
console.log('PHP Version:', version); // Outputs: 8.3.x
```

### Error handling

```
try {
  const cliServer = await runCLI({
    command: 'server',
    debug: true, // Enable PHP error logging.
  });

  // Your test code here

} catch (error) {
  console.error('Server failed to start:', error);
}
```

# Providing content for your demo with Playground

One of the things you may want to do to provide a good demo with WordPress  
Playground is to load default content to better highlight the features of your  
plugin or theme. This default content may include images or other assets.

There are several [Blueprint steps](https://developer.wordpress.org/playground/blueprints/steps) and strategies you can  
use to import content (or generate it) in the Playground instance. This guide  
walks through the available sources. For a focused comparison of XML, PHP, and  
ZIP imports—including pros, cons, and measured performance—see  
[Importing content into WordPress with Blueprints](https://developer.wordpress.org/playground/guides/import-content-with-blueprints).

## `importWxr`

With the [`importWxr` step](https://developer.wordpress.org/playground/blueprints/steps#importWxr), you can import content from a  
WordPress eXtended RSS (WXR) `.xml` file previously  
[exported from an existing WordPress installation](https://wordpress.org/documentation/article/tools-export-screen/).

The step can fetch attachments, rewrite URLs, include or exclude comments, and  
control how imported authors map to local users. This example assigns imported  
content to the existing `admin` user and leaves attachment downloads disabled:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/wp-admin/edit.php",
    "login": true,
    "steps": [
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint-content.xml"
            },
            "fetchAttachments": false,
            "rewriteUrls": true,
            "importComments": true,
            "authorsMode": "default-author",
            "defaultAuthorUsername": "admin"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json)   [See blueprint.json](https://github.com/WordPress/blueprints/blob/eb6da7dfa295a095eea2e424c0ae83a219803a8d/blueprints/install-activate-setup-theme-from-gh-repo/blueprint.json#L43)

Set `authorsMode` to `create` to create local users for imported authors, or to  
`map` and provide `authorsMap` when the corresponding users already exist. You  
can also provide `urlMapping` for explicit old-to-new URL replacements.

To download the media referenced by the export, set `fetchAttachments` to  
`true` and enable Blueprint networking. The original media URLs must still be  
available:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "features": {
        "networking": true
    },
    "steps": [
        {
            "step": "importWxr",
            "file": {
                "resource": "url",
                "url": "https://example.com/wordpress-export.xml"
            },
            "fetchAttachments": true
        }
    ]
}
```

When the original attachment URLs are unavailable, one approach is to upload  
the images to the repository that hosts the Blueprint and replace their paths  
in the exported `.xml` file. For a public GitHub repository, use a raw URL such  
as `https://raw.githubusercontent.com/{repo}/{branch}/{image\_path}`.

```
<!-- wp:image {"lightbox":{"enabled":false},"id":4751,"width":"78px","sizeSlug":"full","linkDestination":"none","align":"center","className":"no-border"} -->
<figure class="wp-block-image aligncenter size-full is-resized no-border">
    <img src="https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/images/avatars.png" alt="" class="wp-image-4751" style="width:78px" />
</figure>
<!-- /wp:image -->
```

For a self-contained demo, place the exported `.xml` file and its assets next  
to `blueprint.json` in a [Blueprint bundle](https://developer.wordpress.org/playground/blueprints/bundles), and use a  
[`bundled` resource](https://developer.wordpress.org/playground/blueprints/steps/resources) instead of a remote URL.

## `importWordPressFiles`

With the [`importWordPressFiles` step](https://developer.wordpress.org/playground/blueprints/steps), you can restore the  
top-level WordPress files from a `.zip` file into the instance’s root folder.  
For example, if an archive contains `wp-content` and `wp-includes`, those  
directories replace the corresponding directories in Playground.

The ZIP can be created from a Playground instance with the **Download as zip**  
option in the [Playground Options Menu](https://developer.wordpress.org/playground/web-instance).  
Current Playground exports include a manifest that lets the import step update  
Playground scope URLs after restoration.

You can prepare a demo for your WordPress theme or plugin—including the  
database, images, plugins, themes, and settings—in a Playground instance and  
then export a snapshot of that demo. The snapshot can be restored later using  
`importWordPressFiles`. This example expects `site.zip` next to `blueprint.json`  
in a [Blueprint bundle](https://developer.wordpress.org/playground/blueprints/bundles):

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/",
    "login": true,
    "steps": [
        {
            "step": "importWordPressFiles",
            "wordPressFilesZip": {
                "resource": "bundled",
                "path": "/site.zip"
            }
        }
    ]
}
```

The step can detect a complete WordPress directory inside a wrapper folder. If  
an archive contains more than one site or needs an explicit starting directory,  
set `pathInZip` to the directory that contains the WordPress files. Keep the  
source and destination WordPress, PHP, theme, and plugin versions compatible,  
and only restore ZIP files from sources you trust because they can contain an  
entire database and executable PHP.

## `importThemeStarterContent`

[Some themes have starter content](https://make.wordpress.org/core/2016/11/30/starter-content-for-themes-in-4-7/)  
that can be published to highlight the features of a theme.

With the [`importThemeStarterContent` step](https://developer.wordpress.org/playground/blueprints/steps), you can publish  
the starter content of any installed theme, even if that theme is not activated  
in the Playground instance:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "twentytwenty"
            }
        },
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "twentytwentyone"
            },
            "options": {
                "activate": true
            }
        },
        {
            "step": "importThemeStarterContent",
            "themeSlug": "twentytwenty"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22steps%22:[{%22step%22:%22installTheme%22,%22themeData%22:{%22resource%22:%22wordpress.org/themes%22,%22slug%22:%22twentytwenty%22}},{%22step%22:%22installTheme%22,%22themeData%22:{%22resource%22:%22wordpress.org/themes%22,%22slug%22:%22twentytwentyone%22},%22options%22:{%22activate%22:true}},{%22step%22:%22importThemeStarterContent%22,%22themeSlug%22:%22twentytwenty%22}]})

You can also publish the starter content of a theme while installing it with the  
[`installTheme` step](https://developer.wordpress.org/playground/blueprints/steps) by setting its `importStarterContent`  
option to `true`:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "steps": [
        {
            "step": "installTheme",
            "themeData": {
                "resource": "wordpress.org/themes",
                "slug": "twentytwenty"
            },
            "options": {
                "activate": true,
                "importStarterContent": true
            }
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22steps%22:[{%22step%22:%22installTheme%22,%22themeData%22:{%22resource%22:%22wordpress.org/themes%22,%22slug%22:%22twentytwenty%22},%22options%22:{%22activate%22:true,%22importStarterContent%22:true}}]})

## `wp-cli`

Another way of generating content for your theme or plugin is the  
[`wp-cli` step](https://developer.wordpress.org/playground/blueprints/steps). It runs  
[WP-CLI commands](https://developer.wordpress.org/cli/commands/) such as  
[`wp post generate`](https://developer.wordpress.org/cli/commands/post/generate/):

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/wp-admin/edit.php",
    "login": true,
    "steps": [
        {
            "step": "wp-cli",
            "command": "wp post generate --count=20 --post_type=post --post_date=1999-01-04"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22landingPage%22:%22/wp-admin/edit.php%22,%22login%22:true,%22steps%22:[{%22step%22:%22wp-cli%22,%22command%22:%22wp%20post%20generate%20--count=20%20--post_type=post%20--post_date=1999-01-04%22}]})

You can also combine the `wp-cli` step with the  
[`writeFile` step](https://developer.wordpress.org/playground/blueprints/steps) to create posts from existing content and  
import images into the Playground instance:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/?p=4",
    "login": true,
    "steps": [
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/postcontent.md",
            "data": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/postcontent.md"
            }
        },
        {
            "step": "wp-cli",
            "command": "wp post create --post_title='Welcome to Playground' --post_status='published' /wordpress/wp-content/postcontent.md"
        },
        {
            "step": "writeFile",
            "path": "/wordpress/wp-content/Select-storage-method.png",
            "data": {
                "resource": "url",
                "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/Select-storage-method.png"
            }
        },
        {
            "step": "wp-cli",
            "command": "wp media import wordpress/wp-content/Select-storage-method.png --post_id=4 --title='Select your storage method' --featured_image"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22$schema%22:%22https://playground.wordpress.net/blueprint-schema.json%22,%22meta%22:{%22title%22:%22Use%20wp-cli%20to%20add%20a%20post%20with%20image%22,%22description%22:%22Use%20wp-cli%20to%20create%20a%20post%20from%20text%20file%20with%20block%20markup%20and%20a%20featured%20image%22,%22author%22:%22bph%22,%22categories%22:[%22Content%22,%22wpcli%22]},%22landingPage%22:%22/?p=4%22,%22login%22:true,%22steps%22:[{%22step%22:%22writeFile%22,%22path%22:%22/wordpress/wp-content/postcontent.md%22,%22data%22:{%22resource%22:%22url%22,%22url%22:%22https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/postcontent.md%22}},{%22step%22:%22wp-cli%22,%22command%22:%22wp%20post%20create%20--post_title='Welcome%20to%20Playground'%20--post_status='published'%20/wordpress/wp-content/postcontent.md%22},{%22step%22:%22writeFile%22,%22path%22:%22/wordpress/wp-content/Select-storage-method.png%22,%22data%22:{%22resource%22:%22url%22,%22url%22:%22https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/Select-storage-method.png%22}},{%22step%22:%22wp-cli%22,%22command%22:%22wp%20media%20import%20wordpress/wp-content/Select-storage-method.png%20--post_id=4%20--title='Select%20your%20storage%20method'%20--featured_image%22}]})

Check the  
[“Use wp-cli to add a post with image”](https://github.com/WordPress/blueprints/tree/trunk/blueprints/wpcli-post-with-image)  
example from the  
[Blueprints Gallery](https://github.com/WordPress/blueprints/blob/trunk/GALLERY.md)  
to see the full example showing the connection between the content and the  
featured image.

## `runPHP`

With the [`runPHP` step](https://developer.wordpress.org/playground/blueprints/steps), you can run PHP code to insert or  
configure data in your WordPress installation, for example with the  
[`wp_insert_post` function](https://developer.wordpress.org/reference/functions/wp_insert_post/).  
Load `/wordpress/wp-load.php` before calling WordPress APIs, and handle errors  
so a failed setup does not silently produce an incomplete demo:

```
{
    "$schema": "https://playground.wordpress.net/blueprint-schema.json",
    "landingPage": "/wp-admin/edit.php",
    "login": true,
    "steps": [
        {
            "step": "runPHP",
            "code": "<?php\nrequire_once '/wordpress/wp-load.php';\n\n$post_id = wp_insert_post(\n\tarray(\n\t\t'post_title'   => 'Simple post from PHP',\n\t\t'post_content' => '<!-- wp:paragraph --><p>This is a simple post inserted with wp_insert_post.</p><!-- /wp:paragraph -->',\n\t\t'post_author'  => 1,\n\t\t'post_status'  => 'publish',\n\t),\n\ttrue\n);\n\nif ( is_wp_error( $post_id ) ) {\n\tthrow new RuntimeException( $post_id->get_error_message() );\n}"
        }
    ]
}
```

[Run Blueprint](https://playground.wordpress.net/builder/builder.html#{%22$schema%22:%22https://playground.wordpress.net/blueprint-schema.json%22,%22landingPage%22:%22/wp-admin/edit.php%22,%22login%22:true,%22steps%22:[{%22step%22:%22runPHP%22,%22code%22:%22%3C?php%5Cnrequire_once%20'/wordpress/wp-load.php'%3B%5Cn%5Cn$post_id%20=%20wp_insert_post(%5Cn%5Ctarray(%5Cn%5Ct%5Ct'post_title'%20%20%20=%3E%20'Simple%20post%20from%20PHP',%5Cn%5Ct%5Ct'post_content'%20=%3E%20'%3C!--%20wp:paragraph%20--%3E%3Cp%3EThis%20is%20a%20simple%20post%20inserted%20with%20wp_insert_post.%3C/p%3E%3C!--%20/wp:paragraph%20--%3E',%5Cn%5Ct%5Ct'post_author'%20%20=%3E%201,%5Cn%5Ct%5Ct'post_status'%20%20=%3E%20'publish',%5Cn%5Ct),%5Cn%5Cttrue%5Cn)%3B%5Cn%5Cnif%20(%20is_wp_error(%20$post_id%20)%20)%20{%5Cn%5Ctthrow%20new%20RuntimeException(%20$post_id-%3Eget_error_message()%20)%3B%5Cn}%22}]})

For small, deterministic fixtures, `runPHP` keeps setup logic close to the  
Blueprint. For large editorial datasets or a complete prepared site, WXR or a  
ZIP snapshot is usually easier to maintain. The  
[import comparison guide](https://developer.wordpress.org/playground/guides/import-content-with-blueprints) explains the  
trade-offs in detail.

# WordPress Playground in native iOS apps

## How to ship a real WordPress site in a native iOS app via Playground?

Blocknotes is the first iOS application that ran WordPress natively on iOS devices by leveraging WordPress Playground. Developed by [Ella van Durpe](https://profiles.wordpress.org/ellatrix/), a core committer for WordPress, Blocknotes represents a significant leap in the capabilities of mobile applications by utilizing WebAssembly to run WordPress without the need for a traditional PHP server.

This case study explores the features, technical implementation, and potential implications of Blocknotes for the future of mobile and web development.

**Important!** The current version of Blocknotes isn’t running WordPress Playground anymore. Since the initial release, the app was rewritten to only use the WordPress block editor without the rest of WordPress. This case study covers the early versions of Blocknotes that opened an entire world of new possibilities for WordPress.

## Blocknotes features

Blocknotes allows users to create and edit notes using the WordPress block editor. The notes are automatically saved as HTML files to the user’s iCloud Drive and seamlessly synchronized across devices.

## Technical Implementation

Blocknotes operated as a WebView running an HTML page where a WebAssembly version of PHP was running WordPress. That HTML page was packaged as a native iOS via [Capacitor](https://capacitorjs.com/). This setup allowed WordPress to function in environments traditionally not supported.

In [Blocknotes GitHub repository](https://github.com/blocknotes-org/blocknotes/tree/e08535883332be9a45a0c75b750c54a4e17f6748) you can review the last Playground-based release. Here are the most important parts:

- [A WordPress build](https://github.com/blocknotes-org/blocknotes/blob/e08535883332be9a45a0c75b750c54a4e17f6748/src/js/wp-6.2.data) (packaged as a `.data` file).
- [Static WordPress assets](https://github.com/blocknotes-org/blocknotes/tree/e08535883332be9a45a0c75b750c54a4e17f6748/public).
- [A WebAssembly build of PHP](https://github.com/blocknotes-org/blocknotes/tree/e08535883332be9a45a0c75b750c54a4e17f6748/node_modules/%40php-wasm/web) (via [@php-wasm/web](https://npmjs.com/package/@php-wasm/web)).
- [A web worker running PHP and WordPress](https://github.com/blocknotes-org/blocknotes/blob/e08535883332be9a45a0c75b750c54a4e17f6748/src/js/worker.js).
- [Hypernotes](https://wordpress.com/plugins/hypernotes) WordPress plugin ([installed here](https://github.com/blocknotes-org/blocknotes/blob/e08535883332be9a45a0c75b750c54a4e17f6748/src/js/index.js#L160)) to turn wp-admin into a note-taking app.
- A layer to [load WordPress posts from iOS files](https://github.com/blocknotes-org/blocknotes/blob/e08535883332be9a45a0c75b750c54a4e17f6748/src/js/index.js#L39) and [save changes as iOS files](https://github.com/blocknotes-org/blocknotes/blob/e08535883332be9a45a0c75b750c54a4e17f6748/src/js/save-data.js).

## Building your own iOS app with WordPress Playground

Although Blocknotes proved releasing a WordPress-based iOS app is possible, this is still a highly exploratory area. There are no established workflows, libraries, or knowledge bases.

The best documentation we have is the Blocknotes repository. Use it as a reference and a starting point for exploring your new app. Review the key components like the WebAssembly build of PHP, the integration of the WordPress block editor, and how web workers are utilized to run WordPress efficiently. By dissecting these elements, you can gain insights into building your own iOS app with WordPress Playground, pushing the boundaries of what’s possible with mobile web applications.

As you navigate this innovative space, share your findings and challenges with the Playground team and the broader WordPress community. Publishing your learnings will not only aid in your development but also contribute to a collective knowledge base, driving forward the future of WordPress on mobile.

## Potential and the future

Blocknotes paves the way for a new generation of applications that are more accessible, flexible, and powerful.

Once the app-building workflows mature, we may see an automated pipelines for packaging Playground sites as iOS apps. It would make it extremely easy to run the same codebase on the server, in the browser, and as a mobile app.

By working together and sharing our findings, we can push the boundaries of what’s possible with WordPress and mobile app development