From 83498c11e6935784abe9a7eb8c929504a0cfaab8 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 01/27] Register Personal RAG app in apps index --- apps.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 +} From 903d86e65fdb488057eba748e1c7006b5f4d67c7 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 02/27] Add Personal RAG app blueprint --- apps/personal-rag.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 apps/personal-rag.json 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" + } + } + ] +} From ec34f44cb66dfc0b351f39a253f4f4c4431381ca Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 03/27] Update blueprints/my-wordpress/plugin/de/welcome-post.html --- .../my-wordpress/plugin/de/welcome-post.html | 269 ++++++------------ 1 file changed, 86 insertions(+), 183 deletions(-) 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 -

+ +

Spiel damit

+ + + +

Probier alles aus und mach es zu deinem.

@@ -37,27 +34,18 @@
- -
- -

💾

+ +
+ +

💾

- -

- Bleibt erhalten - Änderungen sind morgen noch da -

+ +

Bleibt erhalten

+ + + +

Deine Änderungen werden gespeichert und sind morgen noch da.

@@ -66,27 +54,18 @@
- -
- -

🔒

+ +
+ +

🔒

- -

- Ganz privat - Läuft im Browser, ohne Konto -

+ +

Ganz privat

+ + + +

Läuft in deinem Browser, ohne Konto.

@@ -95,149 +74,73 @@
- -
- -

- 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.

+ +
+ +
+
- + - -

Was kannst du hier machen?

+ +

Was kannst du hier machen?

    -
  • - Ein eigener Raum — WordPress betreibt einen großen Teil - des Webs, aber hier ist es nur für dich. Nutze es als Notizbuch, - Projektraum oder kreativen Sandkasten — ohne Veröffentlichungszwang. -
  • -
  • - Voll anpassbar — Ändere dein Theme, organisiere alles - nach deinen Wünschen. Du lernst WordPress ganz natürlich nebenbei. -
  • +
  • Ein eigener Raum — WordPress betreibt einen großen Teil des Webs, aber hier ist es nur für dich. Nutze es als Notizbuch, Projektraum oder kreativen Sandkasten — ohne Veröffentlichungszwang.
  • +
  • Voll anpassbar — Ändere dein Theme, organisiere alles nach deinen Wünschen. Du lernst WordPress ganz natürlich nebenbei.
- -

Gut zu wissen

+ +

Gut zu wissen

    -
  • - Es ist an diesen Browser gebunden — Dein Playground - lebt im Speicher dieses Browsers, du kannst also nicht von anderen - Geräten darauf zugreifen. Das macht es zu einem sehr privaten Raum. -
  • -
  • - Sichere deine Arbeit — Nutze die Backup-Funktion im - oberen Menü, um deine Seite zu speichern. So kannst du sie in einem - anderen Browser wiederherstellen oder dich vor Datenverlust schützen. -
  • -
  • - Bereit für mehr? — Verschiebe dein Playground zu einem - dedizierten WordPress-Hosting, um von jedem Gerät darauf zuzugreifen, es - öffentlich zu teilen oder bestimmte Personen zur Zusammenarbeit - einzuladen. -
  • +
  • Es ist an diesen Browser gebunden — Dein Playground lebt im Speicher dieses Browsers, du kannst also nicht von anderen Geräten darauf zugreifen. Das macht es zu einem sehr privaten Raum.
  • +
  • Sichere deine Arbeit — Nutze die Backup-Funktion im oberen Menü, um deine Seite zu speichern. So kannst du sie in einem anderen Browser wiederherstellen oder dich vor Datenverlust schützen.
  • +
  • Bereit für mehr? — Verschiebe dein Playground zu einem dedizierten WordPress-Hosting, um von jedem Gerät darauf zuzugreifen, es öffentlich zu teilen oder bestimmte Personen zur Zusammenarbeit einzuladen.
- -

Das obere Menü

+ +

Das obere Menü

-

- 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:

    -
  • - Installiere Apps — Wir haben einige Plugins - ausgewählt, die gut für den privaten Gebrauch funktionieren. Das - vollständige WordPress-Plugin-Verzeichnis ist auch da, falls du mehr - erkunden möchtest. -
  • -
  • - Backups — Speichere und stelle deine Seite wieder her -
  • -
  • - Neu anfangen — Setze dein WordPress zurück oder besuche - playground.wordpress.net - für schnelle, temporäre Experimente -
  • +
  • Apps installieren — Wir haben einige Plugins ausgewählt, die gut für den privaten Gebrauch funktionieren. Das vollständige WordPress-Plugin-Verzeichnis ist auch da, falls du mehr erkunden möchtest.
  • +
  • Backups — Speichere und stelle deine Seite wieder her.
  • +
  • Neu anfangen — Setze dein WordPress zurück oder besuche playground.wordpress.net für schnelle, temporäre Experimente.
- -

Viel Spaß mit deinem WordPress!

+ +

Viel Spaß mit deinem WordPress!

From 44ad7a52b5c1cfcc917171af515e15d45e7b9305 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 04/27] Update blueprints/my-wordpress/plugin/es/welcome-post.html --- .../my-wordpress/plugin/es/welcome-post.html | 264 ++++++------------ 1 file changed, 86 insertions(+), 178 deletions(-) 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 -

+ +

Experimenta sin miedo

+ + + +

Cambia lo que quieras, prueba cosas nuevas, hazlo tuyo.

@@ -37,27 +34,18 @@
- -
- -

💾

+ +
+ +

💾

- -

- Permanece
Tus cambios se guardan y - estarán aquí mañana -

+ +

Se queda contigo

+ + + +

Tus cambios se guardan y estarán aquí mañana.

@@ -66,27 +54,18 @@
- -
- -

🔒

+ +
+ +

🔒

- -

- Es privado
Se ejecuta en tu navegador, sin - necesidad de crear una cuenta -

+ +

Es privado

+ + + +

Se ejecuta en tu navegador, sin crear una cuenta.

@@ -95,144 +74,73 @@
- -
- -

- É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.

+ +
+ +
+
- + - -

¿Qué puedo hacer aquí?

+ +

¿Qué puedo hacer aquí?

    -
  • - Un espacio solo para ti — WordPress es la base de gran parte de la web, - pero aquí es solo para ti. Úsalo como cuaderno de notas, espacio para proyectos o - lugar para experimentar con tu creatividad; no hace falta que publiques nada. -
  • -
  • - Totalmente personalizable — Cambia el tema y organízalo todo - como más te guste. Aprenderás a usar WordPress de forma natural a medida que avances. -
  • +
  • Un espacio solo para ti — WordPress es la base de gran parte de la web, pero aquí es solo para ti. Úsalo como cuaderno de notas, espacio para proyectos o lugar para experimentar con tu creatividad; no hace falta que publiques nada.
  • +
  • Totalmente personalizable — Cambia el tema y organízalo todo como más te guste. Aprenderás a usar WordPress de forma natural a medida que avances.
- -

Cosas que debes saber

+ +

Cosas que debes saber

    -
  • - Está ligado a este navegador — Tu Playground se almacena en - la memoria de este navegador, por lo que no puedes acceder a él desde otros dispositivos. Esto - lo convierte en un espacio muy privado. -
  • -
  • - Haz copias de seguridad de tu trabajo — Usa la función de copia de seguridad del menú superior - para guardar tu sitio. Esto te permite restaurarlo en otro navegador o - protegerlo contra la pérdida de datos del almacenamiento del navegador. -
  • -
  • - ¿Listo para dar un paso más? — Traslada tu Playground a un - hospedaje dedicado de WordPress para acceder a él desde cualquier dispositivo, compartirlo públicamente o - invitar a personas concretas a colaborar. -
  • +
  • Está ligado a este navegador — Tu Playground se almacena en la memoria de este navegador, por lo que no puedes acceder a él desde otros dispositivos. Esto lo convierte en un espacio muy privado.
  • +
  • Haz copias de seguridad de tu trabajo — Usa la función de copia de seguridad del menú superior para guardar tu sitio. Esto te permite restaurarlo en otro navegador o protegerlo contra la pérdida de datos del almacenamiento del navegador.
  • +
  • ¿Listo para dar un paso más? — Traslada tu Playground a un hospedaje dedicado de WordPress para acceder a él desde cualquier dispositivo, compartirlo públicamente o invitar a personas concretas a colaborar.
- -

El menú superior

+ +

El menú superior

-

- 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:

    -
  • - Instalar aplicaciones — Hemos seleccionado algunos plugins que funcionan bien - para uso privado. También encontrarás allí el directorio completo de plugins de WordPress, por si - quieres echarles un vistazo. -
  • -
  • Copias de seguridad — Guarda y restaura tu sitio web
  • -
  • - Empezar de cero — Restablece tu WordPress o visita - playground.wordpress.net - para realizar experimentos rápidos y temporales -
  • +
  • Instalar aplicaciones — Hemos seleccionado algunos plugins que funcionan bien para uso privado. También encontrarás allí el directorio completo de plugins de WordPress, por si quieres echarles un vistazo.
  • +
  • Copias de seguridad — Guarda y restaura tu sitio web.
  • +
  • Empezar de cero — Restablece tu WordPress o visita playground.wordpress.net para realizar experimentos rápidos y temporales.
- -

¡Disfruta de tu WordPress!

+ +

¡Disfruta de tu WordPress!

From 77ffdbfd6249aef45c0dda4c6a45cd15836697f9 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 05/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.mo --- .../languages/playground-welcome-de_DE.mo | Bin 2523 -> 2646 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 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 b8cefa3afe05ea7a3b42d8d83429c8a6f9658ee7..1dcbe3d92fdb995887f7f439176e79de67edf05b 100644 GIT binary patch delta 775 zcmYMy&r2IY6u|Mx8h=!6wed%-we6^>pu$Q)Q>BNZq8CB*&>nhQqL(1YN~gW$0gFNM;hU=_U7lhE(mtaQlaGjBt7-n=(I8%`SwzY_6v#dyqY zW5#1jb>knDvEnFo4?klSZbj~5CGma~A7GOBD2mUph4=~wvAV3Z$0(+VJ*-u#pf*`N z;K4RF;vqi5?gB|6i_qh0wIED9d3ZJmQ znqkq+X+NQybgv@JOdUiXqrB)x6#vEn;tS-XI?1vNdvFY=@fmL5D?G%zc!QE?4P}(X zUtl}?t4S8J!FwFS57>a;P~!=9;yJ#=x;v$lPT(5x8z1YB5#@FL!A#1G8VHw2K_%YAod-(=KulF&8^JAWB*} zxwMa0vtWin`7xcH>n@i>M17nsFI< zP0Sa2=CEUk&zoD!6`MLgo==Vf`d3G)=gSzT*%pg`Zx=Mb3jcW z*_WVBn9$T}J$qa`l?M}}vr6lsnd~9)TV`br_Y>!kv?5u5ND{0@g JRf+p2JpYRbIDP;C From 37e87668e137c0110fe2d0b9b15972b8ffc00be4 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 06/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-de_DE.po --- .../plugin/languages/playground-welcome-de_DE.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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." From fc126c0b72b59c21c98dfcf37fa8ee17f15b4eec Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 07/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.mo --- .../languages/playground-welcome-es_ES.mo | Bin 2515 -> 2615 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 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 e7ff9f4ee5bea0ad3c6a9b5778a1768cc55ebd6e..9f86a17809e97d12b0ebf7573df532d8e5b596af 100644 GIT binary patch delta 749 zcmXxh&r2IY6u|MxkH+8iN31rsmcb@i6;`mQh0uaWK~RwDdC4wDD!aN#N^&v1^bov7 zycFt1JqTWEKu?}}(32oqp-_5|p8N-Dq0oNcW_>XG*|)nhv-93%{J6XPvn#r#h^N8$ ziz&vB5Eo)KtkgqX!$$lNSiuJJYLI`zB>7H|pW$Qj3w(vQ*pJU6N=@JtwklOtMQ(a| z@DaQ5Gd{sXlnu^t6tA%hW6}Ei1on`R;|R{+Iu@}7f1}KgJWwiuE!c@Ee1?;l4waQ! z;6@g%#_ALI0>5B0&rgE<3=Q%tY{Ceur?3s*;!7OB3ckhzY{OgBc!x<$P{tvAgN^L3 zvfPZ}8pgJn|o3g;y#7 delta 637 zcmX}p&nrYx6u|K_?}zbgjCnJR-yv^-g_0&UDK!gLlCo4L8yhti7E>5mNGzXOh>{&E zi`QgtWg(KHEUc_$;ScaVZ!V|K``r6}-E+^q&Ag;5AD&YcqL=I=e@LHvh;boSEs-`X zVk4G}WsEc5H}fN7fL+`_$7!r#KYn2jTWt~xm=&qWB7+P!)=`VtLH+S5PT(ba_-vkk zVkdKPM8+YDyI8;$yhHu|5xemfQ}~SoXt^SJHaCPG>&r?*{lk)R8=HCY(98pDVt#?z znQL6eCmcqf)n;%W6L^4wc!DEX!%=)k?V!VD5)+tWeVJvTl`WyZxQTf@!()6y-N71F zXklBZ9os|gOclrQ3eBl8s|TBoy2>Nr1frmhs8Ib^G0BCEM6J`d>RD@~NnMm~OFQ`& rdbWCg+S(pc3!#(I&N$Ei=rAKM*syZpy|rKkb8a&9-J4iw$EWOHu*flQ From 2997b53fabb3a51903e057d4981678f3ff715650 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 08/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-es_ES.po --- .../plugin/languages/playground-welcome-es_ES.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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." From 939d9ad2af3e78814d92a17f046bc5999f5cf3ab Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 09/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.mo --- .../languages/playground-welcome-pl_PL.mo | Bin 2583 -> 2690 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 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 e1e144e0f2c2e81f86697ee85855c18cd2e96ac0..994b1d8df7b868cbe5c40246d6e3cd728f5bb028 100644 GIT binary patch delta 759 zcmYMy&rcIU6u|Mp?FJ^<{mh&FF>l_?daOCv_#O?d3GFV| z9j;J7WE_8^whxxbRs4v3xaqrt7m0WM_zNb95B&HS4iO*YB=!e;bIf9zIER--8q%h7 zi-B#7;a9wk-%uy`gAef(qc|4oJ%5TfiJ#*GEMY&kaRs;V65hMe`%emoiPJcM8H_W( zpmmyQ=Sy zlh+#hg6@+xDkdXd@BF4E)}|}q`?IF!MU%X8HX_0fc@t}S$=z!=xa+p+R&W%&m20xP z#pE3os_Y%GLDT~%t8c7ya;9u-$Ei4`QYb7rj{P!aG9^2=Xq%c-H@R22Vmal0va(j7 v?*0uO1x6R@i?vRxTI@DWzT51y_~2R9IdJn!uT8a5uDmT5?Uef?{3Q4v$IDU$ delta 665 zcmXZaJxD@P6u|NGo|=_?m}#FHLWw~|kPr<8(H;eF5SNH*@53bsdJ!!JEky)5G&BV* z;ZjpeP7TdL9|T3UBtb(%ME~==2d=++@6-FvJ;%-1Vbpz$_z#4LoAHY&#=XS{v1E%h z;4)U@hH)E%toO|N$asQn?4RKXmN0>TIE-pQc4!EW|HaU3JOC5;Zw;~F;N6ZT*kxv4}cUlRLK&z-~}-Y;1uVcbDIxPW@|D;&cz zlA{dpt1Zl+mhb{~zl3^$8`Mg@qTcWaO&3{Jf+BdW=8@15>Q9iM)HAezsY#Ku_dfuyT``>ynxLfe>3lM~xUw+4U$A?N?{?0% Oa>a?jn^l|&W_ Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 10/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-pl_PL.po --- .../plugin/languages/playground-welcome-pl_PL.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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ę." From 505df7d5d3ab1423a68065775bf17db2fce3a503 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 11/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.mo --- .../languages/playground-welcome-pt_BR.mo | Bin 2466 -> 2569 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 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 77977625e5d680c5ae52924fa2e5daeffcea6fde..ecdfbbbcba2ff4b369dba1fe84d45e78b7511fc4 100644 GIT binary patch delta 755 zcmXxiO(+Cm7{Kv&$N2tU%lbC7l+-9?qNL>H#DR<3Sd)~UsbQ6p%|$sODM$G@xM45a za&mE{q#PWah?I+)@_%N>+syplXJ%*L=Xu^auG@}fUZVPeVzhC#a_SMKdhruw>}pC? z;U1RYQRq1q6JLdK4im(;Vf==T#NRlCWd->)#?d0Su|lbgI%E>(#u>)&23zm}Wq~j3 z#~+MhkDh-%j19yy*oRA4hKIO;Cs>YMh57$6v5DBiTAadm)>m^(EUMkaY93q{j5(~sN9@3NlwGCHpde5gSxELt1|17t41(|? zEtVmjktF|zR9M3)mHiKPD5y-@D+$Vw1Y}z!K|Y0GtJ(LU6XX+BbTRnA>QO#GwV%?g z#F%S1zVG>lmrSO8-;2y9zRPP=~VEd WX-qrrietFms-2RVnLR7|)&2kl_E59{ delta 665 zcmXZaJxfAS7{Kx8-ZU$-%$Hs*3lfEfAV?Z?X|pMC2$yJRF6%B$*$V_hErmt)Kp{8DAE5po`c@sIp@kb&x_UAWwi1h@zsQpu-_J@_`W(k7-d(a54W%j z_pDV6us*fxbL$ljvwwpN_=My5hgnQ?i1cC+heRr}#w5nZ4vygo>cI`1#e0n4hkgHx zgRH|okr_;54;FD3S22ihsONm+2>##z2K^!vIF4DTB9dd$&xY68&hXHBjCw%LuIt#% z`WmP44h#5z0Tp+~*nv4F1 J(+mY#?mzRBHyZ!| From 3b4409662f0c946913b9f04901814b0cbfd06ab0 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 12/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome-pt_BR.po --- .../plugin/languages/playground-welcome-pt_BR.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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." From 0c379361eab41f1f3c8b4e74ebaa626d8a9fdeb7 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 13/27] Update blueprints/my-wordpress/plugin/languages/playground-welcome.pot --- .../my-wordpress/plugin/languages/playground-welcome.pot | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 "" From 669180323c98afc102a2e895e3dc21621a79beb9 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 14/27] Update blueprints/my-wordpress/plugin/pl/welcome-post.html --- .../my-wordpress/plugin/pl/welcome-post.html | 267 ++++++------------ 1 file changed, 86 insertions(+), 181 deletions(-) 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 -

+ +

Eksperymentuj

+ + + +

Zmieniaj wszystko, próbuj, twórz po swojemu.

@@ -37,27 +34,18 @@
- -
- -

💾

+ +
+ +

💾

- -

- Zostaje zapisane - Zmiany będą tu jutro -

+ +

Zostaje z Tobą

+ + + +

Twoje zmiany są zapisywane i będą tu jutro.

@@ -66,27 +54,18 @@
- -
- -

🔒

+ +
+ +

🔒

- -

- Prywatność - Działa w przeglądarce, bez konta -

+ +

Prywatność

+ + + +

Działa w przeglądarce, bez konta.

@@ -95,147 +74,73 @@
- -
- -

- 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ć.

+ +
+ +
+
- + - -

Co możesz tu robić?

+ +

Co możesz tu robić?

    -
  • - Twoja własna przestrzeń — WordPress napędza znaczną część - internetu, ale tutaj jest tylko dla Ciebie. Używaj go jako notatnik, - przestrzeń projektową lub kreatywną piaskownicę — bez konieczności - publikowania. -
  • -
  • - Pełna personalizacja — Zmieniaj motyw, organizuj wszystko - po swojemu. Nauczysz się WordPressa naturalnie w trakcie używania. -
  • +
  • Twoja własna przestrzeń — WordPress napędza znaczną część internetu, ale tutaj jest tylko dla Ciebie. Używaj go jako notatnik, przestrzeń projektową lub kreatywną piaskownicę — bez konieczności publikowania.
  • +
  • Pełna personalizacja — Zmieniaj motyw, organizuj wszystko po swojemu. Nauczysz się WordPressa naturalnie w trakcie używania.
- -

Warto wiedzieć

+ +

Warto wiedzieć

    -
  • - Przypisany do tej przeglądarki — Twój Playground - przechowywany jest w tej przeglądarce, więc nie masz do niego dostępu z - innych urządzeń. Dzięki temu to bardzo prywatna przestrzeń. -
  • -
  • - Rób kopie zapasowe — Użyj funkcji kopii zapasowej w górnym - menu, aby zapisać swoją stronę. Pozwoli Ci to przywrócić ją w innej - przeglądarce lub zabezpieczyć przed utratą danych. -
  • -
  • - Chcesz więcej? — Przenieś swój Playground na dedykowany - hosting WordPress, aby mieć dostęp z każdego urządzenia, udostępnić go - publicznie lub zaprosić innych do współpracy. -
  • +
  • Przypisany do tej przeglądarki — Twój Playground przechowywany jest w tej przeglądarce, więc nie masz do niego dostępu z innych urządzeń. Dzięki temu to bardzo prywatna przestrzeń.
  • +
  • Rób kopie zapasowe — Użyj funkcji kopii zapasowej w górnym menu, aby zapisać swoją stronę. Pozwoli Ci to przywrócić ją w innej przeglądarce lub zabezpieczyć przed utratą danych.
  • +
  • Chcesz więcej? — Przenieś swój Playground na dedykowany hosting WordPress, aby mieć dostęp z każdego urządzenia, udostępnić go publicznie lub zaprosić innych do współpracy.
- -

Górne menu

+ +

Górne menu

-

- Szukaj ikony siatki () na górnym pasku. Stamtąd możesz: -

+

Szukaj ikony siatki () na górnym pasku. Stamtąd możesz:

    -
  • - Dodać aplikacje — Wybraliśmy wtyczki, które dobrze - sprawdzają się do prywatnego użytku. Pełny katalog wtyczek WordPress też - jest dostępny, jeśli chcesz odkrywać więcej. -
  • -
  • - Kopie zapasowe — Zapisuj i przywracaj swoją stronę -
  • -
  • - Zacznij od nowa — Zresetuj swojego WordPressa lub odwiedź - playground.wordpress.net - dla szybkich, tymczasowych eksperymentów -
  • +
  • Dodać aplikacje — Wybraliśmy wtyczki, które dobrze sprawdzają się do prywatnego użytku. Pełny katalog wtyczek WordPress też jest dostępny, jeśli chcesz odkrywać więcej.
  • +
  • Kopie zapasowe — Zapisuj i przywracaj swoją stronę.
  • +
  • Zacznij od nowa — Zresetuj swojego WordPressa lub odwiedź playground.wordpress.net dla szybkich, tymczasowych eksperymentów.
- -

Miłej zabawy z WordPressem!

+ +

Miłej zabawy z WordPressem!

From 28d0be5c523c589140ac2270d33bea9a55da7503 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 15/27] Update blueprints/my-wordpress/plugin/playground-welcome.css --- .../plugin/playground-welcome.css | 174 +++--------------- 1 file changed, 24 insertions(+), 150 deletions(-) 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%; } } From 595e03318540646fd967cf41dd0c49aed0bf1360 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 16/27] Update blueprints/my-wordpress/plugin/playground-welcome.js --- .../my-wordpress/plugin/playground-welcome.js | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 blueprints/my-wordpress/plugin/playground-welcome.js 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 ); From 9ffff2d848871435755ee9af25b7b9332593a034 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 17/27] Update blueprints/my-wordpress/plugin/playground-welcome.php --- .../plugin/playground-welcome.php | 163 +++++------------- 1 file changed, 39 insertions(+), 124 deletions(-) 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(); ?> -
-
-
- -

-
-

- -
- - -
- - -
- -
- -
- - -

-
- -
- - -
-
- - - -
- - -
-
-
-
- - +
Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 18/27] Update blueprints/my-wordpress/plugin/welcome-post.html --- .../my-wordpress/plugin/welcome-post.html | 264 ++++++------------ 1 file changed, 86 insertions(+), 178 deletions(-) diff --git a/blueprints/my-wordpress/plugin/welcome-post.html b/blueprints/my-wordpress/plugin/welcome-post.html index c7072345..8aa4dc8c 100644 --- a/blueprints/my-wordpress/plugin/welcome-post.html +++ b/blueprints/my-wordpress/plugin/welcome-post.html @@ -1,34 +1,31 @@ - -
+ +
+ +

👋

+ + + +

A WordPress of your own — make yourself at home.

+ +
+ + + +
- -
- -

🎨

+ +
+ +

🎨

- -

- Experiment freely
Change anything, try - things out, make it yours -

+ +

Experiment freely

+ + + +

Change anything, try things out, make it yours.

@@ -37,27 +34,18 @@
- -
- -

💾

+ +
+ +

💾

- -

- It stays
Your changes are saved and will - be here tomorrow -

+ +

It stays with you

+ + + +

Your changes are saved and will be here tomorrow.

@@ -66,27 +54,18 @@
- -
- -

🔒

+ +
+ +

🔒

- -

- It's private
Runs in your browser, no - account needed -

+ +

It's private

+ + + +

Runs in your browser, no account needed.

@@ -95,144 +74,73 @@
- -
- -

- 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.

+ +
+ +
+
- + - -

What can you do here?

+ +

What can you do here?

    -
  • - A space of your own — WordPress powers much of the web, - but here it's just for you. Use it as a notebook, project space, or - creative sandbox—no publishing required. -
  • -
  • - Fully customizable — Change your theme, organize things - however you like. You'll learn WordPress naturally as you go. -
  • +
  • A space of your own — WordPress powers much of the web, but here it's just for you. Use it as a notebook, project space, or creative sandbox — no publishing required.
  • +
  • Fully customizable — Change your theme, organize things however you like. You'll learn WordPress naturally as you go.
- -

Things to know

+ +

Things to know

    -
  • - It's bound to this browser — Your Playground lives in - this browser's storage, so you can't access it from other devices. This - makes it a very private space. -
  • -
  • - Back up your work — Use the backup feature in the top - menu to save your site. This lets you restore it in another browser or - protect against browser storage loss. -
  • -
  • - Ready to go bigger? — Move your Playground to dedicated - WordPress hosting to access it from any device, share it publicly, or - invite specific people to collaborate. -
  • +
  • It's bound to this browser — Your Playground lives in this browser's storage, so you can't access it from other devices. This makes it a very private space.
  • +
  • Back up your work — Use the backup feature in the top menu to save your site. This lets you restore it in another browser or protect against browser storage loss.
  • +
  • Ready to go bigger? — Move your Playground to dedicated WordPress hosting to access it from any device, share it publicly, or invite specific people to collaborate.
- -

The top menu

+ +

The top menu

-

- 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:

    -
  • - Install Apps — We've picked some plugins that work well - for private use. The full WordPress plugin directory is there too if you - want to explore. -
  • -
  • Backups — Save and restore your site
  • -
  • - Start over — Reset your WordPress or visit - playground.wordpress.net - for quick, temporary experiments -
  • +
  • Install Apps — We've picked some plugins that work well for private use. The full WordPress plugin directory is there too if you want to explore.
  • +
  • Backups — Save and restore your site.
  • +
  • Start over — Reset your WordPress or visit playground.wordpress.net for quick, temporary experiments.
- -

Enjoy your WordPress!

+ +

Enjoy your WordPress!

From 10e8810dbec5ba5324c106249142ec9a920fb4b8 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 19/27] Update Personal RAG blueprint --- blueprints/personal-rag/blueprint.json | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 blueprints/personal-rag/blueprint.json 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" + } + } + ] +} From 6097a9e91ca54f0aa808bbe68c216bd5bf20ba54 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 20/27] Add Personal RAG docs seed WXR --- blueprints/personal-rag/content.xml | 10557 ++++++++++++++++++++++++++ 1 file changed, 10557 insertions(+) create mode 100644 blueprints/personal-rag/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 @@ + + + + WordPress Playground Docs for Personal RAG + https://playground.wordpress.net + Starter documentation corpus for the Personal RAG blueprint. + Mon, 11 May 2026 00:00:00 +0000 + en-US + 1.2 + https://playground.wordpress.net + https://playground.wordpress.net + + 1 + + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + 9 + + + + + + 10 + + + + + + 11 + + + + + + 12 + + + + + + 13 + + + + + + 14 + + + + + + 15 + + + + + + + <![CDATA[Getting started]]> + https://playground.wordpress.net/blueprints-getting-started/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=1 + + Getting started with Blueprints +

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

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

There are three ways to use Blueprints:

+ +

What problems are solved by Blueprints?

+

No coding skills required

+

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

+

However, if you do have a development environment, that's great! You can use the Blueprint JSON schema to get autocompletion and validation.

+

HTTP Requests are managed for you

+

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

+

You can link to a Blueprint-preconfigured Playground

+

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

+
<BlueprintExample justButton={true} blueprint={{
+	"preferredVersions": {
+		"php": "8.3",
+  		"wp": "latest"
+	},
+	"steps": [
+        {
+            "step": "installTheme",
+            "themeData": {
+                "resource": "wordpress.org/themes",
+            	"slug": "pendant"
+            },
+            "options": {
+                "activate": true
+            }
+        }
+	]
+}} />
+

Trusted by default

+

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

+

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

+

Write it once, use it anywhere

+

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

+

Original Playground docs source: https://playground.wordpress.net/blueprints/getting-started

]]>
+ + 1 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Using Blueprints]]> + https://playground.wordpress.net/blueprints-using-blueprints/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=2 + + Using Blueprints +

You can use Blueprints in one of the following ways:

+
  • By passing them as a URL fragment to the Playground.
  • By loading them from a URL using the blueprint-url parameter.
  • By using Blueprint bundles (ZIP files or directories).
  • By using the JavaScript API.
+

URL Fragment

+

The easiest way to start using Blueprints is to paste one into the URL "fragment" on WordPress Playground website, e.g. https://playground.wordpress.net/#{"preferredVersions....

+

For example, to create a Playground with specific versions of WordPress and PHP you would use the following Blueprint:

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

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

+

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"
+	}
+}} />
+

Base64 encoded Blueprints

+

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
+

Load Blueprint from a URL

+

When your Blueprint gets too wieldy, you can load it via the ?blueprint-url query parameter in the URL, like this:

+

https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/adamziel/blueprints/trunk/blueprints/latest-gutenberg/blueprint.json

+

Note that the Blueprint must be publicly accessible and served with the correct Access-Control-Allow-Origin header:

+
Access-Control-Allow-Origin: *
+

Blueprint Bundles

+

The ?blueprint-url parameter now also supports Blueprint bundles in ZIP format. A Blueprint bundle is a ZIP file that contains a blueprint.json file at the root level, along with any additional resources referenced by the Blueprint.

+

For example, you can load a Blueprint bundle like this:

+

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

+

When using a Blueprint bundle, you can reference bundled resources using the bundled resource type:

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

For more information on Blueprint bundles, see the Blueprint Bundles documentation.

+

JavaScript API

+

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

+
<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

]]>
+ + 2 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Blueprint data Format]]> + https://playground.wordpress.net/blueprints-data-format/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=3 + + Blueprint data format +

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

+

Here's an example that uses many of them:

+
<BlueprintExample blueprint={{
+	"landingPage": "/wp-admin/",
+	"preferredVersions": {
+		"php": "8.3",
+		"wp": "6.5"
+	},
+	"features": {
+		"networking": true
+	},
+	"steps": [
+		{
+			"step": "login",
+			"username": "admin",
+			"password": "password"
+		}
+	]
+}} />
+

JSON schema

+

JSON files can be tedious to write and easy to get wrong. To help with that, Playground provides a JSON schema file that you can use to get auto-completion and validation in your editor. Just set the $schema property to the following:

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

Landing page

+

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

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

Preferred versions

+

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

+
  • php (string): Loads the specified PHP version. Accepts 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 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"
+	},
+}
+

Features

+

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

+
  • networking: Defaults to true. Enables or disables the networking support for Playground. If enabled, wp_safe_remote_get and similar WordPress functions will actually use fetch() to make HTTP requests. If disabled, they will immediately fail instead. You will need this property enabled if you want the user to be able to install plugins or themes.
+
{
+	"features": {
+		"networking": false
+	},
+}
+

Extra libraries

+

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

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

Steps

+

Arguably the most powerful property, steps allows you to configure the Playground instance with preinstalled themes, plugins, demo content, and more. The following example logs the user in with a dedicated username and password. It then installs and activates the Gutenberg plugin. Learn more about steps.

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

Original Playground docs source: https://playground.wordpress.net/blueprints/data-format

]]>
+ + 3 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Resources References]]> + https://playground.wordpress.net/blueprints-steps-resources/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=4 + + Resources References +

"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:

+

URLReference

+

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

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

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

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

The 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.

+

GitDirectoryReference

+

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

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

Example:

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

Notes:

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

CoreThemeReference

+

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

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

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

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

CorePluginReference

+

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

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

To use the _CorePluginReference_ resource, you need to provide the slug of the plugin. For example, to reference the "Akismet" plugin, you can create a _CorePluginReference_ as follows:

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

VFSReference

+

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

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

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

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

LiteralReference

+

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

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

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

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

BundledReference

+

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

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

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

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

Blueprint bundles can be distributed in various formats, including:

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

For more information on Blueprint bundles, see the Blueprint Bundles documentation.

+

Original Playground docs source: https://playground.wordpress.net/blueprints/steps/resources

]]>
+ + 4 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Shorthands]]> + https://playground.wordpress.net/blueprints-steps-shorthands/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=5 + + Shorthands +

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

+

login

+

Use

+
	"login": true,
+

Or

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

plugins

+

(replaces the installPlugin step)

+

Use

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

Or

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

siteOptions

+

Use

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

Or

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

defineWpConfigConsts

+

(constants only)

+

Use

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

Or

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

---

+

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

+

Which should you choose?

+
  • Use shorthands when brevity is your main concern.
  • Use explicit steps when you need more control over the execution order.
+

Original Playground docs source: https://playground.wordpress.net/blueprints/steps/shorthands

]]>
+ + 5 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Steps]]> + https://playground.wordpress.net/blueprints-steps/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=6 + + Steps +

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

+
<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:

+ +
  • Some steps have a shorthand version. Check the Shorthands section for more information about them.
+
  • For each step listed below, you'll find both a "Blueprint API" and a "Function API". Refer to the API Consistency page for further details.
+

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!

+

---

+

Blueprint Step Reference

+

activatePlugin

+

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"
+}
+
+

activateTheme

+

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"
+}
+
+

cp

+

Copies a file from one path to another.

+

Parameters:

+
  • fromPath (string): Source path
+
  • toPath (string): Target path
+

Example:

+

+{
+		"step": "cp",
+		"fromPath": "/wordpress/index.php",
+		"toPath": "/wordpress/index2.php"
+}
+
+

defineSiteUrl

+

Sets WP_HOME and WP_SITEURL constants for the WordPress installation. Using this step on playground.wordpress.net is moot. It is useful when building a custom Playground-based tool, like wp-now, or deploying Playground on a custom domain.

+

Parameters:

+
  • siteUrl (string): The URL
+

defineWpConfigConsts

+

Defines constants in a wp-config.php file. This step can be called multiple times, and the constants will be merged.

+

Parameters:

+
  • consts (Record): The constants to define
+
  • method ("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
+     }
+}
+
+

enableMultisite

+

Defines the Multisite constants in a wp-config.php file. This step can be called multiple times, and the constants will be merged.

+

Parameters:

+
  • wpCliPath (string optional): wp-cli.phar path
+

Example:

+

+{
+		"step": "enableMultisite"
+}
+
+

importThemeStarterContent

+

Imports a theme Starter Content into WordPress.

+

Parameters:

+
  • themeSlug (string optional): The name of the theme to import content from.
+

Example:

+

+{
+		"step": "importThemeStarterContent"
+}
+
+

importWordPressFiles

+

Imports top-level WordPress files from a given zip file into the documentRoot. For example, if a zip file contains the wp-content and wp-includes directories, they will replace the corresponding directories in Playground's documentRoot. 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"
+ 	}
+}
+
+

importWxr

+

Imports a WXR file into WordPress.

+

Parameters:

+
  • file (ResourceType): The file to import
+
  • importer ("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"
+		}
+}
+
+

installPlugin

+

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
+		}
+}
+
+

installTheme

+

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
+		}
+}
+
+

login

+

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

+

Parameters:

+
  • password (string optional)
+
  • username (string optional): The user to log in as. Defaults to 'admin'.
+

Example:

+

+{
+    "step": "login",
+    "username": "admin"
+}
+
+

mkdir

+

Creates a directory at the specified path.

+

Parameters:

+
  • path (string): The path of the directory you want to create
+

Example:

+

+{
+		"step": "mkdir",
+		"path": "/wordpress/my-new-folder"
+}
+
+

mv

+

Moves a file or directory from one path to another.

+

Parameters:

+
  • fromPath (string): Source path
+
  • toPath (string): Target path
+

Example:

+

+{
+		"step": "mv",
+		"fromPath": "/wordpress/index.php",
+		"toPath": "/wordpress/index2.php"
+}
+
+

resetData

+

Deletes WordPress posts and comments and sets the auto increment sequence for the posts and comments tables to 0.

+

Example:

+

+{
+		"step": "resetData"
+}
+
+

rmdir

+

Removes a directory at the specified path.

+

Parameters:

+
  • path (string): The path to remove
+

Example:

+

+{
+		"step": "rmdir",
+		"path": "/wordpress/wp-admin"
+}
+
+

rm

+

Removes a file at the specified path.

+

Parameters:

+
  • path (string): The path to remove
+

Example:

+

+{
+		"step": "rm",
+		"path": "/wordpress/index.php"
+}
+
+

runPHP

+

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')); ?>"
+}
+
+

runPHPWithOptions

+

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"
+		}
+}
+
+

runSql

+

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

+

Parameters:

+
  • sql (ResourceType): The SQL to run. Each non-empty line must contain a valid SQL query.
+

Example:

+

+{
+		"step": "runSql",
+		"sql": {
+			"resource": "literal",
+			"name": "schema.sql",
+			"contents": "DELETE FROM wp_posts"
+		}
+}
+
+

setSiteLanguage

+

Sets the site language and download translations.

+

Parameters:

+
  • language (string): The language to set, e.g. 'en_US'
+

Example:

+

+{
+		"step": "setSiteLanguage",
+		"language": "en_US"
+}
+
+

setSiteOptions

+

Sets site options. This is equivalent to calling update_option for each option in the options object.

+

Parameters:

+
  • options (Record): The options to set on the site.
+

Example:

+

+{
+    "step": "setSiteOptions",
+    "options": {
+        "blogname": "My Blog",
+        "blogdescription": "A great blog"
+    }
+}
+
+

unzip

+

Unzip a zip file.

+

Parameters:

+
  • extractToPath (string): The path to extract the zip file to
+
  • zipFile (ResourceType optional): The zip file to extract
+
  • zipPath (string optional): The path of the zip file to extract
+

Example:

+

+{
+		"step": "unzip",
+		"zipFile": {
+			"resource": "vfs",
+			"path": "/wordpress/data.zip"
+		},
+		"extractToPath": "/wordpress"
+}
+
+

updateUserMeta

+

Updates user meta. This is equivalent to calling update_user_meta for each meta value in the meta object.

+

Parameters:

+
  • meta (Record): An object of user meta values to set, e.g. { "first_name": "John" }
+
  • userId (number): User ID
+

Example:

+

+{
+    "step": "updateUserMeta",
+    "meta": {
+	       "first_name": "John",
+	       "last_name": "Doe"
+    },
+    "userId": 1
+}
+
+

wp-cli

+

Runs PHP code using WP-CLI.

+

Parameters:

+
  • command (string | string[]): The WP CLI command to run.
+
  • wpCliPath (string optional): wp-cli.phar path
+

Example:

+

+{
+		"step": "wp-cli",
+		"command": "wp post create --post_title='Test post' --post_excerpt='Some content'"
+}
+
+

writeFiles

+

Writes multiple files to a specified directory in the Playground filesystem. `` my-plugin/ ├── index.php └── public/ └── style.css ``

+

Parameters:

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

Example:

+

+{
+		"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; }"
+				}
+			}
+		}
+}
+
+

writeFile

+

Writes data to a file at the specified path.

+

Parameters:

+
  • data (string | Uint8Array | FileResource): The data to write
+
  • path (string): The path of the file to write to
+

Example:

+

+{
+		"step": "writeFile",
+		"path": "/wordpress/test.php",
+		"data": "<?php echo 'Hello World!'; ?>"
+}
+
+

<hr/> </> ))}

+

Original Playground docs source: https://playground.wordpress.net/blueprints/steps

]]>
+ + 6 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Blueprint Bundles]]> + https://playground.wordpress.net/blueprints-bundles/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=7 + + Blueprint Bundles +

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

+

What are Blueprint Bundles?

+

A Blueprint bundle is a collection of files that includes:

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

Blueprint bundles can be distributed in various formats:

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

Using Blueprint Bundles

+

On the Website

+

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

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

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

+

In the CLI

+

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

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

For example:

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

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

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

Creating Blueprint Bundles

+

Basic Structure

+

A basic Blueprint bundle might look like this:

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

Example Blueprint with Bundled Resources

+

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

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

In this example, the Blueprint references several bundled resources:

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

Creating a ZIP Bundle

+

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

+
# Create a directory for your bundle
+mkdir my-blueprint-bundle
+cd my-blueprint-bundle
+
+# Create your blueprint.json and add resources
+# ...
+
+# Zip it up
+zip -r ../my-blueprint-bundle.zip .
+

ZIP File Structure Flexibility

+

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

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

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

+

Example: Both of these ZIP structures work:

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

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

+

Troubleshooting

+

If you encounter issues with Blueprint bundles:

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

Original Playground docs source: https://playground.wordpress.net/blueprints/bundles

]]>
+ + 7 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[API Consistency]]> + https://playground.wordpress.net/blueprints-steps-api-consistency/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=8 + + JSON API and Function API +

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

+

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

+

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

+

Blueprints version 2

+

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

+

The proposed new specification is discussed on a separate GitHub repository, and you’re more than welcome to join (there or on the #playground Slack channel) and help shape the next generation of Playground.

+

Differences between JSON and Function APIs

+

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

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

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

]]>
+ + 8 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Examples]]> + https://playground.wordpress.net/blueprints-examples/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=9 + + Blueprints Examples +

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.

+

Install a Theme and a Plugin

+
<BlueprintExample blueprint={{
+	"steps": [
+		{
+			"step": "installPlugin",
+			"pluginData": {
+				"resource": "wordpress.org/plugins",
+				"slug": "coblocks"
+			}
+		},
+		{
+			"step": "installTheme",
+			"themeData": {
+				"resource": "wordpress.org/themes",
+				"slug": "pendant"
+			}
+		}
+	]
+}} />
+

The meta object

+

The optional meta object provides descriptive information about your Blueprint. While it doesn't affect how the Blueprint executes, this information is crucial for display purposes in galleries, Blueprint selectors, and integrated tools like WordPress Studio and Blueprints Gallery.

+

Properties

+
| Field             | Type            | Description                                      |
+| :---------------- | :-------------- | :----------------------------------------------- |
+| **`title`**       | `string`        | A short, human-readable name for the Blueprint.  |
+| **`description`** | `string`        | A brief summary explaining the setup.            |
+| **`author`**      | `string`        | The name or handle of the creator.               |
+| **`categories`**  | `array<string>` | Tags used for filtering and grouping Blueprints. |
+
{
+	"$schema": "https://playground.wordpress.net/blueprint-schema.json",
+	"meta": {
+		"title": "Default Playground Setup",
+		"description": "A basic setup for a new WordPress site with the latest versions.",
+		"author": "Playground Team",
+		"categories": ["starter", "default"]
+	},
+	"landingPage": "/wp-admin/",
+	"preferredVersions": {
+		"php": "8.3",
+		"wp": "latest"
+	}
+}
+

Run custom PHP code

+
<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
+));
+`
+}
+]
+}} />
+

Enable an option on the Gutenberg Experiments page

+

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 ) );"
+			}
+		]
+}} />
+

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

+

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

+

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

+

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.

+

Showcase a product demo

+
<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"
+			}
+		}
+	]
+}} />
+

Enable networking

+
<BlueprintExample blueprint={{
+	"landingPage": "/wp-admin/plugin-install.php",
+	"features": {
+		"networking": true
+	},
+	"steps": [
+		{
+			"step": "login"
+		}
+	]
+}} />
+

Load PHP code on every request (mu-plugin)

+

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(); } );"
+		}
+	]
+}} />
+

Code editor (as a Gutenberg block)

+
<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.

+

Load an older WordPress version

+

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

+

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

+
<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"
+    }
+  ]
+}} />
+

Run WordPress from trunk or a specific commit.

+

WordPress Playground can run trunk (the latest commit), the HEAD of a specific branch or a specific commit from the WordPress/WordPress GitHub repository.

+

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

+

To specify the latest commit of a particular branch, you can change the reference to the branch version number, eg 6.6. To run a specific commit, you can use the commit hash from WordPress/WordPress, eg 7d7a52367dee9925337e7d901886c2e9b21f70b6.

+

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

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

Using Blueprint Bundles

+

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

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

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

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

You can use this Blueprint bundle by:

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

For more information on Blueprint bundles, see the Blueprint Bundles documentation.

+

Original Playground docs source: https://playground.wordpress.net/blueprints/examples

]]>
+ + 9 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Troubleshoot and debug]]> + https://playground.wordpress.net/blueprints-troubleshoot-and-debug/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=10 + + Troubleshoot and debug Blueprints +

When you build Blueprints, you might run into issues. Here are tips and tools to help you debug them:

+

Review Common gotchas

+
  • Require 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".
+

Common Issues and Solutions

+

WP-CLI: Error Establishing a Database Connection on Mounted Sites

+

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.

+

Blueprints Builder

+

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.

+

Check for the Filesystem and Database

+

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

+

Check for errors in the browser console

+

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.

+

Log your own error messages

+

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.

+

Log errors snapshot

+

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.

+

Ask for help

+

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:

+
  • The Blueprint you’re trying to run.
  • The error message you’re seeing, if any.
  • The full output from the browser developer tools.
  • Any other relevant information that might help us understand the issue: OS, browser version, etc.
+

Original Playground docs source: https://playground.wordpress.net/blueprints/troubleshoot-and-debug

]]>
+ + 10 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Introduction]]> + https://playground.wordpress.net/blueprints/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=11 + + Blueprints Docs +

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>

+
  • Documentation – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
  • 👉 Blueprints (you're here) – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from this Blueprints docs hub.
  • Developers – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in the Developers docs hub.
  • API Reference – All the APIs exposed by WordPress Playground
+

Navigating the Blueprints documentation hub

+

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

+ +
  • Tutorial - Blueprints 101 - Blueprints API crash course. The tutorial will guide you through the complete process of creating a blueprint that loads a theme and plugin (among other things).
+
  • Blueprint data Format: Blueprint JSON files define your Playground instance with various properties. This section highlights the key properties you need to know.
+ +
  • Steps: API Reference of all the available steps that can be set in a blueprint to run tasks such as login, plugin/theme activation, file operations, and more.
+
  • Blueprint Bundles: Learn how to create and use Blueprint bundles - self-contained packages that include a Blueprint and all its resources.
+
  • Examples: Compilation of Blueprint examples for various WordPress Playground setups, including installing themes/plugins, running PHP code, enabling features, and loading specific WordPress versions.
+ +

Original Playground docs source: https://playground.wordpress.net/blueprints

]]>
+ + 11 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[What are Blueprints?]]> + https://playground.wordpress.net/blueprints-tutorial-what-are-blueprints-what-you-can-do-with-them/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=12 + + What are Blueprints, and what can you do with them? +

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

+

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

+

Blueprints support advanced use cases, like file system and database manipulation, and give you fine-grained control over the instance you create. The WordPress Test Team has been using Playground in the 6.5 beta release cycle, creating a Blueprint that loads the latest version, several testing plugins, and dummy data.

+

A simple example

+

A Blueprint might look something like this:

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

The Blueprint above installs the _Akismet_ and _Gutenberg_ plugins and the _Twenty Nineteen_ theme, sets the site name and description, and enables the WordPress debugging mode.

+

The benefits of Blueprints

+

Blueprints are an invaluable tool for building WordPress sites via Playground

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

More Resources

+

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

+ +

Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial/what-are-blueprints-what-you-can-do-with-them

]]>
+ + 12 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[How to run Blueprints]]> + https://playground.wordpress.net/blueprints-tutorial-how-to-load-run-blueprints/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=13 + + How to load and run Blueprints +

URL fragment

+

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> &nbsp; Run Blueprint &nbsp; </kbd>

+

Use this method to run the example code in the next chapter, Build your first Blueprint.

+

Base64 encoded Blueprints

+

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

+

Load Blueprint from a URL

+

When your Blueprint gets too wieldy, you can load it via the ?blueprint-url query parameter in the URL, like this:

+

https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/latest-gutenberg/blueprint.json

+

Note that the Blueprint must be publicly accessible and served with the correct Access-Control-Allow-Origin header:

+
Access-Control-Allow-Origin: *
+

Original Playground docs source: https://playground.wordpress.net/blueprints/tutorial/how-to-load-run-blueprints

]]>
+ + 13 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Build your first Blueprint]]> + https://playground.wordpress.net/blueprints-tutorial-build-your-first-blueprint/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=14 + + Let's build an elementary Blueprint that

+
  1. Creates a new WordPress site
  2. Sets the site title to "My first Blueprint"
  3. Installs the _Adventurer_ theme
  4. Installs the _Hello Dolly_ plugin from the WordPress plugin directory
  5. Installs a custom plugin
  6. Changes the site content
+

1. Create a new WordPress site

+

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

+
{}
+

It may seem like nothing is happening, but this Blueprint already spins up a WordPress site with the latest major version.

+

<kbd> &nbsp; Run Blueprint &nbsp; </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:

+

Autocompletion visualized

+

2. Set the site title to "My first Blueprint"

+

Blueprints consist of a series of steps that define how to build a WordPress site. Before you write the first step, declare an empty list of steps:

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

This Blueprint isn't very exciting—it creates the same default site as the empty Blueprint above. Let's do something about it!

+

WordPress stores the site title in the blogname option. Add your first step and set that option to "My first Blueprint":

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

<kbd> &nbsp; Run Blueprint &nbsp; </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.

+

Shorthands

+

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

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

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

+

3. Install the _Adventurer_ theme

+

Adventurer is an open-source theme available in the WordPress theme directory. Let's install it using the installTheme step:

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

<kbd> &nbsp; Run Blueprint &nbsp; </kbd>

+

The site should now look like the screenshot below:

+

Site with the adventurer theme

+

Resources

+

The themeData defines a resource and references an external file required to complete the step. Playground supports different types of resources, including

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

The example uses the wordpress.org/themes resource, which requires a slug identical to the one used in WordPress theme directory:

+

In this case, https://wordpress.org/themes/<slug>/ becomes https://wordpress.org/themes/adventurer/.

+

Note

+

Learn more about the supported resources in the Blueprint Resources API Reference.

+

4. Install the _Hello Dolly_ plugin

+

A classic WordPress plugin that displays random lyrics from the song "Hello, Dolly!" in the admin dashboard. Let's install it using the installPlugin step:

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

<kbd> &nbsp; Run Blueprint &nbsp; </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.

+

5. Install a custom plugin

+

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

+
<?php
+/*
+Plugin Name: "Hello" on the Dashboard
+Description: A custom plugin to showcase WordPress Blueprints
+Version: 1.0
+Author: WordPress Contributors
+*/
+
+function my_custom_plugin() {
+    echo '<h1>Hello from My Custom Plugin!</h1>';
+}
+
+add_action('admin_notices', 'my_custom_plugin');
+

You can use the installPlugin, but that requires creating a ZIP file. Let's start with something different to see if the plugin works:

+
  1. Create a wp-content/plugins/hello-from-the-dashboard directory using the mkdir step.
  2. Write a plugin.php file using the writeFile step.
  3. Activate the plugin using the activatePlugin step.
+

Here's what that looks like in a Blueprint:

+
{
+	// ...
+	"steps": [
+		// ...
+		{
+			"step": "mkdir",
+			"path": "/wordpress/wp-content/plugins/hello-from-the-dashboard"
+		},
+		{
+			"step": "writeFile",
+			"path": "/wordpress/wp-content/plugins/hello-from-the-dashboard/plugin.php",
+			"data": "<?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> &nbsp; Run Blueprint &nbsp; </kbd>

+

That's what it looks like when you navigate to the dashboard:

+

Site with the custom plugin

+

Create a plugin and zip it

+

Encoding PHP files as JSON can be useful for quick testing, but it's inconvenient and difficult to read. Instead, create a file with the plugin code, compress it, and use the ZIP file as the resource in the installPlugin step to install it (the path in the URL should match the one in your GitHub repository):

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

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

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

<kbd> &nbsp; Run Blueprint &nbsp; </kbd>

+

6. Change the site content

+

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

+

Delete the old content

+

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

+
<?php
+require '/wordpress/wp-load.php';
+
+// Delete all posts and pages
+$posts = get_posts(array(
+    'numberposts' => -1,
+    'post_type' => array('post', 'page'),
+    'post_status' => 'any'
+));
+
+foreach ($posts as $post) {
+    wp_delete_post($post->ID, true);
+}
+

To run that code during the site setup, use the runPHP step:

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

Import the new content

+

Let's use the importWxr step 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> &nbsp; Run Blueprint &nbsp; </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

]]>
+ + 14 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Blueprints 101]]> + https://playground.wordpress.net/blueprints-tutorial/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=15 + + Blueprints 101 - A crash course +

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

+
  1. What are Blueprints, and what can you do with them?
  2. How to load and run Blueprints
  3. Build your first Blueprint
+

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

]]>
+ + 15 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Quick Start Guide for Developers]]> + https://playground.wordpress.net/developers-build-your-first-app/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=16 + + Quick Start Guide for Developers +

WordPress Playground was created as a programmable tool. Below you'll find a few examples of what you can do with it. Each discussed API is described in detail in the APIs section:

+

Embed WordPress on your website

+

Playground can be embedded on your website using the HTML <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.

+

Control the embedded website

+

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

+
  • Query API enable basic operations using only query parameters
  • Blueprints API give you a great degree of control with a simple JSON file
  • JavaScript API give you full control via a JavaScript client from an npm package
+

Learn more about each of these APIs in the APIs overview section.

+

Showcase a plugin or theme from WordPress directory

+

You can install plugins and themes from the WordPress directory with only URL parameters. This iframe preinstalls the coblocks and friends plugins and the pendant theme. This is called Query API and you can learn more about it here.

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

Showcase any plugin or theme

+

What if your plugin is not in the WordPress directory?

+

You can still showcase it on Playground by using JSON Blueprints. For example, this Blueprint would download and install a plugin and a theme from your website and also import some starter content:

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

See getting started with Blueprints to learn more.

+

Preview pull requests from your repository

+

You can preview repository code two ways: directly with git:directory, or by pointing to a .zip from your CI pipeline. Here's the git:directory approach using Blueprints:

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

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

+

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:

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

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

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

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

+
<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"
+			}
+		}
+	]
+	}} />
+

Preview WordPress Core and Gutenberg Branches or PRs

+

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

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

Build a compatibility testing environment

+

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

+

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

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

With JSON Blueprints, you'd use the preferredVersions property:

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

Run PHP code in the browser

+

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

+
<iframe id="wp"></iframe>
+<script type="module">
+	const client = await startPlaygroundWeb({
+		iframe: document.getElementById('wp'),
+		remoteUrl: 'https://playground.wordpress.net/remote.html',
+	});
+	await client.isReady;
+	await client.run({
+		code: `<?php
+		require("/wordpress/wp-load.php");
+
+		update_option("blogname", "Playground is really cool!");
+		echo "Site title updated!";
+		`,
+	});
+	client.goTo('/');
+</script>
+

Combine that with a code editor like Monaco or CodeMirror, and you'll get live code snippets like in this article!

+

Original Playground docs source: https://playground.wordpress.net/developers/build-your-first-app

]]>
+ + 16 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[wp-now]]> + https://playground.wordpress.net/developers-local-development-wp-now/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=17 + +

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 NPM package

+

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:

+
  • Command-line Interface: Easy to use for developers comfortable with CLI.
  • Quick Setup: Set up a local WordPress environment in seconds.
  • Customizable: Allows for configuration to suit specific development needs.
+

@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.

+

Launch wp-now in a plugin or theme directory

+

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
+

Launch wp-now in the wp-content directory with options

+

You 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
+

Install wp-now globally

+

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

]]>
+ + 17 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[VS Code extension]]> + https://playground.wordpress.net/developers-local-development-vscode-extension/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=18 + + VS Code extension +

Start a zero-setup development environment using the VS Code extension, and develop your plugin or theme locally without installing Apache or MySQL.

+

Key Features:

+
  • Integrated Development: Develop WordPress sites directly within VS Code.
  • Ease of Use: Simplifies the development workflow with integrated tools.
+

Documentation

+

The VS Code extension is maintained in a different GitHub repository, Playground Tools. You can find the latest documentation in the dedicated README file.

+

Installation and Usage:

+
  1. Install the Extension: Search for “WordPress Playground” in the VS Code extensions marketplace and install it.
  2. Setup: Follow the setup instructions provided in the extension to configure your development environment.
  3. Develop and Debug: Use the integrated tools to develop and debug your WordPress site.
+

The extension ships with a portable WebAssembly version of PHP and sets up WordPress to use SQLite. Once installed, all you have to do is click the Start WordPress Server button in VS Code:

+

Original Playground docs source: https://playground.wordpress.net/developers/local-development/vscode-extension

]]>
+ + 18 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[php-wasm/node]]> + https://playground.wordpress.net/developers-local-development-php-wasm-node/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=19 + + Using WordPress Playground in Node.js +

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.

+

WebAssembly PHP for Node.js

+

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.

+

Basic usage

+
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);
+

Use cases

+

Run PHP inside Node.js without a native PHP install. Allow developer to produce the following solutions:

+
  • CI/CD jobs and developer tooling.
  • Support education and WordPress workflows: Power interactive tutorials, sandboxes, and coding challenges.
  • Generate content and prototype server behavior.
  • Render HTML using PHP templates, and quickly stand up mock API endpoints to simulate requests.
+

Practical demos

+

We will list some examples using the PHP-WASM package.

+

Demo 1: File system operations

+

Execute PHP scripts that interact with the file system:

+
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);
+

Demo 2: SQLite database operations

+

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);
+

Demo 3: Processing uploaded files (ZIP archives)

+

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);
+

Demo 4: HTTP request/response pattern

+

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);
+

Demo 5: Template rendering engine

+

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>&copy; <?= date('Y') ?> <?= $appName ?>. All rights reserved.</p>
+    </div>
+</body>
+</html>`
+);
+
+// Render template with data
+const templateData = {
+	name: 'Priya Sharma',
+	email: 'priya@example.com',
+	appName: 'MyAwesomeApp',
+	timestamp: Math.floor(Date.now() / 1000),
+	features: ['Dashboard Access', 'API Integration', 'Premium Support', 'Custom Branding'],
+};
+
+// Pass data to template via environment variables or files
+await php.writeFile('/template-data.json', JSON.stringify(templateData));
+
+const result = await php.runStream({
+	code: `<?php
+    $data = json_decode(file_get_contents('/template-data.json'), true);
+    extract($data);
+    include '/templates/email.php';
+  ?>`,
+});
+
+console.log(await result.stdoutText);
+// Now you have rendered HTML that can be sent via email or saved
+

Demo 6: Real-time code execution and streaming

+

Process PHP output as it's generated:

+
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);
+		},
+	})
+);
+

Integration patterns

+

Pattern 1: Express.js middleware

+

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');
+});
+

Pattern 2: Automated testing

+

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);
+	});
+});
+

Pattern 3: Build tool integration

+

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);
+

Advanced features

+

Working with environment variables

+
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);
+

Error handling

+
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);
+}
+

Performance considerations

+
  • Reuse PHP instances: Creating a new PHP instance is expensive. Reuse the same instance when possible.
  • Batch operations: Group multiple file operations together rather than running separate scripts.
  • Memory management: Large files may impact performance. Consider streaming for big datasets.
  • Caching: Cache compiled PHP scripts and frequently accessed data.
+

Original Playground docs source: https://playground.wordpress.net/developers/local-development/php-wasm-node

]]>
+ + 19 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Playground CLI]]> + https://playground.wordpress.net/developers-local-development-wp-playground-cli/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=20 + + Playground CLI +

@wp-playground/cli is a command-line tool that simplifies the WordPress development and testing flow. Playground CLI supports auto-mounting a directory with a plugin, theme, or WordPress installation. But if you need flexibility, the CLI supports mounting commands to personalize your local environment.

+

Key features:

+
  • Quick Setup: Set up a local WordPress environment in seconds.
  • Flexibility: Allows for configuration to adapt to different scenarios.
  • Simple Environment: No extra configuration, just a compatible Node version, and you are ready to use it.
+

The Playground CLI includes two main commands for running WordPress locally:

+
  • start (Simplified): Auto-detects your project type, persists sites between sessions, and opens a browser automatically.
  • server (Advanced): Provides full manual control over configuration. Best for custom setups, CI/CD pipelines, or when you need fine-grained control.
+

Requirements

+

The Playground CLI requires Node.js 20.18 or higher, which is the recommended Long-Term Support (LTS) version. You can download it from the Node.js website.

+

Quickstart

+

To run the Playground CLI, open a command line and use one of the following commands:

+

Using start (Simplified)

+

The start command is the easiest way to get started. It automatically detects your project type, persists your site, and opens the browser:

+
npx @wp-playground/cli@latest start
+

When run inside a plugin or theme directory, start automatically mounts your project:

+
cd my-plugin
+npx @wp-playground/cli@latest start
+

Key differences from server:

+
  • Auto-login is enabled by default
  • Opens browser automatically
  • Auto-mounts the project by default
+

Using server (Advanced)

+

The server command provides full control over configuration:

+
npx @wp-playground/cli@latest server
+

Playground CLI in Action

+

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:

+
  • You want a clean WordPress installation
  • Testing fresh installation scenarios
  • Your site data became corrupted or inconsistent
+

The --reset flag works only with start. For server, manually delete the persisted site directory at ~/.wordpress-playground/sites/<path-hash>/.

+

Choosing a WordPress and PHP Version

+

By default, the CLI loads the latest stable version of WordPress and PHP 8.3 due to its improved performance. To specify your preferred versions, you can use the flag --wp=<version> and --php=<version>:

+
npx @wp-playground/cli@latest server --wp=6.8 --php=8.3
+

Loading Blueprints

+

One way to take your Playground CLI development experience to the next level is to integrate with Blueprints. For those unfamiliar with this technology, it allows developers to configure the initial state for their WordPress Playground instances.

+

Using the --blueprint=<blueprint-address> flag, developers can run a Playground with a custom initial state. We’ll use the example below to do this.

+

(my-blueprint.json)

+
{
+  "landingPage": "/wp-admin/options-general.php?page=akismet-key-config",
+  "login": true,
+  "plugins": [
+    "hello-dolly",
+    "https://raw.githubusercontent.com/adamziel/blueprints/trunk/docs/assets/hello-from-the-dashboard.zip"
+  ]
+}
+

CLI command loading a blueprint:

+
npx @wp-playground/cli@latest server --blueprint=my-blueprint.json
+

Mounting folders manually

+

Some projects have a specific structure that requires a custom configuration; for example, your repository contains all the files in the /wp-content/ folder. So in this scenario, you can specify to the Playground CLI that it will mount your project from that folder using the --mount flag.

+
npx @wp-playground/cli@latest server --mount=.:/wordpress/wp-content/plugins/MY-PLUGIN-DIRECTORY
+

Mounting before WordPress installation

+

Consider mounting your WordPress project files before the WordPress installation begins. This approach is beneficial if you want to override the Playground boot process, as it can help connect Playground with WP-CLI. The --mount-before-install flag supports this process.

+
npx @wp-playground/cli@latest server --mount-before-install=.:/wordpress/
+

On Windows, the path format /host/path:/vfs/path can cause issues. To resolve this, use the flags --mount-dir and --mount-dir-before-install. These flags let you specify host and virtual file system paths in an alternative format: "/host/path" "/vfs/path".

+

Understanding Data Persistence and SQLite Location in server mode

+

By default, Playground CLI stores WordPress files and the SQLite database in temporary directories on your operating system:

+
<OS-TEMP-DIR>/playground-<random-id>/
+├── wordpress/          # WordPress installation
+├── internal/          # Playground runtime config
+└── tmp/              # Temporary PHP files
+

Finding Your Temp Directory:

+

The actual location depends on your OS (these are examples or common possibilities):

+
  • macOS/Linux: May be under /tmp/ or /private/var/folders/ (varies by system)
  • Windows: C:\Users\<username>\AppData\Local\Temp\
+

To see the exact temp directory path being used, run the CLI with the --verbosity=debug flag:

+
npx @wp-playground/cli@latest server --verbosity=debug
+

This will output something like:

+
Native temp dir for VFS root:
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC
+Mount before WP install: /home ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/home
+Mount before WP install: /tmp ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/tmp
+Mount before WP install: /wordpress ->
+/private/var/folders/c8/mwz12ycx4s509056kby3hk180000gn/T/node-playground-cli-site-62926--62926-yQNOdvJVIgYC/wordpress
+

Where is the SQLite Database Stored?

+

The database location depends on what you mount:

+
  • Auto-mounting wp-content or full WordPress:
  • Database: <your-local-project>/wp-content/database/.ht.sqlite
  • ✅ Persisted locally in your project folder
+
  • Auto-mounting plugin/theme only:
  • Database: <OS-TEMP-DIR>/playground-<id>/wordpress/wp-content/database/.ht.sqlite
  • ⚠️ Lost when server stops (temp directories are cleaned up)
+
  • Custom mounts: Database location follows your mount configuration
+

Automatic Cleanup: Playground CLI automatically removes temp directories that are:

+
  • Older than 2 days
  • No longer associated with a running process
+

Recommendation: To persist both your code and database when developing plugins or themes, mount the entire wp-content directory instead of just the plugin/theme folder.

+

Example: Mounting wp-content for persistence

+
# Mount your entire wp-content directory
+cd my-wordpress-project
+npx @wp-playground/cli@latest server --mount=./wp-content:/wordpress/wp-content
+

Data Persistence in start mode

+

Running 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.

+

Persistence behavior

+
  • Default (no explicit mount): WordPress files and database persist in ~/.wordpress-playground/sites/<path-hash>/. Changes survive between CLI restarts.
  • Explicit /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:

+
  • Default (automatic persistence):
  • Database: ~/.wordpress-playground/sites/<path-hash>/wordpress/wp-content/database/.ht.sqlite
  • Persisted automatically between sessions
+

Resetting a persisted site

+

To start fresh, use the --reset flag with the start command:

+
npx @wp-playground/cli@latest start --reset
+

Command and Arguments

+

Playground CLI is simple, configurable, and unopinionated. You can set it up according to your unique WordPress setup. With the Playground CLI, you can use the following top-level commands:

+
  • start: (Simplified) Starts a local WordPress server with automatic project detection, site persistence, and browser opening.
  • server: (Advanced) Starts a local WordPress server with full manual control over configuration.
  • run-blueprint: Executes a Blueprint file without starting a web server.
  • build-snapshot: Builds a ZIP snapshot of a WordPress site based on a Blueprint.
+

The start command 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.

+

Need some help with the CLI?

+

With the Playground CLI, you can use the --help flag to get the full list of available commands and arguments.

+
npx @wp-playground/cli@latest --help
+

Programmatic usage

+

The Playground CLI can also be controlled programmatically from JavaScript/TypeScript using the runCLI function. See the Programmatic Usage guide for details on automation and testing.

+

Original Playground docs source: https://playground.wordpress.net/developers/local-development/wp-playground-cli

]]>
+ + 20 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Local Development]]> + https://playground.wordpress.net/developers-local-development/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=21 + + Local WordPress Development with Playground +

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

+

For a quick start, use a public Playground web instance at https://playground.wordpress.net/. Alternatively, you can host your own WordPress Playground.

+

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

+
  • @wp-playground/cli: A command-line tool to quickly launch a WordPress site from your terminal.
  • Visual Studio Code Extension: Integrates WordPress development directly into VS Code for a seamless experience within the popular editor.
+

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

+ +

Original Playground docs source: https://playground.wordpress.net/developers/local-development

]]>
+ + 21 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[APIs overview]]> + https://playground.wordpress.net/developers-apis/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=22 + + WordPress Playground APIs overview +

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

+

Query API

+

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

+

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

+

Or a theme:

+

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

+

This is called Query API and you can learn more about it here. Once you have a URL that you like, you can embed it in your website using an iframe:

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

Check the Query API section for more info.

+

Blueprints

+

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

+
<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.

+

JavaScript API

+

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

+
<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
+<script type="module">
+	// Use unpkg for convenience
+	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.

+

Playground APIs Concepts

+

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

+

Link to the Playground site

+

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

+

To prepare such a link, use either the Query API (easy) or the JSON Blueprints API (medium).

+

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

+

Embed in an <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.

+

Browser APIs

+

The following Playground APIs are available in the browser:

+
  • Query API enable basic operations using only query parameters
  • Blueprints API give you a great degree of control with a simple JSON file
  • JavaScript API give you full control via a JavaScript client from an npm package
+

In Node.js

+

The following Playground APIs are available in Node.js:

+ +

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/

]]>
+ + 22 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[JavaScript API]]> + https://playground.wordpress.net/developers-apis-javascript-api/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=23 + + JavaScript API +

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

+

API here doesn't mean "REST API"

+

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

+

Quick start

+

To use the JavaScript API, you'll need:

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

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

+
<iframe id="wp" style="width: 100%; height: 300px; border: 1px solid #000;"></iframe>
+<script type="module">
+	// Use unpkg for convenience
+	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.

+

Controlling the website

+

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

+ +

Debugging and testing

+

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

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

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

+

Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api

]]>
+ + 23 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[remote.html vs index.html]]> + https://playground.wordpress.net/developers-apis-javascript-api-html-vs-remote-html/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=24 + + remote.html vs index.html +

playground.wordpress.net exposes two distinct APIs through two separate HTML files: remote.html and index.html. Here's an overview of their functions and differences:

+
  • index.html uses WordPress Playground API client to control the "endpoint" that is remote.html.
  • The Query API is exclusively provided by index.html, independent of the WordPress Playground JavaScript API.
  • The JavaScript API is exclusively provided by remote.html. Only that file can be used as an "endpoint" for the PlaygroundClient class.
+

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

+

Remote.html

+

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

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

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

+

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

+

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

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

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

+

Index.html

+

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

+

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

+

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

+

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

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

Note that playground is a Proxy object in this context and you won't get any autocompletion from the browser.

+

Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/-html-vs-remote-html

]]>
+ + 24 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Playground API Client]]> + https://playground.wordpress.net/developers-apis-javascript-api-playground-api-client/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=25 + + Playground API Client +

The PlaygroundClient object implements the UniversalPHP interface. All the methods from that interface are also available in Node.js and same-process PHP instances (Playground runs PHP in a web worker).

+

Broadly speaking, you can use the client to perform three types of operations:

+
  • Running PHP code
  • Customizing PHP.ini
  • Managing files and directories
+

Running PHP code

+

The two methods you can use to run PHP code are:

+
  • run() - runs PHP code and returns the output
  • request() - makes an HTTP request to the website
+

In Node.js, you can also use the cli() method to run PHP in a CLI mode.

+

The run() method

+

The request() method

+

Customizing PHP.ini

+

The API client also allows you to change the php.ini file:

+
await setPhpIniEntries(client, {
+	display_errors: 'On',
+	error_reporting: 'E_ALL',
+});
+

Managing files and directories

+

The client object provides you with a low-level API for managing files and directories in the PHP filesystem:

+
await client.mkdirTree('/wordpress/test');
+// Create a new PHP file
+await client.writeFile(
+	'/wordpress/test/index.php',
+	`<?php
+     echo "Hello, world!<br/>";
+     // List all the files in current directory
+     print_r(glob(__DIR__ . '/*'));
+  `
+);
+// Create files named 1, 2, and 3
+await client.writeFile('/wordpress/test/1', '');
+await client.writeFile('/wordpress/test/2', '');
+await client.writeFile('/wordpress/test/3', '');
+// Remove the file named 1
+await client.unlink('/wordpress/test/1');
+// Navigate to our PHP file
+await client.goTo('/test/index.php');
+

For a complete list of these methods, refer to the PlaygroundClient interface.

+

Sending messages to JavaScript

+

You can pass messages from PHP to JavaScript using the post_message_to_js() function. It accepts one argument:

+
  • $data (string) – Data to pass to JavaScript.
+

For example, here's how you would send a message with a JSON-encoded post ID and title:

+
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!' }
+

The cli() method

+

In Node.js, you also have access to the cli() method that runs PHP in a CLI mode:

+
// Run PHP in a CLI mode
+client.cli(['-r', 'echo "Hello, world!";']);
+// Outputs "Hello, world!"
+

Once cli() method finishes running, the PHP instance is no longer usable and should be discarded. This is because PHP internally cleans up all the resources and calls exit().

+

Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/playground-api-client

]]>
+ + 25 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Blueprints JSON and the API Client]]> + https://playground.wordpress.net/developers-apis-javascript-api-blueprint-json-in-api-client/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=26 + + Blueprints JSON and the API Client +

The Playground API client can be initialized with a JSON Blueprint. This is a convenient way of preconfiguring it in any way you like without worrying about progress bars and fetching remote files:

+
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

]]>
+ + 26 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Blueprints Functions and the API Client]]> + https://playground.wordpress.net/developers-apis-javascript-api-blueprint-functions-in-api-client/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=27 + + Blueprints Functions and the API Client +

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

+

For example:

+
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

]]>
+ + 27 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Mount data]]> + https://playground.wordpress.net/developers-apis-javascript-api-mount-data/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=28 + + Mount data +

Mount a directory from the browser

+

You can mount a directory from the browser to Playground using the window.showDirectoryPicker API. Check the Browser compatibility before using this API.

+
window.showDirectoryPicker().then(function (directoryHandle) {
+	window.parent.postMessage({
+		type: 'mount-directory-handle',
+		directoryHandle,
+		mountpoint: '/wordpress/wp-content/uploads/markdown/',
+	});
+});
+

Mount Browser's OPFS Storage

+

You can mount OPFS storage available within the browser as well. Under the hood, we sync the memory filesystem to OPFS at the end of every PHP request served. It's advisable to delay mounting of OPFS after boot as shown below, so that WordPress installation doesn't trigger a sync of over 3000 files slowing down the boot process.

+
const hasWordPressSiteInOPFS = false; // roll your logic to track this
+const blueprint = {
+	preferredVersions: {
+		php: '8.4',
+		wp: 'latest',
+	},
+	features: {
+		networking: true,
+	},
+	login: true,
+	steps: [], // add steps
+};
+
+try {
+	const mountDescriptor: MountDescriptor = {
+		device: {
+			type: 'opfs',
+			path: `my-unique-prefix/my-site`,
+		},
+		mountpoint: '/wordpress',
+		initialSyncDirection: hasWordPressSiteInOPFS ? 'opfs-to-memfs' : 'memfs-to-opfs',
+	};
+
+	const client = await startPlaygroundWeb({
+		iframe: document.getElementById('wp'),
+		remoteUrl: 'https://playground.wordpress.net/remote.html',
+		blueprint: blueprint,
+		shouldInstallWordPress: !hasWordPressSiteInOPFS,
+		mounts: hasWordPressSiteInOPFS ? [mountDescriptor] : [],
+	});
+
+	if (!hasWordPressSiteInOPFS) {
+		await client.mountOpfs(mountDescriptor);
+	}
+
+	await client.isReady();
+	return client;
+} catch (error) {
+	// handle error here
+}
+

For persistence guarantees, check Storage quotes and eviction criterias.

+

Original Playground docs source: https://playground.wordpress.net/developers/apis/javascript-api/mount-data

]]>
+ + 28 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Query API]]> + https://playground.wordpress.net/developers-apis-query-api/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=29 + + Query API +

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

+

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

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

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

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

Available options

+
| Option             | Default Value         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
+| ------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `php`              | `8.5`                 | Loads the specified PHP version. Accepts `7.4`, `8.0`, `8.1`, `8.2`, `8.3`, `8.4`, `8.5` 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).

+

GitHub Export Options

+

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

+
  • gh-ensure-auth: If set to yes, Playground will display a modal to ensure the
+

user is authenticated with GitHub before proceeding.

+
  • ghexport-repo-url: The URL of the GitHub repository to export to.
  • ghexport-pr-action: The action to take when exporting (create or update).
  • ghexport-playground-root: The root directory in the Playground to export from.
  • ghexport-repo-root: The root directory in the repository to export to.
  • ghexport-content-type: The content type of the export (plugin, theme, wp-content, custom-paths).
  • ghexport-plugin: Plugin path. When the content type is plugin, pre-select the plugin to export.
  • ghexport-theme: Theme directory name. When the content type is theme, pre-select the theme to export.
  • ghexport-path: A path relative to ghexport-playground-root. Can be provided multiple times. When the
+

content type is custom-paths, it pre-populates the list of paths to export.

+
  • ghexport-commit-message: The commit message to use when exporting.
  • ghexport-allow-include-zip: Whether to offer an option to include a zip file in the GitHub
+

export (yes, no). Optional. Defaults to yes.

+

Original Playground docs source: https://playground.wordpress.net/developers/apis/query-api

]]>
+ + 29 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + +
+ + + <![CDATA[Debugging with Xdebug in WordPress Playground]]> + https://playground.wordpress.net/developers-xdebug-introduction/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=30 + + Debugging with Xdebug in WordPress Playground +

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

+

Why Xdebug matters for PHP WASM

+

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

+

XDebug on WordPress Playground

+

For a quick start, check the getting started with Xdebug guide

+

You'll learn to debug:

+
  • Form processing logic
  • Input validation
  • WordPress hooks and filters
+

Two debugging approaches

+

WordPress Playground supports two ways to debug with Xdebug:

+

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

+

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

+

What you'll need

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

Next: Getting Started with Xdebug →

+

Original Playground docs source: https://playground.wordpress.net/developers/xdebug/introduction

]]>
+ + 30 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Getting Started with Xdebug]]> + https://playground.wordpress.net/developers-xdebug-getting-started/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=31 + + Getting Started with Xdebug +

This guide shows you how to enable Xdebug in WordPress Playground and start debugging your code.

+

PHP WASM CLI vs Playground CLI

+

First, Xdebug is present in two different CLIs:

+
  • @php-wasm/cli: Run standalone PHP scripts. Use this when debugging PHP code without needing a WordPress environment.
  • @wp-playground/cli: Run a full WordPress installation. Useful for debugging WordPress plugins, themes, or core functionality.
+

For this guide, we'll use @wp-playground/cli. If you're not familiar with the tool, we recommend reading the @wp-playground/cli guide, but the same process can also be applied to debugging PHP applications with @php-wasm/cli.

+

Quick start with npx

+

The fastest way to get started is using npx, which doesn't require installation:

+
npx @wp-playground/cli@latest server --xdebug
+

This starts WordPress on http://127.0.0.1:9400 with Xdebug enabled. Now you can connect a debugger.

+

Only one project can be debugged at a time.

+

Starting with IDE integration

+

Similar to the process with DevTools, let's use the same plugin code from before to debug with VS Code, and add the --experimental-unsafe-ide-integration=vscode flag. This flag will optimize the setup process for VS Code. If you're working with PhpStorm, add the --experimental-unsafe-ide-integration=phpstorm flag.

+

This flag is marked as unsafe because it edits the IDE config files to set Xdebug path mappings and web server details. CAUTION: If there are bugs, this feature may cause your IDE configuration files to break. Please consider backing up your IDE configs before using this feature.

+

To debug in VS Code, you'll need the following prerequisites:

+
  1. An extension to add PHP profiling support, for example, PHP Debug or another debugger extension to your preference.
  2. Have a .vscode/ folder.
  3. Enable breakpoints in your IDE. Some IDEs come with this feature disabled, so be aware of this detail.
+

If everything is ready, you run the command:

+
npx @wp-playground/cli@latest server --xdebug --experimental-unsafe-ide-integration=vscode --auto-mount
+

If you don't have a .vscode/launch.json file, the terminal will create a file similar to this:

+
{
+    "configurations": [
+        {
+            "name": "WP Playground CLI - Listen for Xdebug",
+            "type": "php",
+            "request": "launch",
+            "port": 9003,
+            "pathMappings": {
+                "/": "${workspaceFolder}/.playground-xdebug-root",
+                "/wordpress/wp-content/plugins/test-xdebug": "${workspaceFolder}/"
+            }
+        }
+    ]
+}
+

Now, you can go to your code, add the breakpoints, start the debugging session named by your IDE, and happy testing.

+

Xdebug in action on VS Code

+

Early Access: DevTools debugger

+

Playground CLI ships an unstable, highly exploratory workflow that enables debugging PHP programs using Chrome DevTools.

+

To try it, use the --experimental-devtools flag:

+
npx @wp-playground/cli@latest server --xdebug --experimental-devtools
+

The terminal will display a URL to connect to Chrome DevTools:

+
Starting a PHP server...
+Setting up WordPress latest
+Resolved WordPress release URL: https://downloads.w.org/release/wordpress-6.8.3.zip
+Fetching SQLite integration plugin...
+Booting WordPress...
+WordPress is running on http://127.0.0.1:9400 with 1 worker(s)
+Starting XDebug Bridge...
+Connect Chrome DevTools to CDP at:
+devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229
+
+Chrome connected! Initializing Xdebug receiver...
+XDebug receiver running on port 9003
+Running a PHP script with Xdebug enabled...
+

By clicking on the provided URL, for example, devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229, you can access DevTools connected to your application, with the ability to inspect all files of a WordPress instance.

+

Chrome Devtools integrated with Xdebug

+

For a more practical example, let's debug a plugin that has the following code:

+
<?php
+/**
+ * Plugin Name: Simple Admin Message
+ * Description: Displays a simple message in the WordPress admin
+ * Version: 1.0
+ * Author: Playground Team
+ */
+
+// Prevent direct access
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+// Display admin notice
+function sam_display_admin_message() {
+    $message = 'Hello! This is a simple admin message.';
+    ?>
+    <div class="notice notice-info is-dismissible">
+        <p><?php _e($message, 'simple-admin-message'); ?></p>
+    </div>
+    <?php
+}
+add_action('admin_notices', 'sam_display_admin_message');
+

In the folder where the plugin is located, let's run the command in our terminal:

+
npx @wp-playground/cli@latest server --xdebug --experimental-devtools --auto-mount
+

The Playground CLI(@wp-playground/cli) will automatically detect the plugin folder and mount it. Opening the project in your browser and DevTools, you'll be able to add breakpoints in your plugin's code and test it line by line.

+

Chrome Devtools integrated with Xdebug

+

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

]]>
+ + 31 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Architecture]]> + https://playground.wordpress.net/developers-architecture/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=32 + + Architecture overview +

WordPress Playground consists of the following high-level components:

+ +

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

+

Tooling

+

NX: building packages and projects

+

WordPress Playground uses NX, a build system designed for monorepos.

+

The dependencies between Playground packages and projects are too complex for a bundler like Webpack, and NX handles this complexity much better:

Dependency graph

+

To learn more, head over to the NX developer docs.

+

Lerna: publishing packages and projects

+

WordPress Playground includes several NPM packages, a VS Code extension, WordPress plugins, a web app, and other GitHub releases, all managed across two monorepos: the main wordpress-playground and Playground Tools.

+

We use Lerna to build, manage, and publish all JavaScript/TypeScript packages. Lerna handles everything simultaneously: it increments the version number, sets a new tag, and publishes the modified packages to npm.

+

The published packages share the same version number, so when updating a single package, Lerna bumps the version number of all dependent packages.

+

Original Playground docs source: https://playground.wordpress.net/developers/architecture

]]>
+ + 32 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WebAssembly PHP]]> + https://playground.wordpress.net/developers-architecture-wasm-php-overview/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=33 + + WebAssembly PHP +

WordPress Playground build the PHP interpreter to WebAssembly using Emscripten and a dedicated pipeline.

+

Building C programs to WebAssembly

+

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.

+

Building PHP to WebAssembly

+

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.

+

Building different versions of PHP to WebAssembly

+

Networking support varies between platforms

+

When it comes to networking, WebAssembly programs are limited to calling JavaScript APIs. It is a safety feature, but also presents a challenge. How do you support low-level, synchronous networking code used by PHP with the high-level asynchronous APIs available in JavaScript?

+

In Node.js, the answer involves a WebSocket to TCP socket proxy, Asyncify, and patching deep PHP internals like php_select. It's complex, but there's a reward. The Node.js-targeted PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

+

In the browser, networking is supported in two ways:

+
  • A fast transport for wp_safe_remote_get to translate them into fetch() calls.
  • A slower transport for all other network calls that parses the TLS transmission initiated by PHP and translates it to a fetch() call.
+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-overview

]]>
+ + 33 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Compiling PHP]]> + https://playground.wordpress.net/developers-architecture-wasm-php-compiling/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=34 + + Compiling PHP +

The build pipeline lives in a Dockerfile. It was originally forked from seanmorris/php-wasm

+

In broad strokes, that Dockerfile:

+
  • Installs all the necessary linux packages (like build-essential)
  • Downloads PHP and the required libraries, e.g. sqlite3.
  • Applies a few patches.
  • Compiles everything using Emscripten, a drop-in replacement for the C compiler.
  • Compiles php_wasm.c – a convenient API for JavaScript.
  • Outputs a php.wasm file and one or more JavaScript loaders, depending on the configuration.
  • Transforms the Emscripten's default php.js output into an ESM module with additional features.
+

To find out more about each step, refer directly to the Dockerfile.

+

Building

+

To build all PHP versions, run nx recompile-php:all php-wasm-web (or php-wasm-node) in the repository root. You'll find the output files in packages/php-wasm/php-web/public. To build a specific version, run nx recompile-php:all php-wasm-node --PHP_VERSION=8.0 --WITH_JSPI=yes (and repeat with --WITH_JSPI=no).

+

PHP extensions

+

PHP is built with several extensions listed in the Dockerfile.

+

Some extensions, like zip, can be turned on or off during the build. Others, like sqlite3, are hardcoded.

+

If you need to turn off one of the hardcoded extensions, feel free to open an issue in this repo. Better yet, this project needs contributors. You are more than welcome to open a PR and author the change you need.

+

C API exposed to JavaScript

+

The C API exposed to JavaScript lives in the php_wasm.c file. The most important functions are:

+
  • void phpwasm_init() – It creates a new PHP context and must be called before running any PHP code.
  • int phpwasm_run(char *code) – Runs a PHP script and writes the output to /tmp/stdout and /tmp/stderr. Returns the exit code.
  • void phpwasm_refresh() – Destroy the current PHP context and starts a new one. Call it after running one PHP script and before running another.
+

Refer to the inline documentation in php_wasm.c to learn more.

+

Build configuration

+

The build is configurable via the Docker --build-arg feature. You can set them up through the build.js script, just run this command to get the usage message:

+
nx recompile-php php-wasm-web
+

Supported build options:

+
  • PHP_VERSION – The PHP version to build, default: 8.0.24. This value must point to an existing branch of the https://github.com/php/php-src.git repository when prefixed with PHP-. For example, 7.4.0 is valid because the branch PHP-7.4.0 exists, but just 7 is invalid because there's no branch PHP-7. The PHP versions that are known to work are 7.4.* and 8.0.*. Others likely work as well but they haven't been tried.
  • EMSCRIPTEN_ENVIRONMENT – web or node, default: web. The platform to build for. When building for web, two JavaScript loaders will be created: php-web.js and php-webworker.js. When building for Node.js, only one loader called php-node.js will be created.
  • WITH_LIBXML – yes or no, default: no. Whether to build with libxml2 and the dom, xml, and simplexml PHP extensions (DOMDocument, SimpleXML, ..).
  • WITH_LIBZIP – yes or no, default: yes. Whether to build with zlib, libzip, and the zip PHP extension (ZipArchive).
  • WITH_NODEFS – yes or no, default: no. Whether to include the Emscripten's NODEFS JavaScript library. It's useful for loading files and mounting directories from the local filesystem when running php.wasm from Node.js.
+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-compiling

]]>
+ + 34 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[PHP.js JavaScript module]]> + https://playground.wordpress.net/developers-23-architecture-04-wasm-php-javascript-module/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=35 + + PHP.js JavaScript module +

The php.js file generated by the WebAssembly PHP build pipeline is not a vanilla Emscripten module. Instead, it's an ESM module that wraps the regular Emscripten output and adds some extra functionality.

+

Here's the API it exposes:

+
// php.wasm size in bytes:
+export const dependenciesTotalSize = 5644199;
+
+// php.wasm filename:
+export const dependencyFilename = 'php.wasm';
+
+// Run Emscripten's generated module:
+export default function (jsEnv, emscriptenModuleArgs) {}
+

The generated JavaScript module is not meant for direct use. Instead, it can be consumed through the PHP class:

+
// In Node.js:
+const php = new PHP(await loadNodeRuntime('8.0'));
+
+// On the web:
+const php = new PHP(await loadWebRuntime('8.0'));
+

Both 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.

+

Loading the PHP runtime

+

The load() method handles the entire PHP initialization pipeline. In particular, it:

+
  • Instantiates the Emscripten PHP module
  • Wires it together with the data dependencies and loads them
  • Ensures is all happens in a correct order
  • Waits until the entire loading sequence is finished
+

Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/04-wasm-php-javascript-module

]]>
+ + 35 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[PHP Filesystem]]> + https://playground.wordpress.net/developers-architecture-wasm-php-filesystem/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=36 + + PHP Filesystem +

The PHP module has its own filesystem separate from your computer's filesystem. It is provided by Emscripten's FS library and the default APIs is low-level and cumbersome to use. The PHP JavaScript class shipped with WordPress Playground wraps it with a more convenient higher-level API.

+

In general, WordPress Playground uses an in-memory virtual filesystem.

+

However, in Node.js, you can also mount a real directory from the host filesystem into the PHP filesystem.

+

Here's how to interact with the filesystem in WordPress Playground:

+
// Recursively create a /var/www directory
+php.mkdirTree('/var/www');
+
+console.log(php.fileExists('/var/www/file.txt'));
+// false
+
+php.writeFile('/var/www/file.txt', 'Hello from the filesystem!');
+
+console.log(php.fileExists('/var/www/file.txt'));
+// true
+
+console.log(php.readFile('/var/www/file.txt'));
+// "Hello from the filesystem!
+
+// Delete the file:
+php.unlink('/var/www/file.txt');
+

For more details consult the BasePHP class directly – it has some great documentation strings.

+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-php-filesystem

]]>
+ + 36 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Asyncify and JSPI – Stack Switching in PHP WebAssembly]]> + https://playground.wordpress.net/developers-architecture-wasm-asyncify/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=37 + + Asyncify and JSPI: Stack Switching in PHP WebAssembly +

Asyncify lets synchronous C or C++ code interact with asynchronous JavaScript. Technically, it saves the entire C call stack before yielding control back to JavaScript, and then restores it when the asynchronous call is finished. This is called stack switching.

+

Networking support in the WebAssembly PHP build is implemented using Asyncify. When PHP makes a network request, it yields control back to JavaScript, which makes the request, and then resumes PHP when the response is ready. It works well enough that PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

+

Asyncify crashes

+

Stack switching requires wrapping all C functions that may be found at a call stack at a time of making an asynchronous call. Blanket-wrapping of every single C function adds a significant overhead, which is why we maintain a list of specific function names:

+

https://github.com/WordPress/wordpress-playground/blob/15a660940ee9b4a332965ba2a987f6fda0c159b1/packages/php-wasm/compile/Dockerfile#L624-L632

+

Unfortunately, missing even a single item from that list results in a WebAssembly crash whenever that function is a part of the call stack when an asynchronous call is made. It looks like this:

+

A screenshot of an asyncify error in the terminal

+

Asyncify can auto-list all the required C functions when built without ASYNCIFY_ONLY, but that auto-detection is overeager and ends up listing about 70,000 C functions which increases the startup time to 4.5s. That's why we maintain the list manually.

+

If you are interested in more details, see GitHub issue 251.

+

Fixing Asyncify crashes

+

Pull Request 253 adds a fix-asyncify command that runs a specialized test suite and automatically adds any identified missing C functions to the ASYNCIFY_ONLY list.

+

If you run into a crash like the one above, you can fix it by:

+
  1. Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that.
  2. Adding a test case that triggers a crash to packages/php-wasm/node/src/test/php-asyncify.spec.ts
  3. Running: npm run fix-asyncify
  4. Committing the test case, the updated Dockerfile, and the rebuilt PHP.wasm
+

JSPI: The Modern Alternative to Asyncify

+

The JavaScript Promise Integration (JSPI) API handles stack switching natively in V8, eliminating the need for Asyncify's function wrapping. WordPress Playground now ships JSPI builds alongside Asyncify builds for all PHP versions (7.4–8.5).

+

Current status:

+
  • The Playground CLI auto-detects JSPI support and enables it automatically — no manual flags needed
  • Node.js 23+ supports JSPI natively; Node.js 22 requires the --experimental-wasm-jspi flag (handled automatically by the CLI)
  • Node.js 24+ is expected to have JSPI unflagged
  • Browser support varies: JSPI is available in Chrome/Chromium-based browsers behind flags
+

Binary Size Optimization with MAIN_MODULE=2

+

Both Asyncify and JSPI builds are compiled with Emscripten's MAIN_MODULE=2 flag, which performs dead code elimination on exported symbols. Only symbols that dynamic extensions actually need are exported.

+

Impact:

+
  • Total binary size reduced by 122 MB (13.7%)
  • .wasm files reduced by 109 MB (16%)
  • JavaScript glue code reduced by 14.5 MB (63%)
+

This optimization applies across all PHP versions (7.4–8.5) for both Node.js and Web targets. The exported symbol list is centrally managed in the Dockerfile, with conditional exports for specific extensions (e.g., __c_longjmp for Xdebug, _wasm_recv for Memcached).

+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/wasm-asyncify

]]>
+ + 37 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Running PHP apps in the browser with ServiceWorkers and Worker Threads]]> + https://playground.wordpress.net/developers-architecture-browser-concepts/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=38 + + Running PHP apps in the browser with ServiceWorkers and Worker Threads +

On a high level, WordPress Playground works in web browsers as follows:

+
  • The index.html file on playground.wordpress.net loads the remote.html file via an <iframe src="/remote.html">.
  • remote.html starts a Worker Thread and a ServiceWorker and sends back the download progress information.
  • The Worker Thread starts PHP and populates the filesystem with a WordPress patched to run on SQLite.
  • The ServiceWorker starts intercepting all HTTP requests and forwarding them to the Worker Thread.
  • remote.html creates an <iframe src="/index.php">, and the Service Worker forwards the index.php request to the Worker Thread where the WordPress homepage is rendered.
+

Visually, it looks like this:

+

Architecture overview

+

High-level ideas

+

The @php-wasm/web is built on top of the following ideas:

+
  • Browser tab orchestrates everything – The browser tab is the main program. Closing or reloading it means destroying the entire execution environment.
  • Iframe-based rendering – Every response produced by the PHP server must be rendered in an iframe to avoid reloading the browser tab when the user clicks on a link.
  • PHP Worker Thread – The PHP server is slow and must run in a web worker, otherwise handling requests freezes the website UI.
  • Service Worker routing – All HTTP requests originating in that iframe must be intercepted by a Service worker and passed on to the PHP worker thread for rendering.
+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-concepts

]]>
+ + 38 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Browser tab orchestrates the execution]]> + https://playground.wordpress.net/developers-architecture-browser-tab-orchestrates-execution/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=39 + + Browser tab orchestrates the execution +

The main index.html ties the entire application together. It starts all the concurrent processes and displays the PHP responses. The app only lives as long as the main index.html.

+

Keep this point in mind as you read through the rest of the docs. At this point it may seem obvious, by the lines may get blurry later on. This package runs code outside of the browser tab using Web Workers, Service Workers, and, in the future, Shared Workers. Some of these workers may keep running even after the browser tab with index.html is closed.

+

Boot sequence

+

Here's what a boot sequence for a minimal app looks like:

+

The boot sequence

+

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.

+

Data flow

+

Here's what happens whenever the iframe issues a same-domain request:

+

The data flow

+

A step-by-step breakdown:

+
  1. The request is intercepted by the Service Worker
  2. The Service Worker passes it to the Worker Thread
  3. The Worker Thread calls PHP.request to convert that request to a response
  4. The Worker Thread passes the response to the Service Worker
  5. The Service Worker provides the browser with a response
+

At this point, if the request was triggered by user clicking on a link, the browser will render PHPRequestHandler's response inside the iframe.

+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-tab-orchestrates-execution

]]>
+ + 39 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Iframe-based rendering]]> + https://playground.wordpress.net/developers-architecture-browser-iframe-rendering/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=40 + + Iframe-based rendering +

To avoid page reloads, all the PHPRequestHandler responses must be rendered in an iframe. Remember, the entire setup only lives as long as the main index.html. We want to avoid reloading the main app at all costs.

+

In our app example above, index.php renders the following HTML:

+
<a href="page.php">Go to page.php</a>
+

Imagine our index.html rendered it in a <div> instead of an <iframe>. As soon as you click on that link, the browser will try to navigate from index.html to page.php. However, index.html runs the entire PHP app, including the Worker Thread, the PHPRequestHandler, and the traffic control connecting them to the Service Worker. Navigating away from it would destroy the app.

+

Now, consider an iframe with the same link in it:

+
<iframe srcdoc='<a href="page.php">Go to page.php</a>'></iframe>
+

This time, click the link in the browser to load page.php inside the iframe. The top-level index.html, where the PHP application runs, remains unaffected. That's why iframes are crucial for the @php-wasm/web setup.

+

Crash reports

+

Playground doesn't collect crash reports automatically. Instead, it prompts users to submit a crash report when an instance fails to run in the browser.

+

The report includes a log, description, and a URL, and users can modify it before submitting it.

+

The Logger API handles it from there. This simple REST API validates the data and sends it to the Making WordPress #playground-logs Slack channel.

+

Iframes caveats

+
  • target="_top" isn't handled yet, so clicking links with target="_top" will reload the page you’re working on.
  • JavaScript popups originating in the iframe may not always display.
+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/browser-iframe-rendering

]]>
+ + 40 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[PHP Worker Threads]]> + https://playground.wordpress.net/developers-architecture-browser-php-worker-threads/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=41 + + PHP Worker Threads +

PHP is always ran in a web worker to ensure the PHP runtime doesn't slow down the user interface of the main website.

+

Imagine the following code:

+
<button onclick="for(let i=0;i<100000000;i++>) {}">Freeze the page</button>
+<input type="text" />
+

As soon as you click that button the browser will freeze and you won't be able to type in the input. That's just how browsers work. Whether it's a for loop or a PHP server, running intensive tasks slows down the user interface.

+

Initiating web workers

+

Web workers are separate programs that can process heavy tasks outside of the main application. They must be initiated by the main JavaScript program living in the browser tab. Here's how:

+
const phpClient = consumeAPI<PHPClient>(
+	spawnPHPWorkerThread(
+		'/worker-thread.js' // Valid Worker script URL
+	)
+);
+await phpClient.isReady();
+await phpClient.run({ code: `<?php echo "Hello from the thread!";` });
+

Controlling web workers

+

Exchanging messages is the only way to control web workers. The main application has no access to functions or variables inside of a web worker. It can only send and receive messages using worker.postMessage and worker.onmessage = function(msg) { }.

+

This can be tedious, which is why Playground provides a convenient consumeAPI 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

]]>
+ + 41 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Service Workers]]> + https://playground.wordpress.net/developers-architecture-browser-service-workers/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=42 + + Service Workers +

A Service Worker is used to handle the HTTP traffic using the in-browser PHPRequestHandler.

+

Imagine your PHP script renders the following page in the iframe viewport:

+
<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:

+

Service worker data flow

+

Service Worker setup

+

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

]]>
+ + 42 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Scopes]]> + https://playground.wordpress.net/developers-23-architecture-13-browser-scopes/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=43 + + Scopes +

Scopes keep your app working when you open it in two different browser tabs.

+

The Service Worker passes the intercepted HTTP requests to the PHPRequestHandler for rendering. Technically, it sends a message through a BroadcastChannel 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:

+
  • Brand the outgoing HTTP requests with a unique tab id
  • Ignore any BroadcastChannel messages with a different id
+

Technically, a scope is a string included in the PHPRequestHandler.absoluteUrl. For example:

+
  • In an unscoped app, /index.php would be available at http://localhost:8778/wp-login.php
  • In an scoped app, /index.php would be available at http://localhost:8778/scope:96253/wp-login.php
+

The service worker is aware of this concept and will attach the /scope: found in the request URL to the related BroadcastChannel communication.

+

A worker thread initiated with a scoped absoluteUrl is said to be scoped:

+
import {
+	PHP,
+	setURLScope,
+	exposeAPI,
+	parseWorkerStartupOptions,
+} from '@php-wasm/web';
+
+// Don't use the absoluteURL directly:
+const absoluteURL = 'http://127.0.0.1'
+
+// Instead, set the scope first:
+const scope = Math.random().toFixed(16)
+const scopedURL = setURLScope(absoluteURL, scope).toString()
+
+const { phpVersion } = parseWorkerStartupOptions<{ phpVersion?: string }>();
+const php = await PHP.load('8.0', {
+	requestHandler: {
+		documentRoot: '/',
+		absoluteUrl: scopedSiteUrl
+	}
+});
+
+// Expose the API to app.ts:
+const [setApiReady, ] = exposeAPI( php );
+setApiReady();
+

Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/13-browser-scopes

]]>
+ + 43 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Cross-process communication]]> + https://playground.wordpress.net/developers-23-architecture-14-browser-cross-process-communication/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=44 + + Cross-process communication +

@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

]]>
+ + 44 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress support]]> + https://playground.wordpress.net/developers-architecture-wordpress/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=45 + + WordPress support +

WordPress, as a PHP application, can run on PHP WebAssembly. However, there are a few caveats.

+

SQLite

+

First, WordPress requires MySQL. However, there isn't a WebAssembly version of MySQL you could run in the browser. WordPress Playground, therefore, ships PHP with the native SQLite driver and leans on SQLite.

+

But how can WordPress run on a different database?

+

Behind the scenes, the official SQLite Database Integration plugin intercepts all MySQL queries and rewrites them in SQLite dialect. The x.0 release ships a new WordPress Playground-informed translation layer that allows WordPress on SQLite to pass 99% of the WordPress unit test suite.

+

WordPress in the browser

+

You can use any WordPress build in the browser. For convenience and to reduce the data transfer size, WordPress Playground ships a few minified WordPress releases that you can use in the browser.

+

WordPress in Node.js

+

In Node.js, you'll typically want to mount WordPress from a disk directory.

+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/wordpress

]]>
+ + 45 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[How does it work?]]> + https://playground.wordpress.net/developers-23-architecture-16-wordpress-database/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=46 + + Database is supported via MySQL\<\-\>SQLite translation layer +

WordPress requires MySQL. However, there isn't a WebAssembly version of MySQL you could run in the browser. WordPress Playground therefore ships PHP with the native SQLite driver and leans on SQLite.

+

But how can WordPress run on a different database?

+

Behind the scenes, the official SQLite Database Integration plugin intercepts all MySQL queries and rewrites them in SQLite dialect. The 2.0 release ships a new WordPress Playground-informed translation layer that allows WordPress on SQLite to pass 99% of the WordPress unit test suite.

+

Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/16-wordpress-database

]]>
+ + 46 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Bundling WordPress for the browser]]> + https://playground.wordpress.net/developers-23-architecture-17-browser-wordpress/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=47 + + Bundling WordPress for the browser +

The web bundler Dockerfile turns a vanilla WordPress into a browser-optimized one:

+
  • Makes WordPress run on SQLite using the official drop-in plugin as MySQL is unsupported in the browser.
  • Reduces the WordPress website size from about 70MB to about 10MB, or 5MB compressed.
  • Runs the WordPress installation wizard.
  • Bundles WordPress as a minified zip file.
+

Build a new bundle with nx bundle-wordpress playground-wordpress-builds --wp-version=<version>, e.g.:

+
nx bundle-wordpress playground-wordpress-builds --wp-version=6.1
+

The bundler outputs:

+
  • packages/playground/wordpress-builds/public/wp-6.1.zip – zipped WordPress files
  • packages/playground/wordpress-builds/public/wp-6.1/ – a directory with static assets for the specified WordPress versions
+

Consult the web bundler Dockerfile for more details (like the list of supported WordPress versions) and modify it to customize the default WordPress installation.

+

Original Playground docs source: https://playground.wordpress.net/developers/23-architecture/17-browser-wordpress

]]>
+ + 47 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Host your own Playground]]> + https://playground.wordpress.net/developers-architecture-host-your-own-playground/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=48 + + Host your own Playground +

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.

+

Before you start

+

Self-hosting Playground gives you full control, but requires understanding a few key concepts:

+

What to expect

+
  • Initial setup complexity: Building and deploying Playground involves multiple steps. Allow time for troubleshooting during your first deployment.
  • Static file hosting: Playground is primarily static files (HTML, JS, WASM) with minimal server-side requirements.
  • Browser-based execution: All WordPress processing happens in the user's browser via WebAssembly—your server only delivers files.
+

Performance considerations

+

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   |
+

Browser compatibility

+

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.

+

Usage

+

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`,
+});
+

Static assets

+

There are several ways to get the static assets necessary to host the Playground.

+

In order of convenience and ease:

+
  • Download pre-built package
  • Fork the repository and build with GitHub Action
  • Build locally
+

Download pre-built package

+

To host the Playground as is, without making changes, you can download the built artifact from the latest successful GitHub Action.

+
  • Click on Deploy Playground website.
  • In the section Artifacts at the bottom of the page, click playground-website.
  • It's a zip package with the same files deployed to the public site.
+

Fork the repository and build with GitHub Action

+

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.

+

Build locally

+

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.

+

Summary of included files

+

The static assets include:

+
  • Data and WASM files for all available PHP and WordPress versions
  • remote.html - the core of Playground
  • index.html - the shell, or browser chrome
  • Web Worker script
+

You 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.

+
  • Apache server directive .htaccess file from the package remote
+

For these to work, you need a server environment with Apache and PHP installed.

+

NGINX configuration

+

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.

+

Customize bundled data

+

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.

+

Install plugins

+

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.

+

Import content

+

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.

+

Production deployment checklist

+

Before going live, verify your self-hosted Playground meets these requirements:

+

Server configuration

+
  • [ ] MIME types: Ensure .wasm files are served with application/wasm content type
  • [ ] CORS headers: If embedding cross-origin, configure appropriate CORS headers
  • [ ] Caching: Set long cache times for WASM and static assets (they're versioned)
  • [ ] Compression: Enable gzip/brotli for faster file transfers
  • [ ] HTTPS: Required for service workers and some browser features
+

Performance optimization

+
  • [ ] CDN: Serve static assets from a CDN for faster global delivery
  • [ ] Pre-installed plugins: Bundle frequently-used plugins in your WordPress build
  • [ ] Limit additional downloads: Minimize runtime plugin installations (by blueprints, for example)
+

Troubleshooting

+

Common issues and solutions

+

Playground fails to load or shows blank screen

+

Possible causes:

+
  • Server doesn't serve WASM files with correct MIME type
  • Deployment missing required files
  • JavaScript errors in browser console
+

Solutions:

+
  1. Check browser console for errors (F12 → Console tab)
  2. Verify .wasm files return application/wasm content type
  3. Verify you deployed all build files
+

Slow initial loading (30+ seconds)

+

Possible causes:

+
  • Installing large plugins at runtime
  • Missing CDN or caching configuration
  • User on slow network connection
+

Solutions:

+
  1. Pre-install plugins in your WordPress build instead of runtime installation
  2. Configure CDN with proper caching headers
  3. Show loading indicators to set user expectations
+

Original Playground docs source: https://playground.wordpress.net/developers/architecture/host-your-own-playground

]]>
+ + 48 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Limitations]]> + https://playground.wordpress.net/developers-limitations/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=49 + + Limitations +

WordPress Playground is under active development and has some limitations you should keep in mind when running it and developing with it.

+

You can track the status of these issues on the Playground Project board.

+

In the browser

+

Temporary by design

+

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:

+
  • Save: Enable browser storage via the "Save" button (top right, next to address bar), before refreshing the page via the browser bar.
  • For development: Use Playground CLI which supports persistent local storage
+

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.

+

Refresh Playground Button

+

<blockquote> <figure> <figcaption><i>1. Exporting Playground:</i></figcaption>

+

Save Button

+

</figure>

+

<figure> <figcaption><i>2. Save button:</i></figcaption>

+

Save Button

+

</figure> </blockquote>

+

Browser support

+

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

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

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

+

Performance expectations

+

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

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

Save Button

+

Factors that affect performance:

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

<blockquote> <strong>Note:</strong> Opera Mini support is not currently confirmed. </blockquote>

+

When developing with Playground

+

Iframe quirks

+

Playground renders WordPress in an iframe so clicking links with target="_top" will reload the page you’re working on. Also, JavaScript popups originating in the iframe may not always display.

+

Run WordPress PHP functions

+

Playground supports running PHP code in Blueprints using the runPHP step. To run WordPress-specific PHP functions, you’d need to first require wp-load.php:

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

Using WP-CLI

+

You can execute wp-cli commands via the Blueprints wp-cli step. However, since Playground runs in the browser, it doesn't support the full array of available commands. While there is no definite list of supported commands, experimenting in the online demo will help you assess what's possible.

+

Original Playground docs source: https://playground.wordpress.net/developers/limitations

]]>
+ + 49 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground Developers Docs]]> + https://playground.wordpress.net/developers/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=50 + + Developers Docs +

Hi! Welcome to WordPress Playground Developer documentation.

+

<p class="docs-hubs">The WordPress Playground documentation is distributed across four separate hubs (subsites):</p>

+
  • Documentation – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
  • Blueprints – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub.
  • 👉 Developers (you're here) – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in this Developers docs hub.
  • API Reference – All the APIs exposed by WordPress Playground
+

Navigating the Developers documentation hub

+

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

+ +
  • Local Development: Discover the tools provided by Playground to streamline the process of setting up and managing WordPress sites and build your apps.
+ +
  • Architecture: The specifics of WordPress Playground's architecture, including its various components and tools.
+
  • Limitations: Learn more about the current limitations of WordPress Playground, which include disabled network connections, temporary data, iframe quirks, PHP function execution requirements, and partial WP-CLI support.
+

Original Playground docs source: https://playground.wordpress.net/developers/

]]>
+ + 50 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Build]]> + https://playground.wordpress.net/about-build/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=51 + + Build +

WordPress Playground can help you to create and learn WordPress quickly, even on mobile with no signal. You can use Playground where you work best, whether that’s in the browser, Node.js, mobile apps, VS Code, or elsewhere.

+

Setting up a local WordPress environment quickly

+

You can seamlessly integrate Playground into your development workflow to launch a local WordPress environment quickly for testing your code. You can do this directly from the terminal or your preferred IDE.

+

Save changes done on a Block Theme and create GitHub Pull Requests

+

You can connect your Playground instance to a GitHub repository and create a Pull Request with the changes you’ve made through the WordPress UI, leveraging the Create Block Theme plugin.

+

With this workflow, you could build a block theme completely in your browser and save your changes to GitHub, or you could improve/fix an existing one.

+

Embedded media: https://www.youtube.com/embed/94KnoFhQg1g

+

Some more examples of this workflow:

+ +

Synchronize your playground instance with a local folder and create GitHub Pull Requests

+

Storage Type Device Snapshot

+

With Google Chrome you can synchronize your Playground instance with a local directory, that can be either:

+
  • An empty directory – to save this Playground and start syncing
  • An existing directory – to load it here and start syncing
+

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:

+
  • Files changed in the Playground will be synchronized to your computer.
  • Files changed on your computer will not be synchronized to Playground. You'll need to click the "Sync local files" button.
+

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

+

Integrate with other APIs to create new tools.

+

Playground can be combined with different APIs to create amazing tools. The possibilities are endless.

+

You can use WordPress Playground in Node.js to create new tools. The @php-wasm/node package, which ships the PHP WebAssembly runtime, is the package used for https://playground.wordpress.net/, for example.

+

Another interesting app built on top of Playground is Translate Live (see example) which, in combination with OpenAI provides a WordPress translations tool “in place” where translations can be seen and modified in their real context (see example). Read more about this tool at Translate Live: Updates to the Translation Playground

+

Work offline and as a native app

+

When you first visit playground.wordpress.net, your browser automatically caches all the necessary files to use Playground. From that point on, you can access playground.wordpress.net, even without an internet connection, ensuring you can continue working on your projects without interruptions.

+

You can also install Playground on your device as a Progressive Web App (PWA) to launch the Playground directly from your home screen—just like a native app.

+

Read Introducing Offline Mode and PWA Support for WordPress Playground for more info.

+

Embed a WordPress site in non-web environments

+

The How to ship a real WordPress site in a native iOS app via Playground? guide shows how we can leverage Playground to wrap a WordPress site into an IOS app.

+

Original Playground docs source: https://playground.wordpress.net/about/build

]]>
+ + 51 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[About WordPress Playground]]> + https://playground.wordpress.net/about/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=52 + + About WordPress Playground +

What is WordPress Playground?

+

WordPress Playground is the platform that lets you run WordPress instantly on any device without a host. It allows you to experiment and learn about WordPress without affecting your live website. It's a virtual sandbox where you can play around with different features, designs, and settings in a safe and controlled environment.

+

WordPress Playground is your place to build, test, and launch:

+
  • Build: WordPress Playground can help you to build products with WordPress. Use it from where you work best, whether that's in the browser, Node.js, mobile apps, VS Code, or elsewhere.
  • Test: Upgrade your QA process with WordPress Playground. Quickly test your plugins or themes, experiment in a private sandbox, and create PRs from your WP Playground instance to any repo.
  • Launch: Use WordPress Playground to showcase your product, let users try it live, or launch it in the App Store with zero lead time.
+

Why WordPress Playground?

+

Try themes and plugins on the fly

+

With WordPress Playground, you can explore any theme. You can choose from a wide range of themes and see how they look on your site. You can also modify the colors, fonts, layouts, and other visual elements to create a unique design.

+

In addition to themes, you can experiment with plugins too. With WordPress Playground, you can install and test different plugins to see how they work and what they can do for your site. This allows you to explore and understand the capabilities of WordPress without worrying about breaking anything.

+

Create content on the go

+

Another great feature of WordPress Playground is the ability to create and edit content. You can write blog posts, create pages, and add media like images and videos to your site. This helps you understand how to organize and structure your content effectively.

+

The content you create is limited to the Playground on your device and disappears once you leave it, so you are free to explore and play without risking breaking any actual site.

+

But hey! You can also connect your Playground instance to a GitHub repo and create a PR to persist those changes.

+

It's super safe

+

Overall, WordPress Playground provides a risk-free environment for beginners to learn and get hands-on experience with WordPress. It helps you to gain confidence and knowledge before making changes to your live website.

+

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.

+

How does WordPress Playground work?

+

When you first start using WordPress Playground, you'll be provided with a separate space where you can create and customise your own WordPress website. This space is completely isolated from your actual website.

+

Streamed, not served.

+

The WordPress you see when you open Playground in your browser is a WordPress that should function like any WordPress, with a few limitations 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.

+

What makes Playground different from running WordPress on a web server or local desktop app?

+

Web applications like WordPress have long relied on server technologies to run logic 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

]]>
+ + 52 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Launch]]> + https://playground.wordpress.net/about-launch/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=53 + + Launch +

Reach your clients or customers faster. Showcase your product, let users try it live, or launch it in the App Store with zero lead time.

+

Embed interactive product demos on websites.

+

Leverage blueprints' potential to create interactive demos of your plugins or themes. For example, you can provide a link to your users/clients to showcase how your custom plugin integrates with an adapted theme, demonstrating their combined functionality and appearance.

+

Read more about this at How to use WordPress Playground for interactive demos

+

Get inspiration about the type of interactive demos you can create at the Blueprints Gallery

+

The Blueprints builder tool allows you edit your blueprint online and run it directly in a Playground instance.

+

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!

+

Embed demos in posts and pages with the WordPress Playground block

+

This WordPress Playground block allows you to embed WordPress Playground in your posts and pages. You can also include an interactive code editor to demonstrate and teach your readers how WordPress plugins are built.

+

With this block you have a straightforward and effective way to create live WordPress environments that can be embedded within your blog posts.

+

For any issues or questions about the WordPress Playground Block, please open a GitHub issue in the playground-tools repository.

+

Put a native app running WordPress in the App Store.

+

Check the How to ship a real WordPress site in a native iOS app via Playground? guide for info on this use case

+

Original Playground docs source: https://playground.wordpress.net/about/launch

]]>
+ + 53 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Test]]> + https://playground.wordpress.net/about-test/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=54 + + Test +

Upgrade your QA process with the ability to review progress in your browser in a single click. When you’re ready, push updates instantly.

+

Test any theme or plugin

+

With Playground, you can test any plugin or theme. Use the Query API to quickly load any plugin or theme published in wordpress.org plugins and themes directories.

+

For example, the following link will load the “pendant” theme and the “gutenberg” plugin on a Playground instance:

+

https://playground.wordpress.net/?theme=pendant&plugin=gutenberg

+

But you can also test more elaborate configurations using blueprints, for example testing a plugin’s code from a gist (see blueprint and live demo)

+

Live preview pull requests

+

Testing pull requests is one of the most exciting use cases for the Playground project. With Playground, you can enable a Live preview link on each Pull Request of a WordPress-related project in GitHub so that developers can see in action the effects of code in that Pull Request. Read more about this at Preview WordPress Core Pull Requests with Playground.

+

There are some public implementations of this use case such as WordPress Core PR previewer and Gutenberg PR previewer. Users can input the PR number or URL to be redirected to a WordPress instance, powered by Playground, where the changes from the PR are applied.

+

You can add automated PR preview buttons to your own plugin or theme repository using the WordPress Playground PR Preview GitHub Action. When someone opens a pull request, the action automatically adds a button that launches a configured WordPress instance with the changes ready to test. For detailed setup instructions and advanced configurations, see the Adding PR Preview Buttons with GitHub Actions guide.

+

Clone your site and experiment in a private sandbox.

+

With the Sandbox Site powered by Playground plugin you can create a private WordPress Playground copy of your site to test plugins safely or do any other experiments on your site’s replica without uploading any data to the cloud and without affecting the original site.

+

Test different WordPress and PHP versions.

+

With Playground, you can quickly test any major WordPress or PHP version by _customizing its settings_ or using a custom blueprint with the preferredVersions property.

+

For example, you can always test the latest development version of WordPress, also called Beta Nightly, from this link: https://playground.wordpress.net/?wp=nightly

+

During the Beta period of any WordPress release, you can also test the latest WordPress Beta or RC release with theme test data and debugging plugins (see blueprint and live demo).

+

You can also load any theme, plugin, or configuration in any of the available WordPress and PHP versions to check how they work in that environment.

+

The WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress provides a great overview of the testing possibilities with Playground.

+

Original Playground docs source: https://playground.wordpress.net/about/test

]]>
+ + 54 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Changelog]]> + https://playground.wordpress.net/changelog/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=55 + + Changelog +

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.

+

[v3.1.21] (2026-04-20)

+

PHP WebAssembly

+
  • Add PHP 5.2 WebAssembly builds and runtime support. (#3501)
+

Various

+
  • [Docs] Update trusted publisher setup instructions. (#3505)
  • [PHP] Align @php-wasm/\{web,node\}-5-2 with workspace and add READMEs. (#3506)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @JanJakes

+

[v3.1.20] (2026-04-16)

+

Tools

+

GitHub integration

+
  • [Github Actions] Fix GitHub release publishing wrong version. (#3488)
+

Documentation

+
  • [ xdebug ] Add Xdebug use case examples. (#3075)
+

Documentation

+
  • [Docs] Removing documentation fragments. (#3470)
+

PHP WebAssembly

+
  • [PHP] Fix gzip crash – add missing zlib/curl functions to ASYNCIFY_ONLY. (#3491)
+

Bug Fixes

+
  • Fix Safari failing to make cross-origin HTTP requests. (#3440)
  • Restore the missing mounts.spec.ts. (#2478)
+

Reliability

+
  • [php-wasm-node-polyfills] Remove @php-wasm/node-polyfills package. (#3476)
+

Various

+
  • Remove extraneous file and update package-lock.json. (#3477)
  • [Docs] Updating asyncify page. (#3459)
  • [Docs] Updating blueprint bundles page. (#3465)
  • [i18n] Add Bengali translation for blueprints tutorial pages. (#3461)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @fellyph @mho22 @perashanid

+

[v3.1.19] (2026-04-13)

+

Documentation

+
  • [Docs] Removing heading IDs to make documentation compatible to migrate to org. (#3466)
+

PHP WebAssembly

+
  • [ xdebug ] Skip paths alongside path mappings in IDE configs. (#3366)
  • [AI] Add compilation and debugging skills for PHP.wasm. (#3445)
+

Website

+
  • Add custom error message when artifact is expired. (#3453)
  • Define DB_NAME via auto-prepend instead of rewriting wp-config.php. (#3458)
+

Bug Fixes

+
  • Fix broken deploy workflow guards. (#3479)
+

Various

+
  • Exclude loopback requests from trying to pre-fetch. (#3305)
  • Skip running irrelevant actions on forks. (#3473)
  • [Docs] Add overlay parameter to Playground URL options. (#3457)
  • [i18n] Add Bengali translation for blueprints documentation. (#3462)
  • [i18n] Added Gujarati translation for wp-playground agent skill guide. (#3333)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ashfame @brandonpayton @dd32 @fellyph @JanJakes @mho22 @perashanid @Rima1889

+

[v3.1.18] (2026-04-07)

+

[v3.1.17] (2026-04-07)

+

Enhancements

+
  • [CLI] Fix intermittent ENOTDIR crash when applying post-install mounts. (#3468)
+

Documentation

+
  • [Docs] Load assets from githubusercontent to make it compatible with handbook. (#3467)
+

Various

+
  • [cli] Update WordPress version validation to include "beta" as a valid slug. (#3446)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph

+

[v3.1.16] (2026-04-06)

+

Website

+
  • Add WebMCP support to the Playground website. (#3298)
  • Centralized site management API and expose as window.playgroundSites. (#3401)
+

Bug Fixes

+
  • Fix phpMyAdmin and Adminer crash on PHP 8.5. (#3444)
  • [CLI] Fix --skip-sqlite-setup false pre-flight database error. (#3456)
+

Various

+
  • Update SQLite refresh script to support the SQLite monorepo. (#3452)
  • [i18n] Add Bengali translation for blueprints/05-steps.md. (#3432)
  • [i18n] ja translations developer architecture Service Workers and Scopes. (#3431)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @JanJakes @mho22 @noruzzamans @shimotmk

+

[v3.1.15] (2026-03-31)

+

PHP WebAssembly

+
  • [PHP] Fix "Controller is already closed" crash in CLI server. (#3441)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.1.14] (2026-03-30)

+

PHP WebAssembly

+
  • [Redis] va_arg long to va_arg zend_long for WASM32 ABI compatibility. (#3417)
+

Internal

+
  • Allow manual trigger for GitHub Release workflow. (#3430)
  • Create GitHub Release on every npm publish. (#3429)
  • Update fast-xml-parser to fix Dependabot issue. (#3422)
+

Bug Fixes

+
  • [Playground CLI] Support overriding WP_DEBUG using define-bool and define-number. (#3254)
+

Various

+
  • Load SQLite for phpMyAdmin and Adminer using WP configuration. (#3420)
  • Revert "Update SQLite database path (#3428)". (#3435)
  • Update SQLite database path. (#3428)
  • [MCP] Add REST API authentication to request tool. (#3439)
  • [i18n] Add Bengali translation for blueprints tutorial and guide pages. (#3412)
  • [i18n] Add French translation for build-your-first-blueprint.md. (#3358)
  • [i18n] Create index.md and add French translation. (#3357)
  • [i18n] ja translations developer architecture Iframe-based rendering and PHP Worker Threads. (#3404)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @bgrgicak @JanJakes @mho22 @perashanid @shimotmk @wojtekn

+

[v3.1.13] (2026-03-23)

+

Enhancements

+
  • [CLI] Auto restart with --experimental-wasm-jspi support. (#3281)
+

PHP WebAssembly

+
  • Fix Emscripten PIPEFS race condition in proc_open. (#3399)
+

Bug Fixes

+
  • Fix Dependabot exclude paths to match dirs recursively. (#3426)
+

Performance

+
  • CLI: Add site editor performance benchmark. (#3408)
+

###

+
  • CLI]: Consider it a lint error for CLI to depend on large Playground web packages. (#3410)
  • Claude] Harden allow/deny lists and clarify dev server behavior. (#3373)
+

Various

+
  • Bump transitive dependency flatted from 3.4.1 to 3.4.2. (#3424)
  • Remove unnecessary phpMyAdmin guard. (#3413)
  • Support blueprint.json in ZIP at root or inside a directory. (#3223)
  • [Website] Provide better UX for wasm and script download failures. (#3348)
  • [personal-wp] Add Playwright end-to-end tests. (#3405)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @bgrgicak @brandonpayton @wojtekn

+

[v3.1.12] (2026-03-16)

+

Enhancements

+

Personal Playground

+
  • Remove Google Analytics from personal-wp. (#3381)
+

Bug Fixes

+
  • Fix: Force x-safari scheme for Linkedin app. (#3392)
  • Fix: Ignore ERR_STREAM_UNABLE_TO_PIPE in CLI server. (#3397)
+

Personal Playground

+
  • [Web] Fix bugs with running personal WP dev server. (#3391)
+

Various

+
  • Add a notice to open Playground in a separate view. (#3387)
  • CLI: Simplify and optimize process existence check for temp dir cleanup. (#3395)
  • Move WP refresh concurrency group to workflow level. (#3380)
  • Re-enable webkit end-to-end tests. (#3379)
  • [CLI] Use local phars for php command phar tests. (#3382)
  • [personal-wp] Adding open-graph My WordPress. (#3390)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @brandonpayton @fellyph @zaerl

+

[v3.1.11] (2026-03-12)

+

Enhancements

+
  • [CLI] php command to run PHP scripts. (#2641)
+

Various

+
  • [PHP] Mount parent directory for file symlinks so DIR works. (#3377)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.1.10] (2026-03-12)

+

Various

+
  • Use deploy keys instead of username/token for workflow pushes to trunk. (#3374)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.1.9] (2026-03-11)

+

Enhancements

+
  • [MCP] Preserve MCP connection parameters when opening site in new tab. (#3371)
+

PHP WebAssembly

+
  • [Website] Use content-type: Application/octet-stream for CORS proxy requests. (#3364)
  • [PHP] Recompile PHP.wasm ASYNCIFY with MAIN_MODULE set to 2. (#3335)
+

Various

+
  • Ignore worktree and .claude dirs in .nxignore and .gitignore. (#3368)
  • Redirect unscoped WP URLs back to scoped WP URLs in service worker. (#3370)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @mho22

+

[v3.1.8] (2026-03-10)

+

Website

+
  • Fix redirect loop on Safari by awaiting cookie store population and re-scoping redirect URLs. (#3365)
+

Bug Fixes

+
  • Remove ws from root overrides to fix Lerna publish. (#3367)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @mho22

+

[v3.1.7] (2026-03-10)

+

[v3.1.6] (2026-03-10)

+

PHP WebAssembly

+
  • [PHP] Call php.mount instead of php.FS.mount in proxyFileSystem. (#3346)
+

Website

+
  • Support streamed PHP responses in the service worker. (#3361)
  • [Playground Website] Playground MCP server. (#3290)
+

Various

+
  • Correct error message formatting in resources.ts. (#3363)
  • [docs] Adding playwright guide. (#3360)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @fellyph @pkevan

+

[v3.1.5] (2026-03-09)

+

Documentation

+
  • [Docs] Updating run cli demos. (#2833)
  • [docs] Adding runCLI and git action video to the documentation. (#3101)
+

PHP WebAssembly

+
  • [Website] Support CURLFile uploads. (#3341)
  • [tcp-over-fetch] Buffer request body for non-HTTPS fetches. (#3356)
  • [tcp-over-fetch] Strip Content-Encoding from response headers. (#3355)
  • [PHP] Recompile PHP.wasm JSPI with MAIN_MODULE set to 2. (#3332)
+

Website

+
  • Allow web deploy from WP update workflow. (#3343)
+

Various

+
  • Ignore ERR_STREAM_PREMATURE_CLOSE in CLI server. (#3304)
  • Make error logging conditional on WP_DEBUG_LOG. (#3300)
  • [CORS Proxy] Fix FirewallInterferenceError on local dev server. (#3353)
  • [Website] Make Google Analytics configurable via environment variable. (#3322)
  • [Website] Support Git repo URLs in ?plugin= Query API parameter. (#3339)
  • [docs] Adding cli start command. (#3239)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @andreilupu @bcotrim @brandonpayton @dd32 @fellyph @JanJakes @mho22

+

[v3.1.3] (2026-03-02)

+

Tools

+
  • [AI] Add devcontainer configuration. (#3276)
+

Documentation

+
  • Clarify Document-Isolation-Policy comments in service worker. (#3319)
+

PHP WebAssembly

+
  • [ xdebug ] Map paths and skip paths with Xdebug 3.5. (#3115)
  • [PHP] Add cp method to Universal PHP. (#3234)
  • [PHP] Add few more function names and Refactor test files. (#3287)
+

Website

+
  • [Web] Avoid cross-origin policy errors in editor. (#3312)
+

Bug Fixes

+
  • Fix /wp-admin blank page when response crosses Comlink worker boundary. (#3301)
  • [Playground CLI] Return a readable error and hint when given port is already in use. (#3238)
  • [WordPress Builds] Fix semver crash on two-part WordPress versions. (#3299)
  • [WordPress Builds] Remove || true artifact in wordpress build. (#3296)
+

Various

+
  • Add ability to disable save. (#3271)
  • Correct plugin directory reference in documentation. (#3323)
  • Move MountDevice type declaration to @wp-playground/storage. (#3315)
  • Reduce Playground CLI target worker count to five. (#3308)
  • Update .nvmrc from Node 20 to Node 22. (#3297)
  • [AI] Symlink .claude/skills to .agents/skills. (#3293)
  • [docs] Adding skills guide. (#3282)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @dd32 @epeicher @fellyph @fredrikekelund @JanJakes @mho22 @n8finch @zaerl

+

[v3.1.2] (2026-02-23)

+

Tools

+
  • [AI] Add Playground website debugging skill. (#3278)
  • [AI] Improve AGENTS.md configuration. (#3275)
+

Bug Fixes

+
  • Fix Node.js version for WP rebuilds which require JSPI. (#3294)
+

Various

+
  • Add streaming support for large file downloads (&gt;2GB). (#3222)
  • Introduce token-based WP configuration transformer. (#3264)
  • Suppress Vite warning in dev env. (#3292)
  • [AI] Add Nx MCP plugin and workspace guidelines to AGENTS.md. (#3286)
  • [AI] Add path manipulation guidance to AGENTS.md. (#3291)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ashfame @bgrgicak @brandonpayton @epeicher @JanJakes

+

[v3.1.1] (2026-02-18)

+

Bug Fixes

+
  • [CLI] Eliminate duplicate Playground CLI Ready message. (#3288)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.1.0] (2026-02-18)

+

Bug Fixes

+
  • [CLI] Fix native file locking for Windows within same multi-threaded architecture. (#3150)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.0.54] (2026-02-18)

+

Blueprints

+
  • Define $\_SERVER['HTTP_HOST'] in the enableMultisite step. (#3214)
+

Tools

+
  • Upgrade NX from 21.3.12 to 22.5.1. (#3279)
  • [AI] Add shared Claude Code settings. (#3277)
+

Various

+
  • Allow publishing npm packages from non-trunk branches. (#3285)
  • [docs] Correct "OpenAI" spacing. (#3280)
  • [i18n] Add Bengali translation for guides/for-plugin-developers.md. (#3273)
  • [i18n] ja translations developer Limitations. (#3272)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bcotrim @bookchiq @JanJakes @noruzzamans @shimotmk

+

[v3.0.53] (2026-02-16)

+

Various

+
  • Further fixes for selected Node.js versions and related CI errors. (#3265)
  • Use Node.js 22 in refresh workflows to fix .mts loader errors. (#3269)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.0.52] (2026-02-12)

+

[v3.0.51] (2026-02-12)

+

Bug Fixes

+
  • Fix npm publish by adding glob as a devDependency. (#3263)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.50] (2026-02-12)

+

PHP WebAssembly

+
  • Upgrade nx 19 → 21 and lerna 7 → 9 to fix publish. (#3262)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.49] (2026-02-12)

+

Various

+
  • Update NPM packages with vulnerabilities. (#3261)
+

Contributors

+

The following contributors merged PRs in this release:

+

@mho22

+

[v3.0.48] (2026-02-11)

+

Enhancements

+
  • [CLI] Add --phpmyadmin flag for database management. (#3224)
+

PHP WebAssembly

+
  • [PHP-wasm] Convert paths to Posix paths during symlink mounting. (#3253)
+

Website

+
  • Add URL route to open Blueprint Gallery via ?overlay=blueprints. (#3220)
  • Remove breakpoint helpers, rewrite AI-generated comments. (#3259)
+

Bug Fixes

+
  • Avoid blueprints package dependency on @php-wasm/web. (#3257)
+

Various

+
  • Add PHPProcessManager test for request queuing and instance reuse. (#3233)
  • Add ashfame to workflow maintainers list. (#3250)
  • Add clarifying comment. (#3251)
  • Downgrade nvm version to 20.18.3. (#3260)
  • Revert "Update @types/node and node versions in package.json (#3247)". (#3256)
  • Switch to a robust check for node env detection. (#3248)
  • Update @types/node and node versions in package.json. (#3247)
  • Update NPM packages with vulnerabilities. (#3241)
  • [Docs] Adding playground for everyone guide. (#3103)
  • [docs] Updating host own playground page. (#3242)
  • [docs] Updating screenshots and content from web instance page. (#3188)
  • [i18n] Add Bengali translation for guides/index.md. (#3230)
  • [i18n] Add Bengali translation for guides/playground-for-everyone.md. (#3249)
  • [i18n] Add Bengali translation for guides/wordpress-native-ios-app.md. (#3231)
  • [i18n] Add Bengali translation for test.md. (#3229)
  • [i18n] Update translations.md for multiple languages. (#3177)
  • [i18n] ja translations developer architecture PHP browser-concepts an…. (#3228)
  • feat: [website] UI Architecture Improvement Plan #3100 - Code Splitting, Modal Registry, and Responsive Design. (#3109)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @bgrgicak @bph @brandonpayton @fellyph @JanJakes @mho22 @noruzzamans @Omcodes23 @shimotmk

+

[v3.0.47] (2026-02-02)

+

Blueprints

+
  • Fix wp-cli step failing on STDERR output with successful exit codes. (#3210)
+

PHP WebAssembly

+
  • [PHP web] Lower initial memory allocation for PHP instances. (#3192)
  • [PHPProcessManager] Spawn at most 2 PHP instances. (#3187)
  • [PHP] Remove considerPrimary option from acquirePHPInstance. (#3191)
+

Website

+
  • Disable pattern picker modal to prevent iOS Safari OOM crashes. (#3226)
  • Fix Safari PWA cache failures by stripping Range headers. (#3200)
  • Replace .blob() with .arrayBuffer() to avoid disk space errors. (#3212)
  • Retry import("main.js") when it fails in Safari. (#3216)
  • Fix Safari loading stale index.html after deployments. (#3208)
  • Improve OAuth flow for private GitHub repositories. (#3181)
+

Internal

+
  • [php-wasm-progress] Add a throttle of 500 milliseconds to the notify function. (#3198)
+

Bug Fixes

+
  • Fix Bad Gateway and optimize PHP workers. (#3219)
  • Fix Xdebug hang in packaged Electron apps. (#3125)
+

Various

+
  • Load SQLite driver from trunk instead of develop. (#3206)
  • Translate title and description for 08-examples.md. (#3166)
  • [Docs] Document git:Directory resource options. (#3179)
  • [Docs] Update translations and configuration in docusaurus.config.js. (#3176)
  • [Personal-WP] Port Safari fixes from website package. (#3221)
  • [Website] Fix Safari failing to import main module after deployments. (#3215)
  • [Website] Fix plugin-proxy artifact lookup for busy repos. (#3211)
  • [docs] Enable Bengali at the language switcher dropdown. (#3207)
  • [i18n] Add Bengali translation for about/launch.md. (#3183)
  • [i18n] Add Bengali translation for intro.md. (#3194)
  • [i18n] Add Bengali translation for quick-start-guide.md. (#3196)
  • [i18n] Add Bengali translation for resources.md. (#3201)
  • [i18n] Add Bengali translation for web-instance.md. (#3202)
  • [website] Switch error modal Kapa widget to hcaptcha. (#3209)
+

Personal Playground

+
  • [Personal-WP] Fix ?plugin= URL parameter not installing plugins. (#3218)
  • [Personal-WP] Fix broadcast message loop in tab coordinator. (#3217)
  • [Personal-WP] Improve recovery mode and add direct OPFS file browser access. (#3182)
  • [personal-wp] Add app catalog. (#3173)
  • [personal-wp] Add deployment workflow for my.wordpress.net. (#3184)
  • [personal-wp] Add multi-tab coordination. (#3163)
  • [personal-wp] Backup reminder UI. (#3162)
  • [personal-wp] Fix backup status showing "Infinity days since backup". (#3213)
  • [personal-wp] Skip CORS proxy for localhost requests. (#3199)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @beryl-dlg @epeicher @fellyph @JanJakes @mho22 @noruzzamans

+

[v3.0.46] (2026-01-26)

+

PHP WebAssembly

+
  • [PHP] Fix PHP web asyncify build socket function exports. (#3168)
+

Various

+
  • [i18n] Add Bengali translation for about/build.md. (#3165)
  • [website] Fix broken end-to-end tests because of sqlite db integration update. (#3172)
+

Personal Playground

+
  • [personal-wp] Add Health Check recovery. (#3157)
  • [personal-wp] Introduce a Personal Playground mode. (#3155)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @noruzzamans

+

[v3.0.45] (2026-01-22)

+

Tools

+

PHP WebAssembly

+
  • [PHP] Redis as a dynamic extension for Node.js. (#3129)
+

PHP WebAssembly

+
  • [PHP] Add php-memcached as a dynamically loadable extension for Node.js. (#3133)
+

Website

+
  • More reliably relay blueprints to kapa. (#3153)
+

Bug Fixes

+
  • Fix "Cannot read properties of undefined (reading 'data')" with nx. (#3151)
+

Various

+
  • Added changes to correct automatically translated text in French for …. (#3159)
  • Corrections to automattic translation for 08-examples.md. (#3161)
  • Update lock file in dev-tools. (#3154)
  • [PHP] Use synchronous connect() for Asyncify builds. (#3160)
  • [i18n] Add Bengali translation for about/index.md. (#3149)
  • [i18n] Add Bengali translation for releases.md. (#3142)
  • [i18n] ja translations developer architecture PHP Filesystem and Asyncify. (#3141)
+

Personal Playground

+
  • [website] Refactor out an Overlay component from saved-playgrounds-overlay. (#3152)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @beryl-dlg @noruzzamans @shimotmk

+

[v3.0.44] (2026-01-20)

+

Enhancements

+
  • [CLI] Enable defining WordPress constants via CLI flags. (#3128)
+

PHP WebAssembly

+
  • [PHP] gethostbyname – native DNS resolution in Node.js builds. (#2988)
+

Website

+
  • Fix sitemap.xml redirect to wp-sitemap.xml. (#3140)
+

Internal

+
  • Update CLAUDE.md with Node.js version guidance. (#3144)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.43] (2026-01-19)

+

Enhancements

+
  • Add CLAUDE.md for AI assistant guidance. (#3136)
+

PHP WebAssembly

+
  • Remove support for PHP 7.2 and 7.3. (#3127)
  • [PHP] Add mmap support to proxyFS. (#3126)
  • [PHP] Cleanup 7.2 and 7.3-specific build code. (#3137)
+

Website

+
  • Always offer to save the site when unrecognized ?site-slug= is used. (#3135)
+

Various

+
  • Updating @vitejs/plugin-react to version 5.1.2 in package.json. (#3110)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph

+

[v3.0.42] (2026-01-15)

+

Enhancements

+
  • Clean up CLI output and simplify mount tracking. (#3122)
+

Various

+
  • [Docs] Updating limitations page. (#3108)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph

+

[v3.0.41] (2026-01-14)

+

Enhancements

+
  • Update Node.js version in .nvmrc to v22.12.0. (#3114)
  • [CLI] Add high-level start command for easy local WordPress development. (#3040)
  • [CLI] Deprecate --debug switch in favor of --verbosity=debug. (#3084)
  • [CLI] @wp-playground/cli start persist sites. (#3119)
+

PHP WebAssembly

+
  • [Node] Improve error messages when dynamic imports fail in Node vm contexts. (#3121)
  • [PHP] Fix external dynamic imports issue in PHP.wasm Web. (#3093)
+

Website

+
  • Customizable page title via ?page-title query parameter. (#3116)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph @mho22

+

[v3.0.40] (2026-01-12)

+

Documentation

+
  • [docs] Replace PNG images with WEBP format for improved performance. (#3105)
+

Various

+
  • Document how to publish new npm packages. (#3068)
  • [docs] Updating images from Release page to webp. (#3107)
  • [i18n] Add Bengali translation for contributor-day.md. (#3096)
  • [i18n] Add Bengali translation for translations.md. (#3106)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph @noruzzamans

+

[v3.0.39] (2026-01-07)

+

PHP WebAssembly

+
  • [CLI] Fix \_\_dirname not defined error in intl extension. (#3094)
+

Various

+
  • Gujarati translation: For-plugin-developers documentation. (#3091)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @iamsohilvahora

+

[v3.0.38] (2026-01-06)

+

[v3.0.37] (2026-01-06)

+

PHP WebAssembly

+
  • [PHP] Refactor files following up on split PHP.wasm packages. (#3085)
+

Bug Fixes

+
  • Exclude wordpress-builds/public path in dependabot. (#3088)
+

Various

+
  • Update express dependency to 4.22.0 to fix qs security issue. (#3089)
  • [i18n] Docs: Add Bengali translation for contributor-day-table-lead.md. (#3087)
+

Contributors

+

The following contributors merged PRs in this release:

+

@mho22 @noruzzamans

+

[v3.0.36] (2026-01-05)

+

[v3.0.35] (2025-12-29)

+

PHP WebAssembly

+
  • [PHP] Use platform: "browser" in esbuild configuration for web builds. (#3083)
+

Internal

+
  • [PHP] Remove unnecessary createRequire() banner from the remaining builds. (#3081)
+

Various

+
  • [i18n] Translate code md to bangla. (#3082)
  • [i18n] ja translations developer architecture PHP.js JavaScript module. (#3080)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @noruzzamans @shimotmk

+

[v3.0.34] (2025-12-24)

+

PHP WebAssembly

+
  • [PHP] Enable dynamic-import for published CJS packages. (#3077)
  • [PHP] Remove unnecessary createRequire() from @php-wasm/web build scripts. (#3076)
+

Website

+
  • Add quick navigation popover to address bar. (#3070)
  • Add standalone playground block demo page. (#3071)
+

Internal

+
  • [CI] Extract npm package integration tests to reusable scripts. (#3078)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.33] (2025-12-22)

+

Website

+
  • Add save status indicator to the toolbar. (#3067)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.32] (2025-12-18)

+

PHP WebAssembly

+
  • [PHP] Split php-wasm packages into per-PHP-version packages. (#3062)
+

Website

+
  • Await the iframe load event in playground.goTo(). (#3066)
  • Skip WordPress installation when WordPress files already exist. (#3065)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.31] (2025-12-17)

+

[v3.0.30] (2025-12-17)

+

Documentation

+
  • Rephrase Opcache module injection comments. (#3053)
+

PHP WebAssembly

+
  • [ Intl ] Parallelize memoizedFetch methods when enabling Intl in PHP.wasm web. (#3058)
+

Bug Fixes

+
  • Update package lock files in WordPress builds themes to fix current security issues. (#3057)
+

Contributors

+

The following contributors merged PRs in this release:

+

@mho22

+

[v3.0.29] (2025-12-17)

+

Tools

+
  • Add WordPress Playground DevTools browser extension. (#3056)
+

Website

+
  • Use a kapa.ai widget for blueprint error troubleshooting. (#2987)
+

Internal

+
  • Add CI workflow to build and release the DevTools extension. (#3059)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk

+

[v3.0.28] (2025-12-17)

+

[v3.0.27] (2025-12-16)

+

Internal

+
  • [Meta] Normalize ws package version override in package.json. (#3055)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.26] (2025-12-16)

+

[v3.0.25] (2025-12-16)

+

Website

+
  • Blueprint Editor: Update the URL when typing. (#2959)
+

Bug Fixes

+
  • Fix npm trusted publisher by upgrading to Lerna 9 and npm 11.5.1+. (#3052)
+

Various

+
  • [CI] Fix flaky end-to-end test for blueprint share button. (#3051)
  • [CI] Restore GH_TOKEN for pushing to protected branch. (#3049)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.24] (2025-12-16)

+

Website

+
  • Target temporary Playground in Import ZIP, Import from GitHub. Don't overwrite a saved site. (#2990)
+

Internal

+
  • [CI] Enable trusted publisher-based publishing. (#3048)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.23] (2025-12-16)

+

Enhancements

+
  • Improve variable name and simplify comment in Document-Isolation-Policy rewriter. (#3044)
  • [CLI] Define CLI --flags for each command individually. (#2930)
  • [CLI] Explore spawning new OS processes in CLI spawnProcess. (#3001)
  • [CLI] Readable commands help message. (#2927)
  • [CLI] SinglePHPInstanceManager, spawn OS subprocesses in the spawn handler in Blueprint V2 worker. (#3004)
  • [CLI] Support TERM=dumb. (#2929)
+

Blueprints

+
  • Don't treat extra output during plugin activation as a failure. (#2904)
  • Preserve more error information on failed fetch. (#2946)
  • Support multiline SQL queries in the runSql step. (#2928)
  • Updated runPHPWithOptions example. (#2986)
  • [ Intl ] Implement Intl in blueprints. (#2979)
+

Tools

+
  • Correct issues related to unsupported parameter property in strip-only mode. (#2945)
  • [ xdebug ] Add --experimental-unsafe-ide-integration option in PHP.wasm CLI. (#2947)
+

PHP WebAssembly

+
  • [ Intl ] Implement Intl in Playground CLI. (#3011)
  • [ php-wasm ] Disable profiling data file generation at runtime. (#2998)
  • [PHP] Add php-wasm-cli smoke test for proc_open in CI. (#3013)
  • [PHP] Require explicit spawn handler for process spawning. (#3008)
+

GitHub integration

+
  • Add mho22 to GitHub workflows actors. (#3046)
  • Update Lerna from 6.6.2 to 7.1.3 in order to run Release NPM packages Github Action. (#3038)
  • [Website] Add support for private github repos via git:Directory. (#2856)
+

Blueprints

+
  • [Blueprint Editor] Add multiline string editor modal. (#2962)
  • [Blueprint editor] Blueprint Validation. (#2967)
+

Blueprints Builder

+
  • [Website] Add "Copy link" button to Blueprint editor. (#3029)
+

Website

+
  • [Website] Blueprint bundle editor. (#2942)
+

Documentation

+
  • Removed trailing space contained within link text. (#2911)
  • [Docs] Adding blueprints meta info reference. (#3026)
  • [Docs] Document how to use Playground CLI as a developer. (#2925)
  • [Docs] Move XDebug IDE integration closer to the top, explain the DevTools workflow is highly exploratory. (#2975)
  • [Docs] Updating repository preview info. (#2890)
  • [Docs] Use pull_request_target in the GitHub action PR Preview doc page. (#2915)
  • [docs] Removing GitHub proxy references. (#3009)
  • [docs] Updating resource page. (#3003)
  • [i18n] ja translations developer architecture. (#3000)
+

Experiments

+

GitHub integration

+
  • [ playground-storage ] Add vite-plugin-dts to Playground Storage. (#3035)
+

Website

+
  • [ Vite ] Regroup ignore-*-imports Vite plugin inside a vite-extension. (#2999)
+

File Synchronization

+
  • [Website] fs-journal: Replace recursion with a loop to stop exceeding the call stack. (#2912)
+

PHP WebAssembly

+
  • Add file locking integration tests for php-wasm/node. (#2908)
  • Follow up on PHP 8.5 - Post process with wasm-opt in dynamic extensions. (#3017)
  • PHP 8.5 support. (#2950)
  • Remove custom processManager support in PHPRequestHandler. (#3005)
  • [ Vite ] Regroup preserve-*-loaders-imports Vite plugin inside a vite-extension. (#3002)
  • [ php-wasm ] Add intl dynamic extension to @php-wasm/web. (#2591)
  • [ php-wasm ] Remove static intl extension artifacts. (#2970)
  • [PHP WASM] Transfer ArrayBuffer instead of cloning in PHPResponse serialization. (#3036)
  • [PHP web] Remove runtimes from internal map after initialization to prevent memory leaks. (#3023)
  • [PHP.wasm] Upgrade emscripten version from 4.0.5 to 4.0.19. (#2910)
  • [PHP] Move setImmediate polyfill to loadWebRuntime. (#3024)
  • [PHP] Upgrade GD to 2.3.3. (#2944)
+

Website

+
  • Blueprint editor. (#2815)
  • Correct out of memory issue. (#3021)
  • Enforce HTTPS in fetchWithCorsProxy(). (#2901)
  • Rewrite COEP/COOP headers as Document-Isolation-Policy in supporting browsers. (#3028)
  • Deprecate github-proxy.com with automatic migration to native resources. (#3007)
  • Improve error reporting. (#2874)
  • Move dynamic extension files in assets/extensions directory. (#2984)
  • MySQL admin tool improvements. (#2971)
  • Prevent Query API defaults from overriding login info provided by the Blueprint. (#2871)
  • Replace WordPress nightly with trunk. (#2916)
  • Saved Playgrounds overlay redesign. (#2985)
  • Stop CSP frame-ancestors from breaking embedded Playgrounds. (#2980)
  • Updating Run Blueprint button. (#2953)
  • [Analytics] Track install and error events. (#1984)
  • [Blueprints] Detect network firewall interference during requests. (#3031)
+

Blueprints

+
  • Rewrite scoped URLs when importing a Playground export. (#2989)
+

Internal

+
  • Add .orig and .rej files to .gitignore for patching leftovers. (#2981)
  • Disable nx cache. (#2941)
  • Inject Playground commit ID to the built index.html. (#2918)
  • [CI] Explore fixing two notoriously flaky tests. (#2935)
  • [CI] Fix flaky API documentation end-to-end test. (#2963)
  • [CI] Preserve artifacts from failed API documentation end-to-end tests. (#2960)
  • [CI] Rebalance unit test groups. (#2933)
  • [CI] Reduce CLI log output in CI jobs. (#2934)
  • [CI] Speed up end-to-end tests. (#2961)
  • [CI] Stabilize flaky CI tests. (#2931)
  • [Website] Fix flaky phpMyAdmin test. (#2977)
+

Bug Fixes

+
  • Fix SQLite plugin logging to respect skip option. (#2924)
  • Fix runPHPWithOptions demo. (#2978)
  • [Docs] Fix API reference. (#2905)
  • [Docs] Fix documentation site build failures. (#2913)
+

###

+
  • Xdebug ] Relocate xdebug into shared library directory. (#3045)
+

Various

+
  • Add SQLITE_ENABLE_COLUMN_METADATA and update SQLite. (#2948)
  • Add file locking tests for Playground CLI. (#2920)
  • Add non-blocking F_SETLKW based on F_SETLK. (#2972)
  • [Blueprints] Add dispose functions in afterEach hooks in tests. (#2921)
  • [Docs] Updating the resource page and adding new playground videos. (#2914)
  • [Storage] Fix fileExists() to return true for directories. (#2968)
  • [Storage] Require explicit recursive flag for mkdir (and tweak the semantics of writeFile and listFiles). (#2969)
  • [Website] Database panel with Adminer & phpMyAdmin. (#2926)
  • [i18n] Add contributor day leadership guide in Portuguese. (#2995)
  • [i18n] complete pt_br translation of blueprints documentation. (#3015)
  • [i18n] ja translations developer architecture WebAssembly PHP and Compiling PHP. (#3033)
  • i18n: Full Translation of "For Plugin Developers" to Spanish. (#2900)
  • i18n: Minor fixes on resources.md Spanish translation. (#2898)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @andr3ribeiro @bgrgicak @brandonpayton @epeicher @fellyph @JanJakes @jeffpaul @mho22 @shimotmk @SirLouen @Utsav-Ladani @wojtekn

+

[v3.0.22] (2025-11-17)

+

Enhancements

+

Boot Flow

+
  • [Boot] Verify permalink structure is actually set. (#2902)
+

Blueprints

+
  • V1] Rewrite github.com/owner/repo/raw URLs. (#2892)
+

Documentation

+
  • Document the new GitHub Preview button action. (#2893)
  • [Docs] Adding link at the documentation sidebar for PR preview link page. (#2895)
  • [Docs] Documentation Expansion for php-wasm/node. (#2780)
  • [Docs] Updating sidebar to add Xdebug pages. (#2881)
+

PHP WebAssembly

+
  • Try avoiding URL.canParse for older version of Safari. (#2887)
+

Website

+
  • Remove "preview WordPress core branch" feature. (#2894)
  • Support previewing WordPress and Gutenberg branches, not just PRs. (#2868)
+

Bug Fixes

+
  • [CLI] Fix null and "latest" WP version resolution and improve unzip error message. (#2889)
  • [CLI] Fix run-cli leak which was revealed by repeated runCLI() calls during test. (#2888)
+

###

+
  • CLI] Allow API consumers to rely upon option validation and default values. (#2883)
+

Various

+
  • Minor stylistic and punctuation improvements. (#2884)
  • Revise error messages and coding guidelines. (#2879)
  • Update footer labels in localization file. (#2878)
  • [i18n] Add Japanese translations to Blueprints JSON and the API Client and Mount data. (#2882)
  • [i18n] Adding Italian translations. (#2865)
  • [i18n] Create contributor-day-table-lead.md for Gujarati. (#2866)
  • i18n: Full Translation of "Build" to Spanish. (#2906)
  • i18n: Minor fixes on web-instance.md Spanish translation. (#2899)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph @mehrazmorshed @praful2111 @shimotmk @SirLouen @Successfulsebunya

+

[v3.0.21] (2025-11-10)

+

Bug Fixes

+
  • Docs: Fix grammar, typos, and formatting issues across documentation. (#2877)
  • Docs: Fix typo in resources.md. (#2876)
  • docs: Fix MDN link for Access-Control-Allow-Origin header. (#2875)
+

Various

+
  • [Docs] Adding ask AI Button. (#2858)
  • [i18n] Add Japanese translations to Blueprints JSON and the API Client. (#2873)
+

Contributors

+

The following contributors merged PRs in this release:

+

@fellyph @hmbashar @huzaifaalmesbah @shimotmk

+

[v3.0.20] (2025-11-06)

+

Enhancements

+
  • [CLI] Add --wordpress-install-mode flag. (#2803)
+

Tools

+

PHP WebAssembly

+
  • [PHP.wasm] Major overhaul of URL rewriting and setting $\_SERVER variables. (#2864)
+

PHP WebAssembly

+
  • [PHP.wasm] ImageMagick extension. (#2834)
+

Bug Fixes

+
  • [CLI] Make messaging clearer for errors and Xdebug IDE integration. (#2869)
+

Various

+
  • [Docs] Add xdebug testing page. (#2840)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph

+

[v3.0.19] (2025-11-04)

+

Enhancements

+
  • [CLI] Support .0 versions, e.g. 6.4.0. (#2848)
+

Documentation

+
  • [Docs] Remove kapa AI button. (#2849)
+

PHP WebAssembly

+
  • Fix unmount of nested mounts in PHP runtime hotswap. (#2859)
+

Website

+
  • Controlled iframe for the latest Guteneberg version. (#2857)
  • [TCP Proxy] Produce the correct request path in parseHttpRequest(). (#2852)
+

Bug Fixes

+
  • [CLI] Avoid breaks during Playground boot when --xdebug enabled. (#2835)
  • [CLI] Fix incorrect Xdebug mappings for absolute host paths. (#2860)
+

Various

+
  • Translate to bangla in contributing index file. (#2851)
  • [i18n] Add Japanese translations to Playground API Client. (#2854)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph @noruzzamans @shimotmk

+

[v3.0.18] (2025-10-31)

+

PHP WebAssembly

+
  • Remove old, unused .wasm files from @php-wasm/web. (#2847)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.0.17] (2025-10-31)

+

PHP WebAssembly

+
  • Remove old, unused php-wasm .wasm files. (#2846)
+

Website

+
  • Remove client package from the list of offline mode assets. (#2841)
+

Bug Fixes

+
  • [CLI] Fix temp dir cleanup when Playground CLI server is killed. (#2836)
+

Various

+
  • Translate contributor-badge page to bangla. (#2843)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton @noruzzamans

+

[v3.0.16] (2025-10-31)

+

Enhancements

+
  • [ xdebug ] Add --experimental-unsafe-ide-integration option in Playground CLI. (#2777)
+

Tools

+

PHP WebAssembly

+
  • Add support for AVIF in GD. (#2814)
+

Documentation

+
  • [Docs] Adding data persistence information. (#2798)
  • [i18n] Adding cli flags to Portuguese and Spanish. (#2829)
+

PHP WebAssembly

+
  • [PHP] SOAP extension. (#2832)
+

Website

+
  • Make saved sites sidebar more narrow. (#2826)
  • Add refresh button to the left of address bar. (#2828)
  • Wider site info on smaller breakpoints. (#2830)
+

Various

+
  • Translate documentation content to Bangla language. (#2839)
  • [Docs] Updating CLI flags list. (#2812)
  • [i18n] Adding Luganda structure. (#2774)
  • [i18n] Create contributor-day.md for Gujarati. (#2791)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @Dhruval-678 @fellyph @mho22 @noruzzamans

+

[v3.0.15] (2025-10-27)

+

Tools

+

Import/Export

+
  • [Blueprints] Use \_SERVER['HTTPS'] in the import step. (#2802)
+

Website

+
  • Add Playground logo and 'import' menu to the sidebar. (#2806)
  • Ask for a Playground name before saving. (#2768)
  • File browser and code editor. (#2813)
  • Move the sidebar icon to the right. (#2811)
  • Refresh button icons next to the browser bar. (#2807)
  • Resizable sidebar. (#2809)
  • Restore the visible button selection in the sidebar. (#2804)
  • Retain address bar when opening the sidebar. (#2808)
+

Various

+
  • [i18n] Add Japanese translations to remote.html vs index.html. (#2817)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @shimotmk

+

[v3.0.14] (2025-10-21)

+

Various

+
  • [i18n] Add Japanese translations to JavaScript API. (#2800)
+

Contributors

+

The following contributors merged PRs in this release:

+

@shimotmk

+

[v3.0.13] (2025-10-20)

+

Enhancements

+
  • Playground CLI: Print temp dir and mounts when --verbosity=debug. (#2799)
+

Blueprints

+
  • Support creating a local .git directory via git:Directory resource. (#2787)
  • ensure git:Directory resource returns non-empty-name. (#2779)
+

Documentation

+
  • [Docs] Adding table lead guide documentation. (#2708)
  • [docs] Removing deprecated run method reference. (#2778)
  • [i18n] Updating translation guide for Portuguese and Spanish. (#2773)
+

Internal

+
  • Document the rationale for setting php.ini values in wordpress/src/boot.ts. (#2784)
+

Various

+
  • Add some improvements in the Spanish translations. (#2788)
  • [i18n] Add Japanese translations to Query API. (#2781)
  • [i18n] Adding French translation to guides/index.md. (#2790)
  • [i18n] Adding quick start guide and web instance pages in Spanish. (#2785)
  • [i18n] Fixing typos in French translation of documentation.md. (#2792)
  • [i18n] Updating Contributor badge and Contributor day pages. (#2782)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @amieiro @beryl-dlg @brandonpayton @fellyph @shimotmk

+

[v3.0.12] (2025-10-13)

+

Internal

+
  • [CI] Remove a few predefined runner files before releasing npm packages. (#2775)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.11] (2025-10-13)

+

[v3.0.10] (2025-10-13)

+

Enhancements

+
  • Update form-data npm dependency via package.json overrides. (#2761)
  • Update sha to 2.4.12. (#2729)
  • Update tmp package to the latest version. (#2730)
  • [XDebug Bridge] Read files from VFS when a PHP instance is provided. (#2722)
+

Blueprints

+
  • Accept branch names and tags in git:Directory resource. (#2760)
+

Tools

+
  • [CLI] Respond with HTTP 500 response when the request handler throws an error. (#2715)
+

Documentation

+
  • Add docblock for createInvertedReadableStream. (#2726)
  • Adding Kapa AI script tag. (#2727)
  • [Docs] Updating resources page. (#2771)
  • [i18n] Adding Bengali Structure. (#2754)
+

PHP WebAssembly

+
  • [ xdebug ] Configure xdebug for phpstorm compatibility. (#2747)
+

Website

+
  • Adding new icons to Playground web instance. (#2696)
  • Allow remote.html from same origin as Playground client script. (#2765)
  • Make deployment script more usable for self-hosting. (#2764)
+

Internal

+
  • Add Dependabot configuration for npm updates. (#2733)
  • [Meta] Fix package-lock.json after dependabot. (#2745)
+

Bug Fixes

+
  • Fixing old documentation regarding PHP code examples. (#2721)
  • [i18n] Fix issue related to link in french translation. (#2748)
+

Various

+
  • Deploy web app less often to reduce cache invalidation. (#2762)
  • [Meta] Fix package-lock.json after dependabot. (#2746)
  • [i18n] Add Japanese translations to APIs overview. (#2770)
  • [i18n] Add translation to page title in resources.md. (#2728)
  • [i18n] Adding French translation to intro.md. (#2724)
  • [i18n] Fixing typos in French translation quick-start-guide.md. (#2725)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @brandonpayton @fellyph @mho22 @shimotmk @wojtekn @zaerl

+

[v3.0.9] (2025-10-06)

+

Tools

+
  • Fix local package repository command running issue. (#2720)
+

Various

+
  • [i18n] Added Gujarati Translation for Local Development 02-vscode-extension.md File. (#2718)
  • [i18n] Added Gujarati Translation for Local Development 03-php-wasm-node File. (#2719)
  • docs: Add browser support information to limitations documentation. (#2711)
+

Contributors

+

The following contributors merged PRs in this release:

+

@getdave @mho22 @shail-mehta

+

[v3.0.8] (2025-10-02)

+

Documentation

+
  • [i18n] Added Gujarati Translation for Documentation Contributions. (#2610)
+

Website

+
  • [PHP Playground] Attach client to https://playground.wordpress.net/remote.html. (#2717)
+

Internal

+
  • Add name to Playground Website and Website extras cli packages. (#2716)
+

Various

+
  • [i18n] Fixing typos in French translation of quick-start-guide.md. (#2712)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @dilipom13

+

[v3.0.7] (2025-10-01)

+

Tools

+

PHP WebAssembly

+
  • [PHP] Add "ls" and "pwd" CLI commands. (#2701)
+

Documentation

+
  • Revert Quick guide to the English version. (#2709)
  • [i18n] Add French translation for quick-start-guide.md. (#2710)
+

PHP WebAssembly

+
  • Preserve chroot across all worker-managed PHP instances. (#2704)
  • [PHP] Allow removing CWD during runtime rotation. (#2714)
  • [PHP] Do not reap() PHP in the worker until the streaming response ends. (#2703)
  • [PHP] Move registerWorkerListeners() from a specific worker to the parent PHPWorker. (#2705)
  • [PHP] Support subprocesses in cli() calls. (#2702)
+

Website

+
  • IDE-like PHP playground. (#2699)
+

Internal

+
  • [PHP utils] Export splitShellCommand. (#2706)
+

Bug Fixes

+
  • CORS proxy: Fix custom origin validation during deployment. (#2698)
+

Various

+
  • [i18] Add French translation for launch.md. (#2697)
  • [i18n] Add French translation for quick-start-guide.md. (#2692)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @brandonpayton @fellyph

+

[v3.0.6] (2025-09-29)

+

Documentation

+
  • [i18n] Add French translation for resources.md. (#2680)
+

Website

+
  • [Client] Enable calling playground.runStream() and .cli() from the client. (#2688)
+

Various

+
  • Update Playground JS API tutorial Link. (#2685)
  • [i18n] Add Japanese translations to Playground CLI. (#2683)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @jdahir0789 @shimotmk

+

[v3.0.5] (2025-09-25)

+

Bug Fixes

+
  • Playground CLI: Log unhandled rejections and stop them from crashing workers. (#2682)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v3.0.4] (2025-09-25)

+

Experiments

+

GitHub integration

+
  • [CLI] Polyfill the Buffer class without making it an empty object in CLI. (#2681)
+

PHP WebAssembly

+

Website

+
  • [Website] Disable curl_share_init by default (to make Composer work). (#2679)
+

Website

+
  • Resolve the Blueprint declaration for the 'View Blueprint' button. (#2675)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.3] (2025-09-23)

+

Blueprints

+
  • Replace randomString() with randomFilename() in installAsset(). (#2677)
+

Documentation

+
  • [Docs] Adding steps to translate documentation with GitHub UI. (#2666)
  • [i18n] Add French translation for documentation.md. (#2670)
  • [i18n] Adding Architecture page to Brazilian Portuguese. (#2667)
+

PHP WebAssembly

+
  • [PHP Worker] listen to all PHP instances events via worker.addEventListener(). (#2673)
+

Website

+

Blueprints

+
  • Flatten the stored runtime configuration format. (#2671)
  • Report Blueprint v2 progress. (#2674)
  • [Blueprints] Separate computing the runtime configuration from compiling a Blueprint. (#2672)
+

Various

+
  • [i18n] Add Japanese translations to VS Code extension. (#2668)
  • [i18n] Add Japanese translations to php-wasm/node. (#2669)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @beryl-dlg @fellyph @shimotmk

+

[v3.0.2] (2025-09-22)

+

Blueprints

+
  • [Website] Split Playground remote initialization logic into Blueprint-version specific workers. (#2652)
+

Documentation

+
  • [i18n] Added Gujarati Translation for Local Development 01-wp-now.md file. (#2664)
+

Website

+

Blueprints

+
  • Add Blueprint v2 handlers (noop). (#2657)
  • Declare the correct Blueprints v2 types. (#2655)
  • Isolate resolveBlueprintFromURL() calls. (#2654)
  • [Client] Expose Blueprints v2 runner via a feature flag. (#2658)
+

Various

+
  • [i18n] Added Missing Description in Intro and Quick Start Guide Pages for Gujarati Language. (#2660)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @shail-mehta

+

[v3.0.1] (2025-09-19)

+

Blueprints

+
  • [Client] Explicit Blueprints v1 handler. (#2651)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v3.0.0] (2025-09-19)

+

Breaking Changes

+

Blueprints

+
  • [Blueprints] Rename Blueprints v1 types, add Blueprint v2 types. (#2648)
+

Blueprints

+
  • Move Blueprints v1-specific files to v1 subdirectory. (#2644)
  • Rename BlueprintDeclaration to BlueprintV1Declaration. (#2645)
  • V1] Use the wordpress-importer plugin for the importWxr step. (#2640)
+

Documentation

+
  • [i18n] Added Gujarati Translation for 01 What are Blueprints and What you can do with them File. (#2617)
  • [i18n] Added Gujarati Translation for Local Development Intro.md file. (#2623)
  • [i18n] Tagalog translations for using blueprints page. (#2628)
  • [i18n] Tagalog translations of API Consistency. (#2629)
  • [i18n] Tagalog version of Blueprints/Steps/Shorthands. (#2630)
+

Experiments

+

PHP WebAssembly

+
  • Playground CLI: Mount /wordpress, /internal, and /tmp dirs within real temporary dir. (#2446)
+

PHP WebAssembly

+
  • Function mismatch fix in GD jpeg extension for PHP below 8.0. (#2634)
  • [PHP] Add unit tests for nested mounts. (#2639)
+

Website

+
  • [WordPress] Separate bootWordPress() from bootRequestHandler(). (#2650)
+

Blueprints

+
  • [Blueprints] Log step names without parsing. (#2649)
+

Bug Fixes

+
  • [Xdebug Bridge] Fix issue if non existent file is read. (#2625)
+

Reliability

+
  • Fix Playground CLI boot from native dirs on Windows. (#2642)
+

Various

+
  • Keep wordpress-importer.zip for older versions of Playground. (#2647)
  • Update build.md. (#2638)
  • [Documentation] Adding troubleshooting sections for WP-CLI database connections. (#2333)
  • [i18n] Add Japanese translations to wp-now NPM package. (#2615)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akkspros @brandonpayton @fellyph @mho22 @rollybueno @shail-mehta @shimotmk

+

[v2.0.22] (2025-09-16)

+

Enhancements

+
  • Statically analyzable build. (#2632)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v2.0.21] (2025-09-15)

+

Enhancements

+
  • [CLI] Build worker thread with stable filenames. (#2631)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v2.0.20] (2025-09-15)

+

Tools

+

Blueprints

+
  • Fix to ensure that Site Editor templates are associated with the correct taxonomy upon import. (#2584)
+

PHP WebAssembly

+
  • Translate 'localhost' to 127.0.0.1 in MySQL connections. (#2627)
  • [PHP] Rebuild with sync fd_close on the web. (#2620)
+

Website

+
  • [Web Client] Remove onBeforeBlueprint. (#2622)
+

Internal

+
  • [CI] Do not block CI tests on linter. (#2619)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk

+

[v2.0.19] (2025-09-11)

+

PHP WebAssembly

+
  • [Intl] Correct intl errors during compilation and build. (#2613)
+

Bug Fixes

+
  • Playground CLI: Skip WP install when using existing WP files. (#2616)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @mho22

+

[v2.0.18] (2025-09-08)

+

Documentation

+
  • Added Gujarati Translation for Code.md File. (#2606)
  • Tagalog translations of Blueprints -&gt; Blueprint Data Format. (#2609)
  • [Translations] Adding local environment section to pt-BR. (#2572)
+

Bug Fixes

+
  • Fixing header from Gujarati translation. (#2614)
  • [Xdebug Bridge] Fix create hash error in xdebug bridge. (#2612)
+

Contributors

+

The following contributors merged PRs in this release:

+

@fellyph @mho22 @rollybueno @shail-mehta

+

[v2.0.17] (2025-09-08)

+

Documentation

+
  • Add Japanese translations to Local Development. (#2604)
+

Various

+
  • Added Gujarati Translation for Blueprints Tutorials Index.md file. (#2607)
  • Added Gujarati Translation for How to Run Blueprints. (#2611)
+

Contributors

+

The following contributors merged PRs in this release:

+

@shail-mehta @shimotmk

+

[v2.0.16] (2025-09-04)

+

Enhancements

+
  • Add structured issue templates for bugs, enhancements, and documentation. (#2571)
  • Allow installing multiple themes via URL, as required by parent/child themes. (#2581)
+

Blueprints

+
  • Move blueprints.phar to the Blueprints package. (#2471)
  • Move runBlueprintV2 to the blueprints package. (#2594)
+

Tools

+
  • [XDebug Bridge] Highlight syntax of php scripts from mime type in Devtools. (#2566)
  • [XDebug Bridge] Load files in Devtools before running PHP with Xdebug enabled. (#2527)
+

Documentation

+
  • Added Gujarati Translation for Index.md File. (#2576)
  • Added Gujarati Translation for JSON API and Function API File. (#2577)
  • Adding french translation guide. (#2541)
  • Adding more internal links between pages. (#2499)
  • Adding reference to multiple theme URL parameter. (#2593)
  • Adding runCLI section to the Playground CLI page. (#2583)
  • Updating runCLI demo at wp-playground/cli README file. (#2553)
  • Updating wp versions blueprints. (#2573)
  • [Docs] Adding how to contribute to the WordPress Playground. (#2597)
+

PHP WebAssembly

+
  • Increase file locking safety. (#2506)
  • [ php-wasm ] Add intl dynamic extension to @php-wasm/node ASYNCIFY #2501. (#2557)
  • [PHP] Expose php.cli() on the web. (#2590)
  • [PHP] Include CLI SAPI in the web PHP.wasm build. (#2589)
  • [PHP] Inline rotatePHPRuntime() into the PHP class. (#2559)
  • [PHP] Rotate the spawn handler in hotSwapPHPRuntime(). (#2588)
+

Website

+
  • A specific error when a GitHub artifact is not found. (#2596)
  • Apply base URLs to relative redirection URLs. (#2595)
+

Various

+
  • Add Japanese translations to Quick Start Guide for Developers. (#2587)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @dd32 @fellyph @mho22 @shail-mehta @shimotmk

+

[v2.0.15] (2025-09-01)

+

Documentation

+
  • Added Gujarati Translation for intro-devs.md file. (#2574)
+

Experiments

+

File Synchronization

+
  • [Website] Renaming stored Playgrounds. (#2486)
+

Bug Fixes

+
  • Fix error from adding blank target to external links. (#2575)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @draganescu @shail-mehta

+

[v2.0.14] (2025-08-29)

+

PHP WebAssembly

+
  • Add test to verify mount() correctly handles files (closes #503). (#2564)
  • Use unique tmp.zip filename in unzipFile() to avoid conflicts in async calls. (#2567)
  • [ php-wasm ] Add intl dynamic extension to @php-wasm/node JSPI. (#2501)
  • [WordPress][CLI] Allow requesting internal URLs via http_request_host_is_external. (#2569)
+

Various

+
  • Tagalog translations of Blueprints -&gt; Tutorial -> Build Your 1st Blueprint. (#2546)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @jnealey88 @mho22 @rollybueno

+

[v2.0.13] (2025-08-26)

+

Enhancements

+
  • [CLI] --site-url option for Playground CLI. (#2558)
+

Tools

+
  • [XDebug Bridge] Fetch all array keys when inspecting an array. (#2409)
+

PHP WebAssembly

+
  • [PHP web] Export setErrNo() to make less work as pager. (#2554)
+

Website

+
  • [Web] tolerate empty e.target in addTargetBlankToExternalLinks. (#2555)
+

Bug Fixes

+
  • Playground CLI: Wire up execution modes for Blueprints v2. (#2519)
+

Reliability

+
  • [php-wasm-logger] Filter logs by severity in Logger and assign severity based on verbosity argument in CLIs. (#2436)
+

Various

+
  • Added Gujarati Translation for Index.md file. (#2550)
  • Use SQLite AST driver also with Blueprints v2. (#2538)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @JanJakes @mho22 @shail-mehta

+

[v2.0.12] (2025-08-25)

+

Enhancements

+
  • Playground CLI: Support --auto-mount=path option. (#2525)
+

Documentation

+
  • Added Gujarati Translation for Contributor Badge File. (#2549)
  • Added Gujarati Translation for coding-standards.md file. (#2507)
  • Japanese translations: Blueprints Unifying assets folder. (#2536)
  • Updating Translation Documentation Page. (#2534)
+

Various

+
  • Add Japanese translations to WordPress Playground Developers documentation. (#2551)
  • Add Japanese translations to WordPress Playground badge. (#2545)
  • Added Gujarati translation for about, build, test, launch pages. (#2533)
  • Added Original Comment and Description in Launch.md file. (#2542)
  • Tagalog translations of Blueprints -&gt; Getting Started. (#2547)
+

Contributors

+

The following contributors merged PRs in this release:

+

@aslamdoctor @brandonpayton @fellyph @rollybueno @sandipr942 @shail-mehta @shimotmk

+

[v2.0.11] (2025-08-21)

+

Tools

+

Blueprints

+
  • [Blueprints] Use the local worker in Builder in development mode. (#2495)
+

Documentation

+
  • Adding blueprints documentation description. (#2524)
+

PHP WebAssembly

+
  • Fix corrupt zip download from Site Editor export. (#2531)
+

Bug Fixes

+
  • Fix wrong license page within the languages es, pt-BR and ja. (#2483)
+

Various

+
  • Add Japanese translations to Build your first Blueprint. (#2529)
  • Add Japanese translations to How to run Blueprints. (#2526)
  • Added Gujarati Translation for web-instance.md file. (#2532)
  • Unifying assets folder. (#2521)
+

Contributors

+

The following contributors merged PRs in this release:

+

@aslamdoctor @brandonpayton @fellyph @josevarghese @mho22 @shimotmk

+

[v2.0.10] (2025-08-18)

+

Documentation

+
  • Add Japanese translations to Blueprints 101 and What are Blueprints?. (#2511)
  • Adding the documentation meta description to the main folder. (#2504)
  • Translating fragments documentation and adding Contributor badge page. (#2503)
  • Add GitDirectoryReference resource documentation. (#2492)
+

PHP WebAssembly

+
  • Re-enable and fix native file locking tests. (#2505)
+

Website

+

Documentation

+
  • Fix customize bundle instructions in "Host Own Playground" documentation. (#2510)
+

Bug Fixes

+
  • Fix: "referrences" should be "references". (#2514)
+

Various

+
  • Add Japanese documentation meta description to the main folder. (#2518)
  • Add Japanese translations to Introduction. (#2489)
  • Add docs-description blueprints tutorial and What are Blueprints?. (#2512)
  • Added Gujarati Translation for Launch.md file. (#2480)
  • Added Gujarati Translation for quick-start-guide.md. (#2494)
  • Adding Contributor Badge page at the documentation. (#2491)
  • Tagalog translations of Blueprints -&gt; Tutorial -> What are Blueprints?. (#2520)
  • Translation: Blueprints -&gt; Tutorial -> How to run Blueprints. (#2522)
  • Updated Typos in contributor-badge.md File. (#2493)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @fellyph @juanmaguitar @mukeshpanchal27 @ravigadhiya007 @rollybueno @shail-mehta @shimotmk

+

[v2.0.9] (2025-08-11)

+

Breaking Changes

+
  • [Website] Default demo content. (#2412)
+

Enhancements

+
  • [PHP Playground] Select WordPress version. (#2472)
+

Blueprints

+
  • [WordPress] Support auto-login with customize.php as a landing page. (#2467)
+

Documentation

+
  • Tagalog: Add translations for Blueprints Tutorial. (#2477)
  • Tagalog: Translations of Blueprints documentation page. (#2476)
+

Website

+
  • Open external links in a new tab/window. (#2468)
+

Internal

+
  • Skip flaky end-to-end tests. (#2475)
+

Various

+
  • Add Japanese translations to Blueprint Examples and Troubleshoot and debug. (#2474)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @rollybueno @shimotmk @zaerl

+

[v2.0.8] (2025-08-07)

+

Blueprints

+
  • Fallback to URL-based file name when fetching remote ZIP files. (#2470)
+

Documentation

+
  • [Docs] Adding Resources, Web instance, and guides pages on Brazilian Portuguese. (#2351)
  • [Docs] Enabling language switcher. (#2465)
+

PHP WebAssembly

+
  • Fix resolving mounted symlinks in secondary PHP instances. (#2444)
  • [XDebug Bridge] List additional ASYNCIFY_ONLY functions to prevent unreachable crashes when using Devtools. (#2454)
+

Website

+
  • [Browser] Do not use the Buffer class in web browsers. (#2469)
  • Add PHP Playground. (#2463)
  • Suppress PHP warnings when pre-fetching WordPress updates. (#2458)
+

Bug Fixes

+
  • Fix invalid filename "&lt;7.4" on Windows. (#2461)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph @JanJakes @mho22

+

[v2.0.7] (2025-08-05)

+

Tools

+
  • [ CLI ] Rename CLI binaries in @php-wasm/cli and @wp-playground/cli. (#2441)
+

Documentation

+
  • Updating Docusaurus version from 3.7 to 3.8. (#2457)
  • [Docs] Adding fixes to broken URLs. (#2451)
  • [Docs] Bump PHP version to avoid WordPress PHP version Warning. (#2443)
+

PHP WebAssembly

+
  • Patch the premature "request in progress" semaphore release. (#2455)
+

Bug Fixes

+
  • Re-enable Playground CLI tests. (#2445)
+

Various

+
  • Add check for SQLite driver missing in target folder. (#2440)
  • Adding CLI reference to Playground README. (#2433)
  • Adding Gujarati Intro documentation. (#2450)
  • Bump WordPress old version demo to version 6.2.1. (#2460)
  • Removing blog references on Docusaurus. (#2456)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph @mho22 @nikunj8866 @zaerl

+

[v2.0.6] (2025-08-04)

+

Documentation

+
  • Adding Contribution pages Spanish translation. (#2431)
+

PHP WebAssembly

+
  • Fix file locking for PROXYFS nodes that wrap NODEFS. (#2437)
+

Bug Fixes

+
  • Translations: Fix command typo in docs. (#2449)
+

Various

+
  • Add Japanese translations to Blueprint Bundles and API Consistency. (#2438)
  • Translations: Clean up version from PR #2336. (#2448)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @fellyph @rollybueno @shimotmk @vipul0425

+

[v2.0.5] (2025-07-28)

+

Bug Fixes

+
  • Fix fcntl() F_GETLK fatal due to undefined flock struct address. (#2432)
+

Various

+
  • Add Japanese translations to contributing index and sidebar. (#2434)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @shimotmk

+

[v2.0.4] (2025-07-25)

+

Public API

+

Blueprints

+
  • Distribute @wp-playground/client without any package.json dependencies. (#2426)
+

Tools

+
  • Fix ESLint 9 linting in VSCode. (#2417)
  • [ xdebug ] Add --experimental-devtools option in Playground CLI. (#2411)
+

PHP WebAssembly

+
  • [PHP] Dispatch request.error for all non-zero-exit request handler errors. (#2429)
+

Website

+
  • Query Monitor plugin support. (#2415)
+

Internal

+
  • Skip flaky end-to-end tests in Firefox and Webkit. (#2425)
+

Bug Fixes

+
  • [Xdebug Bridge] Correct error related to unresolved promises in bridge. (#2422)
+

Various

+
  • Documentation add sidebar to blueprints bundles. (#2397)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @mho22 @shimotmk

+

[v2.0.3] (2025-07-24)

+

Tools

+
  • [ xdebug ] Add --experimental-devtools option in php-wasm CLI. (#2408)
+

Bug Fixes

+
  • Playground CLI Allow /wordpress subdirs to be mounted before WP install. (#2382)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @mho22

+

[v2.0.2] (2025-07-23)

+

Tools

+

PHP WebAssembly

+
  • [ xdebug ] Bridge DBGP session with CDP server. (#2402)
+

Internal

+
  • [Build] Preserve optionalDependencies in built package.json. (#2410)
+

###

+
  • Comlink] Throw the original error in the error handler. (#2407)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v2.0.1] (2025-07-23)

+

Blueprints

+
  • Rewrite paths in the wp-cli step. Improve error reporting. (#2406)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v2.0.0] (2025-07-23)

+

Enhancements

+
  • [Playground CLI] Improve error reporting. (#2401)
  • [Playground CLI] Kebab-case yargs options declarations. (#2399)
+

Blueprints

+
  • Expose Blueprints v2 runner in Playground CLI. (#2394)
  • Extract Blueprint v1-specific parts of Playground CLI. (#2392)
  • [Playground CLI] Separate Blueprints v1 and Blueprints v2 code paths. (#2396)
+

Tools

+
  • [XDebug] Add a mock @php-wasm/xdebug-bridge package. (#2398)
+

Documentation

+
  • Adding Brazilian Portuguese translation for developer documentation. (#2391)
+

Website

+
  • [Remote] Use CORS proxy in embedded Playgrounds. (#2369)
+

Bug Fixes

+
  • CLI: Fix --login option and "landingPage" Blueprint property. (#2344)
+

Various

+
  • Add Japanese translations to steps and steps shorthands. (#2386)
  • Add OPCache support. (#2400)
  • [Node] Gracefully handle connection errors in the outbound network proxy. (#2370)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @fellyph @shimotmk @zaerl

+

[v1.2.3] (2025-07-21)

+

Enhancements

+
  • [Playground CLI] Consolidate auto mounting logic. (#2360)
  • [Playground CLI] Move mounting code to mount.ts. (#2362)
+

Blueprints

+
  • [CLI] Move Blueprints v2 to Playground CLI package. (#2364)
+

Public API

+

Blueprints

+
  • [Client] Accept wasm.wordpress.net as an official origin. (#2368)
+

Tools

+
  • Make PHP 8.3 the default version. (#2371)
+

PHP WebAssembly

+
  • Add --xdebug option in php-wasm CLI and wp-playground CLI. (#2346)
+

Documentation

+
  • Adding Playground CLI page and removing wp-now references. (#2337)
  • Adding pt-br About and Launch Page. (#2358)
  • Adding three new flags on the CLI README file. (#2325)
  • Adding video content in Japanese to resources page. (#2354)
  • Updating web instance page documentation. (#2365)
+

PHP WebAssembly

+
  • [PHP-wasm Node] Remove unused node creation code from createNodeFsMountHandler. (#2379)
  • [PHP-wasm] File mounting in NODEFS. (#2338)
  • [PHP] Improve error logging. (#2357)
  • [PHP] Isomorphic, reusable spawn handler. (#2359)
  • [PHP] Refresh the latest PHP versions before recompiling. (#2372)
  • [php-wasm/universal] Try require() before dynamic imprt in comlink-sync.ts. (#2363)
+

Website

+
  • Deploy public web app after WP major/beta update. (#2378)
+

Bug Fixes

+
  • Allow the WP update and changelog workflows to commit to trunk again. (#2377)
  • Fix failing WP update workflows. (#2376)
+

Various

+
  • Add Japanese translations to Resources References. (#2352)
  • Add new logo and open graph image. (#2350)
  • Adding Portuguese version for Code Standards, Code, and Documentation.. (#2343)
  • Adding new icons and og:Image for the Playground Web instance. (#2367)
  • Update Japanese translations to main directory. (#2375)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @fellyph @mho22 @shimotmk

+

[v1.2.2] (2025-07-14)

+

Enhancements

+
  • Add --internal-cookie-store option for Playground CLI. (#2323)
+

Tools

+

PHP WebAssembly

+
  • [ php-wasm ] Add xdebug shared extension to @php-wasm/node ASYNCIFY. (#2326)
+

Documentation

+
  • Add Japanese translations to Using Blueprints. (#2330)
  • Add Japanese translations to Blueprint data Format. (#2340)
  • Adding new videos about WordPress Playground. (#2348)
  • Translate How to contribute pages to Brazilian Portuguese. (#2329)
  • Updating Translation Guide. (#2342)
  • Updating slack channel from #meta-playground to #playground. (#2345)
+

PHP WebAssembly

+
  • Support multiple workers for NODEFS /wordpress mounts – Asyncify. (#2317)
  • [PHP] Support non-blocking read streams. (#2339)
+

Bug Fixes

+
  • Fix: Set an explicit path for docsaurus.config.js. (#2335)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @fellyph @mho22 @rollybueno @shimotmk

+

[v1.0.38] (2025-05-22)

+

Various

+
  • Revert "[ php-wasm ] add Intl support (#150)". (#154)
  • Use the new SQLite driver. (#144)
  • [ php-wasm ] Add intl support. (#155)
  • [ php-wasm ] Intl : Improve integration. (#159)
  • [ php-wasm ] add Intl support. (#150)
  • [CLI] Enable users to automatically mount their current working directory into Playground. (#39)
  • [PHP-wasm Node] Fix Identifier '\_\_filename' has already been declared. (#143)
  • [Website] Support data URLs in blueprint-url query parameter. (#158)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @JanJakes @mho22

+

[v1.0.37] (2025-05-15)

+

Various

+
  • Define DB_NAME constant when it's missing. (#140)
  • Support limiting files added to self-hosted packages. (#149)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @JanJakes

+

[v1.0.36] (2025-05-08)

+

Various

+
  • Document mounting OPFS storage _after_ the boot. (#141)
  • QUERY_STRING should default to empty string. (#139)
  • Remove null steps that are added. (#135)
  • [CLI] Fix package entry point path. (#136)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ashfame @bgrgicak @sejas

+

[v1.0.35] (2025-05-08)

+

[v1.0.34] (2025-05-08)

+

Various

+
  • Document mounting OPFS storage _after_ the boot. (#141)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ashfame

+

[v1.0.33] (2025-05-08)

+

Various

+
  • Add MySQL network calls to Asyncify tests. (#108)
  • Format the plugin-proxy file. (#127)
  • Plugin proxy: Allow all repos from WordPress, Automattic and Woocommerce organizations. (#128)
  • QUERY_STRING should default to empty string. (#139)
  • Remove null steps that are added. (#135)
  • Remove unused dependencies. (#116)
  • [CLI] Fix package entry point path. (#136)
  • [PHP-wasm Node] Add support for resolving wasm paths in Windows. (#129)
  • [PHP-wasm Node] Dynamically mount symlinks. (#125)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @bgrgicak @ingeniumed @sejas

+

[v1.0.32] (2025-05-06)

+

Various

+
  • Add MySQL network calls to Asyncify tests. (#108)
  • Format the plugin-proxy file. (#127)
  • Plugin proxy: Allow all repos from WordPress, Automattic and Woocommerce organizations. (#128)
  • Remove unused dependencies. (#116)
  • [PHP-wasm Node] Add support for resolving wasm paths in Windows. (#129)
  • [PHP-wasm Node] Dynamically mount symlinks. (#125)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @ingeniumed

+

[v1.0.31] (2025-04-29)

+

Bug Fixes

+
  • Fix changelog version order. (#100)
  • Fix openssl library paths in php-wasm build. (#102)
+

Various

+
  • Add private fork links to the changelog. (#98)
  • Allow public access to A8C Playground instance. (#119)
  • Make php-wasm and Playground CLI's fast to run with Node.js. (#107)
  • Playground CLI: Add skipSqliteSetup flag for MySQL support. (#97)
  • Prep the WP update workflows to be re-enabled. (#110)
  • Support building for additional remote origins. (#122)
  • Tweak Blueprint test string to match WP 6.8. (#117)
  • Update Nx, Vite, and ESLint dependencies. (#35)
  • Upgrade octokit to 3.1.2 to fix vulnerability. (#106)
  • Use Node as the Vitest environment. (#96)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton @ivan-ottinger @wojtekn

+

[v1.0.30] (2025-04-07)

+

Bug Fixes

+
  • Fix changelog workflow and backfill changelog entries from recent versions. (#83)
+

Various

+
  • Consider unexpected undefined symbols to be errors. (#86)
  • Ignore ca-bundle created by @php-wasm/cli. (#88)
  • Support Node.js args to built-script executor. (#92)
  • Support quickly testing php-wasm CLI changes with bun. (#85)
  • Update Emscripten version to 4.0.5. (#93)
  • Update Playground Node version to 20.9.0. (#89)
  • Upgrade express to 4.21.2. (#90)
  • [PHP-wasm] Use statfs from NODEFS in the Node version. (#94)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton @wojtekn

+

[v1.0.29] (2025-03-11)

+

Various

+
  • Blueprint bundles. (#75)
  • CORS Proxy: fetch() with credentials: "include". (#66)
  • Exif support for PHP-wasm. (#72)
  • Remove Data Liberation PHP modules. (#79)
  • Stop suppressing 64bit integer PHP warnings as 64bit longs are now suppoted. (#76)
  • [Blueprints] setSiteLanguage fetch translation package URL from WP.org. (#81)
  • [Website] Drop the static &lt;link rel="manifest"&gt; tag and generate one in JavaScript instead. (#78)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v1.0.28] (2025-03-05)

+

Various

+
  • 64bit integer support, drop PHP 7.0 and 7.1 support. (#74)
  • FS Journal: Handle renaming OPFS files via delete + create (instead of file.move()). (#64)
  • GitHub: Cache node_modules per arch to avoid nx error. (#70)
  • Merge from public Playground repo. (#59)
  • OPFS site creation: Use the same "is this directory a site?" check as the site list. (#65)
  • Stop adding source maps to offline assets list. (#68)
  • Support mobile access to private Playground instance. (#73)
  • TLS-&gt;HTTP Proxy: Support outbound request body. (#61)
  • TLS: Only use the server_name extension during server hello. (#62)
  • Website: Blueprints as PWAs with a dynamic manifest.json file. (#67)
  • Website: Preserve the mode query arg when opening OPFS site. (#63)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton

+

[v1.0.27] (2025-02-25)

+

Bug Fixes

+
  • Fix php-wasm build error for WITH_SOURCEMAPS=yes. (#44)
  • Fix php-wasm source map base. (#46)
  • Fix private website deployment workflow. (#57)
  • Fix up private website deployment workflow after initial commit. (#29)
+

Various

+
  • Add WITH_DEBUG option for building php-wasm with DWARF debug info. (#47)
  • Add janjakes to GitHub workflows actors. (#33)
  • Add a dedicated workflow for deploying private playground. (#28)
  • Add remote data blocks to the allowlist. (#30)
  • Allow ashfame to run self-hosted package release workflow. (#22)
  • Allow deploying custom supported domains for CORS proxy. (#54)
  • CORS proxy: Make allowed origins configurable. (#43)
  • Make further fixes for unreliable end-to-end tests. (#9)
  • Move DNS polyfills to a PHP extension and add missing constants. (#27)
  • New playground CLI function interface. (#40)
  • Replace some hardcoded refs to playground.wordpress.net web app. (#42)
  • Restore safety condition for CORS proxy deployment workflow. (#55)
  • Revert "Add the launch browser flag to CLI". (#24)
  • Revert "Support file URL resources in command line environment". (#41)
  • Schedule private deployment and use private environment. (#37)
  • Stop assuming CLI stdout is a TTY write stream. (#51)
  • Store WordPress site cookies in the browser instead of a custom Cookie Store. (#20)
  • Support file URL resources in command line environment. (#34)
  • Switch to relative paths in app manifest. (#36)
  • Switch to runner with more CPU and RAM. (#45)
  • Treat zip files as zip files regardless of file name. (#58)
  • Try more Playwright workers since we have a runner with more resources. (#48)
  • Update private Playground instance to use dedicated CORS proxy. (#56)
  • Use rsync instead of scp in website deploy workflow. (#26)
  • Use specified SQLite version also for PHP &lt; 7.4. (#32)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @ingeniumed @JanJakes @zaerl

+

[v1.0.26] (2025-02-04)

+

Bug Fixes

+
  • Fix workflow for deploying self-hosted packages. (#6)
+

Various

+
  • Add the launch browser flag to CLI. (#18)
  • [Fix] Zip spec compliance for exported backup file. (#7)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ashfame @brandonpayton @zaerl

+

[v1.0.25] (2025-02-04)

+

Various

+
  • Add missing node external to Playground CLI. (#19)
  • Add self-hosted package publishing. (#5)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v1.0.24] (2025-02-03)

+

[v1.0.23] (2025-01-27)

+

[v1.0.22] (2025-01-20)

+

[v1.0.21] (2025-01-13)

+

Enhancements

+
  • [Data Liberation] Add EPub to Blocks converter. (#2097)
  • [Data Liberation] Block markup consumers and producers. (#2121)
  • [Data Liberation] Filesystem entity reader. (#2125)
  • [Data Liberation] Recognize self-closing blocks in WP_Block_Markup_Processor. (#2120)
  • [Data Liberation] Refactor Entity Readers class diagram. (#2096)
+

PHP WebAssembly

+
  • PHP 8.4 support. (#2038)
  • Rewrote fileToUint8Array function to be also NodeJS/Deno compatible. (#2117)
  • [PHP] Restore /internal files and Filesystem mounts after hotswapPhpRuntime is called. (#2119)
+

Bug Fixes

+
  • [CORS Proxy] Support chunked encoding when running in Apache/Nginx/etc. (#2114)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @mbuella

+

[v1.0.20] (2025-01-06)

+

Website

+
  • Avoid login issue in deployment end-to-end tests. (#2065)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v1.0.19] (2024-12-30)

+

[v1.0.18] (2024-12-23)

+

Enhancements

+
  • [Data Liberation] Add HTML to Blocks converter. (#2095)
  • [Data Liberation] Add Markdown parsing libraries. (#2092)
  • [Data Liberation] Build markdown importer as phar. (#2094)
  • [Data Liberation] Move Markdown importer to a separate package. (#2093)
+

Blueprints

+
  • Prevent WSOD when autologin is enabled and a plugin logs a notice. (#2079)
+

Tools

+

GitHub integration

+
  • [Website] GitHub export modal: Correctly compute the root path when exporting the entire site. (#2103)
+

Website

+
  • Enable separate source maps for all package builds. (#2088)
+

Bug Fixes

+
  • Fix README.md typos. (#2091)
+

Various

+
  • Add small comment about lazy init of WXR reader. (#2102)
  • [Blueprints] Prevent plugin activation error if plugin redirects during activation or produces an output. (#2066)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @zaerl

+

[v1.0.17] (2024-12-17)

+

Tools

+

Blueprints Builder

+
  • Use transparent CORS proxy in Blueprint Builder. (#2089)
+

PHP WebAssembly

+
  • Build @php-wasm packages as dual ESM + CJS. (#2087)
+

###

+
  • Add ESLint rule to avoid unintentional dependency on @wp-playgrounds/wordpress-builds. (#2048)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton

+

[v1.0.16] (2024-12-16)

+

Enhancements

+
  • Allow Authorization header pass-through with X-Cors-Proxy-Allowed-Request-Headers. (#2007)
  • [Cors Proxy] Support Transfer-Encoding: Chunked. (#2077)
  • [Website] Enable CORS proxy for all fetches. (#2076)
+

Tools

+

Blueprints

+
  • [Blueprints] Preserve the first char of all filenames sourced from GitDirectoryReference. (#2070)
+

Import/Export

+
  • [Blueprints] Support Data Liberation importer in the importWxr step. (#2058)
+

PHP WebAssembly

+

Website

+
  • [Webiste] Switch the CORS Proxy URL to wordpress-playground-cors-proxy.net. (#2074)
+

Website

+
  • Bugfix: Delist data-liberation-core.phar from the preloaded offline mode assets. (#2072)
  • Don't show the error reporting modal on the initial load. (#2068)
  • Prevent the initial flash of "You have no Playgrounds" message. (#2069)
  • Remove old PR preview HTML files and add redirects to new preview modals. (#2081)
+

Internal

+
  • [Meta] Remove GitHub Board Automation workflow. (#2073)
+

Bug Fixes

+
  • Temporary: Skip more CI-only deployment test failures. (#2071)
+

Various

+
  • Ensure that Site Editor templates are associated with the correct taxonomy. (#1997)
  • PR Preview: Document and simplify targetParams. (#2052)
  • Revert "Remove old PR preview HTML files and add redirects to new preview modals". (#2082)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ajotka @akirk @brandonpayton @maxschmeling

+

[v1.0.15] (2024-12-09)

+

Enhancements

+
  • Shorten and simplify path to CORS proxy. (#2063)
  • Support users choosing how to handle URLs for sites that do not exist. (#2059)
+

Tools

+

GitHub integration

+
  • Add zaerl to GitHub workflows actors. (#2041)
+

PHP WebAssembly

+
  • [Networking] Decrypt TLS 1.2 alert messages. (#2060)
+

Website

+
  • Fix CORS proxy deploy workflow. (#2049)
  • Nightly build bugfix – ship the actual nightly build, not the latest release. (#2056)
  • Remove duplicate "Saved Playgrounds" label. (#2044)
+

Various

+
  • Add deploy workflow for standalone CORS proxy. (#2022)
  • Restore CORS support to CORS proxy. (#2023)
  • [Data Liberation] "Fetch from a different URL" button for failed media downloads, Interactivity API support. (#2040)
  • [Data Liberation] Sync WP_HTML API with WordPress 6.7.1 (and add a new test). (#2062)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @zaerl

+

[v1.0.14] (2024-12-02)

+

Blueprints

+
  • Resolve the latest WordPress version from the API instead of assuming it's the same as the last minified build. (#2027)
+

Tools

+

Blueprints Builder

+
  • Add installPlugin support for single plugin files. (#2033)
+

PHP WebAssembly

+
  • Networking: Preserve the content-type header when fetch()-ing. (#2028)
+

Website

+
  • [Web] Re-enable wp-cron. (#2039)
+

Various

+
  • [Data Liberation] WP_Stream_Importer: User-driven incremental import. (#2013)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton

+

[v1.0.13] (2024-11-25)

+

Enhancements

+
  • E2E: Disable a flaky deployment test. (#2016)
  • [Data Liberation] Add WXR import CLI script. (#2012)
  • [Data Liberation] Re-entrant WP_Stream_Importer. (#2004)
  • [Data Liberation] wp-admin importer page. (#2003)
+

Blueprints

+
  • SetSiteLanguage step – download the latest RC translations for Nightly and Beta builds of WordPress. (#1987)
  • Use the major WordPress version to download RC/beta translations. (#2017)
+

Tools

+

Pull Request Previewer

+
  • Fix path of PR preview URL in production. (#2014)
  • Support submitting PR preview modal with ENTER key. (#2015)
+

Website

+
  • Move WordPress & Gutenberg PR Preview to Playground website. (#1938)
  • Restore basic element styles for modal dialog content. (#2021)
+

Bug Fixes

+
  • Fix test.md link. (#2005)
+

Various

+
  • [Data Liberation] WP_Stream_Importer with support for WXR and Markdown files. (#1982)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ajotka @bgrgicak @brandonpayton @StevenDufresne @zaerl

+

[v1.0.12] (2024-11-18)

+

Website

+
  • [Service Worker] Support redirects to relative URLs in Safari. (#1978)
+

Blueprints

+
  • [Query API] Use the exact redirect URL provided in the ?url= query param. (#1945)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v1.0.11] (2024-11-11)

+

[v1.0.10] (2024-11-08)

+

Enhancements

+
  • [CLI] Set debug constants during boot. (#1983)
+

Bug Fixes

+
  • [CLI] Restore the "login" argument handler. (#1985)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak

+

[v1.0.9] (2024-11-04)

+

Enhancements

+
  • [Data Liberation] Fork humanmade/WordPress-Importer. (#1968)
  • [Data Liberation] Merge both XML processors into a single WP_XML_Processor. (#1960)
  • [Data liberation] Add blueprints-library as a submodule. (#1967)
+

Tools

+

Import/Export

+
  • [Data Liberation] WP_WXR_Reader. (#1972)
+

Documentation

+
  • Rewrite clone examples to use HTTPS instead of SSH. (#1963)
+

Website

+
  • Consistent width of settings, logs, and blueprint gallery sidebars. (#1964)
+

Bug Fixes

+
  • Fix: Import & Export from Github causes reloading the playground even before accept this step. (#1908)
  • [WordPress build] Only build the latest patch version of WordPress. (#1955)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ajotka @bgrgicak

+

[v1.0.8] (2024-10-30)

+

Enhancements

+
  • [Data liberation] wp_rewrite_urls(). (#1893)
+

PHP WebAssembly

+
  • [PHP.wasm for Node] Fix php.js import path in the published npm package. (#1958)
+

Website

+
  • Restore .d.ts files missing from the published @wp-playground/remote npm package. (#1949)
+

Various

+
  • [Data Liberation] Add XML API, Stream API, WXR URL Rewriter API. (#1952)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @psrpinto

+

[v1.0.7] (2024-10-28)

+

[v1.0.6] (2024-10-28)

+

Website

+
  • Query API: Preserve multiple ?plugin= query params. (#1947)
  • [Remote] Enable releasing @wp-playground/remote by making it public. (#1948)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v1.0.5] (2024-10-25)

+

Enhancements

+
  • [CORS Proxy] Rate-limits IPv6 requests based on /64 subnets, not specific addresses. (#1923)
+

Blueprints

+
  • Reload after autologin to set login cookies during boot. (#1914)
  • Skip empty lines in the runSql step. (#1939)
+

Documentation

+
  • Clarified wp beta to also include rc version. (#1936)
+

PHP WebAssembly

+
  • Enable CURL in Playground Web. (#1935)
  • PHP: Implement TLS 1.2 to decrypt https:// and ssl:// traffic and translate it into fetch(). (#1926)
+

Website

+
  • Hide Settings menu after clicking "Restore from .zip. (#1904)
  • Publish @wp-playground/remote (types only). (#1924)
+

Bug Fixes

+
  • CORS Proxy: Index update_at column because it is used for lookup. (#1931)
  • CORS Proxy: Reject targeting self. (#1932)
  • Docs: Fix typo. (#1934)
  • Explicitly request no-cache to discourage WP Cloud from edge caching CORS proxy results. (#1930)
  • Remove test code added in #1914. (#1928)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ajotka @bgrgicak @bph @brandonpayton @ockham @psrpinto

+

[v1.0.4] (2024-10-21)

+

Enhancements

+
  • Support CORS proxy rate-limiting. (#1879)
+

Blueprints

+
  • Allow multisites to load wp-admin pages with the landingPage attribute. (#1913)
+

Tools

+

GitHub integration

+
  • Blueprints: Use ? instead of / to CORS Proxy URLs. (#1899)
+

Import/Export

+
  • Kickoff Data Liberation: Let's Build WordPress-first Data Migration Tools. (#1888)
+

Experiments

+

File Synchronization

+
  • [Remote] Preserve PHP constants when saving a temporary site. (#1911)
+

Website

+
  • Do not display "You have no Playgrounds" message before loading the site. (#1912)
  • Fix build error that only appeared during deployment. (#1896)
  • Fix use of secrets on WP Cloud site. (#1909)
  • Maintain Query API parameters on temporary Playground settings update. (#1910)
  • Stop adding all CORS proxy files to website build. (#1895)
  • Stop responding with default MIME type. (#1897)
  • Stop short-circuiting web host PHP execution. (#1898)
  • Fix progress reporting during Playground load. (#1915)
  • Include CORS proxy with website builds. (#1880)
  • [Blueprints] Stop escaping landingPage URLs when loading WP Admin. (#1891)
+

Bug Fixes

+

Boot Flow

+
  • Prefer pretty permalinks like WP install does. (#1832)
+

Various

+
  • Improve self-host documentation. (#1884)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ashfame @bgrgicak @brandonpayton

+

[v1.0.3] (2024-10-14)

+

Enhancements

+
  • Website: Remove unused React components. (#1887)
+

Tools

+

Blueprints Builder

+
  • Blueprints sidebar section for single-click Playground presets. (#1759)
+

Website

+
  • Replace 100vh with 100dvh to fix an "unscrollable" state on mobile devices. (#1883)
  • Use modal for Site settings form on mobile – mobile Safari l…. (#1885)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v1.0.2] (2024-10-09)

+

PHP WebAssembly

+
  • PHP.wasm: Load correct php.wasm paths in the built Node.js packages. (#1877)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v1.0.1] (2024-10-09)

+

[v1.0.0] (2024-10-09)

+

Blueprints

+
  • Directory Resources. (#1793)
  • Login step – handle passwordless autologin via a PHP mu-plugin. (#1856)
+

Tools

+

Blueprints

+
  • GitDirectoryResource. (#1858)
  • GitDirectoryResource: Accept "/", "", "." as root paths. (#1860)
+

GitHub integration

+
  • Native git support: LsRefs(), sparseCheckout(), GitPathControl. (#1764)
+

Website

+
  • Add core-pr and gutenberg-pr Query API parameters. (#1761)
+

PHP WebAssembly

+
  • Refreshless website deployments – load remote.html using the network-first strategy. (#1849)
  • JSPI: Pass all unit tests, remove stale PHP builds. (#1876)
  • [Remote] Remove the "light" PHP.wasm bundle and only ship the "kitchen-sink" build. (#1861)
+

Website

+
  • Restore the single-click "Edit Settings" flow. (#1854)
  • Restrict CORS proxy to requests from Playground website origin. (#1865)
+

Documentation

+
  • Add /release redirect to WP beta/RC blueprint. (#1866)
+

Internal

+
  • Make isomorphic-git submodule use https, not ssh. (#1863)
+

Bug Fixes

+
  • [CLI] Fix isWordPressInstalled() in CLI by inlining the auto_login.php in index.ts instead of using import ?raw. (#1869)
+

Various

+
  • Add documentation around the GPL license and implications for contribution. (#1776)
  • Allow installing Plugins/Themes into an arbitrary folder. (#1803)
  • Improve documentation. (#1862)
  • [Website] Fix "undefined" as className. (#1870)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @ajotka @bgrgicak @brandonpayton @dd32 @n8finch

+

[v0.9.46] (2024-10-07)

+

Enhancements

+
  • Webapp upgrade protocol: Disable HTTP caching and reload other browser tabs to prevent fatal errors after new deployments. (#1822)
+

Documentation

+
  • Docs: Disable localeDropdown until more pages are translated. (#1824)
  • Docs: Review playground documentation translations page. (#1826)
  • Docs: Playground PR previews through GitHub actions. (#1825)
+

Website

+
  • Use site slug as a stable scope. (#1839)
  • Close Playground Manager by default. (#1831)
  • Fix go-to-site menu items to reveal site view. (#1833)
+

Various

+
  • Add Install instructions to the Playwright README. (#1837)
  • Resolve end-to-end failures. (#1844)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @akirk @brandonpayton @juanmaguitar

+

[v0.9.45] (2024-09-30)

+

Blueprints

+
  • Translate GitHub.com file URLs into CORS-accessible raw.githubusercontent.com. (#1810)
+

Tools

+
  • [UX] Stored Playgrounds (no more data loss), multiple Playgrounds, UI WebApp Redesign. (#1731)
+

Documentation

+
  • Docs: Translation i18n messages - JSON files. (#1807)
+

Website

+
  • Prevent creation of two temporary sites. (#1817)
  • Stop address bar from adding trailing slash to query params. (#1820)
+

Bug Fixes

+
  • Fix broken Playwright tests. (#1819)
+

Various

+
  • Add Playwright tests for UI redesign changes. (#1769)
  • Docs: Contributions to translations. (#1808)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @juanmaguitar

+

[v0.9.44] (2024-09-24)

+

Bug Fixes

+
  • Docs: Fix links intro fr. (#1795)
+

Various

+
  • Add missing functions required to succesfully connect with MySQL DB. (#1752)
+

Contributors

+

The following contributors merged PRs in this release:

+

@jeroenpf @juanmaguitar

+

[v0.9.43] (2024-09-23)

+

Documentation

+
  • Docs: Better paths for links. (#1765)
  • Docs: I18n setup. (#1766)
  • Docs: Remove the outdated "data rependencies" page. (#1785)
+

Website

+
  • Fix troubleshoot-and-debug link. (#1782)
+

Various

+
  • Update link for contributor day. (#1775)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @juanmaguitar @n8finch

+

[v0.9.42] (2024-09-17)

+

PHP WebAssembly

+
  • FS: Use the correct rm/rmdir method when moving files between mounts. (#1770)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.41] (2024-09-16)

+

[v0.9.40] (2024-09-16)

+

Enhancements

+
  • Extend allowable resources available via WordPress/WordPress. (#1721)
+

Tools

+
  • Update actions/upload-artifact version to 4. (#1748)
+

Documentation

+
  • Docs/Blueprints resources: Grammar and typo fixes. (#1741)
+

PHP WebAssembly

+
  • @php-wasm/universal : Add Phar support in php-wasm. (#1716)
+

Website

+
  • Add the components package with PathMappingControl. (#1608)
+

Bug Fixes

+
  • Fix CLI --skipWordPressSetup option. (#1760)
+

Reliability

+
  • Improve Playground CLI logging and fix quiet mode. (#1751)
+

Various

+
  • Docs/Guides: Guides introductions and some minor adjustments. (#1754)
  • Docs/Guides: Normalized and fixed guides links. (#1756)
  • Docs/Guides: Providing content for your demo. (#1747)
  • Docs/Guides: WordPress Playground for plugin developers. (#1750)
  • Docs/Guides: WordPress Playground for theme developers. (#1732)
  • Docs: Links redirections. (#1758)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22 @peterwilsoncc

+

[v0.9.39] (2024-09-09)

+

Bug Fixes

+
  • Use the correct imports in the generated .d.ts files. (#1742)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.38] (2024-09-09)

+

Internal

+
  • Fix changelog updates for latest doc structure. (#1734)
+

Various

+
  • Add support for handling symlinks in the request handler. (#1724)
  • Docs: Playground block in launch section. (#1722)
  • documentation - Blueprints - Resources: Highlight installPlugin and installTheme steps and most common resouces for them. (#1733)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @jeroenpf @juanmaguitar

+

[v0.9.37] (2024-09-05)

+

PHP WebAssembly

+
  • Add more asyncify-listed PHP functions to fix Studio crash. (#1729)
+

Website

+
  • Add initial site info view. (#1701)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.36] (2024-09-03)

+

Bug Fixes

+
  • Fix logger test for inconsistent short month. (#1727)
+

Reliability

+
  • Avoid errors due to log message formatting. (#1726)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.35] (2024-08-29)

+

Enhancements

+
  • Allow specifying a WordPres/WordPress branch and pulling from GitHub. (#1705)
+

PHP WebAssembly

+
  • Fix: Exit http server on php exit. (#1714)
+

Various

+
  • Enable networking for WordPress and Gutenberg PR viewers. (#1715)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ironprogrammer @kozer @peterwilsoncc

+

[v0.9.34] (2024-08-28)

+

PHP WebAssembly

+
  • @php-wasm/node: Publish index.d.ts. (#1713)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.33] (2024-08-28)

+

Internal

+
  • @php-wasm/util: Publish TypeScript types. (#1711)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.32] (2024-08-26)

+

Website

+
  • Make the site switcher work again. (#1698)
  • WebApp Redesign: Interact with sites list via redux. (#1679)
  • deployment: Fix builder redirect. (#1696)
+

Bug Fixes

+
  • Docs: Fix typo and replace en dash with hyphen for consistency. (#1702)
  • Fix broken documentation links. (#1694)
  • Remove Playground branding from site list. (#1700)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @eliot-akira @mirka

+

[v0.9.31] (2024-08-20)

+

Enhancements

+

Boot Flow

+
  • A boot() method to explicitly initialize the PHP worker. (#1669)
+

Website

+
  • Fix builder redirect. (#1693)
+

Internal

+
  • Avoid GH board automation permissions error. (#1691)
  • Refresh sqlite-database-integration from develop branch. (#1692)
+

Bug Fixes

+

Boot Flow

+
  • Fix sqlite-database-integration rename fatal. (#1695)
+

Documentation

+
  • Docs: Fix links to proper pages. (#1690)
+

Various

+
  • Documentation structure overhaul. (#1602)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @juanmaguitar

+

[v0.9.30] (2024-08-19)

+

Website

+
  • Ask users to report errors if Playground load fails. (#1686)
+

Bug Fixes

+
  • Avoid Blueprint schema formatting changes by build. (#1685)
+

Various

+
  • [Website] Improves the messaging around exporting a zip if needed, when connecting to GitHub. (#1689)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton @jonathanbossenger

+

[v0.9.29] (2024-08-12)

+

Tools

+
  • Add max-len rule. (#1613)
+

Experiments

+

GitHub integration

+
  • Add site manager view and sidebar. (#1661)
  • Add sites from the site manager. (#1680)
+

PHP WebAssembly

+
  • Offline mode end-to-end tests. (#1648)
+

Website

+
  • Add nice redirects for the new documentation site. (#1681)
  • Fix site manager button styles. (#1676)
+

Bug Fixes

+
  • Revert "Offline mode end-to-end tests". (#1673)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v0.9.28] (2024-08-05)

+

Blueprints

+
  • Add support for loading wpCli without running blueprint steps. (#1629)
+

Documentation

+
  • Blueprints: Add resetData step to documentation. (#1658)
  • Docs: Redirect from /docs to https://wordpress.github.io/wordpress-playground. (#1671)
+

Website

+
  • Suppress unavoidable Deprecated notices - Networking. (#1660)
  • UI: Explain the Logs modal. (#1666)
+

Blueprints

+
  • Precompile Ajv Blueprint validator to avoid CSP issues. (#1649)
+

Internal

+
  • Reinstantiate Changelog generation in GitHub CI. (#1657)
+

Various

+
  • Rollback artifact creation to enable downloading a pre-built package …. (#1624)
  • Update WordPress packages. (#1672)
  • Update ws package version to fix DOS vulnerability. (#1635)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @PiotrPress

+

[v0.9.27] (2024-07-29)

+

Enhancements

+
  • Support offline mode after the first Playground page load. (#1643)
+

Devrel

+
  • Remove puzzle app package. (#1642)
+

PHP WebAssembly

+
  • Cache Playground assets to enable offline support. (#1535)
  • Rotate PHP runtime after runtime crash. (#1628)
  • Throw error when PHP run() receives no code to run. (#1646)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton

+

[v0.9.26] (2024-07-22)

+

Blueprints

+
  • Add missing blueprints library dep. (#1640)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.25] (2024-07-22)

+

Tools

+
  • Make sure NPM packages declare dependencies. (#1639)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.24] (2024-07-22)

+

Bug Fixes

+
  • Fix plugin-proxy response codes. (#1636)
  • Stop publishing @wp-playground/wordpress-builds package. (#1637)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton

+

[v0.9.23] (2024-07-22)

+

PHP WebAssembly

+
  • Route requests more like a normal web server. (#1539)
+

Website

+
  • Remove old, unused website deployment workflow. (#1633)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.22] (2024-07-19)

+

Bug Fixes

+
  • Remove WP 6.2 support after WP 6.6 release. (#1632)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.9.21] (2024-07-19)

+

Website

+
  • Fix manifest.json URLs. (#1615)
+

Internal

+
  • Fix joinPaths root edge case. (#1620)
+

Various

+
  • Disable PHP 7.0 and 7.1 version switcher end-to-end tests. (#1626)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton

+

[v0.9.20] (2024-07-16)

+

Enhancements

+

Boot Flow

+
  • Backfill the assets removed from minified WordPress bundles. (#1604)
  • Register service worker before spawning the worker thread. (#1606)
+

Website

+
  • Disable website features that don't work while offline. (#1607)
  • Generate a list of assets to cache for offline support. (#1573)
+

Internal

+
  • Build: Ship the default TypeScript .d.ts declaration files, not rollups. (#1593)
+

Bug Fixes

+

Boot Flow

+
  • Fix recursive calls to backfillStaticFilesRemovedFromMinifiedBuild. (#1614)
+

Various

+
  • Add/allow import site gutenberg pr. (#1610)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @smithjw1

+

[v0.9.19] (2024-07-15)

+

Breaking Changes

+
  • Set web worker startup options with messages instead of query strings. (#1574)
+

Blueprints

+
  • Add an Import Theme Starter Content step. (#1521)
  • Add setSiteLanguage step to change the language. (#1538)
  • Mark shorthand properties as stable, not deprecated. (#1594)
+

Documentation

+
  • Add Blueprint 101 to Documentation. (#1556)
+

PHP WebAssembly

+

Website

+
  • Download all WordPress assets on boot. (#1532)
+

Website

+ +

Various

+
  • Revert "Set web worker startup options with messages instead of query strings". (#1605)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @bph @dd32

+

[v0.9.18] (2024-07-09)

+

Website

+
  • Remove the unused isSupportedWordPressVersion export. (#1592)
+

Internal

+
  • Build: Polyfill \_\_dirname in php-wam/node ESM via banner option. (#1591)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.16] (2024-07-09)

+

Internal

+
  • Build: Source external deps from package.json. (#1590)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.15] (2024-07-09)

+

Internal

+
  • Build: Use regular expressions to mark packages as external. (#1589)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.14] (2024-07-09)

+

Devrel

+
  • Remove Puzzle app from the Playground website. (#1588)
+

Internal

+
  • Vite build: Mark all imported modules as external to avoid bundling them with released packages. (#1586)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v0.9.13] (2024-07-08)

+

PHP WebAssembly

+
  • php-wasm/node: Ship as ESM and CJS. (#1585)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.12] (2024-07-08)

+

[v0.9.11] (2024-07-08)

+

PHP WebAssembly

+
  • Build: Treat all dependencies of php-wasm/node as external. (#1584)
+

Various

+
  • Autopublish npm packages every week. (#1542)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.10] (2024-07-08)

+

Internal

+
  • Revert "Use NPM for publishing packages instead of Lerna ". (#1582)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.9] (2024-07-08)

+

Internal

+
  • Use NPM for publishing packages instead of Lerna. (#1581)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.9.4] (2024-07-03)

+

Documentation

+
  • Update the Blueprint data format doc. (#1510)
+

Contributors

+

The following contributors merged PRs in this release:

+

@ndiego

+

[v0.9.3] (2024-07-03)

+

Tools

+

Blueprints

+
  • Importing regression fix – support old exported Playground ZIPs. (#1569)
+

Documentation

+
  • Add GitHub development instructions. (#1551)
+

Internal

+
  • Meta: GitHub Boards Automation. (#1549)
  • Meta: GitHub-sourced Mindmap. (#1559)
+

###

+
  • Add cache version number. (#1541)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak

+

[v0.9.1] (2024-06-26)

+

PHP WebAssembly

+
  • Networking access: Fix wp_http_supports() to work without the kitchen-sink extension bundle. (#1504)
  • Networking: Remove CORS workarounds for WordPress.org API. (#1511)
  • Backfill remote asset listing when needed. (#1531)
+

Website

+
  • Remove "small window mode". (#1540)
  • Detect actual, loaded WP version. (#1503)
+

Various

+
  • Remove deprecation note from shorthand steps. (#1507)
  • Remove trailing semicolon from example URL for loading playground with network access. (#1520)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bph @brandonpayton @dd32 @oskosk

+

[v0.7.20] (2024-05-21)

+

Breaking Changes

+
  • [Breaking] Refactor PHP.ini management, remove php.setPhpIniPath() and php.setPhpIniEntry(). (#1423)
+

Enhancements

+
  • CLI: Distinguish between mount and mountBeforeInstall options. (#1410)
  • CLI: Support fetching WordPress zips from custom URLs. (#1415)
  • Introduce a new @wp-playground/common package to avoid circular depencies. (#1387)
  • Website: Ship the SQLite database integration plugin. (#1418)
+

Boot Flow

+
  • Playground CLI: Don't create /wordpress/wp-config.php on boot. (#1407)
+

Blueprints

+
  • Define constants in auto_prepend_file, silence warnings related to redefining those constants. (#1400)
  • Detect silent failures when activating plugins and theme. (#1436)
  • Re-activate single-file plugins when enabling a multisite. (#1435)
  • Throw an error when activating a theme or plugin that doesn't exist. (#1391)
  • Write sunrise.php to /internal in enableMultisite step. (#1401)
+

Tools

+
  • Add VSCode branch protection. (#1408)
  • Show error log if Playground fails to start. (#1336)
+

Blueprints

+
  • Unzip: Only delete a temporary zip file after unzipping, do not delete the original zip. (#1412)
+

GitHub integration

+
  • GitHub export: Create new commits in your fork when writing to the upstream repo isn't allowed. (#1392)
+

Import/Export

+
  • Support wp_crop_image in import wxr. (#1357)
+

Devrel

+ +

Documentation

+
  • Docs: Use step function names instead of TypeScript type names. (#1373)
  • Updated the GitHub issue link to open in a new tab. (#1353)
  • Use step id name. (#1377)
+

Experiments

+
  • Explore: Setup SQLite database integration without creating wp-content/db.php. (#1382)
+

PHP WebAssembly

+
  • Add shareable extension-to-MIME-type mapping. (#1355)
  • Document php ini functions. (#1430)
  • JSPI: Enable the origin trial on Chrome. (#1346)
  • PHP: Add libjpeg and libwebp support. (#1393)
  • PHP: Always set the auto_prepend_file php.ini entry, even when the auto_prepend_file.php file exists. (#1388)
  • PHP: Move internal shared directories to /internal/shared. (#1386)
  • PHP: Remove mentions of a custom PHP extension. (#1422)
  • PHP: Remove the MODE_EVAL_CODE execution mode. (#1433)
  • PHP: Support php.mv() between devices via recursive copy. (#1411)
  • PHP: Use /internal/shared/php.ini by default. (#1419)
  • PHP: Use auto_prepend_file to preload mu-plugins (instead of creating them in wp-content/mu-plugins). (#1366)
+

Website

+
  • Improve log modal styles, a11y, error message wording. (#1369)
  • Move puzzle app to a Playground package. (#1385)
  • Add secrets on-demand for more endpoints. (#1362)
  • Boot: Move WordPress zip extraction logic to a common unzipWordPress() utility. (#1427)
  • Derive MIME types for PHP served files from shared JSON. (#1360)
  • Fix constant names for GH export oauth. (#1378)
  • Playground Boot: Align the boot process between remote.html and CLI. (#1389)
  • Remote.html: Install WordPress if it isn't installed yet. (#1425)
  • Remote.html: Preload the SQLite database plugin, but only execute it if there's no custom db.php inside wp-content. (#1424)
  • Simplify website deployment workflows. (#1404)
  • Update rsync command to clean up more completely. (#1361)
+

Blueprints

+
  • Provide non-gzipped wp-cli.phar file with website build. (#1406)
  • Simplify runPhpWithZipFunctions() setup. (#1434)
+

Internal

+
  • Fix changelog automation. (#1413)
+

Bug Fixes

+
  • Add name to Puzzle package. (#1443)
  • Fixed images not loading on the page. (#1352)
  • Restore nightly wordpress build. (#1437)
+

Reliability

+
  • Disable console logging when running tests. (#1368)
+

###

+
  • Lint: Disable console warnings for paths where they're not useful. (#1421)
+

Various

+
  • Add links to kitchen sink (PHP extensions), networking. (#1363)
  • Reorganize and update documentation. (#1354)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @flexseth @ironnysh @josevarghese

+

[v0.7.15] (2024-04-30)

+

Website

+
  • Avoid edge-caching conditionally redirected resources. (#1351)
  • Fix deploy-time check for file with PHP-handled redirect. (#1350)
+

Contributors

+

The following contributors merged PRs in this release:

+

@brandonpayton

+

[v0.7.10] (2024-04-30)

+

PHP WebAssembly

+
  • PHP.wasm Node: Revert a part of #1289, do not import a .wasm file. (#1348)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.7.5] (2024-04-30)

+

Internal

+
  • Meta: Move the minified WordPress to the new @wp-playground/wordpress-builds package. (#1343)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.7.3] (2024-04-29)

+

PHP WebAssembly

+ +

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.7.2] (2024-04-29)

+

Breaking Changes

+
  • PHP: Remove setSapiName, setPhpIniEntry, setPhpIniPath methods from the remote PHP API client. (#1321)
  • Remove the wp-playground/node package. (#1323)
+

PHP WebAssembly

+
  • Breaking: Loopback Request Support. (#1287)
+

Tools

+
  • Centralize log storage. (#1315)
+

Documentation

+
  • Link to Installing Nx Globally in the README. (#1325)
+

PHP WebAssembly

+
  • Add PHPResponse.forHttpCode() shorthand. (#1322)
  • Asyncify: List ZEND_FETCH_OBJ_R_SPEC_CV_CV_HANDLER. (#1342)
  • Curl extension for the Node.js build of PHP.wasm. (#1273)
  • Explore curl support. (#1133)
  • PHP Process Manager. (#1301)
  • PHPProcessManager: Clear nextInstance when the concurrency limit is exhausted. (#1324)
  • Spawn handler: Wrap the program call with try/catch, exit gracefully on error. (#1320)
+

Website

+
  • Add initial workflow for deploying the website to WP Cloud. (#1293)
  • Eliminate 404s due to nested files-to-serve-via-php dir. (#1333)
  • Stop WP rewrite rules from matching files like wp-admin.css. (#1317)
  • Stop using PHP to serve most static files on WP Cloud. (#1331)
  • WP Cloud: Relay secrets for error logger. (#1337)
+

Documentation

+
  • Document WP Cloud website setup. (#1338)
+

Reliability

+
  • Add log methods, log handlers, and separate log collection. (#1264)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22

+

[v0.7.1] (2024-04-24)

+

[v0.7.0] (2024-04-24)

+

Breaking Changes

+

PHP WebAssembly

+
  • Breaking: Remove PHPBrowser. (#1302)
+

Enhancements

+
  • Bump TypeScript to 5.4.5. (#1299)
  • Semaphore: Add timeout option. (#1300)
+

Blueprints

+
  • Builder: Fix stuck loader bar. (#1284)
  • Remove setPhpIniEntry step. (#1288)
+

Tools

+

GitHub integration

+
  • GitHub: Don't delete all the files when exporting a theme. (#1308)
  • Urlencode branch name. (#1275)
+

Blueprints

+
  • Blueprints builder: Support ?blueprint-url. (#1309)
+

Documentation

+
  • Use new learning resources in Playground documentation. (#1276)
+

PHP WebAssembly

+
  • Browser: Remove setSpawnHandler function from the public API. (#1303)
  • PHP: Add a cwd argument to hotSwapPHPRuntime(). (#1304)
  • PHP: Remove addServerGlobalEntry() method, accept $\_SERVER as php.run() property. (#1286)
  • PHPRequestHandler: Add a generic PHP argument. (#1310)
  • nit: Clean up after node PHP popen() test. (#1280)
+

Website

+
  • Add more info to crash reports. (#1253)
  • Memoize fetch() responses when requesting php.wasm. (#1306)
  • Progress monitoring: Use a custom instantiateWasm handler to avoid monkey-patching WebAssembly.instantiateStreaming. (#1305)
  • Remove sandbox attribute from iframe. (#1313)
  • Service Worker: Fetch credentialless to play more nicely with server caches (#1311). (#1311)
+

Internal

+
  • Automate Changelog generation after each npm release. (#1312)
  • CI: Fix intermittent documentation build failures. (#1307)
+

Bug Fixes

+
  • Add styles to ensure iframes are responsive. (#1267)
  • Docs: Fix the Blueprint example of the Gutenberg PR preview. (#1268)
  • Docs: Move Steps Shorthands to a separate page to fix Steps TOC. (#1265)
+

Reliability

+
  • Add network error message. (#1281)
  • Explore logging to a file. (#1292)
+

Various

+
  • Add PDF to infer mime type list. (#1298)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @ironnysh @peeranat-dan

+

[v0.6.16] (2024-04-17)

+

Blueprints

+
  • Replace set_current_user call with wp_set_current_user to fix a PHP notice. (#1262)
+

Tools

+
  • Install themes and plugins using the ReadableStream API. (#919)
+

Documentation

+
  • Docs: Update WordPress versions used in the documentation, document using older releases. (#1235)
+

PHP WebAssembly

+
  • Filter Requests library to use the Fetch handler. (#1048)
+
  • PHP: Handle request errors in PHPRequestHandler, return response code 500. (#1249)
  • PHP: Reset exit code before dispatching a request. (#1251)
+

Various

+
  • Add documentation for shorthand alternatives of Blueprint steps. (#1261)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @dd32 @ironnysh @kozer

+

[v0.6.15] (2024-04-16)

+

Blueprints

+
  • Add ifAlreadyInstalled to installPlugin and installTheme steps. (#1244)
  • Support a landingPage value without the initial slash. (#1227)
+

PHP WebAssembly

+
  • Investigate OOB: Run unit tests with instrumented PHP 8.0 code. (#1220)
  • Unit tests: Restore site-data.spec.ts. (#1194)
  • Web PHP: Increase memory limit to 256 M. (#1232)
+

Website

+
  • Browser: Display PHP output when Fatal Error is trigerred. (#1234)
  • Fix accessibility issues found by Axe. (#1246)
  • Request Handler: Urldecode the requested path. (#1228)
+

Bug Fixes

+
  • fix: Set required engine version to 18.18.0. (#1214)
+

Various

+
  • Blueprints/json example. (#1188)
  • Doc: Update 01-index.md. (#1216)
  • Move DefineSiteUrlStep doc warning so it displays in documentation. (#1245)
  • Updated link to native WordPress importer. (#1243)
  • documentation update proposal: Provide more info on features, extensions?. (#1208)
  • php-wasm/node: Update express to newest version, and move it to devDependencies. (#1218)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @artpi @bph @brandonpayton @eliot-akira @flexseth @ironnysh @kirjavascript

+

[v0.6.14] (2024-04-11)

+

Bug Fixes

+
  • Revert changes to the documentation build. (#1226)
+

Reliability

+
  • Update error modal description label. (#1224)
+

Various

+
  • Try memory leak workaround with zeroed mem. (#1229)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton

+

[v0.6.13] (2024-04-10)

+

PHP WebAssembly

+
  • Try to repro memory out of bounds errors in CI. (#1199)
+

Bug Fixes

+
  • Fix docs-site build. (#1222)
+

Contributors

+

The following contributors merged PRs in this release:

+

@bgrgicak @brandonpayton

+

[v0.6.11] (2024-04-09)

+

Tools

+
  • Avoid Service Worker update issues on localhost. (#1209)
+

Import/Export

+
  • importWxr: Preserve backslashes in the imported content. (#1213)
+

PHP WebAssembly

+
  • Catch DNS errors to avoid unhandled exceptions. (#1215)
+
  • Revert "Avoid partial munmap memory leak". (#1195)
  • Try to repro memory out of bounds errors in CI. (#1198)
+

Various

+
  • Adjust link to LICENSE file. (#1210)
  • Try to reproduce the memory access error with files from 096a017. (#1212)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @emmanuel-ferdman @fluiddot

+

[v0.6.10] (2024-04-04)

+

Blueprints

+
  • Rename importFile to importWxr, switch to humanmade/WordPress importer. (#1192)
+

Tools

+

Blueprints

+
  • Explorations: Stream API. (#851)
+

PHP WebAssembly

+
  • Avoid partial munmap memory leak. (#1189)
+

Website

+
  • Make kitchen sink extension bundle the default. (#1191)
+

Bug Fixes

+
  • Fix cross-device mv by switching to copy. (#846)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @brandonpayton @seanmorris

+

[v0.6.9] (2024-04-03)

+

Tools

+
  • Devex: Expose window.playground for quick testing and debugging. (#1125)
+

GitHub integration

+
  • Website: Query API options to preconfigure the GitHub export form. (#1174)
+

Documentation

+
  • Update the wp-cli step code example. (#1140)
+

PHP WebAssembly

+
  • Add PHP iterator and yield support. (#1181)
  • Fix fileinfo support. (#1179)
  • Fix mbregex support. (#1155)
  • PHP.run(): Throw JS exception on runtime error, remove throwOnError flag. (#1137)
+

Website

+
  • Add error report modal. (#1102)
  • Ensure PromiseRejectionEvent has reason before logging it. (#1150)
  • Request handler: Remove everything after # from the URL. (#1126)
  • Web: Make the "Apply changes" button work in Playground settings form. (#1122)
+

Plugin proxy

+
  • Allow requests to WordPress.org. (#1154)
+

Internal

+
  • Refresh WordPress with the latest SQLite integration plugin. (#1151)
+

Bug Fixes

+
  • Fix typo in blueprints/public/schema-readme.md. (#1134)
  • Priority: Fix broken link to VS Code extension. (#1141)
+

Various

+
  • Docs/update - Add implied step. (#1144)
  • Give brandonpayton permission to run Playground GH workflows. (#1139)
  • Logger API: Add rate limiting. (#1142)
  • Remove --disable-all configuration option in PHP compile process. (#1132)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @brandonpayton @flexseth @jblz @mho22

+

[v0.6.8] (2024-03-21)

+

Blueprints

+
  • Allow optional metadata. (#1103)
+

Tools

+
  • Add VSCode Chrome debugging support. (#1088)
  • Website: Support Base64-encoding Blueprints passed in the URL. (#1091)
+

Documentation

+
  • Docs: Expand Details section. (#1109)
  • Update activate-theme.ts to use themeFolderName. (#1119)
+

PHP WebAssembly

+
  • Blueprints: Explore switching to the PHP implementation. (#1051)
  • Explore weird register_shutdown_function behavior. (#1099)
  • Fix post_message_to_js memory out of bounds. (#1114)
  • Fix shutdown errors. (#1104)
  • Fixing build regression [BISON COMPILE]. (#871)
  • PHP : Set appropriate SCRIPT variables in $\_SERVER superglobal. (#1092)
+

Website

+
  • Add logger API. (#1113)
  • Add multisite rewrite rules. (#1083)
  • Service worker: Improve error reporting in non-secure contexts. (#1098)
+

Bug Fixes

+
  • Fix experimental notice in FF ESR. (#1117)
  • Fix php bison dep for building on non-arm64 architectures. (#1115)
+

Reliability

+
  • Add fatal errror listener. (#1095)
+

Various

+
  • Update examples and demos in the documentation. (#1107)
+

Contributors

+

The following contributors merged PRs in this release:

+

@0aveRyan @adamziel @bgrgicak @brandonpayton @ironnysh @mho22 @seanmorris @StevenDufresne

+

[v0.6.7] (2024-03-06)

+

Website

+
  • Node polyfills: Only apply them in Node.js, not in web browsers. (#1089)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.6.6] (2024-03-06)

+

Website

+
  • Comlink API: Pass the context argument to windowEndpoint, not wrap. (#1087)
  • Fix: Playground not starting due to a race condition. (#1084)
  • Hide the "This is experimental WordPress" notice on click. (#1082)
  • Set the API context when using Comlink.wrap(). (#1085)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel

+

[v0.6.5] (2024-03-05)

+

Tools

+

Plugin proxy

+
  • Add Sensei to the allowed repositories for plugin proxy. (#1079)
+

Blueprints

+
  • Snapshot Import Protocol v1. (#1007)
+

Internal

+
  • Build the php-wasm/util package as both ESM and CJS. (#1081)
+

Reliability

+

Blueprints

+
  • Add unit tests to the mkdir step. (#1029)
+

Various

+
  • Website query API: Continue plugin installs on error. (#605)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @eliot-akira @reimic @renatho

+

[v0.6.4] (2024-03-04)

+

Enhancements

+
  • Add logging support to Playground. (#1035)
+

Blueprints

+
  • PHP Blueprints: Display progress. (#1077)
  • Set progress caption and communicate failures in the import file step. (#1034)
+

Tools

+

Blueprints

+
  • PHP Blueprints demo page. (#1070)
  • PHP: Do not prepend a whitespace when encoding body as multipart form data. (#1033)
+

PHP WebAssembly

+
  • Fix response header escaping. (#1050)
  • Fix: Networking broken when extra PHP extensions are enabled. (#1045)
  • PHP.wasm: Yield 0 bytes read on fd_read failure to improve PHP's fread() and feof() behavior. (#1053)
  • PHP: Support $env and $cwd proc_open arguments. (#1064)
  • Parse shell commands in createSpawnHandler. (#1065)
  • Prototype: Spawning PHP sub-processes in Web Workers. (#1031)
  • Spawning PHP sub-processes in Web Workers. (#1069)
+

Website

+
  • Add Google Analytics events to Playground. (#1040)
  • Fix error on reload site click. (#1041)
+

Internal

+
  • Rebuild WordPress every 20 minutes, short-circuit if no new version is found. (#1061)
  • Rebuild WordPress within an hour of a beta release. (#1059)
+

Bug Fixes

+
  • Fix the login message so it doesn't override another. (#1044)
+

Various

+
  • Add arguments to default node spawn method. (#1037)
  • Add bgrgicak to deployment allowlists. (#1057)
  • Allow for CORS requests to api.wordpress.org to pass. (#1009)
  • Default URL rewrites to /index.php. (#1072)
  • Remove repository specific Code of Conduct. (#1038)
  • Ship WordPress 6.5 beta 1. (#1036)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @dd32 @desrosj @johnbillion @mho22

+

[v0.6.3] (2024-02-12)

+

Blueprints

+ +

PHP WebAssembly

+
  • Calls proc_open two times in a row. (#1012)
  • Experiment: Build PHP with OPFS support. (#1030)
  • PHP: Pass request body as UInt8Array. (#1018)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @mho22

+

[v0.6.2] (2024-02-08)

+

PHP WebAssembly

+
  • Networking: Swap Requests transports using the http_api_transports instead of patching the Requests library. (#1004)
  • Remove crypto.randomUUID dependency in favor of a custom function. (#1016)
  • Remove x-request-issuer header on cross-origin requests. (#1010)
  • Update wp_http_fetch.php. (#1002)
+

Website

+
  • Remote.html: Always install the playground mu-plugin. (#1005)
+

Various

+
  • 32bit integer workaround. (#1014)
  • Test/hello world blueprint. (#908)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bgrgicak @jdevalk @sejas @stoph

+

[v0.6.1] (2024-02-05)

+

Website

+

Blueprints

+
  • Remove the applyWordPressPatches step, enable the Site Health Plugin. (#1001)
+

Various

+
  • Add crypto to Polyfills improving Blueprint compatibility for Node. (#1000)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @sejas

+

[v0.6.0] (2024-02-05)

+

Enhancements

+
  • Add wp-cli and code editor examples to the demos page. (#965)
  • WordPress: Preserve PHP attributes and wp-config.php whitespace. (#964)
+

Blueprints

+
  • Add enableMultisite step. (#888)
  • Set_current_user to admin before activating plugins and themes. (#984)
+

Tools

+
  • Use .zip files instead of .data files for loading WordPress. (#978)
+

Blueprints

+
  • Throw on failure. (#982)
+

PHP WebAssembly

+
  • Support wp-cli in the browser. (#957)
+

PHP WebAssembly

+
  • Correcting OOB & Prevent Crash on Saving Large Post. (#870)
  • Memory leak: Add rotatedPHP to kill and recreate PHP instances after a certain number of requests. (#990)
  • PHP : Add args and descriptors dynamic arrays in proc open function. (#969)
  • PHP.wasm: Fix stack overflow in wasm_set_request_body. (#993)
+

Website

+
  • Add .htaccess file to prevent caching of index.html and enable importing the client.js library. (#989)
  • Add og meta tags and meta description. (#980)
  • CORS headers for client/index.js. (#893)
  • wp-cli: Respect quotes when parsing shell commands. (#966)
+

Internal

+
  • Remove the interactive block playground. (#988)
+

Bug Fixes

+
  • Fix "WP-CLI" typos. (#971)
  • Fix footer styling issue in the "Code is Poetry" in wordpress.github.io/wordpress-playground. (#959)
  • WordPress build: Add newlines after PHP annotations. (#986)
+

Various

+
  • Add a blueprint example. (#946)
  • Add terminal to playground site. (#161)
  • Match the .nvmrc node version to the changes made in commit ec2605b. (#972)
  • PHP : Dispatch available descriptor specs in js_open_process function. (#963)
  • PHP : Give access to command arguments if array type is given in php ^7.4 proc_open function. (#944)
  • Rebuild WordPress. (#987)
  • Update the networking disabled error messages in wp-admin for plugins and themes. (#936)
+

Contributors

+

The following contributors merged PRs in this release:

+

@adamziel @bph @ironnysh @marcarmengou @mho22 @rowasc @seanmorris @swissspidy @tyrann0us

+

[v0.5.9] - 2021-09-29

+

Changed

+

– Breaking: Remoddsaved the PHPBrowser class (##1302)

+

Added

+

– Added CHANGELOG.md to keep track of notable changes (##1302)

+

Original Playground docs source: https://playground.wordpress.net/changelog

]]>
+ + 55 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Code contributions]]> + https://playground.wordpress.net/contributing-code/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=56 + + Code contributions +

Like all WordPress projects, Playground uses GitHub to manage code and track issues. The main repository is at https://github.com/WordPress/wordpress-playground and the Playground Tools repository is at https://github.com/WordPress/playground-tools/.

+

Contribute to Playground Tools

+

This guide includes links to the main repository, but all the steps and options apply for both. If you're interested in the plugins or local development tools—start there.

+

Browse the list of open issues to find what to work on. The Good First Issue label is a recommended starting point for first-time contributors.

+

Be sure to review the following resources before you begin:

+ +

Contribute Pull Requests

+

Fork the Playground repository and clone it to your local machine. To do that, copy and paste these commands into your terminal:

+
git clone -b trunk --single-branch --depth 1 --recurse-submodules
+
+# replace `YOUR-GITHUB-USERNAME` with your GitHub username:
+git@github.com:YOUR-GITHUB-USERNAME/wordpress-playground.git
+cd wordpress-playground
+npm install
+

Create a branch, make changes, and test it locally by running the following command:

+
npm run dev
+

Playground will open in a new browser tab and refresh automatically with each change.

+

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.

+

Running a local Multisite

+

WordPress Multisite has a few restrictions when run locally. If you plan to test a Multisite network using Playground's enableMultisite step, make sure you either change 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.

+

Debugging

+

Use VS Code and Chrome

+

If you're using VS Code and have Chrome installed, you can debug Playground in the code editor:

+
  • Open the project folder in VS Code.
  • Select Run > Start Debugging from the main menu or press F5/fn+F5.
+

Debugging PHP

+

Playground logs PHP errors in the browser console after every PHP request.

+

Original Playground docs source: https://playground.wordpress.net/contributing/code

]]>
+ + 56 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Coding principles]]> + https://playground.wordpress.net/contributing-coding-standards/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=57 + + Coding principles +

Error messages

+

A good error message informs the user of the following steps to take. Any ambiguity in errors thrown by Playground Public APIs will prompt the developers to open issues.

+

Consider a network error, for example—can we infer the type of error and display a relevant message summarizing the next steps?

+
  • Network error: "Your internet connection twitched. Try to reload the page.
  • 404: "Could not find the file".
  • 403: "The server blocked access to the file".
  • CORS: clarify it's a browser security feature and add a link to a detailed explanation (on MDN or another reliable source). Suggest the user move their file somewhere else, like raw.githubusercontent.com, and link to a resource explaining how to set up CORS headers on their servers.
+

We handle code formatting and linting automatically. Relax, type away, and let the machines do the work.

+

Public API

+

Playground aims to keep the narrowest possible API scope.

+

Public APIs are easy to add and hard to remove. It only takes one PR to introduce a new API, but it may take a thousand to remove it, especially if other projects have already consumed it.

+
  • Don't expose unnecessary functions, classes, constants, or other components.
+

Blueprints

+

Blueprints are the primary way to interact with Playground. These JSON files describe a set of steps that Playground executes in order.

+

Guidelines

+

Blueprint steps should be concise and focused. They should do one thing and do it well.

+
  • If you need to create a new step, try refactoring an existing one first.
  • If that's not enough, ensure the new step delivers a new capability. Don't replicate the functionality of existing steps.
  • Assume the step would be called more than once.
  • Assume it would run in a specific order.
  • Add unit tests to verify that.
+

Blueprints should be intuitive and straightforward.

+
  • Don't require arguments that can be optional.
  • Use plain argument. For example, slug instead of path.
  • Define constants in virtual JSON files—don't modify PHP files.
  • Define a TypeScript type for the Blueprint. That's how Playground generates its JSON schema.
  • Write a function to handle a Blueprint step. Accept the argument of the type you defined.
  • Provide a usage example in the doc string. It's automatically reflected in the docs.
+

Original Playground docs source: https://playground.wordpress.net/contributing/coding-standards

]]>
+ + 57 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground Contributor Badge]]> + https://playground.wordpress.net/contributing-contributor-badge/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=58 + + Playground Contributor Badge +

This page provides detailed information on the Playground Contributor Badge and the process of requesting it on your WordPress.org profile.

+

---

+

Getting Started

+

Any contribution to the WordPress Playground project is highly valued. The Playground team recognizes contributions across several key areas:

+
  • Playground Code: Making code changes and reviewing the core project.
  • Playground UI: Improving the user interface of the web experience.
  • Documentation: Writing, updating, and reviewing documentation.
  • Translation: Translating any part of the project.
  • Blueprints Gallery: Creating new blueprints or improving existing ones.
+

Playground Contributor Badge

+

To get a Playground Contributor Badge, you must have made at least one eligible contribution from the list above. The team may choose to award the badge for other contributions or a combination of the above at the team’s discretion.

+

Playground Contributor Badge

+

Playground Team Badge

+

If you are currently a contributor and have been actively involved in the Playground project for the past twelve months, you are eligible for a Playground Team Badge.

+

Playground Contributor Badge

+

Requesting a Profile Badge

+

If you meet the criteria, you can request a badge. Please include links to resources (such as GitHub pull requests, issues, or translated strings) that show you have met the criteria. Send a request at the links bellow:

+ +

Request form

+

Playground Contributor Badge

+

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

]]>
+ + 58 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[How to Lead a Contributor Day Table]]> + https://playground.wordpress.net/contributing-table-lead-guide/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=59 + + Contributor Day Table Leadership Guide +

This guide helps table leads prepare for and manage a WordPress Playground contributor table at WordCamp events.

+

Before Contributor Day

+

Pre-Work Checklist

+
  • Curate "Good First Issues": Review and update the good first issues list on GitHub. These should be straightforward tasks that new contributors can complete independently. If you find a bug that is not on the list but could be part of it, contact the playground team at the Slack channel.
  • Coordinate with the Playground Team: Confirm if Playground team members are available online to provide remote support during the event, especially for flagship WordCamps. Due to timezone differences, align in advance at the #playground channel to check their availability.
  • Connect with Local Contributors: Identify regular contributors in the region attending the event. Check on the #playground Slack Channel if an active community member is participating in the contributor day. This is an opportunity to gather feedback and strengthen community connections.
  • Check the Playground Repository: If you never contribute with the WordPress Playground Repository, you should get familiar with this a good section at the documentation that can guide you to understand the project is Developers > Architecture it will contain information how the project is organized. If you have any questions, please get in touch with the team at the Playground Slack channel.
+

Starting the Day

+

Setup and Onboarding

+
  1. Create Your Agenda: Prepare a flexible checklist of key activities while allowing for organic collaboration. Share it in the project documentation if helpful.
+
  1. Guide Contributors to Slack: Direct everyone to the #playground channel on WordPress Slack. This centralizes communication and enables asynchronous collaboration with late arrivals.
+
  1. Post a Welcome Message: Share an initial message in the Slack channel announcing your presence (in-person or online) and welcoming contributions from everyone.
+
  1. Share Essential Links: Post these resources in the #playground channel:
+ +
  1. Introduce the GitHub Repository: Provide a brief walkthrough of the repository structure, highlighting different packages and their purposes for first-time contributors.
+

During the Day

+

Managing Contributions

+

Encourage Different Contribution Types:

+

Check the contributors' levels, try to understand based on their level how they can contribute to the project in the short window of a contributor day. Ask if the participants need help and redirect them to the related documentation page. Also, encourage them to ask questions at the #playground Slack channel. Here are some suggestions for ways of contributing:

+
  • Documentation improvements and translations.
  • Carefully crafted issues describing problems with actionable solutions.
  • Blueprint creation and plugin demos at the WordPress plugin repository.
  • Testing and product feedback.
+

Foster Collaboration: Look for cross-table opportunities. For example, contributors at the Polyglots/Translation table might translate Playground documentation, or the Core Test team could provide valuable Playground feedback.

+

Collect Feedback: Ask contributors about their experience and note improvement suggestions. Report this in the #playground Slack Channel if possible.

+

After the Event

+

Follow-Up and Support

+
  1. Review Pull Requests: List PRs created during the day and assess their completion likelihood. Most contributions have a short momentum window—engagement within the first two weeks is critical.
+
  1. Set Clear Expectations: For incomplete PRs, follow this approach:
+
  • After one month of inactivity, leave a comment asking if the author plans to complete the work.
  • If no response after two more weeks, inform them that the PR may be taken over by another contributor or closed.
+
  1. Stay Active on Slack: Continue supporting new contributors through the #playground channel, answering questions and helping them become regular contributors.
+
  1. Reflect and Improve: Review collected feedback and your experience to refine this guide for future events. Feel free to submit a Pull Request to this guide!
+

Getting Help

+ +

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

]]>
+ + 59 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordCamp Contributor Day]]> + https://playground.wordpress.net/contributing-contributor-day/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=60 + + WordCamp Contributor Day +

WordCamp Contributor Day is an event where the WordPress community comes together to contribute to the WordPress project. This guide focuses on how you can contribute to the WordPress Playground project or how the Playground can assist you in contributing to WordPress Core.

+

Who Can Contribute?

+

Some events will have a dedicated table for the project. The WordPress Playground contributor tables welcome all kinds of contributions, not just from developers. Whether you are a writer, coder, tester, plugin or theme developer, marketer, site owner, or any other type of user, you are encouraged to contribute.

+

We value diverse contributions across various areas, including community building, testing, documentation, and design.

+

How to Contribute to the Playground Project

+

This section outlines how you can contribute directly to the WordPress Playground project and its associated tools:

+
  • Documentation: Enhance our documentation by improving existing content, developing new guides, or translating materials into different languages.
  • Blueprints: Create plugin demos for plugins at the WordPress Plugin repository, or develop new Blueprints to enrich our project documentation.
  • Testing the Playground Environment: Engage in testing the WordPress Playground project itself. You can do this by carefully crafting new issues that describe problems you encounter and suggesting actionable solutions. Test our WordPress web instance (the playground.wordpress.net site), or explore the various applications powered by Playground. Test these tools, observe their functionality, and provide detailed feedback.
  • Product Feedback: Your insights are invaluable for improving the Playground experience. This includes general feedback on the web instance, the application, and any server-side tools.
+

All feedback, including reported issues and test results, can be submitted through our GitHub repository.

+

Follow-up and Continued Engagement

+

While many tasks are completed during the event, your contribution journey doesn't have to end there. You are welcome to continue working on your issues or pull requests after Contributor Day. We anticipate ongoing activity from contributors who take on tasks beyond the event. Please note that if a pull request shows no activity for one month, it may be considered abandoned and subsequently closed.

+

Getting Help and Staying Engaged

+

During Contributor Day, you can find direct assistance and interact with us at the dedicated Playground table. For continuous support and community interaction, you can connect with us on the #playground channel on WordPress Slack or via GitHub.

+

How to use Playground at Contributor Day

+

Now we are going to cover how the Playground can assist you during the Contributor Day. The WordPress Playground VS Code extension and @wp-playground/cli streamline the process of setting up a local WordPress environment. WordPress Playground powers both—no Docker, MySQL, or Apache required.

+

Keep reading to learn how to use these tools for local development when contributing to WordPress. Please note that the extension and the NPM package are under development, and not all Make WordPress teams are fully supported.

+

Getting Started

+

VS Code Playground extension

+

The Visual Studio Code Playground extension is a friendly zero-setup development environment.

+
  1. Open VS Code and navigate to the Extensions tab (View > Extensions).
  2. In the search bar, type _WordPress Playground_ and click Install.
  3. To interact with Playground, click the new icon in the Activity Bar and hit the Start WordPress Server button.
  4. A new tab will open in your browser within seconds.
+

@wp-playground/cli NPM package

+

@wp-playground/cli is a CLI tool that allows you to spin up a WordPress site with a single command. No Docker, MySQL, or Apache are required.

+

Prerequisites

+

@wp-playground/cli requires Node.js 20.18 or newer and NPM. If you haven’t yet, download and install both before you begin.

+

Depending on the Make WordPress team you contribute to, you may need a different Node.js version than the one you have installed. You can use Node Version Manager (NVM) to switch between versions. Find the installation guide here.

+

Running @wp-playground/cli

+

You don’t have to install @wp-playground/cli on your device to use it. Navigate to your plugin or theme directory and start @wp-playground/cli with the following commands:

+
cd my-plugin-or-theme-directory
+npx @wp-playground/cli@latest server --auto-mount
+

Ideas for contributors

+

Create a Gutenberg Pull Request (PR)

+
  1. Fork the Gutenberg repository in your GitHub account.
  2. Then, clone the forked repository to download the files.
  3. Install the necessary dependencies and build the code in development mode.
+
git clone git@github.com:WordPress/gutenberg.git
+cd gutenberg
+npm install
+npm run dev
+

If you’re unsure about the steps listed above, visit the official Gutenberg Project Contributor Guide. Note that in this case, @wp-playground/cli replaces wp-env.

+

Open a new terminal terminal tab, navigate to the Gutenberg directory, and start WordPress using @wp-playground/cli:

+
cd gutenberg
+npx @wp-playground/cli@latest server --auto-mount
+

When you’re ready, commit and push your changes to your forked repository on GitHub and open a Pull Request on the Gutenberg repository.

+

Test a Gutenberg PR

+
  1. To test other Gutenberg PRs, checkout the branch associated with it.
  2. Pull the latest changes to ensure your local copy is up to date.
  3. Next, install the necessary dependencies, ensuring your testing environment matches the latest changes.
  4. Finally, build the code in development mode.
+
# copy the branch-name from GitHub #
+git checkout branch-name
+git pull
+npm install
+npm run dev
+
+# In a different terminal inside the Gutenberg directory *
+npx @wp-playground/cli@latest server --auto-mount
+

Test a Gutenberg PR with Playground in the browser

+

You don’t need a local development environment to test Gutenberg PRs—use Playground to do it directly in the browser.

+
  1. Copy the ID of the PR you’d like to test (pick one from the list of open Pull Requests).
  2. Open Playground’s Gutenberg PR Previewer and paste the ID you copied.
  3. Once you click Go, Playground will verify the PR is valid and open a new tab with the relevant PR, allowing you to review the proposed changes.
+

Translate WordPress Plugins with Playground in the browser

+

You can translate supported WordPress Plugins by loading the plugin you want to translate and use Inline Translation. If the plugin developers have added the option, you'll find the Translate Live link on the top right toolbar of the translation view. You can read more about this exciting new option on this Polyglots blog post.

+

Get help and contribute to WordPress Playground

+

Have a question or an idea for a new feature? Found a bug? Something’s not working as expected? We’re here to help:

+ +

Original Playground docs source: https://playground.wordpress.net/contributing/contributor-day

]]>
+ + 60 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Documentation Contributions]]> + https://playground.wordpress.net/contributing-documentation/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=61 + + Documentation contributions +

WordPress Playground's documentation site is maintained by volunteers like you, who'd love your help.

+

All documentation-related issues are labeled [[Type] Documentation](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.

+

How can I contribute?

+

You can contribute by opening an issue in the project repository and describing what you'd like to add or change.

+

If you feel up to it, write the content in the issue description, and the project contributors will take care of the rest.

+

Would you like to see the documentation in your language? Check the Translation section.

+

Forking the repo, edit files locally and opening Pull Requests

+

If you are familiar with markdown, you can fork the wordpress-playground repo and propose changes and new documentation pages by submitting a Pull Request.

+

The process of creating a branch to open new PRs with translated pages on the WordPress/wordpress-playground repository is the same as contributing to other WordPress repositories such as Gutenberg: 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.

+

Edit in the browser

+

If logged in GitHub, you can also edit existing files (or add new ones) and submit a PR directly from the GitHub UI:

+
  1. Find the page you'd like to edit or the directory of the chapter you'd like to add a new page to.
  2. Click the Add Files button to add a new file, or click on an existing file and then click the pencil icon to edit it.
  3. GitHub will ask you to fork the repository and create a new branch with your changes.
  4. An editor will open where you can make the changes.
  5. When you're done, click the Commit Changes button and submit a Pull Request.
+

That's it! You've just contributed to the WordPress Playground documentation.

+

This approach means you don't need to clone the repository, set up a local development environment, or run any commands.

+

The downside is that you won't be able to preview your changes. Keep reading to learn how to review your changes before submitting a Pull Request.

+

Local preview

+

Clone the repository and navigate to the directory on your device. Now run the following commands:

+
npm install
+npm run build:docs
+npm run dev:docs
+

The documentation site opens in a new browser tab and refreshes automatically with each change. Continue to edit the relevant file in your code editor and test the changes in real-time.

+

Original Playground docs source: https://playground.wordpress.net/contributing/documentation

]]>
+ + 61 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Contributing to WordPress Playground project]]> + https://playground.wordpress.net/contributing/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=62 + + Contributing to WordPress Playground project +

WordPress Playground is an open-source project that welcomes contributors of all kinds, from code to design, documentation to triage.

+

How can I contribute?

+ +

Guidelines

+
  • As with all WordPress projects, we want to ensure a welcoming and respectful environment for everyone. Please read our community's Code of Conduct to learn more.
  • Code contributors should review the coding principles.
  • You maintain copyright over any contribution you make. By submitting a Pull Request, you agree to release that code under WordPress Playground License.
+

Triaging issues

+

Want to help sort through open issues and resolve potential bugs? Here's how:

+
  1. Review the list of open issues and find the ones that you can help with. Same goes for the Playground Tools repository.
  2. Read through the description and comments.
  3. If it's a bug you can reproduce, add a descriptive comment or a potential fix.
  4. Otherwise, add a comment with any additional information that may be helpful.
+

A note on contributing and the GPL license

+

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:

+
  • When you contribute, you agree to license your contributions under the GPLv2 (or later) license
  • The GPL license has strong copyleft provisions that ensure all derivative works remain open-source and under the same license terms, thereby promoting a collaborative development environment.
  • The GPL license encourages contributing any changes, bug fixes, or new features back to the original codebase.
  • The GPL license ensures that the project remains free and open-source, not only in terms of cost but also with respect to the freedom to use, modify, and distribute the software.
+

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

]]>
+ + 62 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Releasing packages]]> + https://playground.wordpress.net/contributing-releases/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=63 + + Releasing packages +

Playground publishes its packages to npm using automated CI workflows. This page explains how the release process works and what you need to know when adding new packages.

+

Automated releases

+

The npm packages are published automatically every Monday via GitHub Actions, or manually by maintainers using the workflow dispatch. The workflow bumps versions using Lerna, tags the release, and publishes all public packages to npm.

+

The CI authenticates with npm using OpenID Connect (OIDC) trusted publishing. This is more secure than using long-lived npm tokens because it generates short-lived credentials for each workflow run and ties package provenance directly to the GitHub repository.

+

Adding a new package

+

When you add a new npm package to the monorepo, the automated release workflow won't be able to publish it on the first run. This is an npm security feature: OIDC trusted publishing only works for packages that already exist and have been configured to trust the GitHub repository.

+

Here's what you need to do:

+

1. Publish the package manually

+

First, authenticate with npm on your local machine:

+
npm login
+

Then publish the package for the first time:

+
cd packages/your-new-package
+npm publish --access public
+

This creates the package on the npm registry under your account.

+

2. Configure trusted publishing

+

After the initial publish, go to the package's settings on npmjs.com and set up OIDC trusted publishing:

+
  1. Navigate to your package on npmjs.com
  2. Go to Settings → Configure Trusted Publishers
  3. Add a new trusted publisher with these settings. All of them are case-sensitive!
+
  • Repository owner: WordPress
  • Repository name: wordpress-playground
  • Workflow filename: publish-npm-packages.yml
  • Environment: npm
+

Setting up OIDC trusted publishing on npm

+

3. Transfer ownership (if needed)

+

If you published under your personal account, transfer the package to the @aspect organization or ensure the appropriate team has publish access.

+

Once configured, subsequent releases will work automatically through the CI workflow.

+

Why OIDC can't publish new packages

+

npm's OIDC implementation requires the package to already exist before a trusted publisher can be configured. This is a chicken-and-egg situation by design—it prevents someone from hijacking a package name through a GitHub workflow before the legitimate owner can claim it.

+

The manual first publish establishes ownership, and trusted publishing then provides secure, token-free authentication for all future releases.

+

Original Playground docs source: https://playground.wordpress.net/contributing/releases

]]>
+ + 63 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Contributions to translations]]> + https://playground.wordpress.net/contributing-translations/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=64 + + Contributions to translations +

Help make WordPress Playground accessible to a global audience by translating its documentation. This guide provides everything you need to know to get started. Contributing translations follows the same workflow as any other documentation change. You can either fork the WordPress/wordpress-playground repository and create a pull request (PR) with your changes or edit pages directly using the GitHub UI.

+

For a detailed guide on the contribution workflow (forking, creating PRs, etc.), please see our documentation contribution guide

+

How Translations Work

+

Playground's documentation site is built with Docusaurus, which handles the internationalization (i18n) features.

+

To learn more about how Docusaurus manages translations, see the Internationalization section of the official Docusaurus documentation.

+

Configuration

+

Available languages are defined in the packages/docs/site/docusaurus.config.js file. For example:

+
i18n: {
+  defaultLocale: 'en',
+  path: 'i18n',
+  locales: ['en', 'fr'],
+  localeConfigs: {
+	en: {
+		label: 'English',
+		path: 'en',
+	},
+	fr: {
+		label: 'French',
+		path: 'fr',
+	},
+  },
+}
+

File Structure

+

All translated documentation pages are located within the packages/docs/site/i18n/ directory, organized by language code.

+

For a language to work correctly, its file structure must mirror the original English documentation found in packages/docs/site/docs.

+

For example, the Spanish (es) translation for docs/main/intro.md must be placed at: packages/docs/site/i18n/es/docusaurus-plugin-content-docs/current/main/intro.md.

+

If a translated file does not exist for a specific language, Docusaurus will automatically fall back to the English version of that page.

+

Generating Translation Files

+

When adding a new language, you can generate the necessary JSON files for UI strings (like button labels and navigation items) by running the following command from the packages/docs/site directory:

+
npm run write-translations -- --locale <LANGUAGE_CODE>
+

With the proper i18n docusaurus.config.js configuration and files under i18n when running npm run build:docs from the root of the project, specific folders under dist for each language will be created.

+

Testing Translations Locally

+

To preview your changes for an existing language:

+
  1. Modify or add a translated file in the appropriate language directory, such as packages/docs/site/i18n/es/docusaurus-plugin-content-docs/current/.
  2. From the /packages/docs/site directory, run the local development server for your target language. For example, to test Spanish (es):
+

+npm run dev -- --locale es
+
+

The Language Switcher

+

The language switcher is a dropdown menu that allows users to select their preferred language.

+

Documentation Language Switcher

+

Making a language publicly available on the Language Switcher

+

We recommend only adding a language to the switcher when a significant portion of the documentation has been translated. This avoids a poor user experience where switching to a new language results in seeing mostly untranslated English content.

+

As a guideline, a language should be made publicly available in the switcher only when the entire "Documentation" hub is translated, including these key sections:

+ +

All languages are available once the i18n setup for a language is complete and the correct file structure is in place under i18n.

+
  • https://wordpress.github.io/wordpress-playground/
  • https://wordpress.github.io/wordpress-playground/es/
  • https://wordpress.github.io/wordpress-playground/fr/
+

Assuming the fr language is the first language with the Documentation hub pages (Quick Start Guide, Playground web instance, About Playground, Guides,... ) completely translated to French, the docusaurus.config.js should look like this in that branch so npm run build:docs properly generate the fr subsite and only displays the french language in the localeDropdown language switcher.

+
  {
+    "i18n": {
+      "defaultLocale": "en",
+      "path": "i18n",
+      "locales": [
+        "en",
+        "fr"
+      ],
+      "localeConfigs": {
+        "en": {
+          "label": "English",
+          "path": "en"
+        },
+        "fr": {
+          "label": "French",
+          "path": "fr"
+        }
+      }
+    }
+  },
+  {
+    "type": "localeDropdown",
+    "position": "right"
+  }
+

Translation Workflow

+

Follow these steps to translate a page:

+
  1. Check for an Existing Translation Issue: First, search the repository issues to see if a tracking issue for your desired language already exists. If it does, comment on the issue to claim the page(s) you would like to translate.
  2. Create a New Translation Issue: If no issue exists, please create a new one to track the translation progress for the language. You can model it after issue #2202 and use the markdown checklist below to track progress.
  3. Translate the File:
+
  • Check if you have the latest version of the documentation
  • Copy the original .md file from packages/docs/site/docs/... to the corresponding path in the language directory (e.g., packages/docs/site/i18n/<LANGUAGE_CODE>/...). It is crucial to replicate the original file structure.
  • Translate the content of the new file, keeping the original content commented out <!-- English Content -->.
  • The assets are listed at packages/docs/site/static/img/ only place assets inside the translation folder when it requires localized content.
  • Once the translations are ready, check if the docs build script is running properly npm run build:docs.
+
  1. Create a pull request with your changes
+
  • Add a prefix to the title [i18n] to help to identify the translations
  • Describe the pages that you translated
  • Request a review at #playground or #polyglots at wordpress.slack.com
+

We highly recommend submitting pull requests with a small number of translated pages. This approach simplifies the review process and allows for a more gradual and manageable integration of your work.

+

Translation Tracking Template

+

You can use the following markdown in your tracking issue:

+
## Remaining translation pages
+
+<details open>
+<summary><h3>Main</h3></summary>
+
+- about
+  - [ ] build.md
+  - [ ] index.md
+  - [ ] launch.md
+  - [ ] test.md
+- contributing
+  - [ ] code.md
+  - [ ] coding-standards.md
+  - [ ] contributor-badge.md
+  - [ ] contributor-day.md
+  - [ ] contributor-day-table-lead.md
+  - [ ] documentation.md
+  - [ ] index.md
+  - [ ] releases.md
+  - [ ] translations.md
+- guides
+  - [ ] for-plugin-developers.md
+  - [ ] for-theme-developers.md
+  - [ ] github-action-pr-preview.md
+  - [ ] index.md
+  - [ ] providing-content-for-your-demo.md
+  - [ ] wordpress-native-ios-app.md
+- [ ] changelog.md
+- [ ] intro.md
+- [ ] quick-start-guide.md
+- [ ] resources.md
+- [ ] web-instance.md
+
+</details>
+
+<details open>
+<summary><h3>Blueprints</h3></summary>
+
+- [ ] 01-index.md
+- [ ] 02-using-blueprints.md
+- [ ] 03-data-format.md
+- [ ] 04-resources.md
+- [ ] 05-steps.md
+- [ ] 05-steps-shorthands.md
+- [ ] 06-bundles.md
+- [ ] 07-json-api-and-function-api.md
+- [ ] 08-examples.md
+- [ ] 09-troubleshoot-and-debug-blueprints.md
+- [ ] intro.md
+- tutorial
+  - [ ] 01-what-are-blueprints-what-you-can-do-with-them.md
+  - [ ] 02-how-to-load-run-blueprints.md
+  - [ ] 03-build-your-first-blueprint.md
+  - [ ] index.md
+
+</details>
+
+<details open>
+<summary><h3>Developers</h3></summary>
+
+- 03-build-an-app
+  - [ ] 01-index.md
+- 05-local-development
+  - [ ] 01-wp-now.md
+  - [ ] 02-vscode-extension.md
+  - [ ] 03-php-wasm-node.md
+  - [ ] 04-wp-playground-cli.md
+  - [ ] intro.md
+- 06-apis
+  - [ ] 01-index.md
+  - javascript-api
+    - [ ] 01-index.md
+    - [ ] 02-index-html-vs-remote-html.md
+    - [ ] 03-playground-api-client.md
+    - [ ] 04-blueprint-json-in-api-client.md
+    - [ ] 05-blueprint-functions-in-api-client.md
+    - [ ] 06-mount-data.md
+  - query-api
+    - [ ] 01-index.md
+- 07-xdebug
+  - [ ] 01-introduction.md
+  - [ ] 02-getting-started.md
+- 23-architecture
+  - [ ] 01-index.md
+  - [ ] 02-wasm-php-overview.md
+  - [ ] 03-wasm-php-compiling.md
+  - [ ] 04-wasm-php-javascript-module.md
+  - [ ] 05-wasm-php-filesystem.md
+  - [ ] 07-wasm-asyncify.md
+  - [ ] 08-browser-concepts.md
+  - [ ] 09-browser-tab-orchestrates-execution.md
+  - [ ] 10-browser-iframe-rendering.md
+  - [ ] 11-browser-php-worker-threads.md
+  - [ ] 12-browser-service-workers.md
+  - [ ] 13-browser-scopes.md
+  - [ ] 14-browser-cross-process-communication.md
+  - [ ] 15-wordpress.md
+  - [ ] 16-wordpress-database.md
+  - [ ] 17-browser-wordpress.md
+  - [ ] 18-host-your-own-playground.md
+- 24-limitations
+  - [ ] 01-index.md
+- [ ] intro-devs.md
+
+</details>
+

Translating with the GitHub Web Interface

+

If you prefer not to use developer tools, you can easily contribute translations directly on the GitHub website. All you need is a free GitHub account.

+

This guide will show you how to both update an existing translation and add a brand-new one.

+

---

+

Updating an Existing Translation

+
  1. Navigate to the file. Go to the repository and find the file you want to update. Translation files are located in a folder named after their language code. For example, all French translations are in packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/.
+
  1. Open the editor. Select the file you wish to edit and click the pencil icon (Edit this file) in the upper right corner.
+

Editing existing translation

+
  1. Fork the repository. GitHub will automatically prompt you to Fork this repository. This creates a personal copy for you to edit safely. Click the button to proceed.
+
  1. Make your changes. The editor will open in your browser. Update the text with your improved translations.
+
  1. Propose your changes. Once you are finished, scroll to the bottom of the page. Add a brief title and description of your changes (e.g., "Fixing typos in French translation") and click the Propose changes button.
+
  1. Create a Pull Request. On the next screen, click the Create pull request button. This will submit your changes to the project maintainers for review.
+

---

+

Adding a New Translation

+
  1. Determine the correct file path. The new file's path and name must mirror the original English file.
+
  • English original: packages/docs/site/docs/main/contributing/documentation.md
  • French translation: packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/main/contributing/documentation.md
+
  1. Create the new file. Navigate to the correct language folder (e.g., /packages/docs/site/i18n/fr/docusaurus-plugin-content-docs/current/). Click Add file > Create new file.
+

Creating a new translation

+
  • Pro Tip: In the filename box, you can create new folders by typing the folder name followed by a /. For example, typing main/contributing/documentation.md will create the main and contributing folders automatically.
+
  1. Fork the repository. Just like before, GitHub will prompt you to Fork this repository. Click the button to create your personal copy.
+
  1. Add the translated content. The editor will open with an empty file. For the convenience of reviewers, please copy the content from the original English file and paste it into your new file, wrapping it in comment tags. Add your translation below it.
+
    <!--
+    This is the original English content.
+    It helps reviewers understand the context of the translation.
+    -->
+
+    Ceci est le contenu traduit en français.
+

GitHub UI Editor

+
  1. Commit the new file. When you are done, scroll to the bottom. Add a title for your new file (e.g., "Add French translation for documentation.md") and click the Commit new file button.
+
  1. Create a Pull Request. On the next screen, click Create pull request to submit your new translation for review.
+

Review Process

+

To simplify the review process, please keep the original English text as a comment directly above the translated content.

+
<!--
+👋 Hi! Welcome to WordPress Playground documentation.
+
+Playground is an online tool to experiment and learn about WordPress. This site (Documentation) is where you will find all the information you need to start using Playground.
+-->
+
+👋 Olá! Bem vindo a documentação oficial do WordPress Playground.
+
+WordPress Playground é uma ferramenta online onde podes testar e aprender mais sobre o WordPress. Nesta página(Documentação) irá encontrar todas as informações necessárias para começar a trabalhar com o Playground.
+

This practice also helps the maintenance team identify outdated translations. When the original English content is updated, we can search the codebase for the old text (now in comments) and flag the corresponding translation for review.

+

To find a reviewer fluent in the language of your PR, you can post a request on the Make WordPress Polyglots blog. 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

]]>
+ + 64 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Using the WordPress Playground Agent Skill]]> + https://playground.wordpress.net/guides-agent-skill-wp-playground/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=65 + + Using the WordPress Playground Agent Skill +

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.

+

Prerequisites

+

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.

+

Installation

+

1. Install via terminal

+

Install the skill using the npx skills CLI:

+
npx skills add wordpress/agent-skills --skill wp-playground
+

2. Install manually

+
# 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 Codex
+

Verify 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
+

Use the skill in the terminal

+

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.

+

Generating content on the fly

+

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.

+

Version compatibility testing

+

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"                              |
+

Complex scenarios

+

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.

+

How the skill works

+

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:

+
  • SKILL.MD — The main procedure: guardrails, step-by-step workflows, verification checks, and failure modes.
  • references/cli-commands.md — A CLI cheatsheet with every flag and default value.
  • references/blueprint.md — Blueprint structure, common steps, and authoring tips.
  • references/debugging.md — Xdebug setup, path mapping, and troubleshooting steps.
+

Your coding agent reads these files before generating commands, ensuring correct flags and warning you about common pitfalls.

+

Next steps

+ +

Original Playground docs source: https://playground.wordpress.net/guides/agent-skill-wp-playground

]]>
+ + 65 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[E2E Testing with Playwright and WordPress Playground]]> + https://playground.wordpress.net/guides-e2e-testing-with-playwright/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=66 + + End-to-end testing verifies that your WordPress plugin or theme works correctly from a user's perspective — clicking buttons, filling forms, and navigating pages in a real browser. This guide shows how to combine Playwright with the WordPress Playground CLI to write reliable E2E tests without Docker, databases, or manual setup.

+

This guide assumes familiarity with WordPress plugin or theme development. For an introduction to using Playground in your development workflow, see WordPress Playground for Plugin Developers. For Blueprint configuration details, see Blueprints Getting Started.

+

Prerequisites

+
  • Node.js 20+ and up
  • A WordPress plugin/theme or an entire WordPress site to test
  • Recommended: enable the @typescript-eslint/no-floating-promises ESLint rule to catch missing await on async Playwright calls
+

Project setup

+

Install dependencies

+

From your plugin or theme root directory:

+
npm init -y
+npm install --save-dev @playwright/test @wp-playground/cli
+npx playwright install chromium
+

This installs Playwright as the test runner, the Playground CLI for creating WordPress instances, and the Chromium browser for test execution.

+

Configure Playwright

+

Create a playwright.config.ts file in your project root:

+
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.

+

First test file

+

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
+

Choosing locators

+

Playwright provides several ways to find elements on the page. Prefer locators that reflect how users see the page, falling back to CSS selectors only when necessary.

+

Locator priority (most to least preferred):

+
  1. page.getByRole() — buttons, headings, links, form controls
  2. page.getByLabel() — form inputs with associated labels
  3. page.getByText() — visible text content
  4. page.getByTestId() — elements with data-testid attributes you add to your plugin
  5. page.locator() — CSS or XPath selectors as a last resort
+

WordPress-specific guidance

+

In the WordPress admin, some core elements (admin bar, meta boxes) rely on IDs and CSS classes rather than ARIA roles. However, many elements work well with semantic locators. This means:

+
  • Use semantic locators for buttons, headings, links, form fields, and admin menu items — WordPress renders standard <button>, <input>, <a>, and <h1> elements that getByRole and getByLabel can find.
  • Use data-testid for your own plugin markup — you control the HTML, so add testable attributes.
  • Use CSS selectors for WordPress core layout elements like #wpadminbar or #wpbody-content — these lack ARIA alternatives.
+

Same element, three approaches

+
// ✅ Preferred: semantic locator (works because WP renders a real <button>)
+await page.getByRole('button', { name: 'Save Changes' }).click();
+
+// ⚠️ Acceptable: test ID you added to your plugin markup
+await page.getByTestId('save-settings').click();
+
+// ❌ Avoid: brittle CSS selector tied to WordPress markup
+await page.locator('#submit').click();
+

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.

+

Auto-waiting and web-first assertions

+

Playwright locators wait automatically for elements to appear, become visible, and become actionable. You do not need manual waitForSelector calls in most cases.

+

Web-first assertions

+

Web-first assertions auto-retry until the condition passes or the timeout expires. Always prefer them over manual checks:

+
// ✅ Web-first assertion (auto-retries until visible or timeout)
+await expect(page.getByText('Settings saved')).toBeVisible();
+
+// ❌ Manual check (no retry — flaky if the element appears after a delay)
+expect(await page.getByText('Settings saved').isVisible()).toBe(true);
+

Soft assertions

+

Use expect.soft() to check multiple things on one page without stopping at the first failure. All failures appear in the test report:

+
await expect.soft(page.getByLabel('API Key')).toHaveValue('test-key-123');
+await expect.soft(page.getByText('Settings saved')).toBeVisible();
+await expect.soft(page.getByRole('heading', { level: 1 })).toContainText('Settings');
+

Writing tests

+

Starting a Playground server

+

The runCLI function starts a local Playground server and returns an object with serverUrl (the URL string) and server (the HTTP server instance). Pass a Blueprint to configure the WordPress instance:

+
const cli = await runCLI({
+	command: 'server',
+	blueprint: {
+		preferredVersions: { php: '8.3', wp: 'latest' },
+		login: true,
+		steps: [
+			{
+				step: 'installPlugin',
+				pluginData: {
+					resource: 'wordpress.org/plugins',
+					slug: 'woocommerce',
+				},
+			},
+		],
+	},
+});
+

Server lifecycle: shared vs. per-test

+

Shared server (beforeAll/afterAll) — one Playground instance serves all tests in a describe block. Faster, but tests can affect each other:

+
test.describe('Plugin settings', () => {
+	test.beforeAll(async () => {
+		cli = await runCLI({ command: 'server', blueprint });
+	});
+	test.afterAll(async () => {
+		await cli?.server?.close();
+	});
+	// Tests share the same WordPress instance
+});
+

Per-test server (beforeEach/afterEach) — each test gets a fresh instance. Slower, but fully isolated:

+
test.beforeEach(async () => {
+	cli = await runCLI({ command: 'server', blueprint });
+});
+test.afterEach(async () => {
+	await cli?.server?.close();
+});
+

Use shared servers when tests only read state (checking pages render). Use per-test servers when tests modify state (creating posts, changing settings).

+

Using Blueprints as test fixtures

+

Blueprints define the WordPress state each test scenario needs. Here are common patterns:

+

Installing a plugin from wordpress.org

+
const blueprint = {
+	preferredVersions: { php: '8.3', wp: 'latest' },
+	login: true,
+	steps: [
+		{
+			step: 'installPlugin',
+			pluginData: {
+				resource: 'wordpress.org/plugins',
+				slug: 'contact-form-7',
+			},
+		},
+	],
+};
+

Installing a local plugin

+

Mount your local plugin directory into the Playground instance:

+
const cli = await runCLI({
+	command: 'server',
+	mount: {
+		'./': '/wordpress/wp-content/plugins/my-plugin',
+	},
+	blueprint: {
+		preferredVersions: { php: '8.3', wp: 'latest' },
+		login: true,
+		steps: [
+			{
+				step: 'activatePlugin',
+				pluginPath: 'my-plugin/my-plugin.php',
+			},
+		],
+	},
+});
+

This maps your current directory to the plugin path inside WordPress, then activates the plugin. Changes to your local files are reflected immediately. The user can set the autoMount property to identify plugins and themes, but the mount property will provide more control to the user to set different folders in the project.

+

Setting options and creating content

+
const blueprint = {
+	login: true,
+	steps: [
+		{
+			step: 'setSiteOptions',
+			options: {
+				blogname: 'Test Site',
+				permalink_structure: '/%postname%/',
+			},
+		},
+		{
+			step: 'runPHP',
+			code: `<?php
+        require '/wordpress/wp-load.php';
+        wp_insert_post([
+          'post_title' => 'Test Post',
+          'post_content' => '<!-- wp:paragraph --><p>Hello World</p><!-- /wp:paragraph -->',
+          'post_status' => 'publish',
+        ]);
+      `,
+		},
+	],
+};
+

Tip

+

Use the Playground Step Library or Pootle Playground to prototype your Blueprint configuration visually before adding it to your test code.

+

Testing WordPress admin pages

+

Navigate to admin pages and interact with the WordPress UI:

+
test('plugin settings page saves options', async ({ page }) => {
+	await page.goto(`${cli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);
+
+	await page.getByLabel('API Key').fill('test-key-123');
+	await page.getByRole('button', { name: 'Save Changes' }).click();
+
+	await expect(page.getByText('Settings saved')).toBeVisible();
+	await expect(page.getByLabel('API Key')).toHaveValue('test-key-123');
+});
+

Handling common admin UI elements

+
// Dismiss WordPress admin notices (WP adds aria-label to dismiss buttons)
+await page.getByRole('button', { name: 'Dismiss this notice' }).first().click();
+
+// Wait for admin bar to load — no ARIA role available, use locator
+await page.locator('#wpadminbar').waitFor();
+
+// Navigate via admin menu
+await page.getByRole('link', { name: 'My Plugin' }).first().click();
+

Testing the front end

+
test('plugin shortcode renders on front end', async ({ page }) => {
+	// Navigate to a page with the shortcode
+	await page.goto(`${cli.serverUrl}/?p=2`);
+
+	// Recommend: add data-testid="my-plugin-widget" to your plugin markup
+	await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
+	await expect(page.getByTestId('my-plugin-widget')).toContainText('Expected content');
+	// Or use CSS if you don't control the markup:
+	// await expect(page.locator(".my-plugin-widget")).toBeVisible();
+});
+
+test('theme displays post correctly', async ({ page }) => {
+	await page.goto(`${cli.serverUrl}/test-post/`);
+
+	await expect(page.getByRole('heading', { level: 1 })).toContainText('Test Post');
+	await expect(page.getByText('Hello World', { exact: true })).toBeVisible();
+});
+

Page Object Model pattern

+

The Page Object Model (POM) wraps page interactions into reusable classes. This reduces duplication and makes tests easier to maintain when your plugin UI changes.

+
// tests/e2e/pages/plugin-settings.ts
+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.

+

Testing across PHP and WordPress versions

+

Parameterized tests cover multiple version combinations without duplicating test code:

+
const versionMatrix = [
+	{ php: '8.1', wp: '6.5' },
+	{ php: '8.2', wp: '6.7' },
+	{ php: '8.3', wp: 'latest' },
+];
+
+for (const { php, wp } of versionMatrix) {
+	test.describe(`PHP ${php} + WP ${wp}`, () => {
+		let versionCli: Awaited<ReturnType<typeof runCLI>>;
+
+		test.beforeAll(async () => {
+			versionCli = await runCLI({
+				command: 'server',
+				blueprint: {
+					preferredVersions: { php, wp },
+					login: true,
+					steps: [
+						{
+							step: 'activatePlugin',
+							pluginPath: 'my-plugin/my-plugin.php',
+						},
+					],
+				},
+			});
+		});
+
+		test('admin page loads without errors', async ({ page }) => {
+			await page.goto(`${versionCli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);
+			// WordPress core elements use CSS selectors — no ARIA roles available
+			await expect(page.locator('.error')).not.toBeVisible();
+			await expect(page.locator('#wpbody-content')).toBeVisible();
+		});
+
+		test('front-end output renders', async ({ page }) => {
+			await page.goto(versionCli.serverUrl);
+			await expect(page.getByTestId('my-plugin-widget')).toBeVisible();
+		});
+
+		test.afterAll(async () => {
+			await versionCli?.server?.close();
+		});
+	});
+}
+

The preferredVersions property in the Blueprint controls which PHP and WordPress versions the Playground instance uses. Supported ranges: PHP 7.4–8.5, WordPress 6.3–6.8+, plus latest, nightly, and beta. For type-safe PHP version values, use the SupportedPHPVersion type from @php-wasm/universal.

+

Running tests in CI/CD

+

GitHub Actions

+

Create .github/workflows/e2e-tests.yml:

+
name: E2E Tests
+
+on:
+    push:
+        branches: [main]
+    pull_request:
+        branches: [main]
+
+jobs:
+    e2e:
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+
+            - uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: 'npm'
+
+            - name: Install dependencies
+              run: npm ci
+
+            - name: Cache Playwright browsers
+              uses: actions/cache@v4
+              id: playwright-cache
+              with:
+                  path: ~/.cache/ms-playwright
+                  key: playwright-${{ hashFiles('package-lock.json') }}
+
+            - name: Install Playwright browsers
+              if: steps.playwright-cache.outputs.cache-hit != 'true'
+              run: npx playwright install chromium --with-deps
+
+            - name: Run E2E tests
+              run: npx playwright test
+
+            - name: Upload test report
+              uses: actions/upload-artifact@v4
+              if: ${{ !cancelled() }}
+              with:
+                  name: playwright-report
+                  path: playwright-report/
+                  retention-days: 30
+

This workflow installs dependencies, downloads Chromium, runs the tests, and uploads the HTML report as an artifact. The --with-deps flag installs system libraries Chromium needs on Ubuntu.

+

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.

+

Troubleshooting

+

Timeout errors — Increase timeout in playwright.config.ts. WordPress boot time varies by environment. CI runners often need 120–180 seconds.

+

Port conflicts — Let Playground auto-assign ports. Do not hardcode port numbers in your configuration. The serverUrl property returns the correct URL.

+

Browser not found — Run npx playwright install chromium to download the browser binary. On CI, add --with-deps for system libraries.

+

WordPress not loading — Check your Blueprint syntax against the Blueprint schema. Invalid steps fail silently in some cases.

+

Tests pass locally but fail in CI — CI runners have less memory and CPU. Increase timeouts, reduce parallel workers, and ensure workers: 1 in the config.

+

Debugging tests

+

When a test fails, Playwright provides several tools to investigate:

+

Playwright Inspector — step through tests interactively with a built-in debugger:

+
npx playwright test --debug
+

Trace viewer — inspect a timeline of actions, DOM snapshots, and network requests from a failed test. The trace: "on-first-retry" setting in the config above captures traces automatically:

+
npx playwright show-trace test-results/plugin-spec-ts/trace.zip
+

UI mode — run tests in a visual interface where you can watch, filter, and re-run them:

+
npx playwright test --ui
+

Screenshot on failure — the screenshot: "only-on-failure" setting in the config saves a screenshot whenever a test fails. Find screenshots in the test-results/ directory.

+

Tip

+

Combine --debug with a specific test file to focus your investigation: npx playwright test tests/e2e/settings.spec.ts --debug

+

Next steps

+ +

Original Playground docs source: https://playground.wordpress.net/guides/e2e-testing-with-playwright

]]>
+ + 66 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground for Plugin Developers]]> + https://playground.wordpress.net/guides-for-plugin-developers/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=67 + + The WordPress Playground is an innovative tool that allows plugin developers to build, test and showcase their plugins directly in a browser environment.

+

This guide will show you how to use WordPress Playground to improve your plugin development workflow, create live demos to showcase your plugin, and simplify your plugin testing and review.

+

Discover how to Build, Test, and Launch your products with WordPress Playground in the About Playground section.

+

Launching a Playground instance with a plugin

+

Plugin in the WordPress plugins directory

+

With WordPress Playground, you can quickly launch a WordPress installation with almost any plugin available in the WordPress Plugins Directory 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> &nbsp; Run Blueprint &nbsp; </kbd>

+

Blueprints can be passed to a Playground instance in several ways.

+

Plugin in a GitHub repository

+

A plugin stored in a GitHub repository can also be loaded in a Playground instance via Blueprints.

+

With the pluginData property of the installPlugin blueprint step, 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> &nbsp; Run Blueprint &nbsp; </kbd>

+

Plugin from code in a file or gist in GitHub

+

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> &nbsp; Run Blueprint &nbsp; </kbd>

+

The Install plugin from a gist example in the Blueprints Gallery shows how to load a plugin from code in a gist

+

Setting up a demo for your plugin with Blueprints

+

When providing a link to a WordPress Playground instance with some plugins activated, you may also want to customize the initial setup for that Playground instance using those plugins. With Playground's Blueprints 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:

+
  • Check the Blueprints Gallery to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.
  • The WordPress Playground Step Library tool provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also create your own steps!
  • The Blueprints builder tool allows you edit your blueprint online and run it directly in a Playground instance.
+

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.

+

plugins

+

If your plugin has dependencies on other plugins you can use the plugins shorthand to install yours along with any other needed plugins.

+
{
+	"landingPage": "/wp-admin/plugins.php",
+	"plugins": ["gutenberg", "sql-buddy", "create-block-theme"],
+	"login": true
+}
+

<kbd> &nbsp; Run Blueprint &nbsp; </kbd>

+

landingPage

+

If your plugin has a settings view or onboarding wizard, you can use the landingPage shorthand to automatically redirect to any page in the Playground instance upon loading.

+
{
+	"landingPage": "/wp-admin/admin.php?page=my-custom-gutenberg-app",
+	"login": true,
+	"plugins": ["https://raw.githubusercontent.com/WordPress/block-development-examples/deploy/zips/data-basics-59c8f8.zip"]
+}
+

<kbd> &nbsp; Run Blueprint &nbsp; </kbd>

+

writeFile

+

With 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"
+			}
+		}
+	]
+}
+

Plugin Development

+

Local plugin development and testing with Playground

+

From a plugins' folder in your local development environment, you can quickly load locally a Playground instance with that plugin loaded and activated.

+

Use the @wp-playground/cli command 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
+

See your local changes in a Playground instance and directly create PRs in a GitHub repo with your changes

+

With Google Chrome you can synchronize a Playground instance with your local plugin's code and your plugin's GitHub repo. With this connection you can:

+
  • See live (in the Playground instance) your local changes
  • Create PRs in the GitHub repo with your changes
+

Here's a little demo of this workflow in action:

+

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

]]>
+ + 67 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground for Theme Developers]]> + https://playground.wordpress.net/guides-for-theme-developers/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=68 + + The WordPress Playground is an innovative tool that allows theme developers to build, test, and showcase their themes directly in a browser environment.

+

This guide will show you how to use WordPress Playground to improve your theme development workflow, create live demos to showcase your theme, and simplify the theme review process.

+

Discover how to Build, Test, and Launch your products with WordPress Playground in the About Playground section

+

Launching a Playground instance with a theme

+

Themes in the WordPress themes directory

+

With WordPress Playground, you can quickly launch a WordPress installation using any theme available in the WordPress Themes Directory. 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> &nbsp; Run Blueprint &nbsp; </kbd>

+

Themes in a GitHub repository

+

A theme stored in a GitHub repository can also be loaded in a Playground instance with Blueprints.

+

With the themeData property of the installTheme blueprint step, 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> &nbsp; Run Blueprint &nbsp; </kbd>

+

A blueprint can be passed to a Playground instance in several ways.

+

Setting up a demo theme with Blueprints

+

When providing a link to a WordPress Playground instance with a specific theme activated, you may also want to customize the initial setup for that theme. With Playground's Blueprints you can load, activate, and configure a theme.

+

Tip

+

Some useful tools and resources provided by the Playground project to work with blueprints are:

+
  • Check the Blueprints Gallery to explore real-world code examples of using WordPress Playground to launch a WordPress site with a variety of setups.
  • The WordPress Playground Step Library tool provides a visual interface to drag or click the steps to create a blueprint for WordPress Playground. You can also create your own steps!
  • The Blueprints builder tool allows you edit your blueprint online and run it directly in a Playground instance.
+

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.

+

resetData

+

With the resetData step, you can remove the default content of a WordPress installation in order to import your own content.

+
"steps": [
+	...,
+	{
+		"step": "resetData"
+	},
+	...
+]
+

<kbd> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </kbd>

+

writeFile

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </kbd>

+

updateUserMeta

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </kbd>

+

setSiteOptions

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </kbd>

+

There's also a siteOptions shorthand that can be used instead of the setSiteOptions step.

+

plugins

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </kbd>

+

You can also use the installPlugin step to install and activate plugins for your Playground instance but the shorthand way is recommended.

+

login

+

With the login shorthand you can launch your Playground instance with the admin user logged in.

+
 "login": true,
+

<kbd> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </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.

+

Theme development

+

Local theme development and testing with Playground

+

From the root folder of a block theme's code, you can quickly load locally a Playground instance with that theme loaded and activated. You can do that by launching, in a theme directory, the @wp-playground/cli command 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
+

Design your theme using the WordPress UI and save your changes as Pull Requests

+

You can connect your Playground instance to a GitHub repository and create a Pull Request with the changes you’ve done through the WordPress UI in the Playground instance, leveraging the Create Block Theme 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

]]>
+ + 68 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Adding PR Preview Buttons with GitHub Actions]]> + https://playground.wordpress.net/guides-github-action-pr-preview/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=69 + + The Playground PR Preview action adds a preview button to your pull requests. Clicking the button launches Playground with your plugin or theme installed from the PR branch:

+

PR Preview Button

+

For complete configuration options and advanced features, see the action-wp-playground-pr-preview workflow README.

+

How it works

+

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.

+

Basic setup for plugins

+

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.

+

Basic setup for themes

+

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: .
+

Testing PRs from forks

+

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.

+

Button placement

+

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
+

Working with built artifacts

+

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:

+
  • A pull_request workflow checks out the PR code, runs the build with read-only permissions, and uploads the ZIP as a GitHub Actions artifact.
  • A 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.

+

Custom blueprints

+

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.

+

Template customization

+

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.

+

Artifact exposure

+

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

+

Troubleshooting

+

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

+

Other ways of previewing a git repository

+

Embedded media: https://www.youtube.com/embed/2VQkCPYyabQ?si=g5zkAZelHZ9bkN1X

+

Examples

+ +

Next steps

+
  • Add demo content (guide)
  • Create custom blueprints (docs)
+

Original Playground docs source: https://playground.wordpress.net/guides/github-action-pr-preview

]]>
+ + 69 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[📖 Guides]]> + https://playground.wordpress.net/guides/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=70 + + Guides +

In this section we present a selection of guides that will help you to both work with, and to better understand, a variety of topics related to WordPress Playground.

+

PHP code snippets and embeds

+

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.

+

WordPress Playground for Everyone

+

Think Playground is only for developers? Think again. This guide shows how WordPress Playground helps beginners, site owners, and everyday users experiment safely — no technical expertise required.

+

How to ship a real WordPress site in a native iOS app via Playground?

+

Check "Blocknotes", the first app to run WordPress natively on iOS via WordPress Playground. It showcases the potential for seamless mobile web integration using WebAssembly and the WordPress block editor.

+

Providing content for your demo with WordPress Playground

+

To provide a good demo of your theme or plugin via Playground, you may want to load it with default content that highlights the features of your product. Check this guide to learn how to do so.

+

WordPress Playground for Theme Developers

+

This guide will show you the essential settings to fully create a theme demo using WordPress Playground and how you can leverage it during the building stage.

+

WordPress Playground for Plugin Developers

+

This guide will show you the basic settings to showcase your plugin using WordPress Playground and how to use it while developing your plugin.

+

Adding PR Preview Buttons with GitHub Actions

+

Learn how to automatically add one-click preview buttons to your pull requests. When someone opens a PR on your plugin or theme repository, they get an instant link to test the changes in a fully configured WordPress instance running in the browser.

+

Using the WordPress Playground Agent Skill

+

Automate WordPress Playground workflows with Claude Code. Learn how to install the wp-playground agent skill and use it for local testing, Blueprint execution, snapshot building, version switching, and debugging.

+

Programmatic Usage of Playground CLI

+

Learn how to use the runCLI function to control WordPress Playground programmatically from JavaScript/TypeScript for automation, end-to-end testing, and CI/CD pipelines.

+

E2E Testing with Playwright and WordPress Playground

+

Set up automated end-to-end tests for your WordPress plugins and themes using Playwright and the WordPress Playground CLI.

+

Original Playground docs source: https://playground.wordpress.net/guides

]]>
+ + 70 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[PHP code snippets and embeds]]> + https://playground.wordpress.net/guides-php-code-snippets/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=71 + + PHP code snippets and embeds +

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.
  • Standalone PHP Playground — a full-page PHP editor at playground.wordpress.net/php-playground.html with a shareable URL.
+

Embedding with <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.

+

Quick start

+
<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.

+

Why a <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">
+	&lt;?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>
+

WordPress is available

+

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>
+

How runtime sharing works

+

The first time a visitor clicks Run on any snippet on the page, the component:

+
  1. Lazy-loads the Playground client (https://playground.wordpress.net/client/index.js),
  2. Creates one hidden iframe pointing at https://playground.wordpress.net/remote.html,
  3. Boots PHP and WordPress inside it.
+

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.

+

Sharing a blueprint across snippets

+

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.

+

Editable snippets

+

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.

+

Attributes

+
| 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

+

Standalone PHP Playground

+

For full-page editing or sharing a one-off snippet via URL, use the standalone PHP Playground at:

+

playground.wordpress.net/php-playground.html

+

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 }.

+

Standalone PHP Playground <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

]]>
+ web component, or share full PHP demos via the standalone PHP Playground.]]> + 71 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground for Everyone]]> + https://playground.wordpress.net/guides-playground-for-everyone/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=72 + + WordPress Playground for Everyone +

WordPress Playground lets you run WordPress instantly—no server, no setup, no risk. It works in your browser at playground.wordpress.net, 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

+

Think of It Like a Car Simulator

+

A car simulator gives you a steering wheel, pedals, and virtual streets. Practice driving, hit cones, make mistakes — nothing bad happens. No real car gets damaged. Want to try again? Just restart.

+

WordPress Playground works the same way. It gives you a complete WordPress site to experiment with, but nothing you do affects any real website. Make changes, break things, learn from mistakes — then start fresh whenever you want.

+

WordPress Playground Landing Page

+

What Can You Do with Playground?

+

When you visit playground.wordpress.net, you get a WordPress site running entirely in your browser. You can:

+
  • Install plugins and themes
  • Edit pages and create content
  • Change WordPress and PHP versions
  • Explore features you've never tried before
+

By default, WordPress Playground loads a landing page 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.

+
  1. Open Launch WordPress Panel
+

Launch WordPress Panel

+
  1. Select to load a Vanilla WordPress version
+

Launching Vanilla WordPress

+

If You're Learning WordPress

+

Are you new to WordPress or trying to understand features like the Site Editor or the new features of the latest WordPress Release? Playground is your perfect practice space.

+

Explore How Pages Are Built

+

Playground logs you in as an administrator, so you can edit any page. Click Edit for editing posts and Edit Site to update the website layout in the top toolbar to open the editor.

+

Editing WordPress websites

+

Want to understand how a page layout was created? Open the List View (the three horizontal lines icon) to see every block that makes up the page.

+

Site Editor List view

+

You can inspect columns, headings, images, and buttons — and see exactly how they're arranged. This is a powerful way to learn by example.

+

Explore the Blueprint Library

+

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.

+
  1. Open the Blueprint gallery at the Playground Launch Panel
+

Open blueprint Gallery

+
  1. Navigate and select the Blueprint to launch at WordPress Playground
+

Open blueprint Gallery

+

Try New Features Safely

+

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.

+

If You Own a WordPress Site

+

Running a live website means every change risks breaking something. Playground lets you test before you commit.

+

Test Plugins Before Installing

+

Curious about a new SEO plugin? Want to compare two contact form options? Install them in Playground first:

+
  1. Open playground.wordpress.net
  2. Go to Plugins → Add New
  3. Search for and install any plugin
  4. Test it thoroughly
+

Your real site stays untouched while you evaluate whether the plugin fits your needs.

+

Installing Plugins

+

Preview Theme Changes

+

Thinking about switching themes? Test your new theme in Playground to see how it handles your content — without disrupting your visitors.

+

Compare Options Side by Side

+

Open multiple browser tabs with different Playground setups. Compare plugin A versus plugin B, or see how your content looks in different themes. Make informed decisions before touching your production site.

+

Your Real Site Stays Safe

+

Every Playground runs independently in your browser. Nothing syncs to any external server, and nothing affects your live WordPress installation.

+

If You Use WordPress Daily

+

Even experienced WordPress users benefit from a safe testing environment.

+

Make Design Experiments

+

Want to try a different font size? Adjust spacing? Change colors? Load a Playground with the same theme that you are using in production and edit it freely:

+
  1. Open any page in the editor
  2. Select a block and modify its settings
  3. See the results immediately
+

If you like what you see, recreate those changes on your real site. If not, just close the tab — no cleanup required.

+

Yes, You Can Save Your Work

+

Playground doesn't have to be temporary. You can save your progress and return to it later.

+

Save to Your Browser

+
  1. Now WordPress Playground teels you if your Playground instance is unsaved on the top right
  2. Click on Save (yellow button)
  3. Set the name of your instance
+

Saving Playgrounds

+

Playground generates a unique link for your saved site. Bookmark it, and you can return to exactly where you left off.

+

Download as a ZIP File

+

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.

+

Next Steps

+

Now that you know Playground is for everyone, explore further:

+ +

Original Playground docs source: https://playground.wordpress.net/guides/playground-for-everyone

]]>
+ + 72 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Programmatic Usage of Playground CLI]]> + https://playground.wordpress.net/guides-programmatic-playground-cli/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=73 + + Programmatic Usage of Playground CLI +

The Playground CLI can also be controlled programmatically from your JavaScript/TypeScript code using the runCLI function. This gives you direct access to all CLI functionalities within your code, which is useful for automating end-to-end tests. The options you pass to runCLI map directly to the CLI flags.

+

Embedded media: https://www.youtube.com/embed/rmNf3CfXbtA?si=cduqQYbBWc6zAPVj

+

Running a WordPress instance with a specific version

+

Using the runCLI function, you can specify options like the PHP and WordPress versions. In the example below, we request PHP 8.3, the latest version of WordPress, and to be automatically logged in. All supported arguments are defined in the RunCLIArgs type.

+
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
+

Setting a blueprint

+

You can provide a blueprint in two ways: either as an object literal directly passed to the blueprint property, or as a string containing the path to an external .json file.

+
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
+      }
+    }
+  ]
+};
+

Mounting a plugin programmatically

+

You can mount local directories programmatically using runCLI. The options mount and mount-before-install are available. The hostPath property expects a path to a directory on your local machine. This path should be relative to where your script is being executed.

+
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',
+    },
+  ],
+});
+

Combining mounts with blueprints

+

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"
+            }
+        ]
+    }
+});
+

Automated testing

+

Integration testing with Vitest

+

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);
+  });
+});
+

Testing with different WordPress/PHP versions

+
test('plugin works with WordPress 6.4 and PHP 8.3', async () => {
+  cliServer = await runCLI({
+    command: 'server',
+    php: '8.3',
+    wp: '6.4',
+    mount: [
+      {
+        hostPath: './my-plugin',
+        vfsPath: '/wordpress/wp-content/plugins/my-plugin'
+      }
+    ],
+    blueprint: {
+      steps: [
+        {
+          step: 'activatePlugin',
+          pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
+        }
+      ]
+    }
+  });
+
+  const homeUrl = new URL('/', cliServer.serverUrl);
+  const response = await fetch(homeUrl);
+
+  expect(response.status).toBe(200);
+});
+

Advanced configuration

+

Skip WordPress and SQLite setup

+

When you only need to test PHP code without WordPress, you can skip the setup for faster testing:

+
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
+

Error handling

+
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

]]>
+ + 73 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Providing content for your demo with Playground]]> + https://playground.wordpress.net/guides-providing-content-for-your-demo/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=74 + + One of the things you may want to do to provide a good demo with WordPress Playground is to load default content to better highlight the features of your plugin or theme. This default content may include images or other assets.

+

There are several blueprint steps and strategies you can use to import content (or generate it) in the Playground instance:

+

importWxr

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd> &nbsp; <kbd> &nbsp; See <code>blueprint.json</code> &nbsp; </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.

+

importWordPressFiles

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd>

+

importThemeStarterContent

+

Some 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> &nbsp; Run Blueprint &nbsp; </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> &nbsp; Run Blueprint &nbsp; </kbd>

+

wp-cli

+

Another 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> &nbsp; Run Blueprint &nbsp; </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> &nbsp; Run Blueprint &nbsp; </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.

+

runPHP

+

With 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> &nbsp; Run Blueprint &nbsp; </kbd>);%20?%3E%22}]}>)

+

Original Playground docs source: https://playground.wordpress.net/guides/providing-content-for-your-demo

]]>
+ + 74 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[WordPress Playground in native iOS apps]]> + https://playground.wordpress.net/guides-wordpress-native-ios-app/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=75 + + How to ship a real WordPress site in a native iOS app via Playground? +

Blocknotes is the first iOS application that ran WordPress natively on iOS devices by leveraging WordPress Playground. Developed by Ella van Durpe, a core committer for WordPress, Blocknotes represents a significant leap in the capabilities of mobile applications by utilizing WebAssembly to run WordPress without the need for a traditional PHP server.

+

This case study explores the features, technical implementation, and potential implications of Blocknotes for the future of mobile and web development.

+

Important! The current version of Blocknotes isn’t running WordPress Playground anymore. Since the initial release, the app was rewritten to only use the WordPress block editor without the rest of WordPress. This case study covers the early versions of Blocknotes that opened an entire world of new possibilities for WordPress.

+

Blocknotes features

+

Blocknotes allows users to create and edit notes using the WordPress block editor. The notes are automatically saved as HTML files to the user’s iCloud Drive and seamlessly synchronized across devices.

+

Technical Implementation

+

Blocknotes operated as a WebView running an HTML page where a WebAssembly version of PHP was running WordPress. That HTML page was packaged as a native iOS via Capacitor. 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:

+ +

Building your own iOS app with WordPress Playground

+

Although Blocknotes proved releasing a WordPress-based iOS app is possible, this is still a highly exploratory area. There are no established workflows, libraries, or knowledge bases.

+

The best documentation we have is the Blocknotes repository. Use it as a reference and a starting point for exploring your new app. Review the key components like the WebAssembly build of PHP, the integration of the WordPress block editor, and how web workers are utilized to run WordPress efficiently. By dissecting these elements, you can gain insights into building your own iOS app with WordPress Playground, pushing the boundaries of what’s possible with mobile web applications.

+

As you navigate this innovative space, share your findings and challenges with the Playground team and the broader WordPress community. Publishing your learnings will not only aid in your development but also contribute to a collective knowledge base, driving forward the future of WordPress on mobile.

+

Potential and the future

+

Blocknotes paves the way for a new generation of applications that are more accessible, flexible, and powerful.

+

Once the app-building workflows mature, we may see an automated pipelines for packaging Playground sites as iOS apps. It would make it extremely easy to run the same codebase on the server, in the browser, and as a mobile app.

+

By working together and sharing our findings, we can push the boundaries of what’s possible with WordPress and mobile app development

+

Original Playground docs source: https://playground.wordpress.net/guides/wordpress-native-ios-app

]]>
+ + 75 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + +
+ + + <![CDATA[Introduction]]> + https://playground.wordpress.net/wordpress-playground-docs/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=76 + + WordPress Playground Docs +

Looking for the official Playground website?

+

WordPress Playground website moved to wordpress.org/playground/. The site you're at now hosts the documentation.

+

👋 Hi! Welcome to WordPress Playground documentation.

+

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

+

<p class="docs-hubs">The WordPress Playground documentation is distributed across four separate hubs (subsites):</p>

+
  • 👉 Documentation (you're here) – Introduction to WP Playground, starter guides and your entry point to WP Playground Docs.
  • Blueprints – Blueprints are JSON files for setting up your WordPress Playground instance. Learn about their possibilities from the Blueprints docs hub.
  • Developers – WordPress Playground was created as a programmable tool. Discover all the things you can do with it from your code in the Developers docs hub.
  • API Reference – All the APIs exposed by WordPress Playground
+

Navigating this documentation hub

+

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

+ + +
  • About Playground: To learn about WordPress Playground, how safe it is, what you can do with and some of its current limitations, visit this section.
+

Discover how you can leverage WordPress Playground to Build, Test, and Launch your products.

+
  • Guides: Explore our comprehensive guides to master new skills, find step-by-step instructions, and unlock valuable insights. Dive in to learn and grow!
+
  • Contributing: WordPress Playground is an open-source project that welcomes all contributors—from code to design, documentation to triage. Learn here how to contribute.
+
  • Links and resources: A nice compilation of useful links and resources related to WordPress Playground.
+

First steps

+

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

+ +

Tip

+

Read Introduction to Playground: running WordPress in the browser blog post in the WordPress Developer Blog for a great introduction to WordPress Playground

+

Take a deep dive

+

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

+ +

Get Involved

+

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

+ +

As with all WordPress projects, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our Code of Conduct.

+

Using Playground with AI assistants

+

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

+
  • Using Playground with AI agents — Install the wp-playground skill for Claude Code, Cursor, Gemini CLI, GitHub Copilot, and other coding agents. Describe what you need; the agent runs the commands.
  • AI-readable site index — Machine-readable summary of Playground's capabilities, APIs, and docs in llms.txt format.
  • AGENTS.md — Guidance for AI coding agents contributing to this codebase.
+

License

+

WordPress Playground is free software released under the terms of the GNU General Public License version 2 or (at your option) any later version. For a complete license, see LICENSE.md.

+

Original Playground docs source: https://playground.wordpress.net/

]]>
+ + 76 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Quick Start Guide]]> + https://playground.wordpress.net/quick-start-guide/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=77 + + Start using WordPress Playground in 5 minutes +

WordPress Playground can help you with any of the following:

+

This page will guide you through each of these. Oh, and if you're a visual learner – here's a video:

+

Embedded media: https://video.wordpress.com/v/3UBIXJ9S?autoPlay=false&amp;height=1080&amp;width=1920&amp;fill=true

+

Start a new WordPress site

+

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!

+

Try a block, a theme, or a plugin

+

You can upload any plugin or theme you want in /wp-admin/.

+

To save a few clicks, you can preinstall plugins or themes from the WordPress plugin directory by adding a plugin or theme parameter to the URL. For example, to install the coblocks plugin, you can use this URL:

+

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

+

Or this URL to preinstall the pendant theme:

+

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

+

In case you would like to install multiple themes and plugins, it is possible to repeat the theme or plugin parameters:

+

https://playground.wordpress.net/?theme=pendant&theme=acai

+

You can also mix and match these parameters and even add multiple plugins:

+

https://playground.wordpress.net/?plugin=coblocks&plugin=friends&theme=pendant

+

This is called Query API and you can learn more about it here.

+

Save your site

+

To keep your WordPress Playground site for longer than a single browser session, you can export it as a .zip file.

+
  1. Open the Playground site manager panel:
+

Site Manager

+
  1. Use the "Download as .zip" button in the additional actions menu
+

Export button

+

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.

+

Restore a saved site

+

You can restore the saved site using the "Import from .zip" button in the Playground dashboard panel:

+
  1. Open the Playground dashboard panel:
+

Open Playground Dashboard

+
  1. Use the "Import .zip" button at the end of the "Start a new Playground" section
+

Open Playground Dashboard

+

Use a specific WordPress or PHP version

+

The quickest way to change the version of WordPress or PHP is by using the settings panel on the official demo site:

+

WordPress Playground Settings menu

+

Test your plugin or theme

+

Compatibility testing with so many WordPress and PHP versions was always a pain. WordPress Playground makes this process effortless – use it to your advantage!

+

You can also use the wp and php query parameters to open Playground with the right versions already loaded:

+
  • https://playground.wordpress.net/?wp=6.5
  • https://playground.wordpress.net/?php=8.3
  • https://playground.wordpress.net/?php=8.2&wp=6.2
+

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.

+

Import a WXR file

+

You can import a WordPress export file by uploading a WXR file in /wp-admin/.

+

You can also use JSON Blueprints. See getting started with Blueprints to learn more.

+

This is different from 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.

+

Build apps with WordPress Playground

+

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

]]>
+ + 77 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Links and Resources]]> + https://playground.wordpress.net/resources/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=78 + + Links and Resources +

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>

+

Frequently sought links

+ +

Apps built with WordPress Playground

+ +

Reading materials

+ +

Videos

+ +

Original Playground docs source: https://playground.wordpress.net/resources

]]>
+ + 78 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+ + + <![CDATA[Web Instance]]> + https://playground.wordpress.net/web-instance/ + Mon, 11 May 2026 00:00:00 +0000 + + https://playground.wordpress.net/?p=79 + + WordPress Playground web instance +

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:

+
  • Browser-based: No local server setup required.
  • Instant Setup: Run WordPress with a single click.
  • Testing Environment: Ideal for testing plugins and themes.
+

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.

+

Playground Toolbar Snapshot

+

Customize Playground

+

On the toolbar, you'll find:

+
  • Playground Settings: A panel for configuring your current instance, like PHP and WordPress versions.
  • Playground Dashboard: This panel lets you manage WordPress Playground instances, save and export them, edit files from your WordPress instance, and create new Blueprints.
  • Playground Launch Panel: The Launch Panel shows all the ways to launch a WordPress Playground instance.
+

Playground Settings

+

snapshot of customize Playground window at Playground instance

+

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.
+

Playground Manager

+

Playground settings panel allow users to save export and edit the WordPress directly

+

This panel lets you manage Playground instances and provides access to the following panels:

+
  • Settings: To manage the current Playground's settings
  • File Browser: Built-in IDE for editing files, uploading plugins and themes, and live editing. Playground auto-reloads changes in real time.
  • Blueprint: A Blueprint editor for creating, saving, and running Blueprints in your Playground web instance.
  • Database: Tools for managing the database with Adminer and phpMyAdmin, and downloading as a .sqlite file.
  • Logs: Displays log messages when something goes wrong.
+

Save Playground Button

+

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:

+

Additional actions menu

+

Additional actions Menu

+
  • Export Pull Request to GitHub: Export WordPress plugins, themes, and entire wp-content directories as pull requests to any public GitHub repository. Watch a demo of this feature.
  • Download as .zip: Creates a .zip file with the setup of the Playground instance, including any themes or plugins installed. This .zip excludes content and database changes.
+

Blueprint Editor

+

Blueprint editor WordPress Playground

+

The Blueprint editor provides the ability to manage multiple Blueprints and to validate code.

+

Launch Playground Panel

+

Playground Launch Panel

+

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

]]>
+ + 79 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + +
+
+
From 07a43b56063390a5f63fc43283010f1a5579fbb4 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 21/27] Add Personal RAG admin styles --- .../plugin/assets/personal-rag.css | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 blueprints/personal-rag/plugin/assets/personal-rag.css diff --git a/blueprints/personal-rag/plugin/assets/personal-rag.css b/blueprints/personal-rag/plugin/assets/personal-rag.css new file mode 100644 index 00000000..ebd2b707 --- /dev/null +++ b/blueprints/personal-rag/plugin/assets/personal-rag.css @@ -0,0 +1,289 @@ +.personal-rag-wrap { + max-width: 1180px; +} + +.personal-rag-shell { + display: flex; + flex-direction: column; + gap: 18px; +} + +.personal-rag-header { + align-items: flex-start; + display: flex; + gap: 16px; + justify-content: space-between; + padding: 10px 0 4px; +} + +.personal-rag-header h1, +.personal-rag-band h2 { + margin: 0; +} + +.personal-rag-header p { + color: #50575e; + font-size: 14px; + margin: 6px 0 0; + max-width: 620px; +} + +.personal-rag-band { + background: #fff; + border: 1px solid #c3c4c7; + padding: 18px; +} + +.personal-rag-grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(4, minmax(150px, 1fr)); + margin-top: 16px; +} + +.personal-rag-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.personal-rag-field span { + color: #1d2327; + font-weight: 600; +} + +.personal-rag-field input, +#personal-rag-question { + box-sizing: border-box; + width: 100%; +} + +.personal-rag-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 16px; +} + +.personal-rag-help { + color: #646970; +} + +.personal-rag-index-head, +.personal-rag-chat-header { + align-items: center; + display: flex; + gap: 16px; + justify-content: space-between; +} + +.personal-rag-status { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.personal-rag-status span { + align-items: baseline; + background: #f6f7f7; + border: 1px solid #dcdcde; + display: inline-flex; + gap: 5px; + min-height: 30px; + padding: 4px 9px; +} + +.personal-rag-status strong { + color: #135e96; + font-size: 18px; +} + +.personal-rag-notice { + border-left: 4px solid #72aee6; + padding: 11px 14px; +} + +.personal-rag-notice-info { + background: #f0f6fc; + border-left-color: #72aee6; +} + +.personal-rag-notice-success { + background: #edfaef; + border-left-color: #00a32a; +} + +.personal-rag-notice-warning { + background: #fcf9e8; + border-left-color: #dba617; +} + +.personal-rag-notice-error, +.personal-rag-error { + background: #fcf0f1; + border-left-color: #d63638; +} + +#personal-rag-progress { + height: 12px; + margin-top: 16px; + width: 100%; +} + +#personal-rag-question { + margin-top: 14px; + min-height: 110px; + resize: vertical; +} + +.personal-rag-answer { + border-top: 1px solid #dcdcde; + margin-top: 18px; + padding-top: 16px; +} + +.personal-rag-answer-text { + font-size: 15px; + line-height: 1.6; + max-width: 820px; +} + +.personal-rag-markdown > *:first-child { + margin-top: 0; +} + +.personal-rag-markdown > *:last-child { + margin-bottom: 0; +} + +.personal-rag-markdown h2, +.personal-rag-markdown h3, +.personal-rag-markdown h4 { + color: #1d2327; + font-weight: 600; + line-height: 1.25; + margin: 22px 0 8px; +} + +.personal-rag-markdown h2 { + font-size: 20px; +} + +.personal-rag-markdown h3 { + font-size: 18px; +} + +.personal-rag-markdown h4 { + font-size: 16px; +} + +.personal-rag-markdown p { + margin: 0 0 14px; +} + +.personal-rag-markdown ul, +.personal-rag-markdown ol { + margin: 0 0 16px 22px; + padding: 0; +} + +.personal-rag-markdown li { + margin: 5px 0; + padding-left: 2px; +} + +.personal-rag-markdown strong { + color: #1d2327; + font-weight: 700; +} + +.personal-rag-markdown code { + background: #f6f7f7; + border: 1px solid #dcdcde; + border-radius: 3px; + color: #1d2327; + font-size: 13px; + padding: 1px 4px; +} + +.personal-rag-markdown pre { + background: #101517; + border-radius: 4px; + color: #f6f7f7; + overflow-x: auto; + padding: 14px; + white-space: pre; +} + +.personal-rag-markdown pre code { + background: transparent; + border: 0; + color: inherit; + padding: 0; +} + +.personal-rag-markdown blockquote { + border-left: 4px solid #72aee6; + color: #3c434a; + margin: 0 0 16px; + padding: 1px 0 1px 14px; +} + +.personal-rag-markdown a { + color: #135e96; +} + +.personal-rag-loading { + color: #50575e; +} + +.personal-rag-error { + border-left: 4px solid #d63638; + padding: 10px 12px; +} + +.personal-rag-sources { + margin-top: 18px; +} + +.personal-rag-sources h3 { + font-size: 14px; + margin: 0 0 8px; +} + +.personal-rag-sources ol { + margin: 0; + padding-left: 22px; +} + +.personal-rag-sources li { + margin-bottom: 7px; +} + +.personal-rag-sources span { + color: #646970; + display: inline-block; + font-size: 12px; + margin-left: 8px; +} + +@media (max-width: 960px) { + .personal-rag-grid { + grid-template-columns: repeat(2, minmax(160px, 1fr)); + } +} + +@media (max-width: 640px) { + .personal-rag-header, + .personal-rag-index-head, + .personal-rag-chat-header { + align-items: stretch; + flex-direction: column; + } + + .personal-rag-grid { + grid-template-columns: 1fr; + } +} From 6862fe4e2caee666257b32552218fa61e374e542 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 22/27] Add Personal RAG admin app --- .../plugin/assets/personal-rag.js | 701 ++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 blueprints/personal-rag/plugin/assets/personal-rag.js diff --git a/blueprints/personal-rag/plugin/assets/personal-rag.js b/blueprints/personal-rag/plugin/assets/personal-rag.js new file mode 100644 index 00000000..696e041d --- /dev/null +++ b/blueprints/personal-rag/plugin/assets/personal-rag.js @@ -0,0 +1,701 @@ +(function () { + 'use strict'; + + var config = window.personalRagSettings || {}; + var defaults = config.defaults || {}; + var state = { + status: null, + isBusy: false, + settings: loadSettings(), + }; + + function loadSettings() { + return { + endpoint: localStorage.getItem('personalRag_endpoint') || defaults.endpoint || 'http://localhost:11434', + embeddingModel: localStorage.getItem('personalRag_embeddingModel') || defaults.embeddingModel || 'embeddinggemma', + chatModel: localStorage.getItem('personalRag_chatModel') || defaults.chatModel || 'gemma4:e4b', + topK: parseInt(localStorage.getItem('personalRag_topK') || defaults.topK || '5', 10), + }; + } + + function saveSettings() { + localStorage.setItem('personalRag_endpoint', state.settings.endpoint); + localStorage.setItem('personalRag_embeddingModel', state.settings.embeddingModel); + localStorage.setItem('personalRag_chatModel', state.settings.chatModel); + localStorage.setItem('personalRag_topK', String(state.settings.topK)); + } + + function endpoint() { + return String(state.settings.endpoint || '').replace(/\/+$/, ''); + } + + function rest(path, options) { + options = options || {}; + var headers = options.headers || {}; + headers['X-WP-Nonce'] = config.nonce; + if (options.body && !headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } + + return fetch(config.restUrl + path, Object.assign({}, options, { headers: headers })) + .then(function (response) { + return response.json().catch(function () { + return {}; + }).then(function (body) { + if (!response.ok) { + throw new Error(body.message || 'Request failed with status ' + response.status); + } + return body; + }); + }); + } + + function ollamaFetch(path, body, stream) { + return fetch(endpoint() + path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(function (response) { + if (!response.ok) { + return response.text().then(function (text) { + throw new Error(text || 'Ollama request failed with status ' + response.status); + }); + } + return stream ? response : response.json(); + }).catch(function (error) { + if (error instanceof TypeError) { + throw new Error('Could not reach Ollama from this browser origin. ' + corsHint()); + } + throw error; + }); + } + + function corsHint() { + return 'Start Ollama with OLLAMA_ORIGINS="' + window.location.origin + '" or add this origin to your existing OLLAMA_ORIGINS value.'; + } + + function escapeHtml(value) { + return String(value || '').replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }[char]; + }); + } + + function setBusy(isBusy) { + state.isBusy = isBusy; + document.querySelectorAll('#personal-rag-app button, #personal-rag-app input, #personal-rag-app textarea').forEach(function (el) { + if (el.dataset.alwaysEnabled === '1') { + return; + } + el.disabled = isBusy; + }); + } + + function setNotice(message, type) { + var notice = document.getElementById('personal-rag-notice'); + if (!notice) { + return; + } + notice.className = 'personal-rag-notice personal-rag-notice-' + (type || 'info'); + notice.textContent = message || ''; + notice.hidden = !message; + } + + function render() { + var root = document.getElementById('personal-rag-app'); + if (!root) { + return; + } + + root.innerHTML = [ + '
', + '
', + '
', + '

Personal RAG

', + '

Ask questions against your local WordPress posts and pages using Ollama.

', + '
', + '', + '
', + '', + '
', + '

Local Models

', + '
', + fieldHtml('Ollama endpoint', 'endpoint', state.settings.endpoint, 'http://localhost:11434'), + fieldHtml('Embedding model', 'embeddingModel', state.settings.embeddingModel, 'embeddinggemma'), + fieldHtml('Chat model', 'chatModel', state.settings.chatModel, 'gemma4:e4b'), + fieldHtml('Sources', 'topK', state.settings.topK, '5', 'number'), + '
', + '
', + '', + 'Required local models: ollama pull embeddinggemma and ollama pull gemma4:e4b.', + '
', + '
', + config.canManageOptions ? indexHtml() : '', + '
', + '
', + '

Ask Your Site

', + '', + '
', + '', + '
', + '', + '
', + '', + '
', + '
', + ].join(''); + + bindEvents(); + renderStatus(); + } + + function fieldHtml(label, key, value, placeholder, type) { + return ''; + } + + function indexHtml() { + return [ + '
', + '
', + '

Local Index

', + '
', + '
', + '
', + '', + '', + '', + '
', + '', + '
', + ].join(''); + } + + function bindEvents() { + document.getElementById('personal-rag-refresh').addEventListener('click', refreshStatus); + document.getElementById('personal-rag-test').addEventListener('click', testOllama); + document.getElementById('personal-rag-ask').addEventListener('click', askQuestion); + + document.querySelectorAll('[data-setting]').forEach(function (input) { + input.addEventListener('change', function () { + var key = input.dataset.setting; + var value = input.type === 'number' ? parseInt(input.value || '5', 10) : input.value.trim(); + if (key === 'topK') { + value = Math.max(1, Math.min(12, value || 5)); + } + state.settings[key] = value; + saveSettings(); + }); + }); + + if (config.canManageOptions) { + document.getElementById('personal-rag-queue').addEventListener('click', function () { + queueIndex(false); + }); + document.getElementById('personal-rag-index').addEventListener('click', rebuildEmbeddings); + document.getElementById('personal-rag-reset').addEventListener('click', resetIndex); + } + } + + function refreshStatus() { + return rest('/status').then(function (status) { + state.status = status; + renderStatus(); + return status; + }).catch(function (error) { + setNotice(error.message, 'error'); + }); + } + + function renderStatus() { + var statusEl = document.getElementById('personal-rag-status'); + if (!statusEl) { + return; + } + + var status = state.status; + if (!status) { + statusEl.textContent = 'Loading status...'; + return; + } + + statusEl.innerHTML = [ + statHtml('Sources', status.sources), + statHtml('Chunks', status.chunks), + statHtml('Queued', status.queued), + statHtml('Embedded', status.embedded), + ].join(''); + } + + function statHtml(label, value) { + return '' + escapeHtml(value) + '' + escapeHtml(label) + ''; + } + + function testOllama() { + setBusy(true); + setNotice('Testing Ollama...', 'info'); + return fetch(endpoint() + '/api/tags') + .then(function (response) { + if (!response.ok) { + throw new Error('Ollama responded with status ' + response.status); + } + return response.json(); + }) + .then(function (data) { + var models = (data.models || []).map(function (model) { + return model.name; + }); + var missing = [state.settings.embeddingModel, state.settings.chatModel].filter(function (model) { + return models.indexOf(model) === -1 && models.indexOf(model + ':latest') === -1; + }); + if (missing.length) { + setNotice('Ollama is reachable, but these models were not listed: ' + missing.join(', ') + '.', 'warning'); + } else { + setNotice('Ollama is reachable and the configured models are available.', 'success'); + } + }) + .catch(function (error) { + if (error instanceof TypeError) { + setNotice('Could not reach Ollama. ' + corsHint(), 'error'); + } else { + setNotice(error.message, 'error'); + } + }) + .finally(function () { + setBusy(false); + }); + } + + function queueIndex(force) { + setBusy(true); + setNotice(force ? 'Rebuilding local content chunks...' : 'Queueing changed content...', 'info'); + return rest('/index/queue', { + method: 'POST', + body: JSON.stringify({ force: !!force }), + }).then(function (result) { + state.status = result.status; + renderStatus(); + setNotice('Queued ' + result.queued + ' sources. ' + result.unchanged + ' unchanged.', 'success'); + return result; + }).catch(function (error) { + setNotice(error.message, 'error'); + }).finally(function () { + setBusy(false); + }); + } + + function rebuildEmbeddings() { + setBusy(true); + setProgress(0); + setNotice('Preparing local index...', 'info'); + + return rest('/index/queue', { + method: 'POST', + body: JSON.stringify({ force: true }), + }).then(function (result) { + state.status = result.status; + renderStatus(); + return embedQueuedChunks(); + }).catch(function (error) { + setNotice(error.message, 'error'); + }).finally(function () { + setProgress(null); + setBusy(false); + }); + } + + function embedQueuedChunks() { + var initialQueued = state.status ? state.status.queued : 0; + var completed = 0; + + function nextBatch() { + return rest('/index/batch?limit=8').then(function (batch) { + var items = batch.items || []; + state.status = batch.status || state.status; + renderStatus(); + + if (!items.length) { + setNotice('Index is ready. Embedded ' + completed + ' chunks locally.', 'success'); + setProgress(100); + return null; + } + + var inputs = items.map(function (item) { + return 'title: ' + (item.title || 'Untitled') + ' | text: ' + item.text; + }); + + setNotice('Embedding ' + items.length + ' chunks with ' + state.settings.embeddingModel + '...', 'info'); + return embed(inputs).then(function (embeddings) { + var payload = items.map(function (item, index) { + return { + chunkId: item.id, + vector: floatsToBase64(embeddings[index]), + dimensions: embeddings[index].length, + }; + }); + + return rest('/index/embeddings', { + method: 'POST', + body: JSON.stringify({ + model: state.settings.embeddingModel, + items: payload, + }), + }); + }).then(function (result) { + completed += result.saved || items.length; + state.status = result.status; + renderStatus(); + if (initialQueued > 0) { + setProgress(Math.min(99, Math.round((completed / initialQueued) * 100))); + } + return nextBatch(); + }); + }); + } + + return nextBatch(); + } + + function setProgress(value) { + var progress = document.getElementById('personal-rag-progress'); + if (!progress) { + return; + } + if (value === null) { + progress.hidden = true; + progress.value = 0; + return; + } + progress.hidden = false; + progress.value = value; + } + + function embed(input) { + return ollamaFetch('/api/embed', { + model: state.settings.embeddingModel, + input: input, + }).then(function (data) { + var embeddings = normalizeEmbeddings(data); + if (!embeddings.length) { + throw new Error('Ollama returned no embeddings.'); + } + return embeddings; + }); + } + + function normalizeEmbeddings(data) { + if (Array.isArray(data.embeddings)) { + return data.embeddings; + } + if (Array.isArray(data.embedding)) { + return [data.embedding]; + } + if (Array.isArray(data.data)) { + return data.data.map(function (item) { + return item.embedding; + }).filter(Boolean); + } + return []; + } + + function resetIndex() { + if (!window.confirm('Reset the local RAG index? Your posts and pages will not be deleted.')) { + return; + } + + setBusy(true); + setNotice('Resetting index...', 'info'); + return rest('/reset', { + method: 'POST', + body: JSON.stringify({}), + }).then(function (status) { + state.status = status; + renderStatus(); + setNotice('Index reset.', 'success'); + }).catch(function (error) { + setNotice(error.message, 'error'); + }).finally(function () { + setBusy(false); + }); + } + + function askQuestion() { + var questionEl = document.getElementById('personal-rag-question'); + var question = questionEl.value.trim(); + if (!question) { + questionEl.focus(); + return; + } + + var answer = document.getElementById('personal-rag-answer'); + answer.hidden = false; + answer.innerHTML = '
Searching local content...
'; + setBusy(true); + setNotice('', 'info'); + + return embed('task: search result | query: ' + question) + .then(function (embeddings) { + return rest('/search', { + method: 'POST', + body: JSON.stringify({ + vector: floatsToBase64(embeddings[0]), + model: state.settings.embeddingModel, + topK: state.settings.topK, + }), + }); + }) + .then(function (search) { + return streamAnswer(question, search.matches || []); + }) + .catch(function (error) { + answer.innerHTML = '
' + escapeHtml(error.message) + '
'; + }) + .finally(function () { + setBusy(false); + }); + } + + function streamAnswer(question, matches) { + var answer = document.getElementById('personal-rag-answer'); + var messages = buildMessages(question, matches); + var sourceHtml = renderSources(matches); + + answer.innerHTML = '
' + sourceHtml; + var textEl = answer.querySelector('.personal-rag-answer-text'); + var content = ''; + + return ollamaFetch('/api/chat', { + model: state.settings.chatModel, + stream: true, + messages: messages, + options: { + temperature: 0.2, + }, + }, true).then(function (response) { + var reader = response.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ''; + + function pump() { + return reader.read().then(function (result) { + if (result.done) { + textEl.innerHTML = formatAnswer(stripThinking(content)); + return; + } + buffer += decoder.decode(result.value, { stream: true }); + var lines = buffer.split('\n'); + buffer = lines.pop(); + lines.forEach(function (line) { + if (!line.trim()) { + return; + } + try { + var chunk = JSON.parse(line); + if (chunk.message && chunk.message.content) { + content += chunk.message.content; + textEl.innerHTML = formatAnswer(stripThinking(content)); + } + } catch (error) {} + }); + return pump(); + }); + } + + return pump(); + }); + } + + function buildMessages(question, matches) { + var system = [ + 'You are a private RAG assistant running inside WordPress.', + 'Answer only from the provided local WordPress sources.', + 'Use citations like [1], [2] for claims.', + 'If the sources do not contain enough evidence, say that clearly and do not guess.', + ].join(' '); + + var sources = matches.length ? matches.map(function (match, index) { + return '[' + (index + 1) + '] ' + match.title + '\nURL: ' + match.url + '\nText: ' + match.text; + }).join('\n\n') : 'No matching local sources were found.'; + + return [ + { role: 'system', content: system }, + { role: 'user', content: 'Question: ' + question + '\n\nLocal sources:\n' + sources }, + ]; + } + + function renderSources(matches) { + if (!matches.length) { + return '

Sources

No local sources matched this question.

'; + } + + return '

Sources

    ' + matches.map(function (match) { + return '
  1. ' + escapeHtml(match.title) + 'Score ' + escapeHtml(match.score) + '
  2. '; + }).join('') + '
'; + } + + function formatAnswer(value) { + return renderMarkdown(value || 'No answer returned.'); + } + + function renderMarkdown(value) { + var lines = String(value || '').replace(/\r\n?/g, '\n').split('\n'); + var html = []; + var index = 0; + + function isBlank(line) { + return !line || !line.trim(); + } + + function isBlockStart(line) { + return /^#{1,4}\s+/.test(line) || + /^```/.test(line) || + /^\s*[-*+]\s+/.test(line) || + /^\s*\d+[.)]\s+/.test(line) || + /^>\s?/.test(line); + } + + while (index < lines.length) { + var line = lines[index]; + + if (isBlank(line)) { + index++; + continue; + } + + var fence = line.match(/^```\s*([A-Za-z0-9_-]+)?\s*$/); + if (fence) { + var codeLines = []; + index++; + while (index < lines.length && !/^```\s*$/.test(lines[index])) { + codeLines.push(lines[index]); + index++; + } + if (index < lines.length) { + index++; + } + html.push('
' + 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('' + renderInlineMarkdown(heading[2]) + ''); + index++; + continue; + } + + if (/^>\s?/.test(line)) { + var quoteLines = []; + while (index < lines.length && /^>\s?/.test(lines[index])) { + quoteLines.push(lines[index].replace(/^>\s?/, '')); + index++; + } + 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('
  • ' + renderInlineMarkdown(match[1]) + '
  • '); + index++; + } + + return '<' + (ordered ? 'ol' : 'ul') + '>' + items.join('') + ''; + } + + function renderInlineMarkdown(value) { + var html = escapeHtml(value); + var codeSpans = []; + + html = html.replace(/`([^`]+)`/g, function (match, code) { + var token = '\u0000CODE' + codeSpans.length + '\u0000'; + codeSpans.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]*?/g, '') + .replace(/[\s\S]*?<\/think>/g, '') + .trim(); + } + + function floatsToBase64(values) { + if (!Array.isArray(values) && !(values instanceof Float32Array)) { + throw new Error('Invalid embedding vector.'); + } + var floats = values instanceof Float32Array ? values : new Float32Array(values); + var bytes = new Uint8Array(floats.buffer); + var binary = ''; + var chunkSize = 0x8000; + for (var i = 0; i < bytes.length; i += chunkSize) { + var chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode.apply(null, chunk); + } + return btoa(binary); + } + + document.addEventListener('DOMContentLoaded', function () { + render(); + refreshStatus(); + }); +})(); From 7d12193a623cfa20a3296c47ec081eccef0bc16f Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 23/27] Add Personal RAG plugin --- .../personal-rag/plugin/personal-rag.php | 889 ++++++++++++++++++ 1 file changed, 889 insertions(+) create mode 100644 blueprints/personal-rag/plugin/personal-rag.php diff --git a/blueprints/personal-rag/plugin/personal-rag.php b/blueprints/personal-rag/plugin/personal-rag.php new file mode 100644 index 00000000..c8de6621 --- /dev/null +++ b/blueprints/personal-rag/plugin/personal-rag.php @@ -0,0 +1,889 @@ +get_charset_collate(); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + dbDelta( + "CREATE TABLE {$tables['sources']} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + source_type varchar(20) NOT NULL DEFAULT 'post', + source_id bigint(20) unsigned NOT NULL, + post_type varchar(20) NOT NULL DEFAULT '', + post_status varchar(20) NOT NULL DEFAULT '', + title text NOT NULL, + url text NOT NULL, + content_hash char(64) NOT NULL DEFAULT '', + updated_at datetime NOT NULL, + indexed_at datetime DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY source_unique (source_type, source_id), + KEY content_hash (content_hash) + ) {$charset};" + ); + + dbDelta( + "CREATE TABLE {$tables['chunks']} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + source_id bigint(20) unsigned NOT NULL, + chunk_index int(11) NOT NULL DEFAULT 0, + chunk_text longtext NOT NULL, + chunk_hash char(64) NOT NULL DEFAULT '', + token_estimate int(11) NOT NULL DEFAULT 0, + embedding_status varchar(20) NOT NULL DEFAULT 'queued', + created_at datetime NOT NULL, + updated_at datetime NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY source_chunk (source_id, chunk_index), + KEY source_id (source_id), + KEY embedding_status (embedding_status) + ) {$charset};" + ); + + dbDelta( + "CREATE TABLE {$tables['vectors']} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + chunk_id bigint(20) unsigned NOT NULL, + model varchar(100) NOT NULL DEFAULT '', + dimensions int(11) NOT NULL DEFAULT 0, + vector longtext NOT NULL, + norm double NOT NULL DEFAULT 0, + created_at datetime NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY chunk_id (chunk_id), + KEY model_dimensions (model, dimensions) + ) {$charset};" + ); + + update_option( self::OPTION_VERSION, self::DB_VERSION ); + } + + private static function table_names() { + global $wpdb; + + return array( + 'sources' => $wpdb->prefix . 'personal_rag_sources', + 'chunks' => $wpdb->prefix . 'personal_rag_chunks', + 'vectors' => $wpdb->prefix . 'personal_rag_vectors', + ); + } + + public function add_admin_page() { + add_management_page( + __( 'Personal RAG', 'personal-rag' ), + __( 'Personal RAG', 'personal-rag' ), + 'read', + 'personal-rag', + array( $this, 'render_admin_page' ) + ); + } + + public function enqueue_admin_assets( $hook ) { + if ( 'tools_page_personal-rag' !== $hook ) { + return; + } + + wp_enqueue_style( + 'personal-rag-admin', + plugin_dir_url( __FILE__ ) . 'assets/personal-rag.css', + array(), + self::VERSION + ); + + wp_enqueue_script( + 'personal-rag-admin', + plugin_dir_url( __FILE__ ) . 'assets/personal-rag.js', + array(), + self::VERSION, + true + ); + + wp_localize_script( + 'personal-rag-admin', + 'personalRagSettings', + array( + 'restUrl' => esc_url_raw( rest_url( self::REST_NAMESPACE ) ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'canManageOptions' => current_user_can( 'manage_options' ), + 'origin' => esc_url_raw( home_url( '/' ) ), + 'defaults' => array( + 'endpoint' => 'http://localhost:11434', + 'embeddingModel' => 'embeddinggemma', + 'chatModel' => 'gemma4:e4b', + 'topK' => 5, + ), + ) + ); + } + + public function render_admin_page() { + ?> +
    +
    +

    +

    +
    +
    + WP_REST_Server::READABLE, + 'callback' => array( $this, 'rest_status' ), + 'permission_callback' => array( $this, 'permission_read' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/index/queue', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'rest_queue_index' ), + 'permission_callback' => array( $this, 'permission_manage' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/index/batch', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'rest_index_batch' ), + 'permission_callback' => array( $this, 'permission_manage' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/index/embeddings', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'rest_save_embeddings' ), + 'permission_callback' => array( $this, 'permission_manage' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/search', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'rest_search' ), + 'permission_callback' => array( $this, 'permission_read' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/reset', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'rest_reset' ), + 'permission_callback' => array( $this, 'permission_manage' ), + ) + ); + } + + public function permission_read() { + return current_user_can( 'read' ); + } + + public function permission_manage() { + return current_user_can( 'manage_options' ); + } + + public function rest_status() { + return rest_ensure_response( $this->get_status() ); + } + + private function get_status() { + global $wpdb; + + $tables = self::table_names(); + + return array( + 'sources' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tables['sources']}" ), + 'chunks' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tables['chunks']}" ), + 'queued' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tables['chunks']} WHERE embedding_status = 'queued'" ), + 'embedded' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tables['vectors']}" ), + 'dbVersion' => get_option( self::OPTION_VERSION ), + 'indexables' => count( $this->get_indexable_post_ids() ), + ); + } + + public function rest_queue_index( WP_REST_Request $request ) { + $params = $request->get_json_params(); + $force = ! empty( $params['force'] ); + + return rest_ensure_response( $this->queue_all_sources( $force ) ); + } + + public function rest_index_batch( WP_REST_Request $request ) { + global $wpdb; + + $tables = self::table_names(); + $limit = max( 1, min( 50, absint( $request->get_param( 'limit' ) ? $request->get_param( 'limit' ) : 8 ) ) ); + + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT c.id, c.chunk_index, c.chunk_text, c.token_estimate, s.title, s.url, s.source_id, s.post_type + FROM {$tables['chunks']} c + INNER JOIN {$tables['sources']} s ON s.id = c.source_id + WHERE c.embedding_status = 'queued' + ORDER BY s.updated_at DESC, c.id ASC + LIMIT %d", + $limit + ), + ARRAY_A + ); + + $items = array(); + foreach ( $rows as $row ) { + $items[] = array( + 'id' => (int) $row['id'], + 'chunkIndex' => (int) $row['chunk_index'], + 'text' => $row['chunk_text'], + 'tokenEstimate' => (int) $row['token_estimate'], + 'title' => $row['title'], + 'url' => $row['url'], + 'sourceId' => (int) $row['source_id'], + 'postType' => $row['post_type'], + ); + } + + return rest_ensure_response( + array( + 'items' => $items, + 'status' => $this->get_status(), + ) + ); + } + + public function rest_save_embeddings( WP_REST_Request $request ) { + global $wpdb; + + $params = $request->get_json_params(); + $items = isset( $params['items'] ) && is_array( $params['items'] ) ? $params['items'] : array(); + $model = sanitize_text_field( isset( $params['model'] ) ? $params['model'] : '' ); + + if ( '' === $model ) { + return new WP_Error( 'personal_rag_missing_model', __( 'Embedding model is required.', 'personal-rag' ), array( 'status' => 400 ) ); + } + + $tables = self::table_names(); + $saved = 0; + $source_ids = array(); + + foreach ( $items as $item ) { + $chunk_id = absint( isset( $item['chunkId'] ) ? $item['chunkId'] : 0 ); + $encoded = isset( $item['vector'] ) ? (string) $item['vector'] : ''; + + if ( ! $chunk_id || '' === $encoded ) { + continue; + } + + $decoded = $this->decode_vector( $encoded ); + if ( is_wp_error( $decoded ) ) { + return $decoded; + } + + $dimension = count( $decoded['values'] ); + if ( isset( $item['dimensions'] ) && absint( $item['dimensions'] ) !== $dimension ) { + return new WP_Error( 'personal_rag_dimension_mismatch', __( 'Embedding dimension mismatch.', 'personal-rag' ), array( 'status' => 400 ) ); + } + + $source_id = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT source_id FROM {$tables['chunks']} WHERE id = %d", + $chunk_id + ) + ); + + if ( ! $source_id ) { + continue; + } + + $stored = $wpdb->replace( + $tables['vectors'], + array( + 'chunk_id' => $chunk_id, + 'model' => $model, + 'dimensions' => $dimension, + 'vector' => $decoded['encoded'], + 'norm' => $decoded['norm'], + 'created_at' => current_time( 'mysql' ), + ), + array( '%d', '%s', '%d', '%s', '%f', '%s' ) + ); + + if ( false === $stored ) { + return new WP_Error( + 'personal_rag_vector_save_failed', + $wpdb->last_error ? $wpdb->last_error : __( 'Could not save embedding vector.', 'personal-rag' ), + array( 'status' => 500 ) + ); + } + + $wpdb->update( + $tables['chunks'], + array( + 'embedding_status' => 'embedded', + 'updated_at' => current_time( 'mysql' ), + ), + array( 'id' => $chunk_id ), + array( '%s', '%s' ), + array( '%d' ) + ); + + $source_ids[ $source_id ] = true; + $saved++; + } + + $this->mark_completed_sources( array_keys( $source_ids ) ); + + return rest_ensure_response( + array( + 'saved' => $saved, + 'status' => $this->get_status(), + ) + ); + } + + public function rest_search( WP_REST_Request $request ) { + global $wpdb; + + $params = $request->get_json_params(); + $encoded = isset( $params['vector'] ) ? (string) $params['vector'] : ''; + $model = sanitize_text_field( isset( $params['model'] ) ? $params['model'] : '' ); + $top_k = max( 1, min( 12, absint( isset( $params['topK'] ) ? $params['topK'] : 5 ) ) ); + + $query_vector = $this->decode_vector( $encoded ); + if ( is_wp_error( $query_vector ) ) { + return $query_vector; + } + + $tables = self::table_names(); + $dimension = count( $query_vector['values'] ); + + if ( '' !== $model ) { + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT v.vector, v.norm, c.id AS chunk_id, c.chunk_index, c.chunk_text, s.title, s.url, s.source_id, s.post_type + FROM {$tables['vectors']} v + INNER JOIN {$tables['chunks']} c ON c.id = v.chunk_id + INNER JOIN {$tables['sources']} s ON s.id = c.source_id + WHERE v.dimensions = %d AND v.model = %s", + $dimension, + $model + ), + ARRAY_A + ); + } else { + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT v.vector, v.norm, c.id AS chunk_id, c.chunk_index, c.chunk_text, s.title, s.url, s.source_id, s.post_type + FROM {$tables['vectors']} v + INNER JOIN {$tables['chunks']} c ON c.id = v.chunk_id + INNER JOIN {$tables['sources']} s ON s.id = c.source_id + WHERE v.dimensions = %d", + $dimension + ), + ARRAY_A + ); + } + + $matches = array(); + foreach ( $rows as $row ) { + $vector = $this->stored_vector_to_floats( $row['vector'] ); + if ( count( $vector ) !== $dimension ) { + continue; + } + + $score = $this->cosine_similarity( $query_vector['values'], $query_vector['norm'], $vector, (float) $row['norm'] ); + if ( null === $score ) { + continue; + } + + $matches[] = array( + 'chunkId' => (int) $row['chunk_id'], + 'sourceId' => (int) $row['source_id'], + 'postType' => $row['post_type'], + 'chunkIndex' => (int) $row['chunk_index'], + 'title' => $row['title'], + 'url' => $row['url'], + 'text' => $row['chunk_text'], + 'score' => round( $score, 6 ), + ); + } + + usort( + $matches, + function ( $a, $b ) { + if ( $a['score'] === $b['score'] ) { + return 0; + } + return ( $a['score'] > $b['score'] ) ? -1 : 1; + } + ); + + return rest_ensure_response( + array( + 'matches' => array_slice( $matches, 0, $top_k ), + 'total' => count( $matches ), + ) + ); + } + + public function rest_reset() { + $this->reset_index(); + return rest_ensure_response( $this->get_status() ); + } + + public function handle_save_post( $post_id, $post, $update ) { + if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { + return; + } + + $this->queue_post( $post_id, false ); + } + + public function handle_delete_post( $post_id ) { + $this->delete_source_by_wp_post( $post_id ); + } + + public function handle_untrashed_post( $post_id ) { + $this->queue_post( $post_id, false ); + } + + private function queue_all_sources( $force = false ) { + global $wpdb; + + $results = array( + 'queued' => 0, + 'unchanged' => 0, + 'skipped' => 0, + 'deleted' => 0, + 'status' => array(), + ); + + $post_ids = $this->get_indexable_post_ids(); + $seen = array(); + + foreach ( $post_ids as $post_id ) { + $seen[ $post_id ] = true; + $result = $this->queue_post( $post_id, $force ); + if ( isset( $results[ $result ] ) ) { + $results[ $result ]++; + } + } + + $tables = self::table_names(); + $sources = $wpdb->get_results( "SELECT id, source_id FROM {$tables['sources']} WHERE source_type = 'post'", ARRAY_A ); + foreach ( $sources as $source ) { + if ( ! isset( $seen[ (int) $source['source_id'] ] ) ) { + $this->delete_source_by_id( (int) $source['id'] ); + $results['deleted']++; + } + } + + $results['status'] = $this->get_status(); + return $results; + } + + private function queue_post( $post_id, $force = false ) { + global $wpdb; + + $post = get_post( $post_id ); + if ( ! $post || ! $this->is_indexable_post( $post ) ) { + $this->delete_source_by_wp_post( $post_id ); + return 'skipped'; + } + + $tables = self::table_names(); + $title = get_the_title( $post ); + $content = $this->extract_post_text( $post ); + $source_text = trim( $title . "\n\n" . $content ); + $content_hash = hash( 'sha256', $source_text ); + $permalink = get_permalink( $post ); + $url = $permalink ? $permalink : home_url( '?p=' . $post_id ); + $now = current_time( 'mysql' ); + + $source = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$tables['sources']} WHERE source_type = %s AND source_id = %d", + 'post', + $post_id + ), + ARRAY_A + ); + + if ( $source && ! $force && hash_equals( $source['content_hash'], $content_hash ) ) { + return 'unchanged'; + } + + $source_data = array( + 'source_type' => 'post', + 'source_id' => $post_id, + 'post_type' => $post->post_type, + 'post_status' => $post->post_status, + 'title' => $title, + 'url' => (string) $url, + 'content_hash' => $content_hash, + 'updated_at' => $now, + 'indexed_at' => null, + ); + + if ( $source ) { + $source_id = (int) $source['id']; + $wpdb->update( + $tables['sources'], + $source_data, + array( 'id' => $source_id ), + array( '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ), + array( '%d' ) + ); + } else { + $wpdb->insert( + $tables['sources'], + $source_data, + array( '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) + ); + $source_id = (int) $wpdb->insert_id; + } + + if ( ! $source_id ) { + return 'skipped'; + } + + $this->delete_chunks_for_source( $source_id ); + + $chunks = $this->chunk_text( $source_text, $title ); + if ( empty( $chunks ) ) { + $this->delete_source_by_id( $source_id ); + return 'skipped'; + } + + foreach ( $chunks as $index => $chunk_text ) { + $wpdb->insert( + $tables['chunks'], + array( + 'source_id' => $source_id, + 'chunk_index' => $index, + 'chunk_text' => $chunk_text, + 'chunk_hash' => hash( 'sha256', $chunk_text ), + 'token_estimate' => $this->estimate_tokens( $chunk_text ), + 'embedding_status' => 'queued', + 'created_at' => $now, + 'updated_at' => $now, + ), + array( '%d', '%d', '%s', '%s', '%d', '%s', '%s', '%s' ) + ); + } + + return 'queued'; + } + + private function get_indexable_post_ids() { + return get_posts( + array( + 'post_type' => array( 'post', 'page' ), + 'post_status' => array( 'publish', 'private' ), + 'posts_per_page' => -1, + 'fields' => 'ids', + 'orderby' => 'modified', + 'order' => 'DESC', + ) + ); + } + + private function is_indexable_post( $post ) { + return in_array( $post->post_type, array( 'post', 'page' ), true ) + && in_array( $post->post_status, array( 'publish', 'private' ), true ); + } + + private function extract_post_text( WP_Post $post ) { + $content = do_blocks( $post->post_content ); + $content = strip_shortcodes( $content ); + $content = wp_strip_all_tags( $content, true ); + $content = html_entity_decode( $content, ENT_QUOTES, get_bloginfo( 'charset' ) ); + $content = preg_replace( '/\s+/', ' ', $content ); + + return trim( $content ); + } + + private function chunk_text( $text, $title ) { + $text = trim( preg_replace( '/\s+/', ' ', $text ) ); + if ( '' === $text ) { + return array(); + } + + $words = preg_split( '/\s+/', $text ); + $target_size = 650; + $overlap = 80; + $chunks = array(); + $count = count( $words ); + + for ( $start = 0; $start < $count; $start += max( 1, $target_size - $overlap ) ) { + $slice = array_slice( $words, $start, $target_size ); + if ( empty( $slice ) ) { + break; + } + + $chunk = implode( ' ', $slice ); + if ( $title && 0 !== strpos( $chunk, $title ) ) { + $chunk = $title . "\n\n" . $chunk; + } + $chunks[] = $chunk; + + if ( $start + $target_size >= $count ) { + break; + } + } + + return $chunks; + } + + private function estimate_tokens( $text ) { + return max( 1, (int) ceil( strlen( $text ) / 4 ) ); + } + + private function delete_source_by_wp_post( $post_id ) { + global $wpdb; + + $tables = self::table_names(); + $id = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT id FROM {$tables['sources']} WHERE source_type = %s AND source_id = %d", + 'post', + $post_id + ) + ); + + if ( $id ) { + $this->delete_source_by_id( $id ); + } + } + + private function delete_source_by_id( $source_id ) { + global $wpdb; + + $tables = self::table_names(); + $this->delete_chunks_for_source( $source_id ); + $wpdb->delete( $tables['sources'], array( 'id' => $source_id ), array( '%d' ) ); + } + + private function delete_chunks_for_source( $source_id ) { + global $wpdb; + + $tables = self::table_names(); + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$tables['vectors']} WHERE chunk_id IN (SELECT id FROM {$tables['chunks']} WHERE source_id = %d)", + $source_id + ) + ); + $wpdb->delete( $tables['chunks'], array( 'source_id' => $source_id ), array( '%d' ) ); + } + + private function reset_index() { + global $wpdb; + + $tables = self::table_names(); + $wpdb->query( "DELETE FROM {$tables['vectors']}" ); + $wpdb->query( "DELETE FROM {$tables['chunks']}" ); + $wpdb->query( "DELETE FROM {$tables['sources']}" ); + } + + private function mark_completed_sources( $source_ids ) { + global $wpdb; + + if ( empty( $source_ids ) ) { + return; + } + + $tables = self::table_names(); + foreach ( $source_ids as $source_id ) { + $queued = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$tables['chunks']} WHERE source_id = %d AND embedding_status = 'queued'", + $source_id + ) + ); + if ( 0 === $queued ) { + $wpdb->update( + $tables['sources'], + array( 'indexed_at' => current_time( 'mysql' ) ), + array( 'id' => $source_id ), + array( '%s' ), + array( '%d' ) + ); + } + } + } + + private function decode_vector( $encoded ) { + $binary = base64_decode( $encoded, true ); + if ( false === $binary || '' === $binary || 0 !== strlen( $binary ) % 4 ) { + return new WP_Error( 'personal_rag_invalid_vector', __( 'Vector payload is invalid.', 'personal-rag' ), array( 'status' => 400 ) ); + } + + $values = $this->binary_to_floats( $binary ); + if ( empty( $values ) ) { + return new WP_Error( 'personal_rag_empty_vector', __( 'Vector payload is empty.', 'personal-rag' ), array( 'status' => 400 ) ); + } + + return array( + 'binary' => $binary, + 'encoded' => base64_encode( $binary ), + 'values' => $values, + 'norm' => $this->vector_norm( $values ), + ); + } + + private function stored_vector_to_floats( $stored ) { + $binary = base64_decode( (string) $stored, true ); + if ( false !== $binary && '' !== $binary && 0 === strlen( $binary ) % 4 ) { + return $this->binary_to_floats( $binary ); + } + + return $this->binary_to_floats( $stored ); + } + + private function binary_to_floats( $binary ) { + $values = unpack( 'f*', $binary ); + return $values ? array_values( $values ) : array(); + } + + private function vector_norm( $values ) { + $sum = 0.0; + foreach ( $values as $value ) { + $sum += (float) $value * (float) $value; + } + return sqrt( $sum ); + } + + private function cosine_similarity( $a, $a_norm, $b, $b_norm ) { + if ( $a_norm <= 0 || $b_norm <= 0 || count( $a ) !== count( $b ) ) { + return null; + } + + $dot = 0.0; + $n = count( $a ); + for ( $i = 0; $i < $n; $i++ ) { + $dot += (float) $a[ $i ] * (float) $b[ $i ]; + } + + return $dot / ( $a_norm * $b_norm ); + } + + public function maybe_register_abilities() { + if ( ! function_exists( 'wp_register_ability_category' ) || ! function_exists( 'wp_register_ability' ) ) { + return; + } + + add_action( 'wp_abilities_api_categories_init', array( $this, 'register_ability_category' ) ); + add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); + add_filter( 'ai_assistant_ability_domains', array( $this, 'ability_domains' ) ); + } + + public function register_ability_category() { + wp_register_ability_category( + 'personal-rag', + array( + 'label' => __( 'Personal RAG', 'personal-rag' ), + 'description' => __( 'Private local retrieval over WordPress content.', 'personal-rag' ), + ) + ); + } + + public function register_abilities() { + wp_register_ability( + 'personal-rag/get-status', + array( + 'label' => __( 'Get Personal RAG Status', 'personal-rag' ), + 'description' => 'Returns local RAG index counts: source count, chunk count, queued embeddings, and embedded vectors.', + 'category' => 'personal-rag', + 'execute_callback' => array( $this, 'ability_get_status' ), + 'permission_callback' => array( $this, 'permission_read' ), + 'meta' => array( + 'show_in_rest' => true, + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) + ); + } + + public function ability_get_status() { + return $this->get_status(); + } + + public function ability_domains( $domains ) { + $domains['personal-rag'] = 'private rag, local search, site knowledge, blog search, embedded WordPress content'; + return $domains; + } +} + +register_activation_hook( __FILE__, array( 'Personal_RAG_Plugin', 'activate' ) ); +Personal_RAG_Plugin::instance(); From 93e584c40445e117403aebd59c2ab077b0092114 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 24/27] Add Personal RAG uninstall cleanup --- blueprints/personal-rag/plugin/uninstall.php | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 blueprints/personal-rag/plugin/uninstall.php diff --git a/blueprints/personal-rag/plugin/uninstall.php b/blueprints/personal-rag/plugin/uninstall.php new file mode 100644 index 00000000..cfa355b3 --- /dev/null +++ b/blueprints/personal-rag/plugin/uninstall.php @@ -0,0 +1,24 @@ +prefix . 'personal_rag_vectors', + $wpdb->prefix . 'personal_rag_chunks', + $wpdb->prefix . 'personal_rag_sources', +); + +foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); +} + +delete_option( 'personal_rag_db_version' ); From 483e959918d21b238cd32e265188dc872eedc4f9 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 25/27] Add Personal RAG docs generator script command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2e8f971c..b27bde08 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "scripts": { + "generate:personal-rag-docs": "node scripts/generate-personal-rag-docs-wxr.js", "shots": "tsx scripts/screenshot-blueprints.ts" }, "devDependencies": { From a0262765585bb179743fe38e3bd435454bafd617 Mon Sep 17 00:00:00 2001 From: Fellyph Cintra Date: Mon, 11 May 2026 16:43:53 +0100 Subject: [PATCH 26/27] Add Personal RAG docs WXR generator --- scripts/generate-personal-rag-docs-wxr.js | 778 ++++++++++++++++++++++ 1 file changed, 778 insertions(+) create mode 100644 scripts/generate-personal-rag-docs-wxr.js diff --git a/scripts/generate-personal-rag-docs-wxr.js b/scripts/generate-personal-rag-docs-wxr.js new file mode 100644 index 00000000..329a8494 --- /dev/null +++ b/scripts/generate-personal-rag-docs-wxr.js @@ -0,0 +1,778 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..'); +const defaultDocsDir = path.resolve( + repoRoot, + '../wordpress-playground/packages/docs/site/docs' +); +const defaultOutFile = path.resolve(repoRoot, 'blueprints/personal-rag/content.xml'); +const sourceRepoRoot = path.resolve(repoRoot, '../wordpress-playground'); +const typedocModelFile = path.join(sourceRepoRoot, 'packages/docs/site/src/model.json'); + +const topLevelCategories = { + main: { label: 'Documentation', slug: 'documentation' }, + developers: { label: 'Developers', slug: 'developers' }, + blueprints: { label: 'Blueprints', slug: 'blueprints' }, +}; + +const fixedDate = '2026-05-11 00:00:00'; +const fixedPubDate = 'Mon, 11 May 2026 00:00:00 +0000'; +const siteUrl = 'https://playground.wordpress.net'; +const rawStaticBase = + 'https://raw.githubusercontent.com/WordPress/wordpress-playground/refs/heads/trunk/packages/docs/site/static'; + +const args = parseArgs(process.argv.slice(2)); +const docsDir = path.resolve(args.get('docs-dir') || defaultDocsDir); +const outFile = path.resolve(args.get('out') || defaultOutFile); + +if (!fs.existsSync(docsDir)) { + throw new Error(`Docs directory does not exist: ${docsDir}`); +} + +const files = findMarkdownFiles(docsDir) + .filter((file) => topLevelCategories[path.relative(docsDir, file).split(path.sep)[0]]) + .sort((a, b) => a.localeCompare(b)); + +const categories = new Map(); +const posts = files.map((file, index) => buildPost(file, index + 1, categories)); +const xml = buildWxr(posts, [...categories.values()]); + +fs.mkdirSync(path.dirname(outFile), { recursive: true }); +fs.writeFileSync(outFile, xml); + +console.log( + `Generated ${posts.length} posts and ${categories.size} categories in ${path.relative( + repoRoot, + outFile + )}` +); + +function parseArgs(values) { + const parsed = new Map(); + for (let index = 0; index < values.length; index++) { + const value = values[index]; + if (!value.startsWith('--')) { + continue; + } + const [rawKey, rawValue] = value.slice(2).split('='); + if (rawValue !== undefined) { + parsed.set(rawKey, rawValue); + } else { + parsed.set(rawKey, values[index + 1]); + index++; + } + } + return parsed; +} + +function findMarkdownFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + let files = []; + + for (const entry of entries) { + if (entry.name === '_fragments' || entry.name === 'node_modules') { + continue; + } + + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files = files.concat(findMarkdownFiles(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath); + } + } + + return files; +} + +function buildPost(file, postId, categories) { + const relativeFile = path.relative(docsDir, file).replaceAll(path.sep, '/'); + const source = fs.readFileSync(file, 'utf8'); + const { frontmatter, body } = splitFrontmatter(source); + const title = frontmatter.title || firstHeading(body) || titleFromFile(file); + const postName = postSlug(frontmatter.slug, relativeFile); + const sourceUrl = sourceUrlFor(frontmatter.slug, relativeFile); + const categoryChain = ensureCategories(relativeFile, categories); + const content = markdownToHtml(normalizeMdx(body, relativeFile)); + const description = frontmatter.description || ''; + + return { + id: postId, + title, + postName, + link: `${siteUrl}/${postName}/`, + guid: `${siteUrl}/?p=${postId}`, + content: [ + content, + `

    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('')) { + flushComponentBlock(componentBlock, normalized, relativeFile); + componentBlock = null; + } + continue; + } + + if (trimmed === '') { + componentBlock = { name: 'InteractiveDocs', lines: [line] }; + continue; + } + + if (trimmed === '') { + continue; + } + + if (/^$/i.test(trimmed) || /^]*>\s*<\/p>$/i.test(trimmed)) { + continue; + } + + if (/^/i.test(trimmed)) { + continue; + } + + if (/^$/i.test(trimmed) || /^$/i.test(trimmed)) { + normalized.push(`\`${trimmed}\``); + continue; + } + + const admonition = trimmed.match(/^:{3,4}([A-Za-z]+)?(?:\s*(.*))?$/); + if (admonition) { + if (admonition[1]) { + const label = labelFromSegment(admonition[1]); + const title = admonition[2] ? `: ${admonition[2]}` : ''; + normalized.push(`> ${label}${title}`); + } + continue; + } + + normalized.push(line); + } + + return normalized.join('\n').trim(); +} + +function flushComponentBlock(block, output, relativeFile) { + if (block.name === 'BlueprintExample') { + output.push(''); + output.push('```jsx'); + output.push(...block.lines); + output.push('```'); + output.push(''); + return; + } + + if (block.name === 'UpdateTopLevelToc') { + return; + } + + if (block.name === 'InteractiveDocs' && relativeFile === 'blueprints/05-steps.md') { + output.push(''); + output.push(buildBlueprintStepsReference()); + output.push(''); + return; + } + + if (block.name === 'BlueprintStep' || block.name === 'InteractiveDocs') { + output.push(''); + output.push('This section renders interactive reference content in the official documentation.'); + output.push(''); + } +} + +function buildBlueprintStepsReference() { + if (!fs.existsSync(typedocModelFile)) { + return 'The interactive Blueprint step reference is generated from TypeDoc in the official documentation.'; + } + + const model = JSON.parse(fs.readFileSync(typedocModelFile, 'utf8')); + const blueprintsApi = findModule(model, '@wp-playground/blueprints'); + if (!blueprintsApi?.children) { + return 'The interactive Blueprint step reference is generated from TypeDoc in the official documentation.'; + } + + const steps = blueprintsApi.children + .filter((entry) => entry.name?.match(/Step$/)) + .filter( + (entry) => + !['CompiledStep', 'CompiledV1Step', 'GenericStep', 'Step'].includes( + entry.name + ) + ) + .filter((entry) => !entry.flags?.isPrivate) + .sort((a, b) => a.name.localeCompare(b.name)); + + const blocks = ['## Blueprint Step Reference']; + for (const step of steps) { + const props = step.children || step.type?.declaration?.children || []; + const stepId = props.find((prop) => prop.name === 'step')?.type?.value || step.name; + const summary = commentText(step.comment?.summary); + const params = props.filter((prop) => prop.name !== 'step'); + const example = firstExample(step); + + blocks.push(`### ${stepId}`); + if (summary) { + blocks.push(summary); + } + if (params.length) { + blocks.push('Parameters:'); + for (const param of params) { + const optional = param.flags?.isOptional ? ' optional' : ''; + const type = typeLabel(param.type); + const description = commentText(param.comment?.summary); + blocks.push( + `- \`${param.name}\`${type ? ` (${type}${optional})` : optional ? ` (${optional.trim()})` : ''}${description ? `: ${description}` : ''}` + ); + } + } + if (example) { + blocks.push('Example:'); + blocks.push('```json'); + blocks.push(example); + blocks.push('```'); + } + } + + return blocks.join('\n\n'); +} + +function findModule(node, name) { + if (node?.name === name) { + return node; + } + for (const child of node?.children || []) { + const found = findModule(child, name); + if (found) { + return found; + } + } + return null; +} + +function commentText(parts = []) { + return parts + .map((part) => { + if (part.kind === 'code') { + return `\`${part.text.replace(/^`|`$/g, '')}\``; + } + return part.text || ''; + }) + .join('') + .replace(/\s+/g, ' ') + .trim(); +} + +function firstExample(step) { + const tag = step.comment?.blockTags?.find((entry) => entry.tag === '@example'); + const raw = tag?.content?.[0]?.text; + if (!raw) { + return ''; + } + return raw + .trim() + .replace(/^```[a-z]*\s*/i, '') + .replace(/```$/i, '') + .trim() + .replace(/^\s*/i, '') + .replace(/\s*<\/code>$/i, '') + .trim(); +} + +function typeLabel(type) { + if (!type) { + return ''; + } + if (type.name) { + return type.name; + } + if (type.value !== undefined) { + return JSON.stringify(type.value); + } + if (type.type === 'array') { + return `${typeLabel(type.elementType)}[]`; + } + if (type.type === 'union') { + return type.types.map(typeLabel).filter(Boolean).join(' | '); + } + if (type.type === 'reflection') { + return 'object'; + } + if (type.type === 'intrinsic') { + return type.name || ''; + } + return type.type || ''; +} + +function markdownToHtml(markdown) { + const lines = markdown.split('\n'); + const html = []; + let index = 0; + + while (index < lines.length) { + const line = lines[index]; + const trimmed = line.trim(); + + if (!trimmed) { + index++; + continue; + } + + const fence = trimmed.match(/^```\s*([A-Za-z0-9_-]+)?/); + if (fence) { + const code = []; + index++; + while (index < lines.length && !lines[index].trim().startsWith('```')) { + code.push(lines[index]); + index++; + } + if (index < lines.length) { + index++; + } + html.push(`
    ${escapeHtml(code.join('\n'))}
    `); + continue; + } + + const heading = trimmed.match(/^(#{1,6})\s+(.+)$/); + if (heading) { + const level = Math.min(6, heading[1].length); + html.push(`${inlineMarkdown(heading[2])}`); + index++; + continue; + } + + if (/^>\s?/.test(trimmed)) { + const quote = []; + while (index < lines.length && /^>\s?/.test(lines[index].trim())) { + quote.push(lines[index].trim().replace(/^>\s?/, '')); + index++; + } + html.push(`
    ${markdownToHtml(quote.join('\n'))}
    `); + continue; + } + + if (/^\s*[-*+]\s+/.test(line)) { + const items = []; + while (index < lines.length && /^\s*[-*+]\s+/.test(lines[index])) { + items.push(lines[index].replace(/^\s*[-*+]\s+/, '')); + index++; + } + html.push(`
      ${items.map((item) => `
    • ${inlineMarkdown(item)}
    • `).join('')}
    `); + continue; + } + + if (/^\s*\d+[.)]\s+/.test(line)) { + const items = []; + while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) { + items.push(lines[index].replace(/^\s*\d+[.)]\s+/, '')); + index++; + } + html.push(`
      ${items.map((item) => `
    1. ${inlineMarkdown(item)}
    2. `).join('')}
    `); + continue; + } + + if (/^\|.*\|$/.test(trimmed)) { + const rows = []; + while (index < lines.length && /^\|.*\|$/.test(lines[index].trim())) { + rows.push(lines[index]); + index++; + } + html.push(`
    ${escapeHtml(rows.join('\n'))}
    `); + continue; + } + + const paragraph = [trimmed]; + index++; + while ( + index < lines.length && + lines[index].trim() && + !isBlockStart(lines[index].trim()) + ) { + paragraph.push(lines[index].trim()); + index++; + } + html.push(`

    ${inlineMarkdown(paragraph.join(' '))}

    `); + } + + return html.join('\n'); +} + +function isBlockStart(line) { + return /^#{1,6}\s+/.test(line) || + line.startsWith('```') || + /^>\s?/.test(line) || + /^\s*[-*+]\s+/.test(line) || + /^\s*\d+[.)]\s+/.test(line) || + /^\|.*\|$/.test(line); +} + +function inlineMarkdown(value) { + let html = escapeHtml(value); + html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => { + return `
    ${escapeHtml(
+			alt
+		)}
    `; + }); + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => { + return `${text}`; + }); + html = html.replace(/`([^`]+)`/g, '$1'); + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); + return html; +} + +function buildWxr(posts, categories) { + const categoryXml = categories + .sort((a, b) => a.id - b.id) + .map( + (category) => ` + ${category.id} + + + + ` + ) + .join('\n'); + + const itemXml = posts.map(postToWxrItem).join('\n\n'); + + return ` + + + WordPress Playground Docs for Personal RAG + ${siteUrl} + Starter documentation corpus for the Personal RAG blueprint. + ${fixedPubDate} + en-US + 1.2 + ${siteUrl} + ${siteUrl} + + 1 + + + + + + +${categoryXml} + +${itemXml} + + +`; +} + +function postToWxrItem(post) { + const categories = post.categories + .map( + (category) => + ` ` + ) + .join('\n'); + + return ` + <![CDATA[${safeCdata(post.title)}]]> + ${escapeHtml(post.link)} + ${fixedPubDate} + + ${escapeHtml(post.guid)} + + + + ${post.id} + + + + + + + + + 0 + 0 + + + 0 +${categories} + + + + + + + + + `; +} + +function resolveAssetUrl(url) { + if (/^[a-z]+:\/\//i.test(url)) { + return url; + } + if (url.startsWith('/img/')) { + return `${rawStaticBase}${url}`; + } + return url; +} + +function resolveLinkUrl(url) { + if (/^[a-z]+:\/\//i.test(url) || url.startsWith('#') || url.startsWith('mailto:')) { + return url; + } + if (url.startsWith('/')) { + return `${siteUrl}${url}`; + } + return url; +} + +function labelFromSegment(segment) { + const words = stripNumberPrefix(segment).replace(/[_-]+/g, ' ').trim().split(/\s+/); + return words.map(titleWord).join(' '); +} + +function titleWord(word) { + const lower = word.toLowerCase(); + const acronyms = new Map([ + ['api', 'API'], + ['apis', 'APIs'], + ['cli', 'CLI'], + ['css', 'CSS'], + ['html', 'HTML'], + ['ios', 'iOS'], + ['json', 'JSON'], + ['php', 'PHP'], + ['pr', 'PR'], + ['wasm', 'WASM'], + ['wp', 'WP'], + ['wpcli', 'WP-CLI'], + ['xdebug', 'Xdebug'], + ]); + return acronyms.get(lower) || lower.charAt(0).toUpperCase() + lower.slice(1); +} + +function stripNumberPrefix(value) { + return value.replace(/^\d+-/, ''); +} + +function slugify(value) { + return stripNumberPrefix(String(value)) + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'doc'; +} + +function stripMarkdown(value) { + return value + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/[`*_#]/g, ''); +} + +function safeCdata(value) { + return String(value || '').replaceAll(']]>', ']]]]>'); +} + +function escapeHtml(value) { + return String(value || '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function escapeAttr(value) { + return escapeHtml(value); +} From 2c92d10fc7c085f0affbf0f3506aa368af3157b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 11 May 2026 15:49:32 +0000 Subject: [PATCH 27/27] chore: add missing Blueprint screenshots --- blueprints/personal-rag/screenshot.jpg | Bin 0 -> 106996 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 blueprints/personal-rag/screenshot.jpg diff --git a/blueprints/personal-rag/screenshot.jpg b/blueprints/personal-rag/screenshot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..73da3b914f1d6ef518d52739f47bb7764529f066 GIT binary patch literal 106996 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 z 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

    9Y%R@R{Gh?|GkF(*&qWSC6LtG^g(!pYKY1 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 E0DoN<=Kufz literal 0 HcmV?d00001