diff --git a/apps.json b/apps.json index fa21a7b8..c7bcdd4f 100644 --- a/apps.json +++ b/apps.json @@ -44,6 +44,16 @@ "Productivity" ] }, + "apps/personal-rag.json": { + "title": "Personal RAG", + "description": "Private local search and answers over your WordPress content using Ollama, EmbeddingGemma, and Gemma 4", + "author": "Playground", + "categories": [ + "Apps", + "AI", + "Productivity" + ] + }, "apps/rss-reader.json": { "title": "RSS Reader", "description": "Follow friends and consume their content in your WordPress", @@ -53,4 +63,4 @@ "Social" ] } -} \ No newline at end of file +} diff --git a/apps/personal-rag.json b/apps/personal-rag.json new file mode 100644 index 00000000..7a1408d0 --- /dev/null +++ b/apps/personal-rag.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "meta": { + "title": "Personal RAG", + "description": "Private local search and answers over your WordPress content using Ollama, EmbeddingGemma, and Gemma 4", + "author": "Playground", + "categories": [ + "Apps", + "AI", + "Productivity" + ] + }, + "landingPage": "/wp-admin/tools.php?page=personal-rag", + "steps": [ + { + "step": "installPlugin", + "pluginData": { + "resource": "git:directory", + "url": "https://github.com/WordPress/blueprints", + "ref": "trunk", + "refType": "branch", + "path": "blueprints/personal-rag/plugin" + }, + "options": { + "activate": true, + "targetFolderName": "personal-rag" + } + }, + { + "step": "importWxr", + "file": { + "resource": "url", + "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/personal-rag/content.xml" + } + } + ] +} diff --git a/blueprints/my-wordpress/plugin/de/welcome-post.html b/blueprints/my-wordpress/plugin/de/welcome-post.html index 5bf0158b..a8b6ec7c 100644 --- a/blueprints/my-wordpress/plugin/de/welcome-post.html +++ b/blueprints/my-wordpress/plugin/de/welcome-post.html @@ -1,34 +1,31 @@ - -
👋
+ + + +Dein eigener WordPress — fühl dich wie zuhause.
+ +🎨
+ +🎨
- -- Spiel damit - Probier alles aus und mach es zu deinem -
+ +Probier alles aus und mach es zu deinem.
💾
+ +💾
- -- Bleibt erhalten - Änderungen sind morgen noch da -
+ +Deine Änderungen werden gespeichert und sind morgen noch da.
🔒
+ +🔒
- -- Ganz privat - Läuft im Browser, ohne Konto -
+ +Läuft in deinem Browser, ohne Konto.
- Schau dir das Apps-Menü an — Suche dieses Symbol in der oberen Leiste, um Apps zu installieren, Backups zu verwalten und mehr. -
- -Schau dir das Apps-Menü an
Finde dieses Symbol in der oberen Leiste, um Apps zu installieren, Backups zu verwalten und mehr.
- ⭐ Setze ein Lesezeichen — Das ist jetzt dein - WordPress. Füge es zu deinen Lesezeichen hinzu, damit du leicht - zurückkommen kannst. -
- + +⭐Setze ein Lesezeichen
Das ist jetzt dein WordPress. Füge es zu deinen Lesezeichen hinzu, damit du leicht zurückkommen kannst.
- Suche nach dem Raster-Symbol () in der oberen Leiste. Von dort aus kannst du: -
+Suche nach dem Raster-Symbol () in der oberen Leiste. Von dort aus kannst du:
Viel Spaß mit deinem WordPress!
+ +Viel Spaß mit deinem WordPress!
diff --git a/blueprints/my-wordpress/plugin/es/welcome-post.html b/blueprints/my-wordpress/plugin/es/welcome-post.html index 15e61425..6921129e 100644 --- a/blueprints/my-wordpress/plugin/es/welcome-post.html +++ b/blueprints/my-wordpress/plugin/es/welcome-post.html @@ -1,34 +1,31 @@ - -👋
+ + + +Un WordPress tuyo — siéntete como en casa.
+ +🎨
+ +🎨
- -
- Experimenta sin miedo
Cambia lo que quieras, prueba
- cosas nuevas, hazlo tuyo
-
Cambia lo que quieras, prueba cosas nuevas, hazlo tuyo.
💾
+ +💾
- -
- Permanece
Tus cambios se guardan y
- estarán aquí mañana
-
Tus cambios se guardan y estarán aquí mañana.
🔒
+ +🔒
- -
- Es privado
Se ejecuta en tu navegador, sin
- necesidad de crear una cuenta
-
Se ejecuta en tu navegador, sin crear una cuenta.
- Échale un vistazo al menú de aplicaciones — Busca este icono en la barra superior para instalar aplicaciones, gestionar copias de seguridad y mucho más. -
- -Échale un vistazo al menú de aplicaciones
Busca este icono en la barra superior para instalar aplicaciones, gestionar copias de seguridad y mucho más.
- ⭐ Añade esta página a marcadores — Este es ahora tu WordPress. Añádelo - a tus favoritos para poder volver fácilmente. -
- + +⭐Añade esta página a marcadores
Este es ahora tu WordPress. Añádelo a tus favoritos para poder volver fácilmente.
- Busca el icono de la cuadrícula () en la barra superior. Desde aquí puedes: -
+Busca el icono de la cuadrícula () en la barra superior. Desde aquí puedes:
¡Disfruta de tu WordPress!
+ +¡Disfruta de tu WordPress!
diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.mo b/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.mo index b8cefa3a..1dcbe3d9 100644 Binary files a/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.mo and b/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.mo differ diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.po b/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.po index fda19e96..86027633 100644 --- a/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.po +++ b/blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.po @@ -28,7 +28,7 @@ msgid "👋 Welcome to Your WordPress" msgstr "👋 Willkommen bei deinem WordPress" #: playground-welcome.php -msgid "Welcome to Your WordPress" +msgid "Welcome to your WordPress" msgstr "Willkommen bei deinem WordPress" #: playground-welcome.php @@ -83,6 +83,10 @@ msgstr "Importiere..." msgid "Not now" msgstr "Nicht jetzt" +#: playground-welcome.php +msgid "An error occurred. Please try again." +msgstr "Ein Fehler ist aufgetreten. Bitte versuche es erneut." + #: playground-welcome.php msgid "Security check failed." msgstr "Sicherheitsprüfung fehlgeschlagen." diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.mo b/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.mo index e7ff9f4e..9f86a178 100644 Binary files a/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.mo and b/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.mo differ diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.po b/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.po index a6ad8e33..1111e677 100644 --- a/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.po +++ b/blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.po @@ -24,7 +24,7 @@ msgid "👋 Welcome to Your WordPress" msgstr "👋 Te damos la bienvenida a tu WordPress" #: playground-welcome.php -msgid "Welcome to Your WordPress" +msgid "Welcome to your WordPress" msgstr "Este es tu WordPress" #: playground-welcome.php @@ -85,6 +85,10 @@ msgstr "Importando..." msgid "Not now" msgstr "Ahora no" +#: playground-welcome.php +msgid "An error occurred. Please try again." +msgstr "Se ha producido un error. Inténtalo de nuevo." + #: playground-welcome.php msgid "Security check failed." msgstr "La comprobación de seguridad ha fallado." diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.mo b/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.mo index e1e144e0..994b1d8d 100644 Binary files a/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.mo and b/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.mo differ diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.po b/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.po index 8af37a89..fb18c223 100644 --- a/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.po +++ b/blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.po @@ -28,7 +28,7 @@ msgid "👋 Welcome to Your WordPress" msgstr "👋 Witaj w swoim WordPressie" #: playground-welcome.php -msgid "Welcome to Your WordPress" +msgid "Welcome to your WordPress" msgstr "Witaj w swoim WordPressie" #: playground-welcome.php @@ -83,6 +83,10 @@ msgstr "Importowanie..." msgid "Not now" msgstr "Nie teraz" +#: playground-welcome.php +msgid "An error occurred. Please try again." +msgstr "Wystąpił błąd. Spróbuj ponownie." + #: playground-welcome.php msgid "Security check failed." msgstr "Weryfikacja bezpieczeństwa nie powiodła się." diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.mo b/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.mo index 77977625..ecdfbbbc 100644 Binary files a/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.mo and b/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.mo differ diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.po b/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.po index 87a38338..f6676926 100644 --- a/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.po +++ b/blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.po @@ -28,7 +28,7 @@ msgid "👋 Welcome to Your WordPress" msgstr "👋 Bem-vindo ao seu WordPress" #: playground-welcome.php -msgid "Welcome to Your WordPress" +msgid "Welcome to your WordPress" msgstr "Bem-vindo ao seu WordPress" #: playground-welcome.php @@ -83,6 +83,10 @@ msgstr "Importando..." msgid "Not now" msgstr "Agora não" +#: playground-welcome.php +msgid "An error occurred. Please try again." +msgstr "Ocorreu um erro. Tente novamente." + #: playground-welcome.php msgid "Security check failed." msgstr "A verificação de segurança falhou." diff --git a/blueprints/my-wordpress/plugin/languages/playground-welcome.pot b/blueprints/my-wordpress/plugin/languages/playground-welcome.pot index 897032ae..276617e9 100644 --- a/blueprints/my-wordpress/plugin/languages/playground-welcome.pot +++ b/blueprints/my-wordpress/plugin/languages/playground-welcome.pot @@ -21,7 +21,7 @@ msgid "👋 Welcome to Your WordPress" msgstr "" #: playground-welcome.php -msgid "Welcome to Your WordPress" +msgid "Welcome to your WordPress" msgstr "" #: playground-welcome.php @@ -76,6 +76,10 @@ msgstr "" msgid "Not now" msgstr "" +#: playground-welcome.php +msgid "An error occurred. Please try again." +msgstr "" + #: playground-welcome.php msgid "Security check failed." msgstr "" diff --git a/blueprints/my-wordpress/plugin/pl/welcome-post.html b/blueprints/my-wordpress/plugin/pl/welcome-post.html index 15cf7768..094a28fd 100644 --- a/blueprints/my-wordpress/plugin/pl/welcome-post.html +++ b/blueprints/my-wordpress/plugin/pl/welcome-post.html @@ -1,34 +1,31 @@ - -👋
+ + + +Twój własny WordPress — poczuj się jak u siebie.
+ +🎨
+ +🎨
- -- Eksperymentuj - Zmieniaj wszystko, próbuj, twórz po swojemu -
+ +Zmieniaj wszystko, próbuj, twórz po swojemu.
💾
+ +💾
- -- Zostaje zapisane - Zmiany będą tu jutro -
+ +Twoje zmiany są zapisywane i będą tu jutro.
🔒
+ +🔒
- -- Prywatność - Działa w przeglądarce, bez konta -
+ +Działa w przeglądarce, bez konta.
- Sprawdź menu Aplikacje — Szukaj tej ikony na górnym pasku, aby instalować aplikacje, zarządzać kopiami zapasowymi i więcej. -
- -Sprawdź menu Aplikacje
Szukaj tej ikony na górnym pasku, aby instalować aplikacje, zarządzać kopiami zapasowymi i więcej.
- ⭐ Dodaj do zakładek — To teraz Twój WordPress. Dodaj tę - stronę do zakładek, żeby łatwo tu wrócić. -
- + +⭐Dodaj do zakładek
To teraz Twój WordPress. Dodaj tę stronę do zakładek, żeby łatwo tu wrócić.
- Szukaj ikony siatki () na górnym pasku. Stamtąd możesz: -
+Szukaj ikony siatki () na górnym pasku. Stamtąd możesz:
Miłej zabawy z WordPressem!
+ +Miłej zabawy z WordPressem!
diff --git a/blueprints/my-wordpress/plugin/playground-welcome.css b/blueprints/my-wordpress/plugin/playground-welcome.css index dd8372bc..73919389 100644 --- a/blueprints/my-wordpress/plugin/playground-welcome.css +++ b/blueprints/my-wordpress/plugin/playground-welcome.css @@ -9,178 +9,52 @@ display: none !important; } -.playground-welcome-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 135deg, - #1e1e1e 0%, - #2f2f2f 100% - ); - display: flex; - align-items: flex-start; - justify-content: center; - z-index: 999999; - padding: 20px; - box-sizing: border-box; - overflow-y: auto; +body.tools_page_playground-welcome { + background: #f0f0f0; } -.playground-welcome-dialog { - background: var(--wp-components-color-background, #fff); - border-radius: 2px; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - padding: 40px 48px; - max-width: 520px; - width: 100%; - animation: slideUp 0.4s ease-out; - margin: auto; +body.tools_page_playground-welcome #wpbody-content { + padding-bottom: 0; } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } +#playground-welcome-root { + min-height: 100vh; } -.playground-welcome-dialog header { - display: flex; - align-items: center; - gap: 8px; - margin: 0 0 8px 0; +.playground-welcome-modal .components-modal__frame { + width: calc(100% - 32px); + max-width: 520px; } -.playground-welcome-dialog header .dashicons { - font-size: 28px; - width: 28px; - height: 28px; - color: var(--wp-admin-theme-color, #3858e9); +.playground-welcome-form, +.playground-welcome-import-fields { + display: grid; + gap: 24px; } -.playground-welcome-dialog .playground-welcome-dialog-title { +.playground-welcome-intro { margin: 0; - font-size: 28px; - font-weight: 600; - color: var(--wp-components-color-foreground, #1e1e1e); - text-wrap: balance; - line-height: 1.3; -} - -.playground-welcome-dialog .intro { - margin: 0 0 32px 0; - color: var(--wp-components-color-gray-700, #757575); - font-size: 16px; + color: #757575; line-height: 1.5; } -.field-group { - margin-bottom: 24px; -} - -.field-group label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--wp-components-color-foreground, #1e1e1e); - font-size: 14px; +.playground-welcome-import-panel { + border: 1px solid #ddd; } -/* Ensure inputs fill the form width */ -.field-group .components-text-control__input, -.field-group .components-select-control__input { - width: 100%; - box-sizing: border-box; -} - -.playground-welcome-overlay .button-group { +.playground-welcome-actions { display: flex; justify-content: flex-end; - gap: 16px; - margin-top: 32px; -} - -.playground-welcome-overlay .components-button { - padding: 8px 20px; - font-size: 16px; -} - -.playground-welcome-overlay .components-notice { - margin-top: 16px; -} - -.import-details { - margin-bottom: 24px; - border: 1px solid var(--wp-components-color-gray-300, #ddd); - border-radius: var(--wp-components-border-radius, 2px); - overflow: hidden; -} - -.import-details summary { - padding: 14px 16px; - font-weight: 500; - color: var(--wp-components-color-foreground, #1e1e1e); - font-size: 14px; - cursor: pointer; - background: var(--wp-components-color-gray-100, #f0f0f0); - transition: background 0.2s; - list-style: none; -} - -.import-details summary::-webkit-details-marker { - display: none; -} - -.import-details summary::before { - content: '+'; - display: inline-block; - width: 20px; - font-weight: 600; - color: var(--wp-components-color-gray-700, #757575); -} - -.import-details[open] summary::before { - content: '\2212'; -} - -.import-details summary:hover { - background: var(--wp-components-color-gray-200, #e0e0e0); -} - -.import-details[open] summary { - border-bottom: 1px solid var(--wp-components-color-gray-300, #ddd); -} - -.import-details .field-group { - padding: 16px; - margin-bottom: 0; -} - -.import-details .field-group:first-of-type { - padding-top: 20px; -} - -.import-details .field-group:last-of-type { - padding-bottom: 20px; + gap: 12px; } @media (max-width: 600px) { - .playground-welcome-dialog { - padding: 24px; - } - - .playground-welcome-dialog .playground-welcome-dialog-title { - font-size: 22px; + .playground-welcome-actions { + flex-direction: column-reverse; } - .playground-welcome-overlay .button-group { - flex-direction: column; + .playground-welcome-actions .components-button { + justify-content: center; + width: 100%; } } diff --git a/blueprints/my-wordpress/plugin/playground-welcome.js b/blueprints/my-wordpress/plugin/playground-welcome.js new file mode 100644 index 00000000..81324469 --- /dev/null +++ b/blueprints/my-wordpress/plugin/playground-welcome.js @@ -0,0 +1,171 @@ +( function ( wp, settings ) { + if ( ! wp || ! wp.components || ! wp.element || ! wp.domReady || ! settings ) { + return; + } + + const { Button, Modal, Notice, PanelBody, SelectControl, TextControl } = wp.components; + const { createElement: el, render, useState } = wp.element; + const { strings } = settings; + + function PlaygroundWelcomeModal() { + const [ displayName, setDisplayName ] = useState( '' ); + const [ feedUrl, setFeedUrl ] = useState( '' ); + const [ maxItems, setMaxItems ] = useState( '10' ); + const [ notice, setNotice ] = useState( null ); + const [ isSubmitting, setIsSubmitting ] = useState( false ); + + function goHome() { + window.location.href = settings.homeUrl; + } + + function submitForm( event ) { + event.preventDefault(); + setIsSubmitting( true ); + setNotice( null ); + + const formData = new window.FormData(); + formData.append( 'action', 'playground_welcome_save' ); + formData.append( 'nonce', settings.nonce ); + formData.append( 'display_name', displayName ); + formData.append( 'feed_url', feedUrl ); + formData.append( 'max_items', maxItems ); + + window + .fetch( settings.ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: formData, + } ) + .then( ( response ) => response.json() ) + .then( ( response ) => { + if ( response.success ) { + setNotice( { + status: 'success', + message: response.data.message, + } ); + window.setTimeout( goHome, 1500 ); + return; + } + + setNotice( { + status: 'error', + message: response.data.message || strings.errorMessage, + } ); + setIsSubmitting( false ); + } ) + .catch( () => { + setNotice( { + status: 'error', + message: strings.errorMessage, + } ); + setIsSubmitting( false ); + } ); + } + + return el( + Modal, + { + title: strings.title, + className: 'playground-welcome-modal', + isDismissible: false, + shouldCloseOnClickOutside: false, + shouldCloseOnEsc: false, + onRequestClose: goHome, + }, + el( + 'form', + { + className: 'playground-welcome-form', + onSubmit: submitForm, + }, + el( 'p', { className: 'playground-welcome-intro' }, strings.intro ), + el( TextControl, { + label: strings.displayNameLabel, + value: displayName, + onChange: setDisplayName, + autoFocus: true, + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + } ), + el( + PanelBody, + { + title: strings.importTitle, + initialOpen: false, + className: 'playground-welcome-import-panel', + }, + el( + 'div', + { className: 'playground-welcome-import-fields' }, + el( TextControl, { + label: strings.feedUrlLabel, + help: strings.feedUrlHelp, + placeholder: 'example.com', + value: feedUrl, + onChange: ( value ) => { + setFeedUrl( value ); + if ( notice && notice.status === 'error' ) { + setNotice( null ); + } + }, + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + } ), + el( SelectControl, { + label: strings.maxItemsLabel, + value: maxItems, + options: [ + { label: strings.fivePosts, value: '5' }, + { label: strings.tenPosts, value: '10' }, + { label: strings.twentyPosts, value: '20' }, + { label: strings.fiftyPosts, value: '50' }, + ], + onChange: setMaxItems, + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + } ) + ) + ), + notice && + el( + Notice, + { + status: notice.status, + isDismissible: false, + }, + notice.message + ), + el( + 'div', + { className: 'playground-welcome-actions' }, + el( + Button, + { + variant: 'secondary', + href: settings.homeUrl, + disabled: isSubmitting, + }, + strings.notNow + ), + el( + Button, + { + variant: 'primary', + type: 'submit', + isBusy: isSubmitting, + disabled: isSubmitting, + }, + isSubmitting ? strings.importing : strings.continue + ) + ) + ) + ); + } + + wp.domReady( function () { + const root = document.getElementById( 'playground-welcome-root' ); + if ( root ) { + render( el( PlaygroundWelcomeModal ), root ); + } + } ); +} )( window.wp, window.playgroundWelcomeSettings ); diff --git a/blueprints/my-wordpress/plugin/playground-welcome.php b/blueprints/my-wordpress/plugin/playground-welcome.php index 97abcb89..e97e2f1a 100644 --- a/blueprints/my-wordpress/plugin/playground-welcome.php +++ b/blueprints/my-wordpress/plugin/playground-welcome.php @@ -41,7 +41,7 @@ private function maybe_setup_content() { if ($post) { wp_update_post([ 'ID' => $post->ID, - 'post_title' => __('Welcome to Your WordPress', 'playground-welcome'), + 'post_title' => __('Welcome to your WordPress', 'playground-welcome'), 'post_content' => self::get_welcome_post_content(), 'post_name' => 'welcome-to-your-wordpress', ]); @@ -108,12 +108,14 @@ public static function allow_svg_tags($tags) { 'xmlns' => true, 'width' => true, 'height' => true, + 'fill' => true, 'aria-hidden' => true, 'focusable' => true, 'style' => true, ]; $tags['path'] = [ 'd' => true, + 'fill' => true, 'fill-rule' => true, 'clip-rule' => true, ]; @@ -142,133 +144,46 @@ public function enqueue_styles($hook) { ['wp-components'], '1.0.0' ); + + wp_enqueue_script( + 'playground-welcome', + plugin_dir_url(__FILE__) . 'playground-welcome.js', + ['wp-components', 'wp-dom-ready', 'wp-element'], + '1.0.0', + true + ); + + wp_localize_script( + 'playground-welcome', + 'playgroundWelcomeSettings', + [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'homeUrl' => home_url('/'), + 'nonce' => wp_create_nonce('playground_welcome_nonce'), + 'strings' => [ + 'title' => __('Welcome to your WordPress', 'playground-welcome'), + 'intro' => __("This is a private WordPress that's free and needs no account. It's stored in your browser and will be here when you come back.", 'playground-welcome'), + 'displayNameLabel' => __("What's your name?", 'playground-welcome'), + 'importTitle' => __('Import content from a website', 'playground-welcome'), + 'feedUrlLabel' => __('Website URL', 'playground-welcome'), + 'feedUrlHelp' => __("Enter a site URL and we'll find and import its RSS feed.", 'playground-welcome'), + 'maxItemsLabel' => __('Maximum posts to import', 'playground-welcome'), + 'fivePosts' => __('5 posts', 'playground-welcome'), + 'tenPosts' => __('10 posts', 'playground-welcome'), + 'twentyPosts' => __('20 posts', 'playground-welcome'), + 'fiftyPosts' => __('50 posts', 'playground-welcome'), + 'continue' => __('Continue', 'playground-welcome'), + 'importing' => __('Importing...', 'playground-welcome'), + 'notNow' => __('Not now', 'playground-welcome'), + 'errorMessage' => __('An error occurred. Please try again.', 'playground-welcome'), + ], + ] + ); } public function render_page() { - $current_user = wp_get_current_user(); ?> - - - + -👋
+ + + +A WordPress of your own — make yourself at home.
+ +🎨
+ +🎨
- -
- Experiment freely
Change anything, try
- things out, make it yours
-
Change anything, try things out, make it yours.
💾
+ +💾
- -
- It stays
Your changes are saved and will
- be here tomorrow
-
Your changes are saved and will be here tomorrow.
🔒
+ +🔒
- -
- It's private
Runs in your browser, no
- account needed
-
Runs in your browser, no account needed.
- Check out the Apps menu — Look for this icon in the top bar to install apps, manage backups, and more. -
- -Check out the Apps menu
Find this icon in the top bar to install apps, manage backups, and more.
- ⭐ Bookmark this page — This is your WordPress now. Add - it to your bookmarks so you can easily come back. -
- + +⭐Bookmark this page
This is your WordPress. Add it to your bookmarks so you can come back easily.
- Look for the grid icon () in the top bar. From there you can: -
+Look for the grid icon () in the top bar. From there you can:
Enjoy your WordPress!
+ +Enjoy your WordPress!
diff --git a/blueprints/personal-rag/blueprint.json b/blueprints/personal-rag/blueprint.json new file mode 100644 index 00000000..24b805ce --- /dev/null +++ b/blueprints/personal-rag/blueprint.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "meta": { + "title": "Personal RAG", + "description": "A private local RAG app for WordPress Playground using Ollama, EmbeddingGemma, Gemma 4, and SQLite-backed WordPress data.", + "author": "Playground", + "categories": [ + "Apps", + "AI" + ] + }, + "login": true, + "landingPage": "/wp-admin/tools.php?page=personal-rag", + "steps": [ + { + "step": "mkdir", + "path": "/wordpress/wp-content/plugins/personal-rag" + }, + { + "step": "mkdir", + "path": "/wordpress/wp-content/plugins/personal-rag/assets" + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/personal-rag/personal-rag.php", + "data": { + "resource": "bundled", + "path": "./plugin/personal-rag.php" + } + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/personal-rag/uninstall.php", + "data": { + "resource": "bundled", + "path": "./plugin/uninstall.php" + } + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/personal-rag/assets/personal-rag.js", + "data": { + "resource": "bundled", + "path": "./plugin/assets/personal-rag.js" + } + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/personal-rag/assets/personal-rag.css", + "data": { + "resource": "bundled", + "path": "./plugin/assets/personal-rag.css" + } + }, + { + "step": "activatePlugin", + "pluginPath": "personal-rag/personal-rag.php" + }, + { + "step": "importWxr", + "file": { + "resource": "bundled", + "path": "./content.xml" + } + } + ] +} diff --git a/blueprints/personal-rag/content.xml b/blueprints/personal-rag/content.xml new file mode 100644 index 00000000..b96f6df6 --- /dev/null +++ b/blueprints/personal-rag/content.xml @@ -0,0 +1,10557 @@ + +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:
+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.
+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.
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:
+<BlueprintExample justButton={true} blueprint={{
+ "preferredVersions": {
+ "php": "8.3",
+ "wp": "latest"
+ },
+ "steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "pendant"
+ },
+ "options": {
+ "activate": true
+ }
+ }
+ ]
+}} />
+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.
+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.
+Original Playground docs source: https://playground.wordpress.net/blueprints/getting-started
]]>You can use Blueprints in one of the following ways:
+blueprint-url parameter.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"}}.
+Tip
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"}}
+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:
+<BlueprintExample justButton={true} blueprint={{
+ "preferredVersions": {
+ "php": "8.3",
+ "wp": "6.5"
+ }
+}} />
+Some tools, including GitHub, might not format the Blueprint correctly when pasted into the URL. In such cases, encode your Blueprint in Base64 and append it to the URL. For example, that's the above Blueprint in Base64 format: eyIkc2NoZW1hIjogImh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L2JsdWVwcmludC1zY2hlbWEuanNvbiIsInByZWZlcnJlZFZlcnNpb25zIjogeyJwaHAiOiAiNy40Iiwid3AiOiAiNi41In19.
To run it, go to https://playground.wordpress.net/#eyIkc2NoZW1hIjogImh0dHBzOi8vcGxheWdyb3VuZC53b3JkcHJlc3MubmV0L2JsdWVwcmludC1zY2hlbWEuanNvbiIsInByZWZlcnJlZFZlcnNpb25zIjogeyJwaHAiOiAiNy40Iiwid3AiOiAiNi41In19
++Tip
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
+When your Blueprint gets too wieldy, you can load it via the ?blueprint-url query parameter in the URL, like this:
Note that the Blueprint must be publicly accessible and served with the correct Access-Control-Allow-Origin header:
Access-Control-Allow-Origin: *
+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.
+You can also use Blueprints with the JavaScript API using the startPlaygroundWeb() function from the @wp-playground/client package. Here's a small, self-contained example you can run on JSFiddle or CodePen:
<iframe id="wp-playground" style="width: 1200px; height: 800px"></iframe>
+<script type="module">
+ import { startPlaygroundWeb } from 'https://playground.wordpress.net/client/index.js';
+
+ const client = await startPlaygroundWeb({
+ iframe: document.getElementById('wp-playground'),
+ remoteUrl: `https://playground.wordpress.net/remote.html`,
+ blueprint: {
+ landingPage: '/wp-admin/',
+ preferredVersions: {
+ php: '8.3',
+ wp: 'latest',
+ },
+ steps: [
+ {
+ step: 'login',
+ username: 'admin',
+ password: 'password',
+ },
+ {
+ step: 'installPlugin',
+ pluginData: {
+ resource: 'wordpress.org/plugins',
+ slug: 'friends',
+ },
+ },
+ ],
+ },
+ });
+
+ const response = await client.run({
+ // wp-load.php is only required if you want to interact with WordPress.
+ code: '<?php require_once "/wordpress/wp-load.php"; $posts = get_posts(); echo "Post Title: " . $posts[0]->post_title;',
+ });
+ console.log(response.text);
+</script>
+Original Playground docs source: https://playground.wordpress.net/blueprints/using-blueprints
]]>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:
+<BlueprintExample blueprint={{
+ "landingPage": "/wp-admin/",
+ "preferredVersions": {
+ "php": "8.3",
+ "wp": "6.5"
+ },
+ "features": {
+ "networking": true
+ },
+ "steps": [
+ {
+ "step": "login",
+ "username": "admin",
+ "password": "password"
+ }
+ ]
+}} />
+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",
+}
+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",
+}
+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, or latest. Minor versions like 7.4.1 are not supported.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"
+ },
+}
+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
+ },
+}
+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" ],
+}
+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"
+ }
+ },
+ ]
+}
+Original Playground docs source: https://playground.wordpress.net/blueprints/data-format
]]>"Resource References" allow you use external files in Blueprints
+Blueprints 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:
+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 resource url type works really in combination 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 or a CI artifact direct-download URL.
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.
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:
+ref, you must specify refType (e.g. "refType": "branch"). Without it, only HEAD is reliably resolved..git suffix. Extra trailing slashes are ignored.installPlugin and installTheme.".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.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.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"
+}
+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"
+}
+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"
+}
+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!"
+}
+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:
+blueprint.json fileblueprint.json file and related resourcesFor more information on Blueprint bundles, see the Blueprint Bundles documentation.
+Original Playground docs source: https://playground.wordpress.net/blueprints/steps/resources
]]>You can specify some steps using a shorthand syntax. The following steps are currently supported:
loginUse
+ "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"
+ }
+ }
+]
+siteOptionsUse
+ "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?
+shorthands when brevity is your main concern.steps when you need more control over the execution order.Original Playground docs source: https://playground.wordpress.net/blueprints/steps/shorthands
]]>The steps property of a Blueprint is an array of steps to run. For example this Blueprint logs the user in as an admin:
<BlueprintExample blueprint={{
+ "steps": [
+ {
+ "step": "login",
+ "username": "admin",
+ "password": "password"
+ }
+ ]
+}} />
+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:
++Tip
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!
+---
+Activates a WordPress plugin (if it's installed).
+Parameters:
+pluginName (string optional): 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:
+
+{
+ "step": "activatePlugin",
+ "pluginName": "Gutenberg",
+ "pluginPath": "/wordpress/wp-content/plugins/gutenberg"
+}
+
+Activates a WordPress theme (if it's installed).
+Parameters:
+themeFolderName (string): The name of the theme folder inside wp-content/themes/Example:
+
+{
+ "step": "activateTheme",
+ "themeFolderName": "storefront"
+}
+
+Copies a file from one path to another.
+Parameters:
+fromPath (string): Source pathtoPath (string): Target pathExample:
+
+{
+ "step": "cp",
+ "fromPath": "/wordpress/index.php",
+ "toPath": "/wordpress/index2.php"
+}
+
+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 URLDefines 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 definemethod ("rewrite-wp-config" | "define-before-run" optional): 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 optional)Example:
+
+{
+ "step": "defineWpConfigConsts",
+ "consts": {
+ "WP_DEBUG": true
+ }
+}
+
+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 optional): wp-cli.phar pathExample:
+
+{
+ "step": "enableMultisite"
+}
+
+Imports a theme Starter Content into WordPress.
+Parameters:
+themeSlug (string optional): The name of the theme to import content from.Example:
+
+{
+ "step": "importThemeStarterContent"
+}
+
+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. Any files that Playground recognizes as "excluded from the export" will carry over from the existing document root into the imported directories. For example, the sqlite-database-integration plugin.
Parameters:
+pathInZip (string optional): 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:
+
+{
+ "step": "importWordPressFiles",
+ "wordPressFilesZip": {
+ "resource": "url",
+ "url": "https://mysite.com/import.zip"
+ }
+}
+
+Imports a WXR file into WordPress.
+Parameters:
+file (ResourceType): The file to importimporter ("default" | "data-liberation" optional): The importer to use. Possible values: - default: The importer from https://github.com/humanmade/WordPress-Importer - data-liberation: The experimental Data Liberation WXR importer developed at https://github.com/WordPress/wordpress-playground/issues/1894 This option is deprecated. The syntax will not be removed, but once the Data Liberation importer matures, it will become the only supported importer and the importer option will be ignored.Example:
+
+{
+ "step": "importWxr",
+ "file": {
+ "resource": "url",
+ "url": "https://your-site.com/starter-content.wxr"
+ }
+}
+
+Installs a WordPress plugin in the Playground.
+Parameters:
+ifAlreadyInstalled ("error" | "overwrite" | "skip" optional): What to do if the asset already exists.options (InstallPluginOptions optional): Optional installation options.pluginData (FileResource | DirectoryResource): 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 optional): @deprecated. Use 'pluginData' instead.Example:
+
+{
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "gutenberg"
+ },
+ "options": {
+ "activate": true
+ }
+}
+
+Installs a WordPress theme in the Playground.
+Parameters:
+ifAlreadyInstalled ("error" | "overwrite" | "skip" optional): What to do if the asset already exists.options (InstallThemeOptions optional): Optional installation options.themeData (FileResource | DirectoryResource): 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 optional): @deprecated. Use 'themeData' instead.Example:
+
+{
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "pendant"
+ },
+ "options": {
+ "activate": true,
+ "importStarterContent": true
+ }
+}
+
+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 optional)username (string optional): The user to log in as. Defaults to 'admin'.Example:
+
+{
+ "step": "login",
+ "username": "admin"
+}
+
+Creates a directory at the specified path.
+Parameters:
+path (string): The path of the directory you want to createExample:
+
+{
+ "step": "mkdir",
+ "path": "/wordpress/my-new-folder"
+}
+
+Moves a file or directory from one path to another.
+Parameters:
+fromPath (string): Source pathtoPath (string): Target pathExample:
+
+{
+ "step": "mv",
+ "fromPath": "/wordpress/index.php",
+ "toPath": "/wordpress/index2.php"
+}
+
+Deletes WordPress posts and comments and sets the auto increment sequence for the posts and comments tables to 0.
+Example:
+
+{
+ "step": "resetData"
+}
+
+Removes a directory at the specified path.
+Parameters:
+path (string): The path to removeExample:
+
+{
+ "step": "rmdir",
+ "path": "/wordpress/wp-admin"
+}
+
+Removes a file at the specified path.
+Parameters:
+path (string): The path to removeExample:
+
+{
+ "step": "rm",
+ "path": "/wordpress/index.php"
+}
+
+Runs PHP code. When running WordPress functions, the code key must first load wp-load.php and start with "<?php require_once '/wordpress/wp-load.php'; ".
Parameters:
+code (string | object): The PHP code to run.Example:
+
+{
+ "step": "runPHP",
+ "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>"
+}
+
+Runs PHP code. When running WordPress functions, the code key must first load wp-load.php and start with "<?php require_once '/wordpress/wp-load.php'; ".
Parameters:
+options (PHPRunOptions): Run options (See /wordpress-playground/api/universal/interface/PHPRunOptions/))Example:
+
+{
+ "step": "runPHPWithOptions",
+ "options": {
+ "code": "<?php require_once '/wordpress/wp-load.php'; update_option('blogname', file_get_contents('php://input'));?>",
+ "body": "Site Name Modified by runPHPWithOptions"
+ }
+}
+
+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:
+
+{
+ "step": "runSql",
+ "sql": {
+ "resource": "literal",
+ "name": "schema.sql",
+ "contents": "DELETE FROM wp_posts"
+ }
+}
+
+Sets the site language and download translations.
+Parameters:
+language (string): The language to set, e.g. 'en_US'Example:
+
+{
+ "step": "setSiteLanguage",
+ "language": "en_US"
+}
+
+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:
+
+{
+ "step": "setSiteOptions",
+ "options": {
+ "blogname": "My Blog",
+ "blogdescription": "A great blog"
+ }
+}
+
+Unzip a zip file.
+Parameters:
+extractToPath (string): The path to extract the zip file tozipFile (ResourceType optional): The zip file to extractzipPath (string optional): The path of the zip file to extractExample:
+
+{
+ "step": "unzip",
+ "zipFile": {
+ "resource": "vfs",
+ "path": "/wordpress/data.zip"
+ },
+ "extractToPath": "/wordpress"
+}
+
+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 IDExample:
+
+{
+ "step": "updateUserMeta",
+ "meta": {
+ "first_name": "John",
+ "last_name": "Doe"
+ },
+ "userId": 1
+}
+
+Runs PHP code using WP-CLI.
+Parameters:
+command (string | string[]): The WP CLI command to run.wpCliPath (string optional): wp-cli.phar pathExample:
+
+{
+ "step": "wp-cli",
+ "command": "wp post create --post_title='Test post' --post_excerpt='Some content'"
+}
+
+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 toExample:
+
+{
+ "step": "writeFiles",
+ "writeToPath": "/wordpress/wp-content/plugins/my-plugin",
+ "filesTree": {
+ "name": "my-plugin",
+ "files": {
+ "index.php": "<?php echo '<a>Hello World!</a>'; ?>",
+ "public": {
+ "style.css": "a { color: red; }"
+ }
+ }
+ }
+}
+
+Writes data to a file at the specified path.
+Parameters:
+data (string | Uint8Array | FileResource): The data to writepath (string): The path of the file to write toExample:
+
+{
+ "step": "writeFile",
+ "path": "/wordpress/test.php",
+ "data": "<?php echo 'Hello World!'; ?>"
+}
+
+<hr/> </> ))}
+Original Playground docs source: https://playground.wordpress.net/blueprints/steps
]]>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.
A Blueprint bundle is a collection of files that includes:
+blueprint.json file that defines the Blueprint configurationBlueprint bundles can be distributed in various formats:
+blueprint.json file and additional resourcesblueprint.json resides alongside other resourcesThe WordPress Playground website supports Blueprint bundles 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.
The Playground CLI supports Blueprint bundles through the --blueprint= option. You can provide:
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
+A basic Blueprint bundle might look like this:
+my-blueprint-bundle/
+├── blueprint.json
+├── theme.zip
+├── plugin.zip
+└── content/
+ └── sample-content.wxr
+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:
+/bundled-text-file.txt/theme.zip/plugin.zip/content/sample-content.wxrTo 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 .
+Blueprint bundles support blueprint.json at two locations within a ZIP file:
blueprint.json sits directly at the ZIP rootblueprint.json sits inside a single top-level directoryThis 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.
If you encounter issues with Blueprint bundles:
+blueprint.json file is at the root level of your ZIP file or inside a single top-level directory--blueprint-may-read-adjacent-files flagOriginal Playground docs source: https://playground.wordpress.net/blueprints/bundles
]]>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.
+There are two main differences between the JSON and Function APIs:
++Note
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
+Original Playground docs source: https://playground.wordpress.net/blueprints/steps/api-consistency
]]>+Tip
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.
+<BlueprintExample blueprint={{
+ "steps": [
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "coblocks"
+ }
+ },
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "pendant"
+ }
+ }
+ ]
+}} />
+meta objectThe 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.
| Field | Type | Description |
+| :---------------- | :-------------- | :----------------------------------------------- |
+| **`title`** | `string` | A short, human-readable name for the Blueprint. |
+| **`description`** | `string` | A brief summary explaining the setup. |
+| **`author`** | `string` | The name or handle of the creator. |
+| **`categories`** | `array<string>` | Tags used for filtering and grouping Blueprints. |
+{
+ "$schema": "https://playground.wordpress.net/blueprint-schema.json",
+ "meta": {
+ "title": "Default Playground Setup",
+ "description": "A basic setup for a new WordPress site with the latest versions.",
+ "author": "Playground Team",
+ "categories": ["starter", "default"]
+ },
+ "landingPage": "/wp-admin/",
+ "preferredVersions": {
+ "php": "8.3",
+ "wp": "latest"
+ }
+}
+<BlueprintExample
+display={`{
+ "steps": [
+ {
+ "step": "runPHP",
+ "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array( 'post_title' => 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1 )); "
+ }
+ ]
+}` }
+blueprint={{
+ "steps": [
+ {
+ "step": "runPHP",
+ "code": `<?php
+require_once '/wordpress/wp-load.php';
+wp_insert_post(array(
+'post_title' => 'Post title',
+'post_content' => 'Post content',
+'post_status' => 'publish',
+'post_author' => 1
+));
+`
+}
+]
+}} />
+Here: Switch on the "new admin views" feature.
+<BlueprintExample
+display={`{
+ "steps": [
+ {
+ "step": "runPHP",
+ "code": "<?php require '/wordpress/wp-load.php'; update_option( 'gutenberg-experiments', array( 'gutenberg-dataviews' => true ) );"
+ }
+ ]
+}`}
+blueprint={{
+ "steps": [
+ {
+ "step": "runPHP",
+ "code": "<?php require '/wordpress/wp-load.php'; update_option( 'gutenberg-experiments', array( 'gutenberg-dataviews' => true ) );"
+ }
+ ]
+}} />
+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.
+Note
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:
+<BlueprintExample blueprint={{
+ "plugins": [ "sqlite-database-integration" ]
+}} />
+For a detailed explanation of why this is needed, refer to the Troubleshoot and Debug Blueprints section.
+<BlueprintExample noButton blueprint={{
+ "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"
+ }
+ }
+ ]
+}} />
+<BlueprintExample blueprint={{
+ "landingPage": "/wp-admin/plugin-install.php",
+ "features": {
+ "networking": true
+ },
+ "steps": [
+ {
+ "step": "login"
+ }
+ ]
+}} />
+Use the writeFile step to add code to a mu-plugin that runs on every request.
<BlueprintExample blueprint={{
+ "landingPage": "/category/uncategorized/",
+ "features": {
+ "networking": true
+ },
+ "steps": [
+ {
+ "step": "login"
+ },
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/mu-plugins/rewrite.php",
+ "data": "<?php add_action( 'after_setup_theme', function() { global $wp_rewrite; $wp_rewrite->set_permalink_structure('/%postname%/'); $wp_rewrite->flush_rules(); } );"
+ }
+ ]
+}} />
+<BlueprintExample blueprint={{
+ "landingPage": "/wp-admin/post.php?post=4&action=edit",
+ "steps": [
+ {
+ "step": "login",
+ "username": "admin",
+ "password": "password"
+ },
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "interactive-code-block"
+ }
+ },
+ {
+ "step": "runPHP",
+ "code": "<?php require '/wordpress/wp-load.php'; wp_insert_post(['post_title' => 'WordPress Playground block demo!','post_content' => '<!-- wp:wordpress-playground/playground /-->', 'post_status' => 'publish', 'post_type' => 'post',]);"
+ }
+ ]
+}} />
+You can share your own Blueprint examples in this dedicated wiki.
+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.
<BlueprintExample blueprint={{
+ "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"
+ }
+ ]
+}} />
+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.
<BlueprintExample blueprint={{
+ "landingPage": "/wp-admin",
+ "login" : true,
+ "preferredVersions" : {
+ "php": "8.3",
+ "wp": "https://playground.wordpress.net/plugin-proxy.php?build-ref=trunk"
+ }
+}} />
+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 fileYou can use this Blueprint bundle by:
+?blueprint-url=https://example.com/my-blueprint-bundle.zipFor more information on Blueprint bundles, see the Blueprint Bundles documentation.
+Original Playground docs source: https://playground.wordpress.net/blueprints/examples
]]>When you build Blueprints, you might run into issues. Here are tips and tools to help you debug them:
+wp-load: to run a WordPress PHP function using the runPHP step, you’d need to require wp-load.php. So, the value of the code key should start with "<?php require_once('wordpress/wp-load.php'); REST_OF_YOUR_CODE".When using wp-cli with a mounted Playground site (e.g., via --mount-before-install), you might encounter an "Error establishing a database connection." This happens because WordPress Playground loads the SQLite database integration plugin from its internal files by default, not from the mounted directory, meaning it's not persisted for external wp-cli calls.
To resolve this, you need to explicitly install and configure the SQLite database integration plugin within your Blueprint.
+Solution: Add the following steps to your Blueprint:
+{
+ "plugins": ["sqlite-database-integration"]
+}
+Example Usage:
+To test this locally, combine the Blueprint with your Playground CLI command:
+mkdir wordpress
+# Ensure your blueprint with the above steps is saved as, for example, './blueprint.json'
+npx @wp-playground/cli server --mount-before-install=wordpress:/wordpress --blueprint=./blueprint.json
+cd wordpress
+wp post list
+This will ensure the SQLite plugin is installed correctly and configured within your mounted WordPress site, allowing wp-cli commands to function correctly.
You can use an in-browser Blueprints editor to build, validate, and preview your Blueprints in the browser.
++Danger: Caution
The editor is under development and the embedded Playground sometimes fails to load. To get around it, refresh the page. We're aware of that, and are working to improve the experience.
+Some blueprint steps (such as writeFile) alter the internal Filesystem structure of the Playground instance and some others (such as runSql) alter the internal WordPress database.
To check the final internal filesystem structure and database (after the blueprint steps have been applied) we can leverage some WordPress plugins that provide a SQL manager and a file explorer such as SQL Buddy and WPide (you can see them in action from https://playground.wordpress.net/?plugin=sql-buddy&plugin=wpide)
+Tip
There are a bunch of methods we can launch from the console of any WordPress Playground instance to inspect the internals of that instance. They're exposed as part of window.playground object (see Developers > JavaScript API > Debugging and testing). Some examples:
> await playground.isDir("/wordpress/wp-content/plugins")
+true
+> await playground.listFiles("/wordpress/wp-content/plugins")
+(3) ['hello.php', 'index.php', 'WordPress-Importer-master']
+Full list of methods we can use is available here
+If your Blueprint isn’t running as expected, open the browser developer tools to check for any errors.
+To open the developer tools in Chrome, Firefox, Safari\*, and Edge: press Ctrl + Shift + I on Windows/Linux or Cmd + Option + I on macOS.
+Caution
If you haven't yet, enable the Develop menu: go to Safari > Settings... > Advanced and check Show features for web developers.
+The developer tools window allows you to inspect network requests, view console logs, debug JavaScript, and examine the DOM and CSS styles applied to your webpage. This is crucial for diagnosing and fixing issues with Blueprints.
+You can error_log your own error messages through runPHP step (see blueprint example and live demo) and check them from the "View Logs" option or from the browser's console.

When you download your Playground instance as a zip through the "Download as zip" option you'll also download the debug.log file containing all the logs from your Playground instance.
The community is here to help! If you have questions or comments, open a new issue in this repository. Remember to include the following details:
+Original Playground docs source: https://playground.wordpress.net/blueprints/troubleshoot-and-debug
]]>+Tip
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.
+<p class="docs-hubs">The WordPress Playground documentation is distributed across four separate hubs (subsites):</p>
+This docs hub is focused on Blueprints info and is divided into the following major sections:
+Original Playground docs source: https://playground.wordpress.net/blueprints
]]>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 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.
+Blueprints are an invaluable tool for building WordPress sites via Playground
+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.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:
+Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial/what-are-blueprints-what-you-can-do-with-them
]]>The fastest way to run Blueprints is to paste one 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:
<kbd> Run Blueprint </kbd>
+Use this method to run the example code in the next chapter, Build your first Blueprint.
+Some tools, including GitHub, might not format the Blueprint correctly when pasted into the URL. In such cases, encode your Blueprint in Base64 and append it to the URL. For example, that's the above Blueprint in Base64 format: eyJwcmVmZXJyZWRWZXJzaW9ucyI6IHsicGhwIjoiNy40IiwgIndwIjoiNS45In19.
To run it, go to https://playground.wordpress.net/#eyJwcmVmZXJyZWRWZXJzaW9ucyI6IHsicGhwIjoiNy40IiwgIndwIjoiNS45In19
+When your Blueprint gets too wieldy, you can load it via the ?blueprint-url query parameter in the URL, like this:
Note that the Blueprint must be publicly accessible and served with the correct Access-Control-Allow-Origin header:
Access-Control-Allow-Origin: *
+Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial/how-to-load-run-blueprints
]]>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.
+<kbd> Run Blueprint </kbd>
++Tip: 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:
+
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"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+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.
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.
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"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+The site should now look like the screenshot below:
+
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), orliteral.The example uses the wordpress.org/themes resource, which requires a slug identical to the one used in WordPress theme directory:
In this case, https://wordpress.org/themes/<slug>/ becomes https://wordpress.org/themes/adventurer/.
+Note
Learn more about the supported resources in the Blueprint Resources API Reference.
+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"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+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.
Let's install a custom WordPress plugin that adds a message to the admin dashboard:
+<?php
+/*
+Plugin Name: "Hello" on the Dashboard
+Description: A custom plugin to showcase WordPress Blueprints
+Version: 1.0
+Author: WordPress Contributors
+*/
+
+function my_custom_plugin() {
+ echo '<h1>Hello from My Custom Plugin!</h1>';
+}
+
+add_action('admin_notices', 'my_custom_plugin');
+You can use the installPlugin, but that requires creating a ZIP file. Let's start with something different to see if the plugin works:
+wp-content/plugins/hello-from-the-dashboard directory using the mkdir step.plugin.php file using the writeFile step.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": "<?php\n/*\nPlugin Name: \"Hello\" on the Dashboard\nDescription: A custom plugin to showcase WordPress Blueprints\nVersion: 1.0\nAuthor: WordPress Contributors\n*/\n\nfunction my_custom_plugin() {\n echo '<h1>Hello from My Custom Plugin!</h1>';\n}\n\nadd_action('admin_notices', 'my_custom_plugin');"
+ },
+ {
+ "step": "activatePlugin",
+ "pluginPath": "hello-from-the-dashboard/plugin.php"
+ }
+ ]
+}
+The last thing to do is log the user in as an admin. You can do that with a shorthand of the login step:
{
+ "login": true,
+ "steps": {
+ // ...
+ }
+}
+Here's the complete Blueprint:
+{
+ "$schema": "https://playground.wordpress.net/blueprint-schema.json",
+ "login": true,
+ "siteOptions": {
+ "blogname": "My first Blueprint"
+ },
+ "steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "adventurer"
+ }
+ },
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "hello-dolly"
+ }
+ },
+ {
+ "step": "mkdir",
+ "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard"
+ },
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/plugins/hello-from-the-dashboard/plugin.php",
+ "data": "<?php\n/*\nPlugin Name: \"Hello\" on the Dashboard\nDescription: A custom plugin to showcase WordPress Blueprints\nVersion: 1.0\nAuthor: WordPress Contributors\n*/\n\nfunction my_custom_plugin() {\n echo '<h1>Hello from My Custom Plugin!</h1>';\n}\n\nadd_action('admin_notices', 'my_custom_plugin');"
+ },
+ {
+ "step": "activatePlugin",
+ "pluginPath": "hello-from-the-dashboard/plugin.php"
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+That's what it looks like when you navigate to the dashboard:
+
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"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+Finally, let's delete the default content of the site and import a new one from a WordPress export file (WXR).
+There isn't a Blueprint step to delete the default content, but you can do that with a snippet of PHP code:
+<?php
+require '/wordpress/wp-load.php';
+
+// Delete all posts and pages
+$posts = get_posts(array(
+ 'numberposts' => -1,
+ 'post_type' => array('post', 'page'),
+ 'post_status' => 'any'
+));
+
+foreach ($posts as $post) {
+ wp_delete_post($post->ID, true);
+}
+To run that code during the site setup, use the runPHP step:
{
+ // ...
+ "steps": [
+ // ...
+ {
+ "step": "runPHP",
+ "code": "<?php\nrequire '/wordpress/wp-load.php';\n\n$posts = get_posts(array(\n 'numberposts' => -1,\n 'post_type' => array('post', 'page'),\n 'post_status' => 'any'\n));\n\nforeach ($posts as $post) {\n wp_delete_post($post->ID, true);\n}"
+ }
+ ]
+}
+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": "<?php\nrequire '/wordpress/wp-load.php';\n\n$posts = get_posts(array(\n 'numberposts' => -1,\n 'post_type' => array('post', 'page'),\n 'post_status' => 'any'\n));\n\nforeach ($posts as $post) {\n wp_delete_post($post->ID, true);\n}"
+ },
+ {
+ "step": "importWxr",
+ "file": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/WordPress/theme-test-data/master/themeunittestdata.wordpress.xml"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+And that's it. Congratulations on creating your first Blueprint! 🥳
+Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial/build-your-first-blueprint
]]>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.
++Tip
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.
+Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial
]]>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:
+Playground can be embedded on your website using the HTML <iframe> tag as follows:
<iframe src="https://playground.wordpress.net/"></iframe>
+Every visitor will get their own private WordPress instance for free. You can then customize it using one of the Playground APIs.
++Caution: 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.
+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:
+Learn more about each of these APIs in the APIs overview section.
+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.
<iframe src="https://playground.wordpress.net/?plugin=coblocks"></iframe>
+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.
+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.
+Tip
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:
+.zip fileThose 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:
+<BlueprintExample
+blueprint={{
+ "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"
+ }
+ }
+ ]
+ }} />
+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.
https://playground.wordpress.net/?core-pr=9500https://playground.wordpress.net/?gutenberg-pr=73010https://playground.wordpress.net/?gutenberg-branch=trunkTest your plugin across PHP and WordPress versions by configuring them in Playground. This helps you verify compatibility before release.
+With the Query API, you'd simply add the php and wp query parameters to the URL:
<iframe src="https://playground.wordpress.net/?php=8.3&wp=6.1"></iframe>
+With JSON Blueprints, you'd use the preferredVersions property:
{
+ "preferredVersions": {
+ "php": "8.3",
+ "wp": "6.1"
+ }
+}
+The JavaScript API provides the run() method which you can use to run PHP code in the browser:
<iframe id="wp"></iframe>
+<script type="module">
+ const client = await startPlaygroundWeb({
+ iframe: document.getElementById('wp'),
+ remoteUrl: 'https://playground.wordpress.net/remote.html',
+ });
+ await client.isReady;
+ await client.run({
+ code: `<?php
+ require("/wordpress/wp-load.php");
+
+ update_option("blogname", "Playground is really cool!");
+ echo "Site title updated!";
+ `,
+ });
+ client.goTo('/');
+</script>
+Combine that with a code editor like Monaco or CodeMirror, and you'll get live code snippets like in this article!
+Original Playground docs source: https://playground.wordpress.net/developers/build-your-first-app
]]>Caution: Package deprecated
+The NPM package @wp-now/wp-now is deprecated and won't receive updates in the future. To use a command-line tool in your developer workflow, use the NPM package @wp-playground/cli.
wp-now is a command-line tool designed to simplify the process of running WordPress locally. It provides a quick and easy way to set up a local WordPress environment with minimal configuration.
+Key Features:
+@wp-now/wp-now is a CLI tool to spin up a WordPress site with a single command. Similarly to the VS Code extension, it uses a portable WebAssembly version of PHP and SQLite. No Docker, MySQL, or Apache are required.
Documentation
+wp-now is maintained in a different GitHub repository, Playground Tools. You can find the latest documentation in the dedicated README file.
Navigate to your plugin or theme directory and start wp-now with the following commands:
cd my-plugin-or-theme-directory
+npx @wp-now/wp-now start
+wp-content directory with optionsYou can also start wp-now from any wp-content folder. The following example passes parameters for changing the PHP and WordPress versions and loading a blueprint file.
cd my-wordpress-folder/wp-content
+npx @wp-now/wp-now start --wp=6.4 --php=8.3 --blueprint=path/to/blueprint.json
+Alternatively, you can install @wp-now/wp-now globally to load it from any directory:
npm install -g @wp-now/wp-now
+cd my-plugin-or-theme-directory
+wp-now start
+Original Playground docs source: https://playground.wordpress.net/developers/local-development/wp-now
]]>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:
+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.
+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:
+Original Playground docs source: https://playground.wordpress.net/developers/local-development/vscode-extension
]]>As a WebAssembly project, you can also use WordPress Playground in Node.js.
+If you need low-level control over the underlying WebAssembly PHP build, take a look at the @php-wasm/node package which ships the PHP WebAssembly runtime. This package is at the core of all WordPress Playground tools for Node.js.
+Consult the complete list of Classes, Functions, Interfaces, and Type Aliases.
+This package ships WebAssembly PHP binaries and the JavaScript API optimized for Node.js. It uses the host file system directly and can access the network if you plug in a custom WS proxy.
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+const output = await php.runStream({
+ code: '<?php phpinfo(); ?>',
+});
+console.log(await output.stdoutText);
+Run PHP inside Node.js without a native PHP install. Allow developer to produce the following solutions:
+We will list some examples using the PHP-WASM package.
+Execute PHP scripts that interact with the file system:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// Create directory structure
+php.mkdir('/app/data');
+
+// Write configuration file
+await php.writeFile(
+ '/app/config.json',
+ JSON.stringify({
+ app: 'MyApp',
+ version: '1.0.0',
+ debug: true,
+ })
+);
+
+// Create and run PHP script that reads the config
+await php.writeFile(
+ '/app/index.php',
+ `<?php
+$config = json_decode(file_get_contents('/app/config.json'), true);
+echo "Application: " . $config['app'] . "\\n";
+echo "Version: " . $config['version'] . "\\n";
+echo "Debug Mode: " . ($config['debug'] ? 'ON' : 'OFF') . "\\n";
+
+// List all files
+echo "\\nFiles in /app:\\n";
+foreach (scandir('/app') as $file) {
+ if ($file !== '.' && $file !== '..') {
+ echo " - $file\\n";
+ }
+}
+?>`
+);
+
+const result = await php.runStream({ scriptPath: '/app/index.php' });
+console.log(await result.stdoutText);
+Use PHP's SQLite extension for data storage:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// Create directory for database
+php.mkdir('/data');
+
+// Create database, insert data, and query
+const result = await php.runStream({
+ code: `<?php
+// Create/connect to SQLite database
+$db = new SQLite3('/data/app.db');
+
+// Create table
+$db->exec('CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+)');
+
+// Insert sample data
+$stmt = $db->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
+$users = [
+ ['Alice Johnson', 'alice@example.com'],
+ ['Bob Smith', 'bob@example.com'],
+ ['Charlie Davis', 'charlie@example.com']
+];
+
+foreach ($users as $user) {
+ $stmt->bindValue(1, $user[0]);
+ $stmt->bindValue(2, $user[1]);
+ $stmt->execute();
+}
+
+// Query data
+echo "All Users:\\n";
+echo str_repeat('-', 50) . "\\n";
+$results = $db->query('SELECT * FROM users ORDER BY name');
+while ($row = $results->fetchArray(SQLITE3_ASSOC)) {
+ echo "ID: {$row['id']} | {$row['name']} ({$row['email']})\\n";
+}
+
+$db->close();
+?>`,
+});
+
+console.log(await result.stdoutText);
+
+// Database file persists in the virtual file system
+const dbExists = await php.fileExists('/data/app.db');
+console.log('\nDatabase persisted:', dbExists);
+Process ZIP files using PHP's Libzip extension:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// Create sample files
+php.mkdir('/uploads');
+await php.writeFile('/uploads/readme.txt', 'This is a sample text file');
+await php.writeFile('/uploads/data.json', JSON.stringify({ name: 'Test', version: '1.0' }));
+
+// Create, process, and extract ZIP archive
+const result = await php.runStream({
+ code: `<?php
+// Create ZIP archive
+$zip = new ZipArchive();
+$zip->open('/uploads/archive.zip', ZipArchive::CREATE);
+$zip->addFromString('readme.txt', file_get_contents('/uploads/readme.txt'));
+$zip->addFromString('data.json', file_get_contents('/uploads/data.json'));
+$zip->addFromString('info.txt', 'Created with PHP WASM');
+$zip->close();
+
+echo "ZIP archive created successfully\\n\\n";
+
+// Read and display archive contents
+$zip->open('/uploads/archive.zip');
+echo "Archive Contents:\\n";
+echo str_repeat('=', 50) . "\\n";
+
+for ($i = 0; $i < $zip->numFiles; $i++) {
+ $stat = $zip->statIndex($i);
+ $size = round($stat['size'] / 1024, 2);
+ echo sprintf("%-40s %10s KB\\n", $stat['name'], $size);
+}
+
+// Extract files
+$zip->extractTo('/uploads/extracted/');
+$zip->close();
+
+echo "\\nExtracted successfully to /uploads/extracted/\\n";
+
+// List extracted files
+echo "\\nExtracted Files:\\n";
+$files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator('/uploads/extracted/')
+);
+foreach ($files as $file) {
+ if ($file->isFile()) {
+ echo " " . $file->getPathname() . "\\n";
+ }
+}
+?>`,
+});
+
+console.log(await result.stdoutText);
+Simulate web server behavior with request handlers:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// Set up a simple API endpoint
+await php.mkdir('/www/api');
+await php.writeFile(
+ '/www/api/users.php',
+ `<?php
+header('Content-Type: application/json');
+
+// Parse request
+$method = $_SERVER['REQUEST_METHOD'];
+$input = json_decode(file_get_contents('php://input'), true);
+
+// Simple routing
+switch ($method) {
+ case 'GET':
+ echo json_encode([
+ 'users' => [
+ ['id' => 1, 'name' => 'John Doe'],
+ ['id' => 2, 'name' => 'Jane Smith']
+ ]
+ ]);
+ break;
+
+ case 'POST':
+ $name = $input['name'] ?? 'Unknown';
+ echo json_encode([
+ 'success' => true,
+ 'user' => [
+ 'id' => 3,
+ 'name' => $name
+ ],
+ 'message' => "User $name created"
+ ]);
+ break;
+
+ default:
+ http_response_code(405);
+ echo json_encode(['error' => 'Method not allowed']);
+}
+?>`
+);
+
+// Make GET request
+const getResponse = await php.runStream({
+ scriptPath: '/www/api/users.php',
+ env: {
+ REQUEST_METHOD: 'GET',
+ SERVER_NAME: 'localhost',
+ SERVER_PORT: '80',
+ },
+});
+console.log('GET Response:', await getResponse.stdoutText);
+
+// Make POST request
+const postResponse = await php.runStream({
+ scriptPath: '/www/api/users.php',
+ env: {
+ REQUEST_METHOD: 'POST',
+ SERVER_NAME: 'localhost',
+ SERVER_PORT: '80',
+ },
+ body: JSON.stringify({ name: 'Alice Wonder' }),
+});
+console.log('\\nPOST Response:', await postResponse.stdoutText);
+Use PHP as a templating engine for dynamic content:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// Create templates directory
+php.mkdir('/templates');
+
+// Create template
+await php.writeFile(
+ '/templates/email.php',
+ `<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ body { font-family: Arial, sans-serif; }
+ .header { background: #4CAF50; color: white; padding: 20px; }
+ .content { padding: 20px; }
+ .footer { background: #f1f1f1; padding: 10px; text-align: center; }
+ </style>
+</head>
+<body>
+ <div class="header">
+ <h1>Welcome, <?= htmlspecialchars($name) ?>!</h1>
+ </div>
+ <div class="content">
+ <p>Thank you for registering with <?= $appName ?>.</p>
+ <p>Your account details:</p>
+ <ul>
+ <li><strong>Email:</strong> <?= htmlspecialchars($email) ?></li>
+ <li><strong>Member Since:</strong> <?= date('F j, Y', $timestamp) ?></li>
+ </ul>
+ <p>You now have access to the following features:</p>
+ <ul>
+ <?php foreach ($features as $feature): ?>
+ <li><?= htmlspecialchars($feature) ?></li>
+ <?php endforeach; ?>
+ </ul>
+ </div>
+ <div class="footer">
+ <p>© <?= date('Y') ?> <?= $appName ?>. All rights reserved.</p>
+ </div>
+</body>
+</html>`
+);
+
+// Render template with data
+const templateData = {
+ name: 'Priya Sharma',
+ email: 'priya@example.com',
+ appName: 'MyAwesomeApp',
+ timestamp: Math.floor(Date.now() / 1000),
+ features: ['Dashboard Access', 'API Integration', 'Premium Support', 'Custom Branding'],
+};
+
+// Pass data to template via environment variables or files
+await php.writeFile('/template-data.json', JSON.stringify(templateData));
+
+const result = await php.runStream({
+ code: `<?php
+ $data = json_decode(file_get_contents('/template-data.json'), true);
+ extract($data);
+ include '/templates/email.php';
+ ?>`,
+});
+
+console.log(await result.stdoutText);
+// Now you have rendered HTML that can be sent via email or saved
+Process PHP output as it's generated:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+await php.writeFile(
+ '/stream-demo.php',
+ `<?php
+// Simulate long-running process
+echo "Starting process...\\n";
+flush();
+
+for ($i = 1; $i <= 10; $i++) {
+ echo "Processing item $i/10...\\n";
+ flush();
+ usleep(100000); // Sleep 100ms
+}
+
+echo "Process complete!\\n";
+?>`
+);
+
+// Run PHP script
+const streamedResponse = await php.runStream({
+ scriptPath: '/stream-demo.php',
+});
+
+streamedResponse.stdout.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ console.log(chunk);
+ },
+ })
+);
+Integrate PHP processing into an Express.js application:
+import express from 'express';
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const app = express();
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+// PHP execution middleware
+app.use('/php', async (req, res, next) => {
+ try {
+ const phpScript = req.query.script || 'index.php';
+ const result = await php.runStream({
+ scriptPath: `/www/${phpScript}`,
+ env: {
+ REQUEST_METHOD: req.method,
+ QUERY_STRING: new URLSearchParams(
+ req.query as Record<string, string>
+ ).toString(),
+ REQUEST_URI: req.url,
+ },
+ });
+
+ res.send(await result.stdoutText);
+ } catch (error) {
+ next(error);
+ }
+});
+
+app.listen(3000, () => {
+ console.log('Server with PHP support running on port 3000');
+});
+Create automated tests for PHP code:
+import { describe, it, expect, beforeAll } from '@jest/globals';
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+describe('PHP Functions', () => {
+ let php: PHP;
+
+ beforeAll(async () => {
+ php = new PHP(await loadNodeRuntime('8.3'));
+ });
+
+ it('should calculate sum correctly', async () => {
+ const result = await php.run({
+ code: `<?php
+ function sum($a, $b) {
+ return $a + $b;
+ }
+ echo sum(5, 3);
+ ?>`,
+ });
+
+ expect(result.text).toBe('8');
+ });
+
+ it('should handle JSON operations', async () => {
+ const input = { name: 'Test', value: 42 };
+ const result = await php.run({
+ code: `<?php
+ $input = json_decode('${JSON.stringify(input)}', true);
+ $output = [
+ 'received' => $input,
+ 'doubled' => $input['value'] * 2
+ ];
+ echo json_encode($output);
+ ?>`,
+ });
+
+ const output = JSON.parse(result.text);
+ expect(output.doubled).toBe(84);
+ });
+});
+Use in build scripts with other Node.js tools:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+import fs from 'fs/promises';
+
+async function generateDocumentation() {
+ const php = new PHP(await loadNodeRuntime('8.3'));
+
+ // Create output directory
+ php.mkdir('/output');
+
+ // Generate documentation
+ const result = await php.runStream({
+ code: `<?php
+echo "Generating documentation...\\n";
+
+$summary = "# Generated Documentation\\n\\n";
+$summary .= "Generated at: " . date('Y-m-d H:i:s') . "\\n\\n";
+
+file_put_contents('/output/summary.md', $summary);
+echo "Documentation generated successfully!\\n";
+?>`,
+ });
+
+ console.log(await result.stdoutText);
+
+ // Extract generated docs back to Node.js file system
+ await fs.mkdir('./docs', { recursive: true });
+ const summaryContent = await php.readFileAsText('/output/summary.md');
+ await fs.writeFile('./docs/summary.md', summaryContent);
+
+ console.log('Documentation saved to ./docs/summary.md');
+}
+
+generateDocumentation().catch(console.error);
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+const result = await php.runStream({
+ code: '<?php echo getenv("CUSTOM_VAR"); ?>',
+ env: {
+ CUSTOM_VAR: 'Hello from Node.js!',
+ },
+});
+
+console.log(await result.stdoutText);
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+try {
+ const result = await php.runStream({
+ code: '<?php trigger_error("Test error", E_USER_ERROR); ?>',
+ });
+
+ const stdout = await result.stdoutText;
+ const stderr = await result.stderrText;
+
+ console.log('stdout:', stdout);
+ console.log('stderr:', stderr);
+
+ if (stderr) {
+ console.error('PHP produced errors:', stderr);
+ }
+} catch (error: any) {
+ console.error('JavaScript Error:', error.message);
+}
+Original Playground docs source: https://playground.wordpress.net/developers/local-development/php-wasm-node
]]>@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:
+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.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.
+To run the Playground CLI, open a command line and use one of the following commands:
+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:
server (Advanced)The server command provides full control over configuration:
npx @wp-playground/cli@latest server
+
Automatic site persistence: By default, the start command keeps your WordPress site persistent across sessions. Your files and database are stored in ~/.wordpress-playground/sites/<path-hash>/, where <path-hash> is derived from your project directory. This means you can stop and restart the CLI without losing your work.
This is useful when:
+The --reset flag works only with start. For server, manually delete the persisted site directory at ~/.wordpress-playground/sites/<path-hash>/.
By default, the CLI loads the latest stable version of WordPress and PHP 8.3 due to its improved performance. To specify your preferred versions, you can use the flag --wp=<version> and --php=<version>:
npx @wp-playground/cli@latest server --wp=6.8 --php=8.3
+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=<blueprint-address> flag, developers can run a Playground with a custom initial state. We’ll use the example below to do this.
(my-blueprint.json)
+{
+ "landingPage": "/wp-admin/options-general.php?page=akismet-key-config",
+ "login": true,
+ "plugins": [
+ "hello-dolly",
+ "https://raw.githubusercontent.com/adamziel/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
+ ]
+}
+CLI command loading a blueprint:
+npx @wp-playground/cli@latest server --blueprint=my-blueprint.json
+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
+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".
server modeBy default, Playground CLI stores WordPress files and the SQLite database in temporary directories on your operating system:
+<OS-TEMP-DIR>/playground-<random-id>/
+├── wordpress/ # WordPress installation
+├── internal/ # Playground runtime config
+└── tmp/ # Temporary PHP files
+Finding Your Temp Directory:
+The actual location depends on your OS (these are examples or common possibilities):
+/tmp/ or /private/var/folders/ (varies by system)C:\Users\<username>\AppData\Local\Temp\To see the exact temp directory path being used, run the CLI with the --verbosity=debug flag:
npx @wp-playground/cli@latest server --verbosity=debug
+This will output something like:
+Native temp dir for VFS root:
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC
+Mount before WP install: /home ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/home
+Mount before WP install: /tmp ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/tmp
+Mount before WP install: /wordpress ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/wordpress
+Where is the SQLite Database Stored?
+The database location depends on what you mount:
+<your-local-project>/wp-content/database/.ht.sqlite<OS-TEMP-DIR>/playground-<id>/wordpress/wp-content/database/.ht.sqliteAutomatic Cleanup: Playground CLI automatically removes temp directories that are:
+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
+start modeRunning in start mode, Playground CLI automatically persists your WordPress site in a dedicated directory:
~/.wordpress-playground/sites/<path-hash>/
+├── wordpress/ # WordPress installation
+├── internal/ # Playground runtime config
+└── tmp/ # Temporary PHP files
+The <path-hash> is derived from your project directory path. This ensures isolation between different projects while persisting changes automatically.
~/.wordpress-playground/sites/<path-hash>/. Changes survive between CLI restarts./wordpress mount: If you provide a mount path for /wordpress, automatic persistence is skipped. Your mount configuration takes precedence.The database location depends on your configuration:
+~/.wordpress-playground/sites/<path-hash>/wordpress/wp-content/database/.ht.sqliteTo start fresh, use the --reset flag with the start command:
npx @wp-playground/cli@latest start --reset
+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 has a dedicated argument:
--reset: Delete the stored site and start fresh. Defaults to false.The server command supports the following optional arguments:
--port=<port>: The port number for the server to listen on. Defaults to 9400.--version: Show version number.--outfile: When building, write to this output file.--site-url=<url>: Site URL to use for WordPress. Defaults to http://127.0.0.1:{port}.--wp=<version>: The version of WordPress to use. Defaults to the latest.--php=<version>: PHP version to use. Choices: 8.5, 8.4, 8.3, 8.2, 8.1, 8.0, 7.4. Defaults to 8.5.--auto-mount[=<path>]: Automatically mount a directory. If no path is provided, mounts the current working directory. You can mount a WordPress directory, a plugin directory, a theme directory, a wp-content directory, or any directory containing PHP and HTML files.--mount=<mapping>: Manually mount a directory (can be used multiple times). Format: "/host/path:/vfs/path".--mount-before-install: Mount a directory to the PHP runtime before WordPress installation (can be used multiple times). Format: "/host/path:/vfs/path".--mount-dir: Mount a directory to the PHP runtime (can be used multiple times). Format: "/host/path" "/vfs/path".--mount-dir-before-install: Mount a directory before WordPress installation (can be used multiple times). Format: "/host/path" "/vfs/path"--blueprint=<path>: The path to a JSON Blueprint file to execute.--blueprint-may-read-adjacent-files: Consent flag: Allow "bundled" resources in a local blueprint to read files in the same directory as the blueprint file.--login: Automatically log the user in as an administrator.--wordpress-install-mode <mode>: Control how Playground prepares WordPress before booting. Defaults to download-and-install. Other options: install-from-existing-files (install using files you've mounted), install-from-existing-files-if-needed (skip setup when an existing site is detected), and do-not-attempt-installing (never download or install WordPress).--skip-sqlite-setup: Do not set up the SQLite database integration.--verbosity=<level>: Output logs and progress messages. Choices: quiet, normal, debug. Defaults to normal.--debug: Print the PHP error log if an error occurs during boot.--follow-symlinks: Allow Playground to follow symlinks by automatically mounting symlinked directories and files encountered in mounted directories.--internal-cookie-store: Enable internal cookie handling. When enabled, Playground will manage cookies internally using an HttpCookieStore that persists cookies across requests. When disabled, cookies are handled externally (e.g., by a browser in Node.js environments). Defaults to false.--phpmyadmin[=<path>]: Install phpMyAdmin for database management. The phpMyAdmin URL will be printed after boot. Optionally specify a custom URL path (default: /phpmyadmin).--xdebug: Enable Xdebug. Defaults to false.--experimental-devtools: Enable experimental browser development tools. Defaults to false.--experimental-unsafe-ide-integration=<ide>: Set up the Xdebug integration on VS Code (vscode) and PhpStorm (phpstorm).--workers=<n|auto>: Number of request-handling worker threads. Pass a positive integer, or auto to use one worker per CPU core (minus one). Defaults to min(6, cpus-1). Useful for multi-client workloads (e.g. parallel e2e suites) that need more than 6 in-flight requests.--experimental-multi-worker=<number>: Deprecated. Use --workers=<n|auto> instead. The value of this flag is ignored.+Caution
With the flag --follow-symlinks, the following symlinks will expose files outside mounted directories to Playground and could be a security risk.
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
+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.
Original Playground docs source: https://playground.wordpress.net/developers/local-development/wp-playground-cli
]]>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:
+For those needing more control, Playground offers tools for Node.js:
+@php-wasm/node package, which includes the PHP WebAssembly runtime.Original Playground docs source: https://playground.wordpress.net/developers/local-development
]]>WordPress Playground exposes a few APIs that you can use to interact with the Playground:
+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:
+<iframe style="width: 800px; height: 500px;" src="https://playground.wordpress.net/?plugin=coblocks"></iframe>
+Check the Query API section for more info.
+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:
+<BlueprintExample
+display={`{
+ "steps": [
+ {
+ "step": "login"
+ },
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "friends"
+ }
+ },
+ {
+ "step": "runPHP",
+ "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'Post title', 'post_content' => 'Post content', 'post_status' => 'publish', 'post_author' => 1)); ?>"
+ }
+ ]
+}` }
+blueprint={{
+ "steps": [
+ {
+ "step": "login"
+ },
+ {
+ step: 'installPlugin',
+ pluginData: {
+ resource: 'wordpress.org/plugins',
+ slug: 'friends',
+ },
+ },
+ {
+ "step": "runPHP",
+ "code": `<?php
+require_once '/wordpress/wp-load.php';
+wp_insert_post(array(
+'post_title' => 'Post title',
+'post_content' => 'Post content',
+'post_status' => 'publish',
+'post_author' => 1
+));
+`
+}
+]
+}} />
+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.
+The @wp-playground/client package provides a JavaScript API you can use to fully control your Playground instance. Here's a simple example of what you can do:
<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
+<script type="module">
+ // Use unpkg for convenience
+ import { startPlaygroundWeb } from 'https://playground.wordpress.net/client/index.js';
+
+ const client = await startPlaygroundWeb({
+ iframe: document.getElementById('wp'),
+ remoteUrl: `https://playground.wordpress.net/remote.html`,
+ });
+ // Let's wait until Playground is fully loaded
+ await client.isReady();
+</script>
+Check the JavaScript API section for more info.
+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:
+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.
+<iframe>WordPress Playground can be embedded in your app using an <iframe>:
<iframe src="https://playground.wordpress.net/"></iframe>
+To customize that Playground instance, you can:
+The JavaScript API gives you the most control, but it is also the least convenient option as it requires loading the Playground Client library.
++Caution: 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.
+The following Playground APIs are available in the browser:
+The following Playground APIs are available in Node.js:
+ +These APIs are very similar to their web counterparts, but, unsurprisingly, they are not based or links or iframes.
+Original Playground docs source: https://playground.wordpress.net/developers/apis/
]]>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.
+To use the JavaScript API, you'll need:
+<iframe> element@wp-playground/client package (from npm or a CDN)Here's the shortest example of how to use the JavaScript API in a HTML page:
+<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
+<script type="module">
+ // Use unpkg for convenience
+ import { startPlaygroundWeb } from 'https://playground.wordpress.net/client/index.js';
+
+ const client = await startPlaygroundWeb({
+ iframe: document.getElementById('wp'),
+ remoteUrl: `https://playground.wordpress.net/remote.html`,
+ });
+ // Let's wait until Playground is fully loaded
+ await client.isReady();
+</script>
+/remote.html is a special URL
+/remote.html is a special URL that loads the Playground API endpoint instead of the demo app with the browser UI. Read more about the difference between / and /remote.html and on this page.
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:
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.
Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api
]]>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.index.html, independent of the WordPress Playground JavaScript API.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 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:
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 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.
Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/-html-vs-remote-html
]]>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:
+PHP.iniThe two methods you can use to run PHP code are:
+ +In Node.js, you can also use the cli() method to run PHP in a CLI mode.
run() methodrequest() methodPHP.iniThe API client also allows you to change the php.ini file:
await setPhpIniEntries(client, {
+ display_errors: 'On',
+ error_reporting: 'E_ALL',
+});
+The client object provides you with a low-level API for managing files and directories in the PHP filesystem:
await client.mkdirTree('/wordpress/test');
+// Create a new PHP file
+await client.writeFile(
+ '/wordpress/test/index.php',
+ `<?php
+ echo "Hello, world!<br/>";
+ // List all the files in current directory
+ print_r(glob(__DIR__ . '/*'));
+ `
+);
+// Create files named 1, 2, and 3
+await client.writeFile('/wordpress/test/1', '');
+await client.writeFile('/wordpress/test/2', '');
+await client.writeFile('/wordpress/test/3', '');
+// Remove the file named 1
+await client.unlink('/wordpress/test/1');
+// Navigate to our PHP file
+await client.goTo('/test/index.php');
+For a complete list of these methods, refer to the PlaygroundClient interface.
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:
+import { PHP } from '@php-wasm/universal';
+import { loadNodeRuntime } from '@php-wasm/node';
+
+const php = new PHP(await loadNodeRuntime('8.3'));
+
+php.onMessage(
+ // The data is always passed as a string
+ function (data: string) {
+ // Let's decode and log the data:
+ console.log(JSON.parse(data));
+ }
+);
+
+// Now that we have a listener in place, let's
+// dispatch a message:
+await php.runStream({
+ code: `<?php
+ post_message_to_js(
+ json_encode([
+ 'post_id' => '15',
+ 'post_title' => 'This is a blog post!'
+ ])
+ );
+ `,
+});
+
+// You will see the following output in the console:
+// { post_id: '15', post_title: 'This is a blog post!' }
+cli() methodIn 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().
Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/playground-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:
+import { startPlaygroundWeb } from 'https://playground.wordpress.net/client/index.js';
+
+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.
+Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/blueprint-json-in-api-client
]]>Every Blueprint step you can declare in the JSON object also provides a handler function that can be used directly.
+For example:
+import { startPlaygroundWeb, login, installPlugin } from 'https://playground.wordpress.net/client/index.js';
+
+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.
+Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/blueprint-functions-in-api-client
]]>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/',
+ });
+});
+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.
+Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/mount-data
]]>WordPress Playground exposes a simple API that you can use to configure the Playground in the browser.
+It works by passing configuration options as query parameters to the Playground URL. For example, to install the pendant theme, you would use the following URL:
+https://playground.wordpress.net/?theme=pendant
+You can go ahead and try it out. The Playground will automatically install the theme and log you in as an admin. You may even embed this URL in your website using an <iframe> tag:
<iframe src="https://playground.wordpress.net/?theme=pendant"></iframe>
+| 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` or `latest`. |
+| `wp` | `latest` | Loads the specified WordPress version. Accepts the last three major WordPress versions. As of June 1, 2024, that's `6.3`, `6.4`, or `6.5`. You can also use the generic values `latest`, `nightly`, or `beta`. |
+| `blueprint-url` | | The URL of the Blueprint that will be used to configure this Playground instance. |
+| `networking` | `yes` | Enables or disables the networking support for Playground. Accepts `yes` or `no`. |
+| `plugin` | | Installs the specified plugin. Use the plugin name from the WordPress Plugins Directory URL. For example, if the URL is `https://wordpress.org/plugins/wp-lazy-loading/`, the plugin name would be `wp-lazy-loading`. You can pre-install multiple plugins by saying `plugin=coblocks&plugin=wp-lazy-loading&…`. Installing a plugin automatically logs the user in as an admin. More than one plugin could be installed, just repeating the `plugin` attribute on the URL. |
+| `theme` | | Installs the specified theme. Use the theme name from the WordPress Themes Directory URL. For example, if the URL is `https://wordpress.org/themes/disco/`, the theme name would be `disco`. Installing a theme automatically logs the user in as an admin. Multiples themes could be installed just repeating the `theme` attribute on the URL. |
+| `url` | `/wp-admin/` | Load the specified initial WordPress page in this Playground instance. |
+| `mode` | `browser-full-screen` | Determines how the WordPress instance is displayed. Either wrapped in a browser UI or full width as a seamless experience. Accepts `browser-full-screen`, or `seamless`. |
+| `lazy` | | Defer loading the Playground assets until someone clicks on the "Run" button. Does not accept any values. If `lazy` is added as a URL parameter, loading will be deferred. |
+| `login` | `yes` | Log the user in as an admin. Accepts `yes` or `no`. |
+| `multisite` | `no` | Enables the WordPress multisite mode. Accepts `yes` or `no`. |
+| `import-site` | | Imports site files and database from a ZIP file specified by a URL. |
+| `import-wxr` | | Imports site content from a WXR file specified by a URL. It uses the WordPress Importer plugin, so the default admin user must be logged in. |
+| `site-slug` | | Selects which site to load from browser storage. If the specified site does not exist, the user will be prompted to save a new site with the specified slug. |
+| `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` | | Default functionality allows for saving Playgrounds to the user's computer or browser. If you would like to turn off the ability for users to save their Playground, add the `?can-save=no` parameter, and options to save will be removed from the UI. |
+| `mcp` | `no` | Starts the MCP (Model Context Protocol) server bridge, allowing external MCP clients to connect to and control the Playground instance. Accepts `yes` or `no`. |
+| `mcp-port` | `7999` | Sets the WebSocket port used by the MCP bridge to communicate with the MCP server. Must be used together with `mcp=yes`. For example, `mcp=yes&mcp-port=8080`. |
+| `overlay` | | Opens a UI overlay on page load. Currently supports `blueprints` to open the Blueprint Gallery directly. For example, `?overlay=blueprints`. The parameter is removed from the URL when the overlay is closed. |
+| `filebrowser` | | Opens the Site Manager directly to the File Browser tab. Accepts an optional file path relative to the WordPress document root, and an optional `:<line>` suffix to place the editor cursor on a 1-based line. |
+For example, the following code embeds a Playground with a preinstalled Gutenberg plugin and opens the post editor:
+<iframe src="https://playground.wordpress.net/?plugin=gutenberg&url=/wp-admin/post-new.php&mode=seamless"> </iframe>
+To open the File Browser tab, use:
+https://playground.wordpress.net/?filebrowser
+To open the File Browser tab with a specific file, use a path relative to the WordPress document root:
+https://playground.wordpress.net/?filebrowser=wp-content/plugins/my-plugin/index.php
+To place the cursor on a specific 1-based line number, append :<line> to the path:
https://playground.wordpress.net/?filebrowser=wp-content/plugins/my-plugin/index.php:20
+CORS policy
+To import files from a URL, such as a site zip package, they must be served with Access-Control-Allow-Origin header set. For reference, see: Cross-Origin Resource Sharing (CORS).
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 theuser 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 thecontent 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 GitHubexport (yes, no). Optional. Defaults to yes.
Original Playground docs source: https://playground.wordpress.net/developers/apis/query-api
]]>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.
+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.
For a quick start, check the getting started with Xdebug guide
+You'll learn to debug:
+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.
+Next: Getting Started with Xdebug →
+Original Playground docs source: https://playground.wordpress.net/developers/xdebug/introduction
]]>This guide shows you how to enable Xdebug in WordPress Playground and start debugging your code.
+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.
npxThe 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.
+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:
+.vscode/ folder.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.
+
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:
+<?php
+/**
+ * Plugin Name: Simple Admin Message
+ * Description: Displays a simple message in the WordPress admin
+ * Version: 1.0
+ * Author: Playground Team
+ */
+
+// Prevent direct access
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+// Display admin notice
+function sam_display_admin_message() {
+ $message = 'Hello! This is a simple admin message.';
+ ?>
+ <div class="notice notice-info is-dismissible">
+ <p><?php _e($message, 'simple-admin-message'); ?></p>
+ </div>
+ <?php
+}
+add_action('admin_notices', 'sam_display_admin_message');
+In the folder where the plugin is located, let's run the command in our terminal:
+npx @wp-playground/cli@latest server --xdebug --experimental-devtools --auto-mount
+The Playground CLI(@wp-playground/cli) will automatically detect the plugin folder and mount it. Opening the project in your browser and DevTools, you'll be able to add breakpoints in your plugin's code and test it line by line.

This feature is in experimental mode. Until it's finished, we'll need your feedback. Please connect with us in the #playground Slack channel and share your thoughts.
+Original Playground docs source: https://playground.wordpress.net/developers/xdebug/getting-started
]]>WordPress Playground consists of the following high-level components:
+Visit each section to learn more about the specific parts of the architecture.
+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.
+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.
+Original Playground docs source: https://playground.wordpress.net/developers/architecture
]]>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.

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:
+wp_safe_remote_get to translate them into fetch() calls.fetch() call.Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-overview
]]>The build pipeline lives in a Dockerfile. It was originally forked from seanmorris/php-wasm
In broad strokes, that Dockerfile:
build-essential)sqlite3.php_wasm.c – a convenient API for JavaScript.php.wasm file and one or more JavaScript loaders, depending on the configuration.php.js output into an ESM module with additional features.To find out more about each step, refer directly to the Dockerfile.
+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 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.
+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.
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.Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-compiling
]]>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 of these classes extend the BasePHP class exposed by the @php-wasm/universal package and implement the UniversalPHP interface that standardizes the API across all PHP environments.
The load() method handles the entire PHP initialization pipeline. In particular, it:
+Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/04-wasm-php-javascript-module
]]>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.
+Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-filesystem
]]>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.
+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.
+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:
+packages/php-wasm/node/src/test/php-asyncify.spec.tsnpm run fix-asyncifyDockerfile, and the rebuilt PHP.wasmThe 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:
+--experimental-wasm-jspi flag (handled automatically by the CLI)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:
+.wasm files reduced by 109 MB (16%)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).
Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-asyncify
]]>On a high level, WordPress Playground works in web browsers as follows:
+index.html file on playground.wordpress.net loads the remote.html file via an <iframe src="/remote.html">.remote.html starts a Worker Thread and a ServiceWorker and sends back the download progress information.remote.html creates an <iframe src="/index.php">, and the Service Worker forwards the index.php request to the Worker Thread where the WordPress homepage is rendered.Visually, it looks like this:
+
The @php-wasm/web is built on top of the following ideas:
Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-concepts
]]>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.
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:
+<script src="/app.ts"></script>
+<iframe id="my-app"></iframe>
+/app.ts:
+import { consumeAPI, PHPClient, registerServiceWorker, spawnPHPWorkerThread } from '@php-wasm/web';
+
+const workerUrl = '/worker-thread.js';
+
+export async function startApp() {
+ const phpClient = consumeAPI<PlaygroundWorkerEndpoint>(
+ await spawnPHPWorkerThread(
+ workerUrl, // Valid Worker script URL
+ {
+ wpVersion: 'latest',
+ phpVersion: '8.3', // Startup options
+ }
+ )
+ );
+
+ // Await the two-way communication channel
+ await phpClient.isReady();
+
+ // Must point to a valid Service Worker script:
+ await registerServiceWorker(
+ phpClient,
+ 'default', // PHP instance scope, keep reading to learn more.
+ '/sw.js', // Valid Service Worker script URL.
+ '1' // Service worker version, used for reloading the script.
+ );
+
+ // Create a few PHP files to browse:
+ await workerThread.writeFile('/index.php', '<a href="page.php">Go to page.php</a>');
+ await workerThread.writeFile('/page.php', '<?php echo "Hello from PHP!"; ?>');
+
+ // Navigate to index.php:
+ document.getElementById('my-app').src = playground.pathToInternalUrl('/index.php');
+}
+startApp();
+Keep reading to learn how all these pieces fit together.
+Here's what happens whenever the iframe issues a same-domain request:
+
A step-by-step breakdown:
+PHP.request to convert that request to a responseAt this point, if the request was triggered by user clicking on a link, the browser will render PHPRequestHandler's response inside the iframe.
+Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-tab-orchestrates-execution
]]>To avoid page reloads, all the PHPRequestHandler responses must be rendered in an iframe. Remember, the entire setup only lives as long as the main index.html. We want to avoid reloading the main app at all costs.
In our app example above, index.php renders the following HTML:
<a href="page.php">Go to page.php</a>
+Imagine our index.html rendered it in a <div> instead of an <iframe>. As soon as you click on that link, the browser will try to navigate from index.html to page.php. However, index.html runs the entire PHP app, including the Worker Thread, the PHPRequestHandler, and the traffic control connecting them to the Service Worker. Navigating away from it would destroy the app.
Now, consider an iframe with the same link in it:
+<iframe srcdoc='<a href="page.php">Go to page.php</a>'></iframe>
+This time, click the link in the browser to load page.php inside the iframe. The top-level index.html, where the PHP application runs, remains unaffected. That's why iframes are crucial for the @php-wasm/web setup.
Crash reports
+Playground doesn't collect crash reports automatically. Instead, it prompts users to submit a crash report when an instance fails to run in the browser.
+The report includes a log, description, and a URL, and users can modify it before submitting it.
+The Logger API handles it from there. This simple REST API validates the data and sends it to the Making WordPress #playground-logs Slack channel.
+target="_top" isn't handled yet, so clicking links with target="_top" will reload the page you’re working on.iframe may not always display.Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-iframe-rendering
]]>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:
+<button onclick="for(let i=0;i<100000000;i++>) {}">Freeze the page</button>
+<input type="text" />
+As soon as you click that button the browser will freeze and you won't be able to type in the input. That's just how browsers work. Whether it's a for loop or a PHP server, running intensive tasks slows down the user interface.
+Web workers are separate programs that can process heavy tasks outside of the main application. They must be initiated by the main JavaScript program living in the browser tab. Here's how:
+const phpClient = consumeAPI<PHPClient>(
+ spawnPHPWorkerThread(
+ '/worker-thread.js' // Valid Worker script URL
+ )
+);
+await phpClient.isReady();
+await phpClient.run({ code: `<?php echo "Hello from the thread!";` });
+Exchanging messages is the only way to control web workers. The main application has no access to functions or variables inside of a web worker. It can only send and receive messages using worker.postMessage and worker.onmessage = function(msg) { }.
This can be tedious, which is why Playground provides a convenient consumeAPI function that abstracts the message exchange and exposes specific functions from the web worker. This is why we can call phpClient.run in the example above.
Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-php-worker-threads
]]>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:
+<html>
+ <head>
+ <title>John's Website</title>
+ </head>
+ <body>
+ <a href="/">Homepage</a>
+ <a href="/blog">Blog</a>
+ <a href="/contact">Contact</a>
+ </body>
+</html>
+When the user clicks, say the Blog link, the browser would normally send a HTTP request to the remote server to fetch the /blog page and then display it instead of the current iframe contents. However, our app isn't running on the remote server. The browser would just display a 404 page.
Enter Service Workers – a tool to intercept the HTTP requests and handle them inside the browser:
+
The main application living in /index.html is responsible for registering the service worker.
Here's the minimal setup:
+/app.js:
+import { registerServiceWorker } from '@php-wasm/web';
+
+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:
+import { initializeServiceWorker } from '@php-wasm/web';
+
+// Intercepts all HTTP traffic on the current domain and
+// passes it to the Worker Thread.
+initializeServiceWorker();
+Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-service-workers
]]>Scopes keep your app working when you open it in two different browser tabs.
+The Service Worker passes the intercepted HTTP requests to the PHPRequestHandler for rendering. Technically, it sends a message through a BroadcastChannel which then gets delivered to every browser tab where the application is open. This is undesirable, slow, and leads to unexpected behaviors.
Unfortunately, the Service Worker cannot directly communicate with the relevant Worker Thread – see PR #31 and issue #9 for more details.
+Scopes enable each browser tab to:
+BroadcastChannel messages with a different idTechnically, a scope is a string included in the PHPRequestHandler.absoluteUrl. For example:
/index.php would be available at http://localhost:8778/wp-login.php/index.php would be available at http://localhost:8778/scope:96253/wp-login.phpThe service worker is aware of this concept and will attach the /scope: found in the request URL to the related BroadcastChannel communication.
A worker thread initiated with a scoped absoluteUrl is said to be scoped:
import {
+ PHP,
+ setURLScope,
+ exposeAPI,
+ parseWorkerStartupOptions,
+} from '@php-wasm/web';
+
+// Don't use the absoluteURL directly:
+const absoluteURL = 'http://127.0.0.1'
+
+// Instead, set the scope first:
+const scope = Math.random().toFixed(16)
+const scopedURL = setURLScope(absoluteURL, scope).toString()
+
+const { phpVersion } = parseWorkerStartupOptions<{ phpVersion?: string }>();
+const php = await PHP.load('8.0', {
+ requestHandler: {
+ documentRoot: '/',
+ absoluteUrl: scopedSiteUrl
+ }
+});
+
+// Expose the API to app.ts:
+const [setApiReady, ] = exposeAPI( php );
+setApiReady();
+Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/13-browser-scopes
]]>@php-wasm/web uses the Comlink library to turns the one-way postMessage available in JavaScript into a two-way communication channel.
If postMessage sounds unfamiliar, it's what JavaScript threads use to communicate. Please review the MDN Docs before continuing.
By default, postMessage does not offer any request/response mechanics. You may send messages to another thread and you may independently receive messages from it, but you can't send a message and await a response to that specific message.
To quote the Comlink library documentation:
+main.js
+import * as Comlink from 'https://unpkg.com/comlink/dist/esm/comlink.mjs';
+async function init() {
+ const worker = new Worker('worker.js');
+ // WebWorkers use `postMessage` and therefore work with Comlink.
+ const obj = Comlink.wrap(worker);
+ alert(`Counter: ${await obj.counter}`);
+ await obj.inc();
+ alert(`Counter: ${await obj.counter}`);
+}
+init();
+worker.js
+importScripts('https://unpkg.com/comlink/dist/umd/comlink.js');
+
+const obj = {
+ counter: 0,
+ inc() {
+ this.counter++;
+ },
+};
+
+Comlink.expose(obj);
+Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/14-browser-cross-process-communication
]]>WordPress, as a PHP application, can run on PHP WebAssembly. However, there are a few caveats.
+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.
+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.
+In Node.js, you'll typically want to mount WordPress from a disk directory.
+Original Playground docs source: https://playground.wordpress.net/developers/architecture/wordpress
]]>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.
+Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/16-wordpress-database
]]>The web bundler Dockerfile turns a vanilla WordPress into a browser-optimized one:
+Build a new bundle with nx bundle-wordpress playground-wordpress-builds --wp-version=<version>, e.g.:
nx bundle-wordpress playground-wordpress-builds --wp-version=6.1
+The bundler outputs:
+packages/playground/wordpress-builds/public/wp-6.1.zip – zipped WordPress filespackages/playground/wordpress-builds/public/wp-6.1/ – a directory with static assets for the specified WordPress versionsConsult the web bundler Dockerfile for more details (like the list of supported WordPress versions) and modify it to customize the default WordPress installation.
+Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/17-browser-wordpress
]]>You can host the Playground on your own domain instead of playground.wordpress.net.
This is useful for having full control over its content and behavior, as well as removing dependency on a third-party server. It can provide a more customized user experience, for example: a playground with preinstalled plugins and themes, default site settings, or demo content.
+Self-hosting Playground gives you full control, but requires understanding a few key concepts:
+Loading times depend on several factors:
+| Factor | Impact | Optimization |
+| ----------------- | ------------------------------------------------------------------- | ------------------------------------------- |
+| **Plugin size** | Large plugins (e.g., WooCommerce) can take 30-60 seconds to install | Pre-install plugins in your WordPress build |
+| **Network speed** | WASM files are ~15-30MB | Use CDN with proper caching headers |
+| **Browser** | Chrome/Edge perform best; Safari uses fallback mechanisms | Test across browsers |
+| **Device** | Mobile devices load slower than desktop | Warn mobile users about longer load times |
+Playground works across modern browsers, but with some differences:
+| Browser | Status | Notes |
+| --------------- | ------------------- | ---------------------------------------------------------------------------------- |
+| Chrome/Edge | ✅ Best performance | Full support for all features |
+| Firefox | ✅ Good | Reliable performance |
+| Safari | ✅ Good | Recent improvements significantly enhanced reliability |
+| Mobile browsers | ⚠️ Limited | Works, but with higher memory usage, and a 4G connection can impact the experience |
+Technical note: Safari uses MessagePorts instead of SharedArrayBuffer for streaming responses. This fallback works reliably but adds slight overhead compared to Chrome/Edge.
+A self-hosted Playground can be embedded as an iframe.
+<iframe src="https://my-playground.com"></iframe>
+Or dynamically loaded by passing the remote URL to the Playground Client.
+import { startPlaygroundWeb } from '@wp-playground/client';
+
+const client = await startPlaygroundWeb({
+ iframe: document.getElementById('wp'),
+ remoteUrl: `https://my-playground.com/remote.html`,
+});
+There are several ways to get the static assets necessary to host the Playground.
+In order of convenience and ease:
+To host the Playground as is, without making changes, you can download the built artifact from the latest successful GitHub Action.
+playground-website.To customize the Playground, you can fork the Git repository.
+Build it from the fork's GitHub page by going to: Actions -> Deploy Playground website -> Run workflow.
+The most flexible and customizable method is to build the site locally.
+Create a shallow clone of the Playground repository, or your own fork.
+git clone -b trunk --single-branch --depth 1 --recurse-submodules https://github.com/WordPress/wordpress-playground.git
+Enter the wordpress-playground directory.
cd wordpress-playground
+Install dependencies, and build the website.
+npm install
+npm run build:website
+This command internally runs the nx task build:wasm-wordpress-net. It copies the built assets from packages remote and website into a new folder at the following path:
dist/packages/playground/wasm-wordpress-net
+The entire service of the Playground consists of the content of this folder.
+The static assets include:
+remote.html - the core of Playgroundindex.html - the shell, or browser chromeYou can deploy the content of the folder to your server using SSH, such as scp or rsync.
It is a static site, except for these dynamic aspects.
+.htaccess file from the package remoteFor these to work, you need a server environment with Apache and PHP installed.
+As an alternative to Apache, here is an example of using NGINX to serve the Playground.
+Refer to the source file
+The example may be outdated. Please check the source file for the latest version.
+The combined Apache .htaccess file looks like this.
AddType application/wasm .wasm
+An equivalent in NGINX.
+location ~* .wasm$ {
+ types {
+ application/wasm wasm;
+ }
+}
+You may need to adjust the above according to server specifics, particularly how to invoke PHP for the path /plugin-proxy.
Caddy web server doesn't require any special config to work.
+The file wp.zip is a bundle of all the files for the virtual file system in Playground. There's a data file for each available WordPress version.
The package at packages/playground/wordpress-builds is responsible for building these data files.
Edit the build script in Dockerfile to create a custom bundle that includes preinstalled plugins or content.
To rebuild the WordPress builds after customizing the Dockerfile, run the following command:
npm run rebuild:wordpress-builds
+To rebuild the website to include the custom WordPress builds, follow the instructions here.
+Here's an example of installing plugins for the data bundle.
+Before the section titled Strip whitespaces from PHP files.
# === Preinstall plugins ===
+
+RUN cd wordpress/wp-content/mu-plugins && \
+ # Install plugins
+ for plugin_name in example-plugin-1 example-plugin-2; do \
+ curl -L https://downloads.wordpress.org/plugin/{$plugin_name}.latest-stable.zip -o {$plugin_name}.zip && \
+ unzip $plugin_file && \
+ rm $plugin_file && \
+ # Create entry file in mu-plugins root
+ echo "<?php require_once __DIR__.'/$plugin_name/$plugin_name.php';" > $plugin_name.php; \
+ done;
+You can download plugins from URLs other than the WordPress plugin directory, or use Git to pull them from elsewhere.
+It's also possible to copy from a local folder. For example, before RUN:
COPY ./build-assets/*.zip /root/
+Then put the plugin zip files in build-assets. In this case, you may want to add their paths to .gitignore.
Here's an example of importing content.
+# === Demo content ===
+
+COPY ./build-assets/content.xml /root/
+RUN cd wordpress ; \
+ echo "Importing content.."; \
+ ../wp-cli.phar --allow-root import /root/content.xml --authors=create
+This assumes that you have put a WXR export file named content.xml in the folder build-assets. You can add its path to .gitignore.
Before going live, verify your self-hosted Playground meets these requirements:
+.wasm files are served with application/wasm content typePossible causes:
+Solutions:
+.wasm files return application/wasm content typePossible causes:
+Solutions:
+Original Playground docs source: https://playground.wordpress.net/developers/architecture/host-your-own-playground
]]>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.
+Playground creates fresh WordPress instances on each page load. Refreshing the browser page discards all database changes, uploads, and modifications.
+Why this happens: Playground streams WordPress directly to your browser rather than serving it from a traditional server. Each refresh starts a clean slate.
+To persist your work:
++Tip
The dedicated refresh button inside Playground only reloads WordPress content—it preserves your PHP/WP state. The browser's refresh button (F5 or Cmd+R) destroys the entire instance.
+
<blockquote> <figure> <figcaption><i>1. Exporting Playground:</i></figcaption>
+
</figure>
+<figure> <figcaption><i>2. Save button:</i></figcaption>
+
</figure> </blockquote>
+WordPress Playground is designed to work across all major desktop and mobile browsers. This includes:
+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.
+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:
+<blockquote> <strong>Note:</strong> Opera Mini support is not currently confirmed. </blockquote>
+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.
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": "<?php require_once('wordpress/wp-load.php'); OTHER_CODE ?>"
+}
+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.
Original Playground docs source: https://playground.wordpress.net/developers/limitations
]]>Hi! Welcome to WordPress Playground Developer documentation.
+<p class="docs-hubs">The WordPress Playground documentation is distributed across four separate hubs (subsites):</p>
+This docs hub is focused on Developers info and is divided into the following major sections:
+Original Playground docs source: https://playground.wordpress.net/developers/
]]>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.
+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.
+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.
+Embedded media: https://www.youtube.com/embed/94KnoFhQg1g
+Some more examples of this workflow:
+
With Google Chrome you can synchronize your Playground instance with a local directory, that can be either:
+This feature is only available for Google Chrome for now. It won't work with other browsers yet.
+Regarding changes done on both sides of the connection:
+With this workflow, you can create GitHub PRs directly from your changes made in your local directory.
+See here a little demo of this workflow in action:
+Embedded media: https://www.youtube.com/embed/UYK88eZqrjo
+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
+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.
+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.
+Original Playground docs source: https://playground.wordpress.net/about/build
]]>WordPress Playground is the platform that lets you run WordPress instantly on any device without a host. It allows you to experiment and learn about WordPress without affecting your live website. It's a virtual sandbox where you can play around with different features, designs, and settings in a safe and controlled environment.
+WordPress Playground is your place to build, test, and launch:
+With WordPress Playground, you can explore any theme. You can choose from a wide range of themes and see how they look on your site. You can also modify the colors, fonts, layouts, and other visual elements to create a unique design.
+In addition to themes, you can experiment with plugins too. With WordPress Playground, you can install and test different plugins to see how they work and what they can do for your site. This allows you to explore and understand the capabilities of WordPress without worrying about breaking anything.
+Another great feature of WordPress Playground is the ability to create and edit content. You can write blog posts, create pages, and add media like images and videos to your site. This helps you understand how to organize and structure your content effectively.
+The content you create is limited to the Playground on your device and disappears once you leave it, so you are free to explore and play without risking breaking any actual site.
+But hey! You can also connect your Playground instance to a GitHub repo and create a PR to persist those changes.
+Overall, WordPress Playground provides a risk-free environment for beginners to learn and get hands-on experience with WordPress. It helps you to gain confidence and knowledge before making changes to your live website.
++Tip
Check the guides section to learn more about how to leverage WordPress Playground to test your themes and plugins and create content on the fly.
+When you first start using WordPress Playground, you'll be provided with a separate space where you can create and customise your own WordPress website. This space is completely isolated from your actual website.
+The WordPress you see when you open Playground in your browser is a WordPress that should function like any WordPress, with a few limitations and the important exception that it's not a permanent server with an internet address which will limit connections to some third-party services (automation, sharing, analysis, email, backups, etc.) in a persistent way.
+The loading screen and progress bar you see on Playground includes both the streaming of those foundational technologies to your browser and configuration steps from WordPress Blueprints (see examples), so that a full server, WordPress software, Theme & Plugin solutions and configuration instructions can be streamed over-the-wire.
+Web applications like WordPress have long relied on server technologies to run logic and store data.
+Using those technologies has meant either running a web server connected to the internet or using those technologies in a desktop service or app (sometimes called a "WordPress local environment") that either leans on a virtual server with the technologies installed or the underlying technologies on the current device.
+Playground is a novel way to stream server technologies—including WordPress (and WP-CLI)—as files that can then run in the browser.
+Original Playground docs source: https://playground.wordpress.net/about
]]>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.
+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.
+Embedded media: https://www.youtube.com/embed/lQzozsoJ3aY
+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!
+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.
+Check the How to ship a real WordPress site in a native iOS app via Playground? guide for info on this use case
+Original Playground docs source: https://playground.wordpress.net/about/launch
]]>Upgrade your QA process with the ability to review progress in your browser in a single click. When you’re ready, push updates instantly.
+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)
+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.
+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.
+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.
+Original Playground docs source: https://playground.wordpress.net/about/test
]]>All notable changes to this project are documented in this file by a CI job that runs on every NPM release. The file follows the Keep a Changelog format.
+The following contributors merged PRs in this release:
+@adamziel @JanJakes
+The following contributors merged PRs in this release:
+@adamziel @ashfame @fellyph @mho22 @perashanid
+overlay parameter to Playground URL options. (#3457)The following contributors merged PRs in this release:
+@ashfame @brandonpayton @dd32 @fellyph @JanJakes @mho22 @perashanid @Rima1889
+The following contributors merged PRs in this release:
+@adamziel @fellyph
+--skip-sqlite-setup false pre-flight database error. (#3456)The following contributors merged PRs in this release:
+@bgrgicak @JanJakes @mho22 @noruzzamans @shimotmk
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @bgrgicak @JanJakes @mho22 @perashanid @shimotmk @wojtekn
+###
+The following contributors merged PRs in this release:
+@adamziel @ashfame @bgrgicak @brandonpayton @wojtekn
+The following contributors merged PRs in this release:
+@adamziel @ashfame @brandonpayton @fellyph @zaerl
+php command to run PHP scripts. (#2641)The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@brandonpayton
+content-type: Application/octet-stream for CORS proxy requests. (#3364)MAIN_MODULE set to 2. (#3335)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @mho22
+The following contributors merged PRs in this release:
+@bgrgicak @mho22
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @fellyph @pkevan
+MAIN_MODULE set to 2. (#3332)ERR_STREAM_PREMATURE_CLOSE in CLI server. (#3304)The following contributors merged PRs in this release:
+@adamziel @andreilupu @bcotrim @brandonpayton @dd32 @fellyph @JanJakes @mho22
+cp method to Universal PHP. (#3234)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @dd32 @epeicher @fellyph @fredrikekelund @JanJakes @mho22 @n8finch @zaerl
+The following contributors merged PRs in this release:
+@ashfame @bgrgicak @brandonpayton @epeicher @JanJakes
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @bcotrim @bookchiq @JanJakes @noruzzamans @shimotmk
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@mho22
+The following contributors merged PRs in this release:
+@adamziel @ashfame @bgrgicak @bph @brandonpayton @fellyph @JanJakes @mho22 @noruzzamans @Omcodes23 @shimotmk
+considerPrimary option from acquirePHPInstance. (#3191)trunk instead of develop. (#3206)The following contributors merged PRs in this release:
+@adamziel @akirk @beryl-dlg @epeicher @fellyph @JanJakes @mho22 @noruzzamans
+The following contributors merged PRs in this release:
+@adamziel @akirk @noruzzamans
+The following contributors merged PRs in this release:
+@adamziel @akirk @beryl-dlg @noruzzamans @shimotmk
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @fellyph
+The following contributors merged PRs in this release:
+@adamziel @fellyph
+start command for easy local WordPress development. (#3040)--debug switch in favor of --verbosity=debug. (#3084)@wp-playground/cli start persist sites. (#3119)?page-title query parameter. (#3116)The following contributors merged PRs in this release:
+@adamziel @fellyph @mho22
+The following contributors merged PRs in this release:
+@adamziel @fellyph @noruzzamans
+The following contributors merged PRs in this release:
+@brandonpayton @iamsohilvahora
+express dependency to 4.22.0 to fix qs security issue. (#3089)The following contributors merged PRs in this release:
+@mho22 @noruzzamans
+The following contributors merged PRs in this release:
+@adamziel @noruzzamans @shimotmk
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+playground.goTo(). (#3066)The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@mho22
+The following contributors merged PRs in this release:
+@adamziel @akirk
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+runPHPWithOptions example. (#2986)--experimental-unsafe-ide-integration option in PHP.wasm CLI. (#2947)mho22 to GitHub workflows actors. (#3046)vite-plugin-dts to Playground Storage. (#3035)ignore-*-imports Vite plugin inside a vite-extension. (#2999)preserve-*-loaders-imports Vite plugin inside a vite-extension. (#3002)intl dynamic extension to @php-wasm/web. (#2591)intl extension artifacts. (#2970)runPHPWithOptions demo. (#2978)###
+xdebug into shared library directory. (#3045)SQLITE_ENABLE_COLUMN_METADATA and update SQLite. (#2948)The following contributors merged PRs in this release:
+@adamziel @akirk @andr3ribeiro @bgrgicak @brandonpayton @epeicher @fellyph @JanJakes @jeffpaul @mho22 @shimotmk @SirLouen @Utsav-Ladani @wojtekn
+###
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph @mehrazmorshed @praful2111 @shimotmk @SirLouen @Successfulsebunya
+The following contributors merged PRs in this release:
+@fellyph @hmbashar @huzaifaalmesbah @shimotmk
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph
+--xdebug enabled. (#2835)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph @noruzzamans @shimotmk
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton @noruzzamans
+--experimental-unsafe-ide-integration option in Playground CLI. (#2777)The following contributors merged PRs in this release:
+@adamziel @akirk @Dhruval-678 @fellyph @mho22 @noruzzamans
+remote.html vs index.html. (#2817)The following contributors merged PRs in this release:
+@adamziel @shimotmk
+The following contributors merged PRs in this release:
+@shimotmk
+--verbosity=debug. (#2799)The following contributors merged PRs in this release:
+@adamziel @amieiro @beryl-dlg @brandonpayton @fellyph @shimotmk
+The following contributors merged PRs in this release:
+@adamziel
+xdebug for phpstorm compatibility. (#2747)The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @brandonpayton @fellyph @mho22 @shimotmk @wojtekn @zaerl
+The following contributors merged PRs in this release:
+@getdave @mho22 @shail-mehta
+The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @dilipom13
+The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @brandonpayton @fellyph
+The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @jdahir0789 @shimotmk
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @beryl-dlg @fellyph @shimotmk
+The following contributors merged PRs in this release:
+@adamziel @shail-mehta
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @akkspros @brandonpayton @fellyph @mho22 @rollybueno @shail-mehta @shimotmk
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @akirk
+The following contributors merged PRs in this release:
+@brandonpayton @mho22
+The following contributors merged PRs in this release:
+@fellyph @mho22 @rollybueno @shail-mehta
+The following contributors merged PRs in this release:
+@shail-mehta @shimotmk
+intl dynamic extension to @php-wasm/node ASYNCIFY #2501. (#2557)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @dd32 @fellyph @mho22 @shail-mehta @shimotmk
+The following contributors merged PRs in this release:
+@adamziel @draganescu @shail-mehta
+intl dynamic extension to @php-wasm/node JSPI. (#2501)The following contributors merged PRs in this release:
+@adamziel @jnealey88 @mho22 @rollybueno
+less work as pager. (#2554)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @JanJakes @mho22 @shail-mehta
+The following contributors merged PRs in this release:
+@aslamdoctor @brandonpayton @fellyph @rollybueno @sandipr942 @shail-mehta @shimotmk
+The following contributors merged PRs in this release:
+@aslamdoctor @brandonpayton @fellyph @josevarghese @mho22 @shimotmk
+The following contributors merged PRs in this release:
+@brandonpayton @fellyph @juanmaguitar @mukeshpanchal27 @ravigadhiya007 @rollybueno @shail-mehta @shimotmk
+The following contributors merged PRs in this release:
+@adamziel @rollybueno @shimotmk @zaerl
+unreachable crashes when using Devtools. (#2454)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph @JanJakes @mho22
+@php-wasm/cli and @wp-playground/cli. (#2441)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph @mho22 @nikunj8866 @zaerl
+The following contributors merged PRs in this release:
+@brandonpayton @fellyph @rollybueno @shimotmk @vipul0425
+The following contributors merged PRs in this release:
+@brandonpayton @shimotmk
+--experimental-devtools option in Playground CLI. (#2411)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @mho22 @shimotmk
+--experimental-devtools option in php-wasm CLI. (#2408)The following contributors merged PRs in this release:
+@brandonpayton @mho22
+###
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @fellyph @shimotmk @zaerl
+--xdebug option in php-wasm CLI and wp-playground CLI. (#2346)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @fellyph @mho22 @shimotmk
+xdebug shared extension to @php-wasm/node ASYNCIFY. (#2326)The following contributors merged PRs in this release:
+@adamziel @brandonpayton @fellyph @mho22 @rollybueno @shimotmk
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @JanJakes @mho22
+DB_NAME constant when it's missing. (#140)The following contributors merged PRs in this release:
+@brandonpayton @JanJakes
+The following contributors merged PRs in this release:
+@ashfame @bgrgicak @sejas
+The following contributors merged PRs in this release:
+@ashfame
+The following contributors merged PRs in this release:
+@adamziel @ashfame @bgrgicak @ingeniumed @sejas
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @ingeniumed
+skipSqliteSetup flag for MySQL support. (#97)The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton @ivan-ottinger @wojtekn
+The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton @wojtekn
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+mode query arg when opening OPFS site. (#63)The following contributors merged PRs in this release:
+@adamziel @brandonpayton
+janjakes to GitHub workflows actors. (#33)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @ingeniumed @JanJakes @zaerl
+The following contributors merged PRs in this release:
+@ashfame @brandonpayton @zaerl
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @mbuella
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @zaerl
+@php-wasm packages as dual ESM + CJS. (#2087)###
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @ajotka @akirk @brandonpayton @maxschmeling
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @zaerl
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @ajotka @bgrgicak @brandonpayton @StevenDufresne @zaerl
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+The following contributors merged PRs in this release:
+@bgrgicak
+The following contributors merged PRs in this release:
+@adamziel @ajotka @bgrgicak
+The following contributors merged PRs in this release:
+@adamziel @psrpinto
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+The following contributors merged PRs in this release:
+@adamziel @ajotka @bgrgicak @bph @brandonpayton @ockham @psrpinto
+? instead of / to CORS Proxy URLs. (#1899)The following contributors merged PRs in this release:
+@adamziel @ashfame @bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+isWordPressInstalled() in CLI by inlining the auto_login.php in index.ts instead of using import ?raw. (#1869)The following contributors merged PRs in this release:
+@adamziel @ajotka @bgrgicak @brandonpayton @dd32 @n8finch
+The following contributors merged PRs in this release:
+@adamziel @akirk @brandonpayton @juanmaguitar
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @juanmaguitar
+The following contributors merged PRs in this release:
+@jeroenpf @juanmaguitar
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @juanmaguitar @n8finch
+The following contributors merged PRs in this release:
+@adamziel
+components package with PathMappingControl. (#1608)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22 @peterwilsoncc
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@brandonpayton @jeroenpf @juanmaguitar
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@ironprogrammer @kozer @peterwilsoncc
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@brandonpayton @eliot-akira @mirka
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton @juanmaguitar
+The following contributors merged PRs in this release:
+@brandonpayton @jonathanbossenger
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+ws package version to fix DOS vulnerability. (#1635)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @PiotrPress
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @smithjw1
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @bph @dd32
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@ndiego
+###
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak
+The following contributors merged PRs in this release:
+@adamziel @bph @brandonpayton @dd32 @oskosk
+###
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @flexseth @ironnysh @josevarghese
+The following contributors merged PRs in this release:
+@brandonpayton
+The following contributors merged PRs in this release:
+@adamziel
+@wp-playground/wordpress-builds package. (#1343)The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22
+iframes are responsive. (#1267)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @ironnysh @peeranat-dan
+shorthand alternatives of Blueprint steps. (#1261)The following contributors merged PRs in this release:
+@adamziel @dd32 @ironnysh @kozer
+The following contributors merged PRs in this release:
+@adamziel @artpi @bph @brandonpayton @eliot-akira @flexseth @ironnysh @kirjavascript
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@bgrgicak @brandonpayton
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton @emmanuel-ferdman @fluiddot
+The following contributors merged PRs in this release:
+@adamziel @brandonpayton @seanmorris
+--disable-all configuration option in PHP compile process. (#1132)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @brandonpayton @flexseth @jblz @mho22
+The following contributors merged PRs in this release:
+@0aveRyan @adamziel @bgrgicak @brandonpayton @ironnysh @mho22 @seanmorris @StevenDufresne
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel
+The following contributors merged PRs in this release:
+@adamziel @eliot-akira @reimic @renatho
+The following contributors merged PRs in this release:
+@adamziel @bgrgicak @dd32 @desrosj @johnbillion @mho22
+The following contributors merged PRs in this release:
+@adamziel @mho22
+crypto.randomUUID dependency in favor of a custom function. (#1016)The following contributors merged PRs in this release:
+@adamziel @bgrgicak @jdevalk @sejas @stoph
+crypto to Polyfills improving Blueprint compatibility for Node. (#1000)The following contributors merged PRs in this release:
+@adamziel @sejas
+The following contributors merged PRs in this release:
+@adamziel @bph @ironnysh @marcarmengou @mho22 @rowasc @seanmorris @swissspidy @tyrann0us
+– Breaking: Remoddsaved the PHPBrowser class (##1302)
+– Added CHANGELOG.md to keep track of notable changes (##1302)
+Original Playground docs source: https://playground.wordpress.net/changelog
]]>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:
+ +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.
++Tip: 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.
+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 wp-now's default port or set a local test domain running via HTTPS.
To change wp-now's default port to the one supported by WordPress Multisite, run it using the --port=80 flag:
npx @wp-now/wp-now 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.
+If you're using VS Code and have Chrome installed, you can debug Playground in the code editor:
+F5/fn+F5.Playground logs PHP errors in the browser console after every PHP request.
+Original Playground docs source: https://playground.wordpress.net/contributing/code
]]>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?
+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.
+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.
+Blueprints are the primary way to interact with Playground. These JSON files describe a set of steps that Playground executes in order.
+Blueprint steps should be concise and focused. They should do one thing and do it well.
+Blueprints should be intuitive and straightforward.
+slug instead of path.Original Playground docs source: https://playground.wordpress.net/contributing/coding-standards
]]>This page provides detailed information on the Playground Contributor Badge and the process of requesting it on your WordPress.org profile.
+---
+Any contribution to the WordPress Playground project is highly valued. The Playground team recognizes contributions across several key areas:
+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.
+
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.
+
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:
+ +
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.
+Original Playground docs source: https://playground.wordpress.net/contributing/contributor-badge
]]>This guide helps table leads prepare for and manage a WordPress Playground contributor table at WordCamp events.
+#playground channel on WordPress Slack. This centralizes communication and enables asynchronous collaboration with late arrivals.#playground channel: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:
+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.
+#playground channel, answering questions and helping them become regular contributors.#playground Slack channel.For more information on contributing to WordPress Playground, see the Contributor Day guide.
+Original Playground docs source: https://playground.wordpress.net/contributing/table-lead-guide
]]>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.
+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.
+This section outlines how you can contribute directly to the WordPress Playground project and its associated tools:
+All feedback, including reported issues and test results, can be submitted through our GitHub repository.
+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.
+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.
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.
+The Visual Studio Code Playground extension is a friendly zero-setup development environment.
+@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.
@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.
+@wp-playground/cliYou 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
+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.
+# 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
+You don’t need a local development environment to test Gutenberg PRs—use Playground to do it directly 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.
+Have a question or an idea for a new feature? Found a bug? Something’s not working as expected? We’re here to help:
+Original Playground docs source: https://playground.wordpress.net/contributing/contributor-day
]]>WordPress Playground's documentation site is maintained by volunteers like you, who'd love your help.
+All documentation-related issues are labeled [[Type] Documentation](https://github.com/WordPress/wordpress-playground/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%5BType%5D%20Documentation%22) or [[Type] Developer Documentation](https://github.com/WordPress/wordpress-playground/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%5BType%5D%20Developer%20Documentation%22) in the WordPress/wordpress-playground 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.
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.
+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: https://developer.wordpress.org/block-editor/contributors/code/git-workflow/
+The documentation files (.md files) are stored in Playground's GitHub repository, under /packages/docs/site/docs for English and /packages/docs/site/i18n for other languages.
If logged in GitHub, you can also edit existing files (or add new ones) and submit a PR directly from the GitHub UI:
+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.
+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.
+Original Playground docs source: https://playground.wordpress.net/contributing/documentation
]]>WordPress Playground is an open-source project that welcomes contributors of all kinds, from code to design, documentation to triage.
+Want to help sort through open issues and resolve potential bugs? Here's how:
+WordPress Playground and the WordPress project are strongly rooted in free and open source software. Specifically, WordPress Playground is licenced under GPLv2 (or later) from the Free Software Foundation. You can read the text of the license here and if that feels overwhelming, WordPress.org has a friendly GPL Primer.
+As such, please be aware of the implications that your contributions will fall under:
+If you have any questions about how the above might affect your contributions, please feel free to reach out on WP Slack and the meta-playground channel.
Thank you again for your contributions! 🎉
+Original Playground docs source: https://playground.wordpress.net/contributing
]]>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.
+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.
+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:
+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.
+After the initial publish, go to the package's settings on npmjs.com and set up OIDC trusted publishing:
+WordPresswordpress-playgroundpublish-npm-packages.ymlnpm
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.
+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.
+Original Playground docs source: https://playground.wordpress.net/contributing/releases
]]>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
+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.
+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',
+ },
+ },
+}
+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.
+When adding a new language, you can generate the necessary JSON files for UI strings (like button labels and navigation items) by running the following command from the packages/docs/site directory:
npm run write-translations -- --locale <LANGUAGE_CODE>
+With the proper i18n docusaurus.config.js configuration and files under i18n when running npm run build:docs from the root of the project, specific folders under dist for each language will be created.
To preview your changes for an existing language:
+packages/docs/site/i18n/es/docusaurus-plugin-content-docs/current/./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 is a dropdown menu that allows users to select their preferred language.
+
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:
+ +All languages are available once the i18n setup for a language is complete and the correct file structure is in place under i18n.
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"
+ }
+Follow these steps to translate a page:
+packages/docs/site/docs/... to the corresponding path in the language directory (e.g., packages/docs/site/i18n/<LANGUAGE_CODE>/...). It is crucial to replicate the original file structure.<!-- English Content -->.packages/docs/site/static/img/ only place assets inside the translation folder when it requires localized content.npm run build:docs.[i18n] to help to identify the translations#playground or #polyglots at wordpress.slack.comWe 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.
+You can use the following markdown in your tracking issue:
+## Remaining translation pages
+
+<details open>
+<summary><h3>Main</h3></summary>
+
+- about
+ - [ ] build.md
+ - [ ] index.md
+ - [ ] launch.md
+ - [ ] test.md
+- contributing
+ - [ ] code.md
+ - [ ] coding-standards.md
+ - [ ] contributor-badge.md
+ - [ ] contributor-day.md
+ - [ ] contributor-day-table-lead.md
+ - [ ] documentation.md
+ - [ ] index.md
+ - [ ] releases.md
+ - [ ] translations.md
+- guides
+ - [ ] for-plugin-developers.md
+ - [ ] for-theme-developers.md
+ - [ ] github-action-pr-preview.md
+ - [ ] index.md
+ - [ ] providing-content-for-your-demo.md
+ - [ ] wordpress-native-ios-app.md
+- [ ] changelog.md
+- [ ] intro.md
+- [ ] quick-start-guide.md
+- [ ] resources.md
+- [ ] web-instance.md
+
+</details>
+
+<details open>
+<summary><h3>Blueprints</h3></summary>
+
+- [ ] 01-index.md
+- [ ] 02-using-blueprints.md
+- [ ] 03-data-format.md
+- [ ] 04-resources.md
+- [ ] 05-steps.md
+- [ ] 05-steps-shorthands.md
+- [ ] 06-bundles.md
+- [ ] 07-json-api-and-function-api.md
+- [ ] 08-examples.md
+- [ ] 09-troubleshoot-and-debug-blueprints.md
+- [ ] intro.md
+- tutorial
+ - [ ] 01-what-are-blueprints-what-you-can-do-with-them.md
+ - [ ] 02-how-to-load-run-blueprints.md
+ - [ ] 03-build-your-first-blueprint.md
+ - [ ] index.md
+
+</details>
+
+<details open>
+<summary><h3>Developers</h3></summary>
+
+- 03-build-an-app
+ - [ ] 01-index.md
+- 05-local-development
+ - [ ] 01-wp-now.md
+ - [ ] 02-vscode-extension.md
+ - [ ] 03-php-wasm-node.md
+ - [ ] 04-wp-playground-cli.md
+ - [ ] intro.md
+- 06-apis
+ - [ ] 01-index.md
+ - javascript-api
+ - [ ] 01-index.md
+ - [ ] 02-index-html-vs-remote-html.md
+ - [ ] 03-playground-api-client.md
+ - [ ] 04-blueprint-json-in-api-client.md
+ - [ ] 05-blueprint-functions-in-api-client.md
+ - [ ] 06-mount-data.md
+ - query-api
+ - [ ] 01-index.md
+- 07-xdebug
+ - [ ] 01-introduction.md
+ - [ ] 02-getting-started.md
+- 23-architecture
+ - [ ] 01-index.md
+ - [ ] 02-wasm-php-overview.md
+ - [ ] 03-wasm-php-compiling.md
+ - [ ] 04-wasm-php-javascript-module.md
+ - [ ] 05-wasm-php-filesystem.md
+ - [ ] 07-wasm-asyncify.md
+ - [ ] 08-browser-concepts.md
+ - [ ] 09-browser-tab-orchestrates-execution.md
+ - [ ] 10-browser-iframe-rendering.md
+ - [ ] 11-browser-php-worker-threads.md
+ - [ ] 12-browser-service-workers.md
+ - [ ] 13-browser-scopes.md
+ - [ ] 14-browser-cross-process-communication.md
+ - [ ] 15-wordpress.md
+ - [ ] 16-wordpress-database.md
+ - [ ] 17-browser-wordpress.md
+ - [ ] 18-host-your-own-playground.md
+- 24-limitations
+ - [ ] 01-index.md
+- [ ] intro-devs.md
+
+</details>
+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.
+---
+packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/.
---
+packages/docs/site/docs/main/contributing/documentation.mdpackages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/main/contributing/documentation.md/packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/). Click Add file > Create new file.
/. For example, typing main/contributing/documentation.md will create the main and contributing folders automatically. <!--
+ This is the original English content.
+ It helps reviewers understand the context of the translation.
+ -->
+
+ Ceci est le contenu traduit en français.
+
To simplify the review process, please keep the original English text as a comment directly above the translated content.
+<!--
+👋 Hi! Welcome to WordPress Playground documentation.
+
+Playground is an online tool to experiment and learn about WordPress. This site (Documentation) is where you will find all the information you need to start using Playground.
+-->
+
+👋 Olá! Bem vindo a documentação oficial do WordPress Playground.
+
+WordPress Playground é uma ferramenta online onde podes testar e aprender mais sobre o WordPress. Nesta página(Documentação) irá encontrar todas as informações necessárias para começar a trabalhar com o Playground.
+This practice also helps the maintenance team identify outdated translations. When the original English content is updated, we can search the codebase for the old text (now in comments) and flag the corresponding translation for review.
+To find a reviewer fluent in the language of your PR, you can post a request on the Make WordPress Polyglots blog. 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.
Original Playground docs source: https://playground.wordpress.net/contributing/translations
]]>Want an AI assistant that already knows how to spin up WordPress instances, run Blueprints, and debug plugins? The wp-playground agent skill teaches coding agents the WordPress Playground CLI and browser workflows. You describe what you need in plain language. The agent handles the commands.
+Your coding agent reads the skill reference — a document with CLI flags, procedures, and troubleshooting steps — before responding. This ensures Playground commands run correctly.
+Before installing the skill, confirm you have:
+| Requirement | Minimum version | Check command |
+| ----------- | --------------------- | --------------- |
+| Node.js | 20.18 | `node -v` |
+| npm / npx | Included with Node.js | `npx --version` |
+You also need a coding agent that supports agent skills: Antigravity, Claude Code, Codex, Copilot, Cursor, or Gemini CLI. Make sure your CLI or IDE runs the latest version. Output quality depends on your chosen model.
+Install the skill using the npx skills CLI:
npx skills add wordpress/agent-skills --skill wp-playground
+# Clone agent-skills
+git clone https://github.com/WordPress/agent-skills.git
+cd agent-skills
+
+# Build the distribution
+node shared/scripts/skillpack-build.mjs --clean
+
+# Install into your WordPress project
+node shared/scripts/skillpack-install.mjs --dest=../your-wp-project --targets=codex,vscode,claude,cursor,antigravity,gemini
+This copies skills into:
+.github/skills/ for VS Code / GitHub Copilot.claude/skills/ for Claude Code.cursor/skills/ for Cursor.agent/skills/ for Antigravity.gemini/skills/ for Gemini CLI.codex/skills/ for CodexVerify installation by checking the skill directory exists for your agent:
+| Agent | Skill directory |
+| -------------- | ------------------------------- |
+| Claude Code | `.claude/skills/wp-playground/` |
+| Gemini CLI | `.gemini/skills/wp-playground/` |
+| GitHub Copilot | `.github/skills/wp-playground/` |
+| Cursor | `.cursor/skills/wp-playground/` |
+| Antigravity | `.agent/skills/wp-playground/` |
+| Codex | `.codex/skills/wp-playground/` |
+Some agents also support listing skills directly:
+# Claude Code
+claude /skills
+
+# Gemini CLI
+gemini /skills list
+With the skill installed, describe your WordPress environment to your coding agent. The agent builds the Blueprint, runs the CLI commands, and starts the server.
+Open your coding agent in the terminal and type your request:
+> Run a WordPress instance with my plugin mounted
+The agent reads the skill reference, detects your project layout, and runs server --auto-mount. The instance starts at http://localhost:9400.
Need sample data for testing or a demo? Describe the content structure you want:
+> Run a WordPress with 10 published posts
+The agent creates a Blueprint with a runPHP step that generates the posts using wp_insert_post().
More examples:
+> Run a WordPress with 3 users where each user has 3 posts
+> Start a WordPress instance with 5 pages and a custom menu linking to all of them
+> Create a WordPress site with 20 posts across 4 categories
+Each prompt produces a complete Blueprint that runs locally, handling user creation, role assignment, post generation, and taxonomy setup through Blueprint steps.
+Does your plugin work on older PHP versions? Ask directly:
+> Test my plugin on WordPress 6.3 with PHP 7.4
+> Run my theme on the latest WordPress nightly with PHP 8.5
+The agent adds --wp and --php flags to match your request. Common combinations:
| Scenario | What to ask |
+| ----------------- | ----------------------------------------------------------- |
+| Latest stable | "Run a WordPress instance" (defaults to latest WP, PHP 8.3) |
+| Minimum supported | "Test my plugin on WordPress 6.3 with PHP 7.4" |
+| Upcoming release | "Run the WordPress nightly build" |
+| Legacy PHP | "Start WordPress with PHP 7.4" |
+Combine multiple requirements in a single prompt:
+> Create a WordPress site with WooCommerce, 3 product categories,
+ 10 sample products, and 2 customer accounts — running on PHP 8.2
+> Run a WordPress instance with my plugin mounted, debug mode enabled,
+ and 5 test posts that include featured images
+The agent breaks these into the right sequence of Blueprint steps and CLI flags. Each request produces a fully configured, running instance.
+The wp-playground skill is a set of Markdown files that your coding agent loads into its context when your request matches Playground-related patterns. The skill includes:
+Your coding agent reads these files before generating commands, ensuring correct flags and warning you about common pitfalls.
+Original Playground docs source: https://playground.wordpress.net/guides/agent-skill-wp-playground
]]>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.
+@typescript-eslint/no-floating-promises ESLint rule to catch missing await on async Playwright callsFrom 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.
+Create a playwright.config.ts file in your project root:
import { defineConfig } from '@playwright/test';
+
+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.
+Tip: [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.
+Tip
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.
+Create tests/e2e/plugin.spec.ts:
import { test, expect } from '@playwright/test';
+import { runCLI } from '@wp-playground/cli';
+
+let cli: Awaited<ReturnType<typeof runCLI>>;
+
+test.beforeAll(async () => {
+ cli = await runCLI({
+ command: 'server',
+ blueprint: {
+ preferredVersions: { php: '8.3', wp: 'latest' },
+ login: true,
+ },
+ });
+});
+
+test.afterAll(async () => {
+ await cli?.server?.close();
+});
+
+test('WordPress dashboard loads', async ({ page }) => {
+ await page.goto(`${cli.serverUrl}/wp-admin/`);
+ // WordPress core admin elements lack ARIA roles — CSS selectors are acceptable here
+ await expect(page.locator('#wpbody-content')).toBeVisible();
+ await expect(page).toHaveTitle(/Dashboard/);
+});
+Run the test:
+npx playwright test
+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 controlspage.getByLabel() — form inputs with associated labelspage.getByText() — visible text contentpage.getByTestId() — elements with data-testid attributes you add to your pluginpage.locator() — CSS or XPath selectors as a last resortIn 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:
+<button>, <input>, <a>, and <h1> elements that getByRole and getByLabel can find.data-testid for your own plugin markup — you control the HTML, so add testable attributes.#wpadminbar or #wpbody-content — these lack ARIA alternatives.// ✅ Preferred: semantic locator (works because WP renders a real <button>)
+await page.getByRole('button', { name: 'Save Changes' }).click();
+
+// ⚠️ Acceptable: test ID you added to your plugin markup
+await page.getByTestId('save-settings').click();
+
+// ❌ Avoid: brittle CSS selector tied to WordPress markup
+await page.locator('#submit').click();
++Tip: [Generate locators automatically]
Run npx playwright codegen localhost:9400/wp-admin/ to open a browser and record interactions. Playwright generates locator code as you click, helping you discover which semantic locators work for each element.
Playwright locators wait automatically for elements to appear, become visible, and become actionable. You do not need manual waitForSelector calls in most cases.
Web-first assertions auto-retry until the condition passes or the timeout expires. Always prefer them over manual checks:
+// ✅ Web-first assertion (auto-retries until visible or timeout)
+await expect(page.getByText('Settings saved')).toBeVisible();
+
+// ❌ Manual check (no retry — flaky if the element appears after a delay)
+expect(await page.getByText('Settings saved').isVisible()).toBe(true);
+Use expect.soft() to check multiple things on one page without stopping at the first failure. All failures appear in the test report:
await expect.soft(page.getByLabel('API Key')).toHaveValue('test-key-123');
+await expect.soft(page.getByText('Settings saved')).toBeVisible();
+await expect.soft(page.getByRole('heading', { level: 1 })).toContainText('Settings');
+The runCLI function starts a local Playground server and returns an object with serverUrl (the URL string) and server (the HTTP server instance). Pass a Blueprint to configure the WordPress instance:
const cli = await runCLI({
+ command: 'server',
+ blueprint: {
+ preferredVersions: { php: '8.3', wp: 'latest' },
+ login: true,
+ steps: [
+ {
+ step: 'installPlugin',
+ pluginData: {
+ resource: 'wordpress.org/plugins',
+ slug: 'woocommerce',
+ },
+ },
+ ],
+ },
+});
+Shared server (beforeAll/afterAll) — one Playground instance serves all tests in a describe block. Faster, but tests can affect each other:
test.describe('Plugin settings', () => {
+ test.beforeAll(async () => {
+ cli = await runCLI({ command: 'server', blueprint });
+ });
+ test.afterAll(async () => {
+ await cli?.server?.close();
+ });
+ // Tests share the same WordPress instance
+});
+Per-test server (beforeEach/afterEach) — each test gets a fresh instance. Slower, but fully isolated:
test.beforeEach(async () => {
+ cli = await runCLI({ command: 'server', blueprint });
+});
+test.afterEach(async () => {
+ await cli?.server?.close();
+});
+Use shared servers when tests only read state (checking pages render). Use per-test servers when tests modify state (creating posts, changing settings).
+Blueprints define the WordPress state each test scenario needs. Here are common patterns:
+const blueprint = {
+ preferredVersions: { php: '8.3', wp: 'latest' },
+ login: true,
+ steps: [
+ {
+ step: 'installPlugin',
+ pluginData: {
+ resource: 'wordpress.org/plugins',
+ slug: 'contact-form-7',
+ },
+ },
+ ],
+};
+Mount your local plugin directory into the Playground instance:
+const cli = await runCLI({
+ command: 'server',
+ mount: {
+ './': '/wordpress/wp-content/plugins/my-plugin',
+ },
+ blueprint: {
+ preferredVersions: { php: '8.3', wp: 'latest' },
+ login: true,
+ steps: [
+ {
+ step: 'activatePlugin',
+ pluginPath: 'my-plugin/my-plugin.php',
+ },
+ ],
+ },
+});
+This maps your current directory to the plugin path inside WordPress, then activates the plugin. Changes to your local files are reflected immediately. The user can set the autoMount property to identify plugins and themes, but the mount property will provide more control to the user to set different folders in the project.
const blueprint = {
+ login: true,
+ steps: [
+ {
+ step: 'setSiteOptions',
+ options: {
+ blogname: 'Test Site',
+ permalink_structure: '/%postname%/',
+ },
+ },
+ {
+ step: 'runPHP',
+ code: `<?php
+ require '/wordpress/wp-load.php';
+ wp_insert_post([
+ 'post_title' => 'Test Post',
+ 'post_content' => '<!-- wp:paragraph --><p>Hello World</p><!-- /wp:paragraph -->',
+ 'post_status' => 'publish',
+ ]);
+ `,
+ },
+ ],
+};
++Tip
Use the Playground Step Library or Pootle Playground to prototype your Blueprint configuration visually before adding it to your test code.
+Navigate to admin pages and interact with the WordPress UI:
+test('plugin settings page saves options', async ({ page }) => {
+ await page.goto(`${cli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);
+
+ await page.getByLabel('API Key').fill('test-key-123');
+ await page.getByRole('button', { name: 'Save Changes' }).click();
+
+ await expect(page.getByText('Settings saved')).toBeVisible();
+ await expect(page.getByLabel('API Key')).toHaveValue('test-key-123');
+});
+// Dismiss WordPress admin notices (WP adds aria-label to dismiss buttons)
+await page.getByRole('button', { name: 'Dismiss this notice' }).first().click();
+
+// Wait for admin bar to load — no ARIA role available, use locator
+await page.locator('#wpadminbar').waitFor();
+
+// Navigate via admin menu
+await page.getByRole('link', { name: 'My Plugin' }).first().click();
+test('plugin shortcode renders on front end', async ({ page }) => {
+ // Navigate to a page with the shortcode
+ await page.goto(`${cli.serverUrl}/?p=2`);
+
+ // Recommend: add data-testid="my-plugin-widget" to your plugin markup
+ await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
+ await expect(page.getByTestId('my-plugin-widget')).toContainText('Expected content');
+ // Or use CSS if you don't control the markup:
+ // await expect(page.locator(".my-plugin-widget")).toBeVisible();
+});
+
+test('theme displays post correctly', async ({ page }) => {
+ await page.goto(`${cli.serverUrl}/test-post/`);
+
+ await expect(page.getByRole('heading', { level: 1 })).toContainText('Test Post');
+ await expect(page.getByText('Hello World', { exact: true })).toBeVisible();
+});
+The Page Object Model (POM) wraps page interactions into reusable classes. This reduces duplication and makes tests easier to maintain when your plugin UI changes.
+// tests/e2e/pages/plugin-settings.ts
+import { type Page, type Locator, expect } from '@playwright/test';
+
+export class PluginSettingsPage {
+ readonly page: Page;
+ readonly apiKeyInput: Locator;
+ readonly saveButton: Locator;
+ readonly successNotice: Locator;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.apiKeyInput = page.getByLabel('API Key');
+ this.saveButton = page.getByRole('button', { name: 'Save Changes' });
+ this.successNotice = page.getByText('Settings saved');
+ }
+
+ async goto(baseUrl: string) {
+ await this.page.goto(`${baseUrl}/wp-admin/options-general.php?page=my-plugin`);
+ }
+
+ async setApiKey(key: string) {
+ await this.apiKeyInput.fill(key);
+ await this.saveButton.click();
+ }
+
+ async expectSaved() {
+ await expect(this.successNotice).toBeVisible();
+ }
+}
+Use the POM in tests:
+import { PluginSettingsPage } from './pages/plugin-settings';
+
+test('save plugin settings', async ({ page }) => {
+ const settings = new PluginSettingsPage(page);
+ await settings.goto(cli.serverUrl);
+ await settings.setApiKey('test-key-123');
+ await settings.expectSaved();
+});
+The Playground project uses this pattern with a WebsitePage class that provides methods like goto(), wordpress(), and getSiteTitle() — encapsulating navigation and WordPress-specific interactions.
Parameterized tests cover multiple version combinations without duplicating test code:
+const versionMatrix = [
+ { php: '8.1', wp: '6.5' },
+ { php: '8.2', wp: '6.7' },
+ { php: '8.3', wp: 'latest' },
+];
+
+for (const { php, wp } of versionMatrix) {
+ test.describe(`PHP ${php} + WP ${wp}`, () => {
+ let versionCli: Awaited<ReturnType<typeof runCLI>>;
+
+ test.beforeAll(async () => {
+ versionCli = await runCLI({
+ command: 'server',
+ blueprint: {
+ preferredVersions: { php, wp },
+ login: true,
+ steps: [
+ {
+ step: 'activatePlugin',
+ pluginPath: 'my-plugin/my-plugin.php',
+ },
+ ],
+ },
+ });
+ });
+
+ test('admin page loads without errors', async ({ page }) => {
+ await page.goto(`${versionCli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);
+ // WordPress core elements use CSS selectors — no ARIA roles available
+ await expect(page.locator('.error')).not.toBeVisible();
+ await expect(page.locator('#wpbody-content')).toBeVisible();
+ });
+
+ test('front-end output renders', async ({ page }) => {
+ await page.goto(versionCli.serverUrl);
+ await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
+ });
+
+ test.afterAll(async () => {
+ await versionCli?.server?.close();
+ });
+ });
+}
+The preferredVersions property in the Blueprint controls which PHP and WordPress versions the Playground instance uses. Supported ranges: PHP 7.4–8.5, WordPress 6.3–6.8+, plus latest, nightly, and beta. For type-safe PHP version values, use the SupportedPHPVersion type from @php-wasm/universal.
Create .github/workflows/e2e-tests.yml:
name: E2E Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ e2e:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ hashFiles('package-lock.json') }}
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: npx playwright install chromium --with-deps
+
+ - name: Run E2E tests
+ run: npx playwright test
+
+ - name: Upload test report
+ uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
+This workflow installs dependencies, downloads Chromium, runs the tests, and uploads the HTML report as an artifact. The --with-deps flag installs system libraries Chromium needs on Ubuntu.
+Tip: [Sharding for faster CI]
Split tests across multiple CI jobs with Playwright's built-in sharding:
+npx playwright test --shard=1/3
+npx playwright test --shard=2/3
+npx playwright test --shard=3/3
+Create three parallel jobs in your workflow matrix, each running a different shard. This reduces total CI time proportionally.
+For manual PR testing alongside automated E2E tests, see Adding PR Preview Buttons with GitHub Actions.
+Timeout errors — Increase timeout in playwright.config.ts. WordPress boot time varies by environment. CI runners often need 120–180 seconds.
Port conflicts — Let Playground auto-assign ports. Do not hardcode port numbers in your configuration. The serverUrl property returns the correct URL.
Browser not found — Run npx playwright install chromium to download the browser binary. On CI, add --with-deps for system libraries.
WordPress not loading — Check your Blueprint syntax against the Blueprint schema. Invalid steps fail silently in some cases.
+Tests pass locally but fail in CI — CI runners have less memory and CPU. Increase timeouts, reduce parallel workers, and ensure workers: 1 in the config.
When a test fails, Playwright provides several tools to investigate:
+Playwright Inspector — step through tests interactively with a built-in debugger:
+npx playwright test --debug
+Trace viewer — inspect a timeline of actions, DOM snapshots, and network requests from a failed test. The trace: "on-first-retry" setting in the config above captures traces automatically:
npx playwright show-trace test-results/plugin-spec-ts/trace.zip
+UI mode — run tests in a visual interface where you can watch, filter, and re-run them:
+npx playwright test --ui
+Screenshot on failure — the screenshot: "only-on-failure" setting in the config saves a screenshot whenever a test fails. Find screenshots in the test-results/ directory.
+Tip
Combine --debug with a specific test file to focus your investigation: npx playwright test tests/e2e/settings.spec.ts --debug
Original Playground docs source: https://playground.wordpress.net/guides/e2e-testing-with-playwright
]]>This guide will show you how to use WordPress Playground to improve your plugin development workflow, create live demos to showcase your plugin, and simplify your plugin testing and review.
+Discover how to Build, Test, and Launch your products with WordPress Playground in the About Playground section.
+With WordPress Playground, you can quickly launch a WordPress installation with almost any plugin available in the WordPress Plugins Directory installed and activated. All you need to do is to add the plugin query parameter to the Playground URL and use the slug of the plugin from the WordPress directory as a value. For example: https://playground.wordpress.net/?plugin=create-block-theme
+Tip
You can install and activate several plugins via query parameters by repeating the plugin parameter for every plugin you want to be installed and activated in the Playground instance. For example: https://playground.wordpress.net/?plugin=gutenberg&plugin=akismet&plugin=wordpress-seo.
You can also load any plugin from the WordPress plugins directory by setting the installPlugin step of a Blueprint passed to the Playground instance.
{
+ "landingPage": "/wp-admin/plugins.php",
+ "login": true,
+ "steps": [
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "wordpress.org/plugins",
+ "slug": "gutenberg"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+Blueprints can be passed to a Playground instance in several ways.
+A plugin stored in a GitHub repository can also be loaded in a Playground instance via Blueprints.
+With the pluginData property of the installPlugin blueprint step, you can define a git:directory resource that will build a plugin from the files from a repository in the Playground instance.
Use the git:directory resource to load plugin source code from a Git repository. It supports branches, tags, commits, and subdirectories without requiring you to create a ZIP archive first. If your plugin needs a Composer, npm, or other build step, publish a built ZIP artifact and install that artifact with a url resource instead.
For example, the following blueprint.json installs a plugin from a GitHub repository:
{
+ "landingPage": "/wp-admin/admin.php?page=add-media-from-third-party-service",
+ "login": true,
+ "steps": [
+ {
+ "step": "installPlugin",
+ "pluginData": {
+ "resource": "git:directory",
+ "url": "https://github.com/wptrainingteam/devblog-dataviews-plugin",
+ "ref": "HEAD",
+ "refType": "refname"
+ }
+ }
+ ]
+}
++Tip
If your plugin is hosted on GitHub, you can automatically add preview buttons to your pull requests using the Playground PR Preview GitHub Action. This lets reviewers test your changes instantly without any setup. See Adding PR Preview Buttons with GitHub Actions for details.
+<kbd> Run Blueprint </kbd>
+By combining the writeFile and activatePlugin steps you can also launch a WP Playground instance with a plugin built on the fly from code stored on a gist or a file in GitHub:
{
+ "landingPage": "/wp-admin/plugins.php",
+ "login": true,
+ "steps": [
+ {
+ "step": "login"
+ },
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/plugins/cpt-books.php",
+ "data": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/custom-post/books.php"
+ }
+ },
+ {
+ "step": "activatePlugin",
+ "pluginPath": "cpt-books.php"
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+The Install plugin from a gist example in the Blueprints Gallery shows how to load a plugin from code in a gist
+When providing a link to a WordPress Playground instance with some plugins activated, you may also want to customize the initial setup for that Playground instance using those plugins. With Playground's Blueprints you can load/activate plugins and configure the Playground instance.
++Tip
Some useful tools and resources provided by the Playground project to work with blueprints are:
+Through properties and steps in the Blueprint, you can configure the Playground instance's initial setup, providing your plugins with the content and configuration needed for showcasing your plugin's compelling features and functionality.
A great demo with WordPress Playground might require that you load default content for your plugin and theme, including images and other assets. Check out the Providing content for your demo guide to learn more about this.
+pluginsIf your plugin has dependencies on other plugins you can use the plugins shorthand to install yours along with any other needed plugins.
{
+ "landingPage": "/wp-admin/plugins.php",
+ "plugins": ["gutenberg", "sql-buddy", "create-block-theme"],
+ "login": true
+}
+<kbd> Run Blueprint </kbd>
+landingPageIf your plugin has a settings view or onboarding wizard, you can use the landingPage shorthand to automatically redirect to any page in the Playground instance upon loading.
{
+ "landingPage": "/wp-admin/admin.php?page=my-custom-gutenberg-app",
+ "login": true,
+ "plugins": ["https://raw.githubusercontent.com/WordPress/block-development-examples/deploy/zips/data-basics-59c8f8.zip"]
+}
+<kbd> Run Blueprint </kbd>
+writeFileWith the writeFile step you can create any plugin file on the fly, referencing code from a \*.php file stored on a GitHub or Gist.
Here’s an example of a plugin that generates Custom Post Types, placed in the mu-plugins folder to ensure the code runs automatically on load:
{
+ "landingPage": "/wp-admin/",
+ "login": true,
+ "steps": [
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/mu-plugins/books.php",
+ "data": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/custom-post/books.php"
+ }
+ }
+ ]
+}
+From a plugins' folder in your local development environment, you can quickly load locally a Playground instance with that plugin loaded and activated.
+Use the @wp-playground/cli command from your plugin's root directory using your preferred command line program.
With Visual Studio Code IDE, you can also use the Visual Studio Code extension while working in the root directory of your plugin.
+For example:
+git clone git@github.com:wptrainingteam/devblog-dataviews-plugin.git
+cd devblog-dataviews-plugin
+npx @wp-playground/cli server --auto-mount
+With Google Chrome you can synchronize a Playground instance with your local plugin's code and your plugin's GitHub repo. With this connection you can:
+Here's a little demo of this workflow in action:
+Embedded media: https://www.youtube.com/embed/UYK88eZqrjo
+Check About Playground > Build > Synchronize your playground instance with a local folder and create GitHub Pull Requests for more info.
+Original Playground docs source: https://playground.wordpress.net/guides/for-plugin-developers
]]>This guide will show you how to use WordPress Playground to improve your theme development workflow, create live demos to showcase your theme, and simplify the theme review process.
+Discover how to Build, Test, and Launch your products with WordPress Playground in the About Playground section
+With WordPress Playground, you can quickly launch a WordPress installation using any theme available in the WordPress Themes Directory. Simply pass the theme query parameter to the Playground URL like this: https://playground.wordpress.net/?theme=disco.
You can also load any theme from the WordPress themes directory by setting the installTheme step of a Blueprint passed to the Playground instance.
{
+ "steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "twentytwenty"
+ },
+ "options": {
+ "activate": true,
+ "importStarterContent": true
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+A theme stored in a GitHub repository can also be loaded in a Playground instance with Blueprints.
+With the themeData property of the installTheme blueprint step, you can define a git:directory resource that will build a theme from the files from a repository in the Playground instance.
Use the git:directory resource to load theme source code from a Git repository. It supports branches, tags, commits, and subdirectories without requiring you to create a ZIP archive first. If your theme needs a build step, publish a built ZIP artifact and install that artifact with a url resource instead.
For example the following blueprint.json installs a theme from a GitHub repository:
{
+ "steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "git:directory",
+ "url": "https://github.com/Automattic/themes",
+ "ref": "trunk",
+ "path": "assembler"
+ },
+ "options": {
+ "activate": true
+ }
+ }
+ ]
+}
++Tip
If your theme is hosted on GitHub, you can automatically add preview buttons to your pull requests using the Playground PR Preview GitHub Action. This lets reviewers test your changes instantly without any setup. See Adding PR Preview Buttons with GitHub Actions for details.
+<kbd> Run Blueprint </kbd>
+A blueprint can be passed to a Playground instance in several ways.
+When providing a link to a WordPress Playground instance with a specific theme activated, you may also want to customize the initial setup for that theme. With Playground's Blueprints you can load, activate, and configure a theme.
++Tip
Some useful tools and resources provided by the Playground project to work with blueprints are:
+Through properties and steps in the blueprint, you can configure the initial setup of your theme in the Playground instance.
To provide a good demo of your theme via Playground, you may want to load it with default content that highlights the features of your theme. Check out the Providing content for your demo guide to learn more about this.
+resetDataWith the resetData step, you can remove the default content of a WordPress installation in order to import your own content.
"steps": [
+ ...,
+ {
+ "step": "resetData"
+ },
+ ...
+]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+writeFileWith the writeFile step, you can write data to a file at a specified path. You may want to use this step to write custom PHP code in a PHP file inside the mu-plugins folder of the Playground WordPress instance, so the code is executed automatically when the WordPress instance is loaded. One of the things you can do through this step is to enable pretty permalinks for your Playground instance:
"steps": [
+ ...,
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/mu-plugins/rewrite.php",
+ "data": "<?php /* Use pretty permalinks */ add_action( 'after_setup_theme', function() { global $wp_rewrite; $wp_rewrite->set_permalink_structure('/%postname%/'); $wp_rewrite->flush_rules(); } );"
+ },
+ ...
+]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+updateUserMetaWith the updateUserMeta step, you can update any user metadata. For example, you could update the metadata of the default admin user of any WordPress installation:
"steps": [
+ ...,
+ {
+ "step": "updateUserMeta",
+ "meta": {
+ "first_name": "John",
+ "last_name": "Doe",
+ "admin_color": "modern"
+ },
+ "userId": 1
+ },
+ ...
+]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+setSiteOptionsWith the setSiteOptions step, you can set site options such as the site name, description, or page to use for posts.
"steps": [
+ ...,
+ {
+ "step": "setSiteOptions",
+ "options": {
+ "blogname": "Rich Tabor",
+ "blogdescription": "Multidisciplinary maker specializing in the intersection of product, design and engineering. Making WordPress.",
+ "show_on_front": "page",
+ "page_on_front": 6,
+ "page_for_posts": 2
+ }
+ },
+ ...
+]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+There's also a siteOptions shorthand that can be used instead of the setSiteOptions step.
pluginsWith the plugins shorthand you can set a list of plugins you want to be installed and activated with your theme in the Playground instance.
"plugins": ["todo-list-block", "markdown-comment-block"]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+You can also use the installPlugin step to install and activate plugins for your Playground instance but the shorthand way is recommended.
loginWith the login shorthand you can launch your Playground instance with the admin user logged in.
"login": true,
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+You can also use the login step to launch your Playground instance logged in with any specific user.
+Tip
The "Stylish Press" and "Loading, activating, and configuring a theme from a GitHub repository" examples from the Blueprints Gallery are great references for loading, activating, importing content, and configuring a block theme on a Playground instance.
+From the root folder of a block theme's code, you can quickly load locally a Playground instance with that theme loaded and activated. You can do that by launching, in a theme directory, the @wp-playground/cli command from your preferred command line program or the Visual Code Studio extension from the Visual Studio Code IDE.
For example:
+git clone git@github.com:WordPress/community-themes.git
+cd community-themes/blue-note
+npx @wp-playground/cli server --auto-mount
+You can connect your Playground instance to a GitHub repository and create a Pull Request with the changes you’ve done through the WordPress UI in the Playground instance, leveraging the Create Block Theme plugin. You can also make changes to that theme and export a zip.
+Note that you'll need the Create Block Theme plugin installed and activated in the Playground instance in order to use this workflow.
+Embedded media: https://www.youtube.com/embed/94KnoFhQg1g
++Tip
Check About Playground > Build > Save changes done on a Block Theme and create GitHub Pull Requests for more info.
+Original Playground docs source: https://playground.wordpress.net/guides/for-theme-developers
]]>
For complete configuration options and advanced features, see the action-wp-playground-pr-preview workflow README.
+The basic workflow runs on the pull_request event (types opened, synchronize, reopened, edited). It reads pull request metadata, builds a Playground URL that points at the PR branch, and updates the PR description or comment.
Forked pull requests need extra care because GitHub makes GITHUB_TOKEN read-only for pull_request workflows from forks. If you need to write a preview button for fork PRs, use pull_request_target only for a small workflow that reads PR metadata and writes the button. If your preview needs a build step, run the build in a separate pull_request workflow and publish the preview from a workflow_run workflow.
+Warning: This is a regular GitHub Action, not a reusable workflow
Reference it as a step inside jobs.<job_id>.steps: (i.e. jobs.<job_id>.steps[*].uses:) — never as jobs.<job_id>.uses: at the job level. The job-level form is valid YAML for reusable workflows, so it is a common mistake (including by AI coding assistants), but it will not work with this action.
For plugins without a build step, create .github/workflows/pr-preview.yml:
name: PR Preview
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+jobs:
+ preview:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Post Playground Preview Button
+ uses: WordPress/action-wp-playground-pr-preview@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ mode: 'append-to-description'
+ plugin-path: .
+The plugin-path: . setting points to your plugin directory. For subdirectories like plugins/my-plugin, use plugin-path: plugins/my-plugin.
See adamziel/preview-in-playground-button-plugin-example for a live example of this workflow in action.
+For themes, use theme-path instead of plugin-path:
name: PR Preview
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+jobs:
+ preview:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Post Playground Preview Button
+ uses: WordPress/action-wp-playground-pr-preview@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ theme-path: .
+Pull requests opened from forked repositories run with a read-only GITHUB_TOKEN, so the default pull_request trigger cannot post or update the preview button. The action may fail with Resource not accessible by integration.
Use pull_request_target only for the workflow that posts the preview button:
on:
+ pull_request_target:
+ types: [opened, synchronize, reopened, edited]
++Danger: Security note
pull_request_target runs in the context of the base repository and can access repository secrets and a write-capable GITHUB_TOKEN. Do not use it to check out PR code, run files from the PR, install PR dependencies, load a blueprint from the PR branch, or pass PR values into shell commands. Keep permissions as narrow as possible, typically contents: read and pull-requests: write for this action.
If you need Composer, npm, tests, or any other step that runs PR code, put that work in a separate pull_request workflow and use workflow_run to publish the preview after the build completes.
By default, the action updates the PR description (mode: append-to-description). To post as a comment instead:
with:
+ plugin-path: .
+ mode: comment
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+The action wraps the button in HTML markers and updates it on subsequent runs. By default, it restores the button if you remove it. To prevent restoration:
+with:
+ plugin-path: .
+ restore-button-if-removed: false
+For plugins or themes requiring compilation, the workflow involves building the code, exposing it via GitHub releases, and creating a blueprint that references the public URL.
++Warning: First-time setup: publish the draft release
The expose-artifact-on-public-url action uploads built files to a GitHub release tagged ci-artifacts by default. On the first run, GitHub creates this release as a draft, which is not publicly fetchable — the preview button will appear but silently 404 when clicked. Go to your repository's Releases page once and either publish the release or mark it as a pre-release. Subsequent runs reuse the same release, so this is only needed once.
Use the two-workflow pattern from the complete artifact documentation:
+pull_request workflow checks out the PR code, runs the build with read-only permissions, and uploads the ZIP as a GitHub Actions artifact.workflow_run workflow runs only after that build succeeds. It has contents: write and pull-requests: write, exposes the uploaded ZIP on a public release URL, builds a Blueprint that installs that ZIP, and posts the preview button.Keep secrets and write permissions out of the build workflow. The publish workflow should not check out or run PR code. The artifacts-to-keep setting controls how many builds to retain per PR. For themes, change installPlugin to installTheme.
See adamziel/preview-in-playground-button-built-artifact-example for a complete working example.
+Use blueprints to configure the Playground environment. You can install additional plugins, set WordPress options, import content, or run custom PHP.
+For the canonical pattern of installing a plugin straight from a GitHub repository — and when to publish a built ZIP instead because your plugin needs a Composer or npm build step — see Plugin in a GitHub repository.
+Example installing your plugin with WooCommerce:
+jobs:
+ create-blueprint:
+ name: Create Blueprint
+ runs-on: ubuntu-latest
+ outputs:
+ blueprint: ${{ steps.blueprint.outputs.result }}
+ steps:
+ - name: Create Blueprint
+ id: blueprint
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const blueprint = {
+ steps: [
+ {
+ step: "installPlugin",
+ pluginData: {
+ resource: "git:directory",
+ // Use head.repo.full_name, not context.repo. PRs from forks
+ // live on the contributor's fork, not the base repository —
+ // pointing at context.repo.* will 404 for every fork PR.
+ url: `https://github.com/${context.payload.pull_request.head.repo.full_name}.git`,
+ ref: context.payload.pull_request.head.sha,
+ refType: "commit",
+ path: "/"
+ }
+ },
+ {
+ step: "installPlugin",
+ pluginData: {
+ resource: "wordpress.org/plugins",
+ slug: "woocommerce"
+ }
+ }
+ ]
+ };
+ return JSON.stringify(blueprint);
+ result-encoding: string
+
+ preview:
+ needs: create-blueprint
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - uses: WordPress/action-wp-playground-pr-preview@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ blueprint: ${{ needs.create-blueprint.outputs.blueprint }}
+Or reference an external blueprint:
+with:
+ blueprint-url: https://example.com/path/to/blueprint.json
+See Blueprints documentation for all available steps and configuration options.
+Customize the preview content using template variables:
+with:
+ plugin-path: .
+ description-template: |
+ ### Test this PR in WordPress Playground
+
+ {{PLAYGROUND_BUTTON}}
+
+ **Branch:** {{PR_HEAD_REF}}
+Available variables: {{PLAYGROUND_BUTTON}}, {{PLUGIN_SLUG}}, {{THEME_SLUG}}, {{PR_NUMBER}}, {{PR_TITLE}}, {{PR_HEAD_REF}}, and more.
See the workflow README for the complete list.
+The expose-artifact-on-public-url action uploads built files to a single release (tagged ci-artifacts by default). Each artifact gets a unique filename like pr-123-abc1234.zip. Old artifacts are automatically cleaned up based on artifacts-to-keep.
Configuration options: Expose Artifact Inputs
+Invalid workflow file or jobs.<id>.uses error: You referenced the action as a reusable workflow. Move uses: WordPress/action-wp-playground-pr-preview@v2 into the job's steps: list (as an item under jobs.<job_id>.steps:), not directly under the job. See How it works.
Button not appearing: The workflow file must exist on the default branch before it runs on PRs. Check the Actions tab for errors.
+Resource not accessible by integration: The PR was opened from a fork and the default pull_request trigger cannot write. Use pull_request_target only for the preview-button workflow described in Testing PRs from forks. If you need to build or run PR code, use the two-workflow artifact pattern in Working with built artifacts.
Button appears but preview fails to load (404): For built-artifact workflows, the ci-artifacts release is still a draft. Publish it once from the Releases page. See Working with built artifacts.
plugin-path or theme-path resolves to an empty directory: The path is relative to the repository root, not to the workflow file. Use . for repo-root plugins, plugins/my-plugin for subdirectories.
Git ref refs/heads/<branch> not found on a fork PR: Your blueprint uses context.repo.owner/context.repo.repo to build the git:directory resource URL, which points at the base repository. Fork PRs live on the contributor's fork — use context.payload.pull_request.head.repo.full_name and head.sha with refType: "commit" instead. Repository URLs with or without a trailing .git suffix are supported.
Blueprint references a legacy ZIP-from-repo proxy service and times out: Look in your blueprint for resource URLs pointing at ZIP-from-repo proxy endpoints, then switch source-based previews to the git:directory resource (shown in Custom blueprints), which fetches directly from GitHub. For plugins or themes that need a build step, publish a built ZIP artifact and install that artifact with a url resource instead.
Plugin/theme not activated: Check the browser console for PHP errors. Dependencies may be missing, or the plugin's main file may not match the directory name.
+Permissions errors: Ensure the job declares permissions: pull-requests: write (and contents: write for built-artifact workflows).
More: workflow README
+Embedded media: https://www.youtube.com/embed/2VQkCPYyabQ?si=g5zkAZelHZ9bkN1X
+Original Playground docs source: https://playground.wordpress.net/guides/github-action-pr-preview
]]>In this section we present a selection of guides that will help you to both work with, and to better understand, a variety of topics related to WordPress Playground.
+Embed runnable PHP snippets in any web page with the <php-snippet> web component, or share full PHP examples via the standalone PHP Playground at playground.wordpress.net/php-playground.html. One script tag, lazy-loaded runtime, shared across every snippet on the page.
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.
+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.
+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.
+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.
+This guide will show you the basic settings to showcase your plugin using WordPress Playground and how to use it while developing your plugin.
+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.
+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.
+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.
Set up automated end-to-end tests for your WordPress plugins and themes using Playwright and the WordPress Playground CLI.
+Original Playground docs source: https://playground.wordpress.net/guides
]]>WordPress Playground ships two ready-made ways to put runnable PHP — and the full WordPress runtime — directly into a web page or a shareable URL. No PHP server, no setup, just a browser.
+<php-snippet> web component — drop one <script> tag into your blog post, docs site, or readme to embed multiple runnable PHP snippets that share a single Playground runtime.playground.wordpress.net/php-playground.html with a shareable URL.<php-snippet>The <php-snippet> custom element renders a syntax-highlighted code block with a Run button. Multiple snippets on the same page share a single hidden Playground runtime that is downloaded only when the visitor clicks Run for the first time.
<script
+ type="module"
+ src="https://playground.wordpress.net/php-code-snippet.js"
+></script>
+
+<php-snippet name="hello.php">
+ <script type="application/x-php">
+<?php
+echo "Hello from PHP " . phpversion();
+ </script>
+</php-snippet>
+That's the whole integration. The script is around 5 KB gzipped and contains no PHP or WordPress — those are fetched lazily on the first Run click.
+<script type="application/x-php"> wrapper?Browsers ignore the contents of script tags whose type they don't understand. That means you can put PHP code that contains literal < characters (HTML strings, generics, comparisons) inside the wrapper without escaping anything.
If your snippet has no characters that need escaping, you can put the code directly inside <php-snippet>:
<php-snippet name="add.php">
+ <?php echo 1 + 2;
+</php-snippet>
+You can also load the code from a separate file:
+<php-snippet name="lazy-load.php" src="./snippets/lazy-load.php"></php-snippet>
+Each snippet runs inside a real WordPress installation. require '/wordpress/wp-load.php' brings in the core APIs:
<php-snippet name="lazy-load-images.php">
+ <script type="application/x-php">
+<?php
+require '/wordpress/wp-load.php';
+
+$html = '<article>
+ <img src="hero.jpg" alt="Hero">
+ <img src="inline.jpg" alt="Inline">
+</article>';
+
+$tags = new WP_HTML_Tag_Processor( $html );
+while ( $tags->next_tag( 'img' ) ) {
+ $tags->set_attribute( 'loading', 'lazy' );
+ $tags->add_class( 'responsive' );
+}
+
+echo $tags->get_updated_html();
+ </script>
+</php-snippet>
+The first time a visitor clicks Run on any snippet on the page, the component:
+https://playground.wordpress.net/client/index.js),https://playground.wordpress.net/remote.html,Every later Run — on the same snippet or any other — calls client.run({ code }) against that same runtime. No additional downloads, no extra processes. A page with five snippets pays the runtime cost once.
While the boot is in progress, every snippet that has its Run clicked shows the same staged progress bar (download → install → ready) drawn from the live progress events Playground emits internally.
+If several snippets need the same baseline — a mu-plugin, a couple of files, a configured option — drop a single <script type="application/json"> on the page that contains a JSON Blueprint, and point each snippet at it with a blueprint attribute:
<script id="toolkit" type="application/json">
+{
+ "steps": [
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/mu-plugins/toolkit.php",
+ "data": "<?php\nfunction toolkit_say($s) { return strtoupper($s); }"
+ }
+ ]
+}
+</script>
+
+<php-snippet name="a.php" blueprint="toolkit">
+ <script type="application/x-php">
+<?php require '/wordpress/wp-load.php'; echo toolkit_say('hello');
+ </script>
+</php-snippet>
+
+<php-snippet name="b.php" blueprint="toolkit">
+ <script type="application/x-php">
+<?php require '/wordpress/wp-load.php'; echo toolkit_say('world');
+ </script>
+</php-snippet>
+Browsers don't execute scripts whose type they don't recognize, so the JSON sits inert until the component reads it. The blueprint is JSON-stringified and folded into the runtime cache key, so two snippets with the same blueprint share one runtime boot. Two snippets with different blueprint values get separate runtimes — usually what you want.
The blueprint attribute accepts either an id or any CSS selector. Any element will do — <script type="application/json"> is recommended because the HTML parser treats its contents as raw text, so a literal <?php inside the JSON is harmless. A <template> works too, but its content is parsed as HTML, and the <? in <?php is treated as the start of a bogus comment that runs to the next >. That can swallow the closing </template> and quietly break the page. If you do use a <template>, escape < as \u003c in the JSON.
Add the editable attribute and visitors can tweak the code before clicking Run. The keystrokes go into a transparent textarea overlaid on the highlighted code, so the syntax colors update as they type.
<php-snippet name="scratch.php" editable>
+ <script type="application/x-php">
+<?php
+$nums = range(1, 10);
+echo "Sum: " . array_sum($nums);
+ </script>
+</php-snippet>
+Useful for "now you try" sections in tutorials, or for letting readers experiment with their own values. Edits live only in the page — there's no persistence — so a refresh resets the snippet to its initial code.
+| Attribute | Default | Purpose |
+| -------------------- | ------------------------------------ | --------------------------------------------- |
+| `name` | `snippet.php` | Filename label shown in the snippet header |
+| `php` | `8.4` | PHP version (see [supported versions][php]) |
+| `wp` | `latest` | WordPress version |
+| `src` | — | Load PHP from a URL instead of inline |
+| `editable` | (off) | Let visitors edit the code before running |
+| `blueprint` | — | Id or CSS selector of a `<script type="application/json">` (or `<template>`) containing a JSON Blueprint to run before the snippet |
+| `playground-origin` | `https://playground.wordpress.net` | Override the runtime origin (local dev, etc.) |
+Snippets that share the same php, wp, and playground-origin values share one runtime; mixing different versions on the same page boots a separate runtime per combination.
[php]: /developers/apis/query-api/#available-options
+For full-page editing or sharing a one-off snippet via URL, use the standalone PHP Playground at:
++
It's a side-by-side editor and preview with PHP and WordPress version selectors. The current code, PHP version, and WordPress version are encoded into the URL fragment, so you can share a working example by copying the URL.
+You can embed it in any page with an iframe:
+<iframe
+ src="https://playground.wordpress.net/php-playground.html#eyJjb2RlIjoiPD9waHBcblxuZWNobyBcIkkgYW0gYSBjb2RlIHNuaXBwZXQhXCI7XG4iLCJwaHAiOiI4LjQifQ=="
+ width="100%"
+ height="600"
+></iframe>
+The fragment is a base64-encoded JSON payload of { code, php, wp }.
<iframe> vs. <php-snippet> — which should I use?| Use case | Pick |
+| ----------------------------------------------------------------- | ------------------- |
+| Multiple read-only runnable examples in a docs page or blog post | `<php-snippet>` |
+| Customizations, e.g. read-only code vs editable code | `<php-snippet>` |
+| A single code example you don't want to load foreign scripts on your site | Standalone PHP Playground |
+Original Playground docs source: https://playground.wordpress.net/guides/php-code-snippets
]]>WordPress Playground lets you run WordPress instantly—no server, no setup, no risk. It works in your browser at playground.wordpress.net, and developers can also use it via CLI, Node.js, or embedded in their own apps. But you don't need to be technical to benefit from it.
+Watch this quick overview:
+Embedded media: https://www.youtube.com/embed/8_rH2k-OQ8E
+A car simulator gives you a steering wheel, pedals, and virtual streets. Practice driving, hit cones, make mistakes — nothing bad happens. No real car gets damaged. Want to try again? Just restart.
+WordPress Playground works the same way. It gives you a complete WordPress site to experiment with, but nothing you do affects any real website. Make changes, break things, learn from mistakes — then start fresh whenever you want.
+
When you visit playground.wordpress.net, you get a WordPress site running entirely in your browser. You can:
+By default, WordPress Playground loads a landing page to introduce some of the features of Playground and where you can learn more about it. But you can also load a vanilla WordPress version without the landing page. At the Launching Playground panel, one option is to load a vanilla WordPress version.
+

Are you new to WordPress or trying to understand features like the Site Editor or the new features of the latest WordPress Release? Playground is your perfect practice space.
+Playground logs you in as an administrator, so you can edit any page. Click Edit for editing posts and Edit Site to update the website layout in the top toolbar to open the editor.
+
Want to understand how a page layout was created? Open the List View (the three horizontal lines icon) to see every block that makes up the page.
+
You can inspect columns, headings, images, and buttons — and see exactly how they're arranged. This is a powerful way to learn by example.
+At the Launch WordPress Playground panel, you will have access to the Blueprint Library, a set of more than 40 blueprints to inspire you and try different types of websites with WordPress Playground, Art Gallery, E-commerce, and Web Portfolio are some of the examples.
+

When the WordPress team releases new features, you can test them in Playground before they affect your real site. Select any WordPress version from the settings panel to explore what's new — or what's coming next.
+Running a live website means every change risks breaking something. Playground lets you test before you commit.
+Curious about a new SEO plugin? Want to compare two contact form options? Install them in Playground first:
+Your real site stays untouched while you evaluate whether the plugin fits your needs.
+
Thinking about switching themes? Test your new theme in Playground to see how it handles your content — without disrupting your visitors.
+Open multiple browser tabs with different Playground setups. Compare plugin A versus plugin B, or see how your content looks in different themes. Make informed decisions before touching your production site.
+Your Real Site Stays Safe
+Every Playground runs independently in your browser. Nothing syncs to any external server, and nothing affects your live WordPress installation.
+Even experienced WordPress users benefit from a safe testing environment.
+Want to try a different font size? Adjust spacing? Change colors? Load a Playground with the same theme that you are using in production and edit it freely:
+If you like what you see, recreate those changes on your real site. If not, just close the tab — no cleanup required.
+Playground doesn't have to be temporary. You can save your progress and return to it later.
+
Playground generates a unique link for your saved site. Bookmark it, and you can return to exactly where you left off.
+Need to move your work elsewhere? Choose Download as .zip to export your entire Playground — including plugins, themes, and content. You can restore it later or even host it on a real server.
++Tip: Keep Your Playground Link
When you save to the browser, copy the unique URL it generates. That link is your way back to your saved work.
+Now that you know Playground is for everyone, explore further:
+Original Playground docs source: https://playground.wordpress.net/guides/playground-for-everyone
]]>The Playground CLI can also be controlled programmatically from your JavaScript/TypeScript code using the runCLI function. This gives you direct access to all CLI functionalities within your code, which is useful for automating end-to-end tests. The options you pass to runCLI map directly to the CLI flags.
Embedded media: https://www.youtube.com/embed/rmNf3CfXbtA?si=cduqQYbBWc6zAPVj
+Using the runCLI function, you can specify options like the PHP and WordPress versions. In the example below, we request PHP 8.3, the latest version of WordPress, and to be automatically logged in. All supported arguments are defined in the RunCLIArgs type.
import { runCLI } from "@wp-playground/cli";
+
+const cliServer = await runCLI({
+ command: 'server',
+ php: '8.3',
+ wp: 'latest',
+ login: true,
+});
+Run the code above using your preferred TypeScript runtime, e.g. tsx:
npx tsx my-script.ts
+You can provide a blueprint in two ways: either as an object literal directly passed to the blueprint property, or as a string containing the path to an external .json file.
import { runCLI, RunCLIServer } from "@wp-playground/cli";
+
+const cliServer: RunCLIServer = await runCLI({
+ command: 'server',
+ wp: 'latest',
+ blueprint: {
+ steps: [
+ {
+ "step": "setSiteOptions",
+ "options": {
+ "blogname": "Blueprint Title",
+ "blogdescription": "A great blog description"
+ }
+ }
+ ],
+ },
+});
+For full type-safety when defining your blueprint object, you can import and use the BlueprintDeclaration type from the @wp-playground/blueprints package:
import type { BlueprintDeclaration } from '@wp-playground/blueprints';
+
+const myBlueprint: BlueprintDeclaration = {
+ landingPage: "/wp-admin/",
+ steps: [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "twentytwentyone"
+ },
+ "options": {
+ "activate": true
+ }
+ }
+ ]
+};
+You can mount local directories programmatically using runCLI. The options mount and mount-before-install are available. The hostPath property expects a path to a directory on your local machine. This path should be relative to where your script is being executed.
import { runCLI } from "@wp-playground/cli";
+
+const cliServer = await runCLI({
+ command: 'server',
+ login: true,
+ 'mount-before-install': [
+ {
+ hostPath: './[my-plugin-local-path]',
+ vfsPath: '/wordpress/wp-content/plugins/my-plugin',
+ },
+ ],
+});
+You can combine mounting parts of the project with blueprints, for example:
+import { runCLI, RunCLIServer } from "@wp-playground/cli";
+
+const cliServer: RunCLIServer = await runCLI({
+ command: 'server',
+ php: '8.3',
+ wp: 'latest',
+ login: true,
+ mount: [
+ {
+ "hostPath": "./plugin/",
+ "vfsPath": "/wordpress/wp-content/plugins/playwright-test"
+ }
+ ],
+ blueprint: {
+ steps: [
+ {
+ "step": "activatePlugin",
+ "pluginPath": "/wordpress/wp-content/plugins/playwright-test/plugin-playwright.php"
+ }
+ ]
+ }
+});
+The programmatic API is excellent for automated testing. Here's a complete example using Vitest:
+import { describe, test, expect, afterEach } from 'vitest';
+import { runCLI, RunCLIServer } from "@wp-playground/cli";
+
+describe('My Plugin Tests', () => {
+ const cliServer: RunCLIServer;
+
+ afterEach(async () => {
+ if (cliServer) {
+ // RunCLIServer exposes Symbol.asyncDispose as its public async cleanup API.
+ await cliServer[Symbol.asyncDispose]();
+ }
+ });
+
+ test('plugin activates successfully', async () => {
+ cliServer = await runCLI({
+ command: 'server',
+ mount: [
+ {
+ hostPath: './my-plugin',
+ vfsPath: '/wordpress/wp-content/plugins/my-plugin'
+ }
+ ],
+ blueprint: {
+ steps: [
+ {
+ step: 'activatePlugin',
+ pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
+ }
+ ]
+ }
+ });
+
+ const homeUrl = new URL('/', cliServer.serverUrl);
+ const response = await fetch(homeUrl);
+
+ expect(response.status).toBe(200);
+ const html = await response.text();
+ expect(html).toContain('My Plugin');
+ });
+
+ test('plugin settings page loads', async () => {
+ cliServer = await runCLI({
+ command: 'server',
+ login: true, // Auto-login as admin
+ mount: [
+ {
+ hostPath: './my-plugin',
+ vfsPath: '/wordpress/wp-content/plugins/my-plugin'
+ }
+ ],
+ blueprint: {
+ steps: [
+ {
+ step: 'activatePlugin',
+ pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
+ }
+ ]
+ }
+ });
+
+ const settingsUrl = new URL(
+ '/wp-admin/options-general.php?page=my-plugin',
+ cliServer.serverUrl
+ );
+ const response = await fetch(settingsUrl);
+
+ // Note: A plain `fetch` call does not send the admin session cookie set by `login: true`,
+ // so this request is typically redirected to the login page instead of returning 200.
+ expect(response.status).toBe(302);
+ });
+});
+test('plugin works with WordPress 6.4 and PHP 8.3', async () => {
+ cliServer = await runCLI({
+ command: 'server',
+ php: '8.3',
+ wp: '6.4',
+ mount: [
+ {
+ hostPath: './my-plugin',
+ vfsPath: '/wordpress/wp-content/plugins/my-plugin'
+ }
+ ],
+ blueprint: {
+ steps: [
+ {
+ step: 'activatePlugin',
+ pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
+ }
+ ]
+ }
+ });
+
+ const homeUrl = new URL('/', cliServer.serverUrl);
+ const response = await fetch(homeUrl);
+
+ expect(response.status).toBe(200);
+});
+When you only need to test PHP code without WordPress, you can skip the setup for faster testing:
+import { runCLI } from "@wp-playground/cli";
+
+const cliServer = await runCLI({
+ command: 'server',
+ php: '8.3',
+ wordpressInstallMode: 'do-not-attempt-installing',
+ skipSqliteSetup: true,
+});
+
+// Test PHP version
+await cliServer.playground.writeFile(
+ '/wordpress/version.php',
+ '<?php echo phpversion(); ?>'
+);
+
+const versionUrl = new URL('/version.php', cliServer.serverUrl);
+const response = await fetch(versionUrl);
+const version = await response.text();
+console.log('PHP Version:', version); // Outputs: 8.3.x
+import { runCLI } from "@wp-playground/cli";
+
+try {
+ const cliServer = await runCLI({
+ command: 'server',
+ debug: true, // Enable PHP error logging.
+ });
+
+ // Your test code here
+
+} catch (error) {
+ console.error('Server failed to start:', error);
+}
+Original Playground docs source: https://playground.wordpress.net/guides/programmatic-playground-cli
]]>There are several blueprint steps and strategies you can use to import content (or generate it) in the Playground instance:
+importWxrWith the importWxr step, you can import your own content via a .xml file previously exported from an existing WordPress installation:
"steps": [
+ ...,
+ {
+ "step": "importWxr",
+ "file": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/blueprint-content.xml"
+ }
+ },
+ ...
+]
+<kbd> Run Blueprint </kbd> <kbd> See <code>blueprint.json</code> </kbd>
+To include images in your imported content, a good approach is to upload the images to your GitHub repo and search/replace the path for them in the exported .xml file using the URL format: https://raw.githubusercontent.com/{repo}/{branch}/{image_path}.
<!-- wp:image {"lightbox":{"enabled":false},"id":4751,"width":"78px","sizeSlug":"full","linkDestination":"none","align":"center","className":"no-border"} -->
+<figure class="wp-block-image aligncenter size-full is-resized no-border">
+ <img src="https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/install-activate-setup-theme-from-gh-repo/images/avatars.png" alt="" class="wp-image-4751" style="width:78px" />
+</figure>
+<!-- /wp:image -->
+It is recommended to upload your exported .xml file and any referenced assets (such as images) to the same directory as your blueprint.json in your GitHub repository.
importWordPressFilesWith the importWordPressFiles step, you can import your own top-level WordPress files from a given .zip file into the instance's root folder. For example, if a .zip file contains the wp-content and wp-includes directories, they will replace the corresponding directories in Playground's root folder.
This zip file can be created from any Playground instance with the "Download as zip" option in the Playground Options Menu.
You can prepare a demo for your WordPress theme or plugin (including images and other assets) in a Playground instance and then export a snapshot of that demo into a .zip file. This file can be imported later using the importWordPressFiles step.
{
+ "landingPage": "/",
+ "login": true,
+ "steps": [
+ {
+ "step": "importWordPressFiles",
+ "wordPressFilesZip": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/adamziel/playground-sites/main/playground-for-site-builders/playground.zip"
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+importThemeStarterContentSome themes have starter content that can be published to highlight the features of a theme.
+With the importThemeStarterContent step you can publish the starter content of any theme even if that theme is not the one activated in the Playground instance.
+"steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "twentytwenty"
+ }
+ },
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "twentytwentyone"
+ },
+ "options": {
+ "activate": true
+ }
+ },
+ {
+ "step": "importThemeStarterContent",
+ "themeSlug": "twentytwenty"
+ }
+ ]
+
+<kbd> Run Blueprint </kbd>
+You can also publish the starter content of a theme when installing it with the installTheme step by setting to true its importStarterContent option:
{
+ "steps": [
+ {
+ "step": "installTheme",
+ "themeData": {
+ "resource": "wordpress.org/themes",
+ "slug": "twentytwenty"
+ },
+ "options": {
+ "activate": true,
+ "importStarterContent": true
+ }
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+wp-cliAnother way of generating content for your theme or plugin is via the wp-cli step that allows you to run WP-CLI commands such as wp post generate:
{
+ "landingPage": "/wp-admin/edit.php",
+ "login": true,
+ "steps": [
+ {
+ "step": "wp-cli",
+ "command": "wp post generate --count=20 --post_type=post --post_date=1999-01-04"
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
+You can also use the wp-cli step in combination with the writeFile step to create posts based on existing content and to import images to the Playground instance:
{
+ "$schema": "https://playground.wordpress.net/blueprint-schema.json",
+ "landingPage": "/?p=4",
+ "login": true,
+ "steps": [
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/postcontent.md",
+ "data": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/postcontent.md"
+ }
+ },
+ {
+ "step": "wp-cli",
+ "command": "wp post create --post_title='Welcome to Playground' --post_status='published' /wordpress/wp-content/postcontent.md"
+ },
+ {
+ "step": "writeFile",
+ "path": "/wordpress/wp-content/Select-storage-method.png",
+ "data": {
+ "resource": "url",
+ "url": "https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/wpcli-post-with-image/Select-storage-method.png"
+ }
+ },
+ {
+ "step": "wp-cli",
+ "command": "wp media import wordpress/wp-content/Select-storage-method.png --post_id=4 --title='Select your storage method' --featured_image"
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>
++Tip
Check the "Use wp-cli to add a post with image" example from the Blueprints Gallery to see the full example showing the connection between the content and the featured image.
+runPHPWith the runPHP step you can run any PHP code you require to insert info into your WordPress installation, for example by using the wp_insert_post function.
{
+ "landingPage": "/wp-admin/edit.php",
+ "login": true,
+ "steps": [
+ {
+ "step": "runPHP",
+ "code": "<?php require_once '/wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'Simple post from PHP', 'post_content' => '<!-- wp:paragraph --><p>This is a simple post inserted with wp_insert_post</p><!-- /wp:paragraph -->', 'post_author' => 1, 'post_status' => 'publish')); ?>"
+ }
+ ]
+}
+<kbd> Run Blueprint </kbd>);%20?%3E%22}]}>)
+Original Playground docs source: https://playground.wordpress.net/guides/providing-content-for-your-demo
]]>Blocknotes is the first iOS application that ran WordPress natively on iOS devices by leveraging WordPress Playground. Developed by Ella van Durpe, a core committer for WordPress, Blocknotes represents a significant leap in the capabilities of mobile applications by utilizing WebAssembly to run WordPress without the need for a traditional PHP server.
+This case study explores the features, technical implementation, and potential implications of Blocknotes for the future of mobile and web development.
+Important! The current version of Blocknotes isn’t running WordPress Playground anymore. Since the initial release, the app was rewritten to only use the WordPress block editor without the rest of WordPress. This case study covers the early versions of Blocknotes that opened an entire world of new possibilities for WordPress.
+Blocknotes allows users to create and edit notes using the WordPress block editor. The notes are automatically saved as HTML files to the user’s iCloud Drive and seamlessly synchronized across devices.
+Blocknotes operated as a WebView running an HTML page where a WebAssembly version of PHP was running WordPress. That HTML page was packaged as a native iOS via Capacitor. This setup allowed WordPress to function in environments traditionally not supported.
+In Blocknotes GitHub repository you can review the last Playground-based release. Here are the most important parts:
+.data file).Although Blocknotes proved releasing a WordPress-based iOS app is possible, this is still a highly exploratory area. There are no established workflows, libraries, or knowledge bases.
+The best documentation we have is the Blocknotes repository. Use it as a reference and a starting point for exploring your new app. Review the key components like the WebAssembly build of PHP, the integration of the WordPress block editor, and how web workers are utilized to run WordPress efficiently. By dissecting these elements, you can gain insights into building your own iOS app with WordPress Playground, pushing the boundaries of what’s possible with mobile web applications.
+As you navigate this innovative space, share your findings and challenges with the Playground team and the broader WordPress community. Publishing your learnings will not only aid in your development but also contribute to a collective knowledge base, driving forward the future of WordPress on mobile.
+Blocknotes paves the way for a new generation of applications that are more accessible, flexible, and powerful.
+Once the app-building workflows mature, we may see an automated pipelines for packaging Playground sites as iOS apps. It would make it extremely easy to run the same codebase on the server, in the browser, and as a mobile app.
+By working together and sharing our findings, we can push the boundaries of what’s possible with WordPress and mobile app development
+Original Playground docs source: https://playground.wordpress.net/guides/wordpress-native-ios-app
]]>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.
+<p class="docs-hubs">The WordPress Playground documentation is distributed across four separate hubs (subsites):</p>
+This docs hub is focused on starting with WordPress Playground and is divided into the following major sections.
+Discover how you can leverage WordPress Playground to Build, Test, and Launch your products.
+Whether you're a developer, a non-technical user, or a contributor, these docs will guide you as you start your learning journey:
++Tip
Read Introduction to Playground: running WordPress in the browser blog post in the WordPress Developer Blog for a great introduction to WordPress Playground
+If you're a developer or tech user, you may want to check directly the APIs available:
+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!
+#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.
+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.
+wp-playground skill for Claude Code, Cursor, Gemini CLI, GitHub Copilot, and other coding agents. Describe what you need; the agent runs the commands.llms.txt format.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.
+Original Playground docs source: https://playground.wordpress.net/
]]>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:
+Embedded media: https://video.wordpress.com/v/3UBIXJ9S?autoPlay=false&height=1080&width=1920&fill=true
+Every time you visit the official demo on playground.wordpress.net, you get a fresh WordPress site.
+You can then create pages, upload plugins, themes, import your own site, and do most things you would do on a regular WordPress.
+It's that easy to start!
+The entire site lives in your browser and is scraped when you close the tab. Want to start over? Just refresh the page!
+WordPress Playground is private
+Everything you build stays in your browser and is not sent anywhere. Once you're finished, you can export your site as a zip file. Or just refresh the page and start over!
+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.
+To keep your WordPress Playground site for longer than a single browser session, you can export it as a .zip file.


The exported file contains the complete site you've built. You could host it on any server that supports PHP and SQLite. All WordPress core files, plugins, themes, and everything else you've added to your site are in there.
+The SQLite database file is also included in the export, you'll find it wp-content/database/.ht.sqlite. Keep in mind that files starting with a dot are hidden by default on most operating systems so you might need to enable the "Show hidden files" option in your file manager.
You can restore the saved site using the "Import from .zip" button in the Playground dashboard panel:
+

The quickest way to change the version of WordPress or PHP is by using the settings panel on the official demo site:
+
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:
This is called Query API and you can learn more about it here.
+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.
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 the import feature described above. The import feature exports the entire site, including the database. This import feature imports a WXR file into an existing site.
+WordPress Playground is programmable, which means you can build WordPress apps, setup 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.
+Original Playground docs source: https://playground.wordpress.net/quick-start-guide
]]>+Tip
There's a set of redirections in place to make it easier the access to some of the tools related to Playground:
+<ul id="list-resources-redirections"> <li><a href="https://playground.wordpress.net/"><strong>https://playground.wordpress.net/</strong></a> → Playground instance</li> <li><a href="https://playground.wordpress.net/docs">https://playground.wordpress.net<strong>/docs</strong></a> → Playground Docs</li> <li><a href="https://playground.wordpress.net/builder">https://playground.wordpress.net<strong>/builder</strong></a> → Playground Blueprints Builder</li> <li><a href="https://playground.wordpress.net/wordpress">https://playground.wordpress.net<strong>/wordpress</strong></a> → Playground PR viewer for WordPress</li> <li><a href="https://playground.wordpress.net/gutenberg">https://playground.wordpress.net<strong>/gutenberg</strong></a> → Playground PR viewer for Gutenberg</li> <li><a href="https://playground.wordpress.net/proxy">https://playground.wordpress.net<strong>/proxy</strong></a> → Legacy Playground Proxy Service <em>(for Git repositories, prefer <a href="/blueprints/steps/resources#gitdirectoryreference">git:directory</a>)</em></li> </ul>
+Original Playground docs source: https://playground.wordpress.net/resources
]]>https://playground.wordpress.net/ lets developers run WordPress in a browser without a server. This environment makes testing plugins, themes, and features quick and easy.
+Some key features:
+The Query Params API allows you to directly load specific configurations into a Playground instance. This includes setting a particular WordPress version, theme, or plugin. You can also define more complex setups using blueprints (see examples here).
+The Playground website includes toolbars that customize your instance and provide quick access to resources and utilities.
+
On the toolbar, you'll find:
+
The Playground Settings Panel includes these Query API options:
+wp: Defines the WordPress version.php: Specifies the PHP version for the instance.language: Sets the WordPress instance language.multisite: Enables WordPress multisite support.networking: Enables network access to the WordPress Plugin Directory and WordPress APIs.
This panel lets you manage Playground instances and provides access to the following panels:
+.sqlite file.
Click "Save" to create an instance and list it in the Playground Launch Panel. The Playground Dashboard also offers export and download options through the Additional actions menu:
+
.zip file with the setup of the Playground instance, including any themes or plugins installed. This .zip excludes content and database changes.
The Blueprint editor provides the ability to manage multiple Blueprints and to validate code.
+
This panel shows all the ways to launch WordPress Playground: import .zip files, load from GitHub repositories, and preview PRs from WordPress core and Gutenberg.
The Launch Panel also lists more than 40 blueprints from the Blueprint Gallery and your Saved Playgrounds.
++Caution
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.
+Original Playground docs source: https://playground.wordpress.net/web-instance
]]>Ask questions against your local WordPress posts and pages using Ollama.
', + 'ollama pull embeddinggemma and ollama pull gemma4:e4b.',
+ 'No local sources matched this question.
' + escapeHtml(codeLines.join('\n')) + '');
+ continue;
+ }
+
+ var heading = line.match(/^(#{1,4})\s+(.+)$/);
+ if (heading) {
+ var level = Math.min(4, heading[1].length + 1);
+ html.push('' + renderMarkdown(quoteLines.join('\n')) + ''); + continue; + } + + if (/^\s*[-*+]\s+/.test(line)) { + html.push(renderList(lines, index, false)); + while (index < lines.length && /^\s*[-*+]\s+/.test(lines[index])) { + index++; + } + continue; + } + + if (/^\s*\d+[.)]\s+/.test(line)) { + html.push(renderList(lines, index, true)); + while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) { + index++; + } + continue; + } + + var paragraph = [line.trim()]; + index++; + while (index < lines.length && !isBlank(lines[index]) && !isBlockStart(lines[index])) { + paragraph.push(lines[index].trim()); + index++; + } + html.push('
' + renderInlineMarkdown(paragraph.join(' ')) + '
'); + } + + return html.join(''); + } + + function renderList(lines, start, ordered) { + var items = []; + var pattern = ordered ? /^\s*\d+[.)]\s+(.+)$/ : /^\s*[-*+]\s+(.+)$/; + var index = start; + + while (index < lines.length) { + var match = lines[index].match(pattern); + if (!match) { + break; + } + items.push('' + code + '');
+ return token;
+ });
+
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, function (match, label, url) {
+ return '' + label + '';
+ });
+ html = html.replace(/\*\*([^*]+)\*\*/g, '$1');
+ html = html.replace(/__([^_]+)__/g, '$1');
+ html = html.replace(/(^|[\s(])\*([^*\n]+)\*/g, '$1$2');
+ html = html.replace(/(^|[\s(])_([^_\n]+)_/g, '$1$2');
+
+ codeSpans.forEach(function (code, codeIndex) {
+ html = html.replace('\u0000CODE' + codeIndex + '\u0000', code);
+ });
+
+ return html;
+ }
+
+ function stripThinking(value) {
+ return String(value || '')
+ .replace(/<\|channel\>thought[\s\S]*?Original Playground docs source: ${escapeHtml(sourceUrl)}
`, + ].join('\n'), + excerpt: description, + categories: categoryChain, + sourceFile: relativeFile, + sourceSlug: frontmatter.slug || '', + }; +} + +function splitFrontmatter(source) { + if (!source.startsWith('---\n')) { + return { frontmatter: {}, body: source }; + } + + const end = source.indexOf('\n---', 4); + if (end === -1) { + return { frontmatter: {}, body: source }; + } + + const raw = source.slice(4, end); + const body = source.slice(source.indexOf('\n', end + 4) + 1); + const frontmatter = {}; + + for (const line of raw.split('\n')) { + const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!match) { + continue; + } + frontmatter[match[1]] = unquote(match[2].trim()); + } + + return { frontmatter, body }; +} + +function unquote(value) { + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); + } + return value; +} + +function firstHeading(body) { + const heading = body.match(/^#\s+(.+)$/m); + return heading ? stripMarkdown(heading[1]).trim() : ''; +} + +function titleFromFile(file) { + const base = path.basename(file, '.md'); + return labelFromSegment(base); +} + +function postSlug(frontmatterSlug, relativeFile) { + if (frontmatterSlug && frontmatterSlug !== '/') { + return slugify(frontmatterSlug.replace(/^\/+|\/+$/g, '').replaceAll('/', '-')); + } + if (frontmatterSlug === '/') { + return 'wordpress-playground-docs'; + } + return slugify(relativeFile.replace(/\.md$/, '').replaceAll('/', '-')); +} + +function sourceUrlFor(frontmatterSlug, relativeFile) { + if (frontmatterSlug) { + return `${siteUrl}${frontmatterSlug.startsWith('/') ? frontmatterSlug : `/${frontmatterSlug}`}`; + } + return `${siteUrl}/${relativeFile.replace(/\.md$/, '')}`; +} + +function ensureCategories(relativeFile, categories) { + const segments = relativeFile.split('/'); + const top = segments[0]; + const folderSegments = segments.slice(1, -1); + const chain = []; + const topCategory = topLevelCategories[top]; + + let parentKey = ''; + let currentKey = top; + let currentSlug = topCategory.slug; + ensureCategory(categories, currentKey, { + label: topCategory.label, + slug: currentSlug, + parentSlug: '', + }); + chain.push(categories.get(currentKey)); + + for (const segment of folderSegments) { + parentKey = currentKey; + currentKey = `${currentKey}/${segment}`; + currentSlug = `${categories.get(parentKey).slug}-${slugify(stripNumberPrefix(segment))}`; + ensureCategory(categories, currentKey, { + label: categoryLabelFor(path.join(docsDir, currentKey)), + slug: currentSlug, + parentSlug: categories.get(parentKey).slug, + }); + chain.push(categories.get(currentKey)); + } + + return chain; +} + +function ensureCategory(categories, key, category) { + if (!categories.has(key)) { + categories.set(key, { + id: categories.size + 1, + ...category, + }); + } +} + +function categoryLabelFor(dir) { + const categoryFile = path.join(dir, '_category_.json'); + if (fs.existsSync(categoryFile)) { + try { + const data = JSON.parse(fs.readFileSync(categoryFile, 'utf8')); + if (data.label) { + return data.label; + } + } catch (error) { + throw new Error(`Could not parse ${categoryFile}: ${error.message}`); + } + } + return labelFromSegment(path.basename(dir)); +} + +function normalizeMdx(markdown, relativeFile) { + const lines = markdown.replace(/\r\n?/g, '\n').split('\n'); + const normalized = []; + let inFence = false; + let skipImport = false; + let componentBlock = null; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith('```')) { + inFence = !inFence; + normalized.push(line); + continue; + } + + if (inFence) { + normalized.push(line); + continue; + } + + if (skipImport) { + if (trimmed.endsWith(';')) { + skipImport = false; + } + continue; + } + + if (componentBlock) { + componentBlock.lines.push(line); + if (trimmed.endsWith('/>') || trimmed === '') { + flushComponentBlock(componentBlock, normalized, relativeFile); + componentBlock = null; + } + continue; + } + + if (/^import\b/.test(trimmed)) { + if (!trimmed.endsWith(';')) { + skipImport = true; + } + continue; + } + + if (/^<(BlueprintExample|UpdateTopLevelToc|BlueprintStep|TSDocstring|TOCInline)\b/.test(trimmed)) { + if (trimmed.startsWith(']*>\s*<\/p>$/i.test(trimmed)) {
+ continue;
+ }
+
+ if (/^
/i.test(trimmed)) {
+ continue;
+ }
+
+ if (/^