WordPress Playground 手册 WordPress Playground 手册,介绍浏览器内 WordPress 运行环境。 Blueprints Docs Check the Blueprints Gallery 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 – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs. 👉 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 – 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 – 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: Quick Start Guide to setting up a WordPress Playground instance using Blueprint JSON files. Tutorial – Blueprints 101 – 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: Blueprint JSON files define your Playground instance with various properties. This section highlights the key properties you need to know. Using Blueprints: Learn in this section different ways to use 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: Learn how to create and use Blueprint bundles – self-contained packages that include a Blueprint and all its resources. 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: 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 – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs. Blueprints – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub. 👉 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 – 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: Begin your development journey with Playground by exploring some examples of what you can achieve using Playground APIs. 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: Explore the main APIs exposed by WordPress Playground to interact with the Playground: Query API, Blueprints, Javascript API Architecture: The specifics of WordPress Playground’s architecture, including its various components and tools. 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/. 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 (you’re here) – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs. Blueprints – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub. 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 – 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: 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 and try a block/theme/plugin or test a specific WordPress/PHP version. Playground web instance: Learn more about the Playground instance you get at https://playground.wordpress.net/ About Playground: 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, Test, and Launch your products. 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: 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: 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 in 5 minutes (and check out the demo site) Get started for developing with WordPress Playground Use Playground as a zero-setup local development environment Read about the limitations WordCamp Contributor Day Read Introduction to Playground: running WordPress in the browser blog post in the WordPress Developer Blog 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 and basic concepts Review links and resources Choose the right API for your app: Query API enables basic operations using only query parameters Blueprints API gives you a great degree of control with a simple JSON file JavaScript API gives you full control via a JavaScript client from an npm package Dive into the 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 for all the details on how you can contribute. Join us in the #playground channel in Slack (see the WordPress Slack page 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. 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 — 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 — Machine-readable summary of Playground’s capabilities, APIs, and docs in llms.txt format. 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. 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: A blueprint.json file that defines the Blueprint configuration 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: Root level (standard): blueprint.json sits directly at the ZIP root 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: Ensure your blueprint.json file is at the root level of your ZIP file or inside a single top-level directory Check that all paths in your bundled resource references are correct Verify that your ZIP file is properly formatted When using the CLI, check if you need the --blueprint-may-read-adjacent-files flag 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! JSON schema JSON files can be tedious to write and easy to get wrong. To help with that, Playground provides a JSON schema 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 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. { "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 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! 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 and Blueprints Gallery. 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 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": " 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1 )); " } ] } Try it out! 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": " true ) );" } ] } Try it out! 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! For a detailed explanation of why this is needed, refer to the Troubleshoot and Debug Blueprints 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! 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": " 'WordPress Playground block demo!','post_content' => '', 'post_status' => 'publish', 'post_type' => 'post',]);" } ] } Try it out! You can share your own Blueprint examples in this dedicated wiki. 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! 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 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, 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! 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: Creating a ZIP file with these files and the blueprint.json Hosting the ZIP file on a server Loading it with ?blueprint-url=https://example.com/my-blueprint-bundle.zip For more information on Blueprint bundles, see the Blueprint 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. Use them with the JavaScript API. Reference a blueprint JSON file via QueryParam blueprint-url 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 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! 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! 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 allow you use external files in Blueprints. Some steps have a shorthand version. Check the 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 page for further details. The WordPress 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! 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​​ 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​​ 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! defineSiteUrl​​ Sets WP_HOME and 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, or deploying Playground on a custom domain. Parameters siteUrl (string) – The URL defineWpConfigConsts​​ Defines constants in a wp-config.php 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! enableMultisite​​ Defines the Multisite 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! 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​​ 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​​ 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​​ 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! 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! 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! 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! 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! 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​​ 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! 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! runPHP​​ Runs PHP code. When running WordPress functions, the code key must first load wp-load.php and start with " 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>" } ] } import { runPHP } from '@wp-playground/blueprints'; runPHP( playground, { "step": "runPHP", "code": " 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>" }, progress ) Try it out! runPHPWithOptions​​ Runs PHP code. When running WordPress functions, the code key must first load wp-load.php and start with "", "body": "Site Name Modified by runPHPWithOptions" } } ] } import { runPHPWithOptions } from '@wp-playground/blueprints'; runPHPWithOptions( playground, { "step": "runPHPWithOptions", "options": { "code": "", "body": "Site Name Modified by runPHPWithOptions" } }, progress ) Try it out! 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! 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! setSiteOptions​​ Sets site options. This is equivalent to calling update_option for each option in the options 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! 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​​ Updates user meta. This is equivalent to calling 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! wp-cli​​ Runs PHP code using WP-CLI. 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! 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": "" } ] } import { writeFile } from '@wp-playground/blueprints'; writeFile( playground, { "step": "writeFile", "path": "/wordpress/test.php", "data": "" }, progress ) Try it out! 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": "Hello World!'; ?>", "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": "Hello World!'; ?>", "public": { "style.css": "a { color: red; }" } } } }, progress ) Try it out! 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 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. 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 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. 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. 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": " 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, then check the Playground Logs panel or the browser console. 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 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. What are Blueprints, and what can you do with them? How to load and run Blueprints Build your first Blueprint If you encounter any issues while following this tutorial, refer to the Troubleshoot and debug Blueprints 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. 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 and 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! 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 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: 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 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: 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 is discussed on a separate GitHub repository, and you’re more than welcome to join (there or on the #playground 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: Blueprints handle the progress bar and error reporting for you. The function API requires you to handle these yourself. 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 issue at 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 or installTheme require a location of the plugin or theme to be installed. That location can be defined as a URL resource of the .zip file containing the theme or plugin. It can also be defined as a wordpress.org/plugins or wordpress.org/themes 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 or installTheme. 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. 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 and installTheme. 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 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 Creates a new WordPress site Sets the site title to “My first Blueprint” Installs the Adventurer theme Installs the Hello Dolly plugin from the WordPress plugin directory Installs a custom plugin 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 Autocomplete If you use an IDE, like VS Code or PHPStorm, you can use the Blueprint JSON Schema 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: 2. Set the site title to “My first Blueprint” Blueprints consist of a series of 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 The setSiteOptions step 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. 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. Let’s install it using the installTheme step: { "siteOptions": { "blogname": "My first Blueprint" }, "steps": [ { "step": "installTheme", "themeData": { "resource": "wordpress.org/themes", "slug": "adventurer" } } ] } Run Blueprint The site should now look like the screenshot below: Resources The themeData defines a resource 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// becomes https://wordpress.org/themes/adventurer/. Learn more about the supported resources in the Blueprint Resources API Reference. 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: { "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 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: Hello from My Custom Plugin!'; } add_action('admin_notices', 'my_custom_plugin'); You can use the installPlugin, but that requires creating a ZIP file. Let’s start with something different to see if the plugin works: Create a wp-content/plugins/hello-from-the-dashboard directory using the mkdir step. Write a plugin.php file using the writeFile step. Activate the plugin using the activatePlugin step. 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": "Hello from My Custom Plugin!';\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: { "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": "Hello from My Custom Plugin!';\n}\n\nadd_action('admin_notices', 'my_custom_plugin');" }, { "step": "activatePlugin", "pluginPath": "hello-from-the-dashboard/plugin.php" } ] } Run Blueprint That’s what it looks like when you navigate to the dashboard: 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 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 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: -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: { // ... "steps": [ // ... { "step": "runPHP", "code": " -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 to import a WordPress export ( WXR) file that helps test WordPress themes. The file is available in the 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": " -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 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 for the full list of UI paths. 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 Use this method to run the example code in the next chapter, Build your first Blueprint. 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, 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: 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, 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 in the WordPress plugin directory, or a preview in a Theme Trac ticket. 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 Embed a pre-configured WordPress site in your website using the WordPress Playground Block. Blueprints examples Demos and apps built with Blueprints 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 and you can learn more about it here. Once you have a URL that you like, you can embed it in your website using an iframe: Check the 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": " 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1)); ?>" } ] } Try it out! 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. Sites API The Playground website at 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 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: Check the 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 (easy) or the JSON Blueprints API (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 To customize that Playground instance, you can: Load it from special link prepared using the Query API (easy) or the JSON Blueprints API (medium). Control it using the 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. Browser APIs The following Playground APIs are available in the browser: Query API enable basic operations using only query parameters Blueprints API give you a great degree of control with a simple JSON file Sites API lets scripts running on playground.wordpress.net manage saved sites 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 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 WebAssembly PHP Browser bindings PHP.wasm packages for the shared API, platform bindings, and version-specific binaries Public API Visit each section to learn more about the specific parts of the architecture. Tooling NX: building packages and projects WordPress Playground uses NX, a build system designed for monorepos. The dependencies between Playground packages and projects are too complex for a bundler like Webpack, and NX handles this complexity much better: To learn more, head over to the NX developer docs. 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 and Playground Tools. We use Lerna 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: Embed WordPress on your website Playground can be embedded on your website using the HTML Every visitor will get their own private WordPress instance for free. You can then customize it using one of the Playground 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. 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 enable basic operations using only query parameters Blueprints API give you a great degree of control with a simple JSON file 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. 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 and you can learn more about it here. 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. 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 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: { "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, 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. 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 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! 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: 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: Combine that with a code editor like Monaco or CodeMirror, and you’ll get live code snippets like in this article! 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. 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. 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. 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 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 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. To run WordPress-specific PHP functions, you’d need to first require wp-load.php: { "step": "runPHP", "code": "" } Using WP-CLI You can execute wp-cli commands via the Blueprints wp-cli step. However, since Playground runs in the browser, it doesn’t support the full array of available commands. While there is no definite list of supported commands, experimenting in the online demo 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 at https://playground.wordpress.net/. Alternatively, you can host your own WordPress Playground. Playground also provides tools for local WordPress development, prioritizing ease of installation and usability: @wp-playground/cli: A command-line tool to quickly launch a WordPress site from your terminal. Visual Studio Code 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: For low-level control over the WebAssembly PHP build, explore the @php-wasm/node package, 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 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 → 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 /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. 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 Blueprint JSON 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 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 bewp-lazy-loading . You can pre-install multiple plugins by sayingplugin=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 theplugin` 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 bedisco . Installing a theme automatically logs the user in as an admin. Multiples themes could be installed just repeating thetheme` 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: 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 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). 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. 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() 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 for direct control instead. Quick start Open 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() 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; Each setting corresponds to an equivalent 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; 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; 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 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; 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; 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; 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; version accepts any value from the AllPHPVersion 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; 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; 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; Next steps JavaScript API — full reference for the PlaygroundClient returned by getClient() Query API — the URL parameters that mirror new-site settings 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. Blueprints JSON and the API Client The Playground API client can be initialized with a JSON Blueprint. 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. If you need to work with an already initialized client, you should look into 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 : 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. `remote.html` vs `index.html` 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 is exclusively provided by index.html, independent of the WordPress Playground JavaScript API. The 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 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. 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///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`, thesourcePath above resolves tohttps://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 publishes wp_mysql_parser, a native lexer/parser used by the SQLite Database Integration plugin. PHP Toolkit Native APIs WASM releases 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. 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: 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() – runs PHP code and returns the output request() – makes an HTTP request to the website In Node.js, you can also use the cli() 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', `"; // 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: ` '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 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 handles it from there. This simple REST API validates the data and sends it to the Making WordPress #playground-logs Slack channel. 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 to ensure the PHP runtime doesn’t slow down the user interface of the main website. Imagine the following code: 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( spawnPHPWorkerThread( '/worker-thread.js' // Valid Worker script URL ) ); await phpClient.isReady(); await phpClient.run({ code: `(); 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 is used to handle the HTTP traffic using the in-browser PHPRequestHandler. Imagine your PHP script renders the following page in the iframe viewport: John's Website Homepage Blog Contact 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 – a tool to intercept the HTTP requests and handle them inside the browser: 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 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: /app.ts: const workerUrl = '/worker-thread.js'; export async function startApp() { const phpClient = consumeAPI( 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', 'Go to page.php'); await workerThread.writeFile('/page.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: A step-by-step breakdown: The request is intercepted by the Service Worker The Service Worker passes it to the Worker Thread The Worker Thread calls PHP.request to convert that request to a response The Worker Thread passes the response to the Service Worker 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 turns a vanilla WordPress into a browser-optimized one: Makes WordPress run on SQLite using the official drop-in plugin 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=, 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 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 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: 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. Fixing Asyncify crashes Pull Request 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: Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that. Adding a test case that triggers a crash to packages/php-wasm/node/src/test/php-asyncify.spec.ts Running: npm run fix-asyncify Committing the test case, the updated Dockerfile, and the rebuilt PHP.wasm JSPI: The Modern Alternative to Asyncify The JavaScript Promise Integration (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. It was originally forked from 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, 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. 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. 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 for the runtime API. C API exposed to JavaScript The C API exposed to JavaScript lives in the 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 to learn more. Build configuration The build is configurable via the Docker --build-arg feature. 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. 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 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 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 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 to WebAssembly using Emscripten and a dedicated pipeline. Building PHP to WebAssembly is very similar to building vanilla PHP. The wasm build required adjusting a function signature here, forcing a config variable there, and applying a few small patches, but there’s relatively few adjustments involved. 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 written in C and a JavaScript PHP class 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. 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, 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 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 and leans on SQLite. But how can WordPress run on a different database? Behind the scenes, the official 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 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 and leans on SQLite. But how can WordPress run on a different database? Behind the scenes, the official 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 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 that you can use in the browser. WordPress in Node.js In Node.js, you’ll typically want to mount WordPress 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. 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 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 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: '', }); 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 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 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', `` ); 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: `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: `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', ` [ ['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', `

Welcome, !

Thank you for registering with .

Your account details:

  • Email:
  • Member Since:

You now have access to the following features:

` ); // 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: ``, }); 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', `` ); // 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 ).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: ``, }); expect(result.text).toBe('8'); }); it('should handle JSON operations', async () => { const input = { name: 'Test', value: 42 }; const result = await php.run({ code: ` $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: ``, }); 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: '', 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: '', }); 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, 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. You can find the latest documentation in the dedicated README file. Installation and Usage: Install the Extension: Search for “WordPress Playground” in the VS Code extensions marketplace and install it. Setup: Follow the setup instructions provided in the extension to configure your development environment. 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 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. 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//. 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 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//, where 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//. 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= and --php=: 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. For those unfamiliar with this technology, it allows developers to configure the initial state for their WordPress Playground instances. Using the --blueprint= 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: /playground-/ ├── 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\\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: /wp-content/database/.ht.sqlite ✅ Persisted locally in your project folder Auto-mounting plugin/theme only: Database: /playground-/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// ├── wordpress/ # WordPress installation ├── internal/ # Playground runtime config └── tmp/ # Temporary PHP files The 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//. Changes survive between CLI restarts. Full WordPress directory or explicit /wordpress mount: Automatic persistence in ~/.wordpress-playground/sites// is skipped. The mounted WordPress directory is the persistent store. The database location depends on your configuration: Default (automatic persistence): Database: ~/.wordpress-playground/sites//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 to the project directory. Defaults to the current working directory. --wp=: WordPress version to use. Defaults to the latest. --php=: PHP version to use. Defaults to PHP 8.3. --port=: The port number for the server to listen on. Defaults to 9400 when available. --blueprint=: 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=: The port number for the server to listen on. Defaults to 9400. --version: Show version number. --site-url=: Site URL to use for WordPress. Defaults to http://127.0.0.1:{port}. --wp=: The version of WordPress to use. Defaults to the latest. --php=: 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[=]: 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=: 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=: 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 : 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=: 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[=]: 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=: 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=: Set up the Xdebug integration on VS Code ( vscode) and PhpStorm ( phpstorm). --workers=: 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=: Deprecated. Use --workers= 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 and Building 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 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, 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: An extension to add PHP profiling support, for example, PHP Debug or another debugger extension to your preference. Have a .vscode/ folder. 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. 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. For a more practical example, let’s debug a plugin that has the following code:

web component. The guide covers custom Blueprints, expected output, pure-PHP snippets, runtime sharing, and the standalone PHP Playground. Run PHP frameworks in Playground 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 . WordPress 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? 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 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 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 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 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 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 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 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 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 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/** → Playground instance https://playground.wordpress.net**/docs** → Playground Docs https://playground.wordpress.net**/builder** → Playground Blueprints Builder https://playground.wordpress.net**/wordpress** → Playground PR viewer for WordPress https://playground.wordpress.net**/gutenberg** → Playground PR viewer for Gutenberg Frequently sought links Demo GitHub Repository Documentation Playground tools Repository Awesome WordPress Playground – a community-curated list of resources, tools, and examples Apps built with WordPress Playground Official demo and the showcase app – install a theme, try out a plugin, create a few pages, export what you’ve built @wp-playground/cli – a CLI tool for instant WordPress dev environment WordPress Playground for VS Code Live Translations: App, announcement, more details Interactive code block which powers the HTML Tag Processor tutorial and the Playground JS API tutorial Gutenberg Pull Request previewer GraphQL REPL Blocknotes – the first ever iOS app running WordPress on your phone Plugin demos on wp.org – a user script that adds a “demo” tab to plugin pages on WordPress.org WordPress Pull Request previewer Synchronization between two Playgrounds Time Travel WP-CLI PHP implementation of Blueprints Reading materials Build in-browser WordPress experiences with WordPress Playground and WebAssembly WordPress Playground on developer.wordpress.org In-Browser WordPress Tech Demos: WordPress Development with WordPress Playground Initial announcement on make.wordpress.org Hackernews discussion Videos Developer Hours Videos: Americas Region (May 23,2023) APAC/EMEA Region (May 24,2023) Creating WordPress Playground Blueprints for Testing and Demos (May 28, 2024) by Birgit Pauli-Haack & Nick Diego Developer Hours: Everything you need to know about WordPress Playground (Dec 17, 2024) by Nick Diego & Ryan Welcher Playground at State of the Word Playground at WCEU 2023 Watch “WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress” by Anne McCarthy How to start using WordPress Playground by Adam Zielinski WordPress Playground, present and future applications by Antonio Sejas WordPress Playground: The Famous « 5 Second » Install by Dave Kellam Testing WP Playground with wp-now by Jonathan Bossenger Translate Live: in-context translation using WordPress Playground by Alex Kirk Testing WP Playground in VS Code by Jonathan Bossenger Submitting an enhancement to the WordPress Playground Block by Jonathan Bossenger WordPress Playground for developers by Berislav Grgicak and Jonathan Bossenger WordPress Playground Block code editor theme support by Jonathan Bossenger WordPress Playground – use WordPress without a server at WCEU 2024 by Adam Zielinski Code, Test, Repeat: Accelerating Development with WordPress Playground at WordCamp Larissa 2024 by Uros Tasic Liberating data with WordPress Playground in a Browser Extension at WordCamp Netherlands 2024 by Alex Kirk Beyond the Playground: WordPress as a Tool and Product Builder at WCUS 2024 by Dennis Snell Create a demo with Playground at WC Asia 2025 by Birgit Pauli-Haack Dissecting WordPress Playground at WordCamp Nepal 2025 by Sakar Upadhyaya Khatiwada Building Automated Test with WordPress Playground at WCEU 2025by Berislav Grgicak From Zero to Demo: Mastering WordPress Playground Blueprints at WCEU 2025 by Birgit Pauli-Haack Playground at WordCamp Gliwice (in Polish) by Adam Zielinski Adam Zielinski: WordPress bez serwera – poznaj WordPress Playground i nowy paradygmat aplikacji (in Polish) by Adam Zielinski WordPress bez instalacji, komputera, i internetu: Poznaj WordPress Playground (in Polish) by Adam Zielinski WordPress Playground at WordCamp Wrocław 2024 (in Polish) by Adam Zielinski WordPress Playground at WordCamp Gdynia 2025 (in Polish) by Magdalena Paciorek Discovering Playground, the demo tool(in Spanish) by Alex Cuadra WordPress Playground: Complete and functional WordPress installation(in Spanish) by Fernando García Rebolledo Playground: A throwaway WordPress within your browser at WordCamp Madrid 2025(in Spanish) by Álvaro Gómez Velasco Use WordPress with just a browser! WordPress Playground Tutorial: Basic usage of Playground (in Japanese) by Shimomura Tomoki WordPress Playground: How to use Blueprints by Shimomura Tomoki Streamlined Block Theme Development: Using WordPress Playground and GitHub for No-Code Version Control of Site Editor Changes by Birgit Pauli-Haack Playground, la mejor herramienta jamás inventada para enseñar WordPress (in Spanish) by Nilo Vélez Testing Faster Than a Red Bull Pit Stop: WordPress Playground and WooCommerce Blueprints by Daniel Dudzic Is WordPress playground only for developers? by Fellyph Cintra How to test the next WordPress release with WordPress Playground by Fellyph cintra Running WordPress directly from the JavaScript code with runCLI by Fellyph Cintra WordPress Playground: The Path to Test Automation by Fellyph Cintra Previewing GitHub branches with WordPress Playground by Fellyph Cintra Introduction to WordPress Playground landing page by Fellyph Cintra Using WordPress Playground to work with AI agents by Fellyph Cintra WordPress Playground Blueprints: De Cero a Pro (in Spanish) by Fellyph Cintra Getting Started with WordPress Blueprints (in Bengali) by Monzur Alam WordPress Playground + AI: Building Autonomous Testing Pipelines by Fellyph Cintra What’s 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 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/. 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 and you can learn more about it here. 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. 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 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. 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. 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 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 and you can learn more about it here. 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. To learn more about preparing content for demos, see the providing content for your demo guide. 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/. You can also use JSON Blueprints. See getting started with Blueprints 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, set up plugin demos, and even use it as a zero-setup local development environment. To learn more about developing with WordPress Playground, check out the development quick start 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. 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. 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: Start a fresh Playground without site-slug. If the fresh site works, the issue is tied to the saved site or imported archive. Export accessible files from the broken saved site using Files, Export, or a local directory copy, if available. 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 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, which persists site files on disk. See the Dock guide 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 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 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 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. 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 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. 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. 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. 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 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. Open Database to use database tools or download the SQLite database. Open Logs to inspect PHP errors, warnings, and notices. Export and share Open Export to download or share the current Playground. 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. 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. On narrow screens, the Dock uses a full-width mobile layout. 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. 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 or your preferred IDE. 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 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 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. 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 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 to create new tools. The @php-wasm/node package, 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) 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 Work offline and as a native app When you first visit playground.wordpress.net, your browser automatically caches all the necessary files to use Playground. From that point on, you can access 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 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? 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’ 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 Get inspiration about the type of interactive demos you can create at the Blueprints Gallery The Blueprints builder 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 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! Embed demos in posts and pages with the WordPress Playground block This WordPress Playground 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 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? 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 to quickly load any plugin or theme published in wordpress.org plugins and themes directories. For example, the following link will load the “pendant” theme and the “gutenberg” plugin on a Playground instance: https://playground.wordpress.net/?theme=pendant&plugin=gutenberg But you can also test more elaborate configurations using blueprints, for example testing a plugin’s code from a gist (see blueprint and live demo) 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. There are some public implementations of this use case such as WordPress Core PR previewer and Gutenberg PR previewer. 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 guide. Clone your site and experiment in a private sandbox. With the Sandbox Site powered by 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, 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 and live demo). You can also load any theme, plugin, or configuration 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 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 tools—start there. Browse the list of open issues to find what to work on. The Good First Issue label is a recommended starting point for first-time contributors. Be sure to review the following resources before you begin: Coding principles Architecture Vision and Philosophy WordPress Playground Roadmap Contribute Pull Requests Fork the Playground repository 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. 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 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 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 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 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 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. 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 Team Badge Request form 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 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 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 Create Your Agenda: Prepare a flexible checklist of key activities while allowing for organic collaboration. Share it in the project documentation if helpful. Guide Contributors to Slack: Direct everyone to the #playground channel on WordPress Slack. This centralizes communication and enables asynchronous collaboration with late arrivals. Post a Welcome Message: Share an initial message in the Slack channel announcing your presence (in-person or online) and welcoming contributions from everyone. Share Essential Links: Post these resources in the #playground channel: WordPress Playground Web Instance Playground Documentation Playground Step Library GitHub Repository Contributor Day guide 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. 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 might translate Playground documentation, or the Core Test team could provide valuable Playground feedback. Collect Feedback: Ask contributors about their experience and note improvement suggestions. Report this in the #playground Slack Channel if possible. After the Event Follow-Up and Support 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. 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. Stay Active on Slack: Continue supporting new contributors through the #playground channel, answering questions and helping them become regular contributors. 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. Report Issues: Submit to the WordPress Playground GitHub repository. For more information on contributing to WordPress Playground, see the Contributor Day guide. 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 and @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 when contributing to WordPress. Please note that the extension and the NPM package are under development, and not all Make WordPress teams are fully supported. Getting Started VS Code Playground extension The Visual Studio Code Playground extension is a friendly zero-setup development environment. Open VS Code and navigate to the Extensions tab (View > Extensions). In the search bar, type WordPress Playground and click Install. To interact with Playground, click the new icon in the Activity Bar and hit the Start WordPress Server button. A new tab will open in your browser within seconds. @wp-playground/cli NPM package @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 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. 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) Fork the Gutenberg repository in your GitHub account. Then, clone the forked repository to download the files. 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. 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 To test other Gutenberg PRs, checkout the branch associated with it. Pull the latest changes to ensure your local copy is up to date. Next, install the necessary dependencies, ensuring your testing environment matches the latest changes. 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 to test Gutenberg PRs—use Playground to do it directly in the browser. Copy the ID of the PR you’d like to test (pick one from the list of open Pull Requests). Open Playground’s Gutenberg PR Previewer and paste the ID you copied. 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. 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. If your focus is the VS Code extension, NPM package, or the plugins, open an issue on the Playground Tools repository. Share your feedback on the #playground Slack channel. Documentation contributions WordPress Playground’s documentation site is maintained by volunteers like you, who’d love your help. All documentation-related issues are labeled [Type] Documentation or [Type] Developer Documentation in the 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 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. Forking the repo, edit files locally and opening Pull Requests If you are familiar with markdown, you can fork 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 repository is the same as contributing to other WordPress repositories such as Gutenberg: Git Workflow The documentation files ( .md files) are stored in Playground’s GitHub repository, under /packages/docs/site/docs for English and /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: Find the page you’d like to edit or the directory of the chapter you’d like to add a new page to. 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. GitHub will ask you to fork the repository and create a new branch with your changes. An editor will open where you can make the changes. 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, tags the release, and publishes all public packages to npm. The CI authenticates with npm using OpenID Connect (OIDC) trusted publishing. 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: Navigate to your package on npmjs.com Go to Settings → Configure Trusted Publishers 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 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 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 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 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 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: Modify or add a translated file in the appropriate language directory, such as packages/docs/site/i18n/es/docusaurus-plugin-content-docs/current/. 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. 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 Playground web instance About Playground Guides Contributing 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: Check for an Existing Translation Issue: First, search the repository issues 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. 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 and use the markdown checklist below to track progress. 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//...). It is crucial to replicate the original file structure. Translate the content of the new file, keeping the original content commented out . 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. 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

Main

- 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

Blueprints

- [ ] 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

Developers

- 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
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 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/. Open the editor. Select the file you wish to edit and click the pencil icon (Edit this file) in the upper right corner. 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. Make your changes. The editor will open in your browser. Update the text with your improved translations. 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. 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 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 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. 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. Fork the repository. Just like before, GitHub will prompt you to Fork this repository. Click the button to create your personal copy. 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. Ceci est le contenu traduit en français. 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. 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. 👋 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. 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. 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 with the WordPress 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. For Blueprint configuration details, see 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>; 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): page.getByRole() — buttons, headings, links, form controls page.getByLabel() — form inputs with associated labels page.getByText() — visible text content page.getByTestId() — elements with data-testid attributes you add to your plugin 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