A browser-based CMS that lets users write content in WordPress, store it as Markdown in a GitHub repo, and auto-deploy as a static Astro site to Cloudflare Pages. No servers, no hosting to manage — everything runs in the browser via WP Playground.
Related repo (optional context): astro-wp-playground is a CLI-oriented prototype that runs WordPress Playground via Node. This repo (astro-wp-web-app/) is the browser SPA.
Exporter PHP lives in php-classes/ at the root of this repo. At build time, vite-plugin-php-inline.js reads those files and exposes them as the virtual module virtual:php-classes (see below). You can point the plugin at another directory with phpInlinePlugin({ phpDir: '/path/to/includes' }) in vite.config.js if you want a single shared checkout with the CLI project.
- Setup screen → User enters GitHub PAT (
repo+workflowscopes) - Select/create repo → Pick existing repo or create a new one (personal account only)
- Cloudflare config (optional) → Enter CF Account ID + API Token → saves as GitHub Actions secrets + pushes deploy workflow
- Launch editor → Boots WP Playground in an iframe with the exporter plugin pre-loaded
- Create content → User writes posts/pages in the standard WP editor
- Sync → Exports posts, pages, images, and navigation menus via REST API; commits additions AND deletions to GitHub via GraphQL
- Auto-deploy → GitHub Actions builds the Astro site and deploys to Cloudflare Pages
| Layer | Tech |
|---|---|
| CMS | WordPress Playground (in-browser, runs in iframe) |
| Build tool | Vite |
| Content format | Markdown with YAML frontmatter |
| Static site | Astro 6 (content collections with Content Layer API: blog + pages; header nav from src/data/menu.json when synced) |
| Git API | GitHub GraphQL (createCommitOnBranch mutation) |
| Secrets encryption | tweetnacl + blakejs (NaCl sealed box) |
| Deploy | GitHub Actions → Cloudflare Pages via wrangler-action |
Entry point. Two screens: setup and editor.
Setup screen layout:
.setup-cardcontains.setup-header(title + subtitle),.setup-steps(the step sections), and.setup-footer(reset session link)- Each step is a
.setup-sectionwith a.step-headercontaining a.step-numbadge and<h2>title - Step numbers are dynamically assigned via JS (
renumberSteps()) — hidden sections (e.g., CF step when already configured) are excluded from numbering, so the user always sees sequential numbers (1, 2, 3) with no gaps - Info tooltips use
.info-tipelements withdata-tipattributes, rendered via CSS::afterpseudo-element (dark popover on hover) - "Reset session" link at the bottom clears
sessionStorageand reloads the page
Editor screen layout:
- Dark header bar with repo name badge, sync status, View site (opens the live Astro site in a new tab; disabled until a deploy URL is available), Menus (
goTo('/wp-admin/nav-menus.php')— there is no address bar), "Sync All" button, and "Back" button - Loading overlay with spinner (shown while WP Playground boots)
- Full-height iframe for WP Playground
Main application logic. Key responsibilities:
Setup flow:
- PAT connection — validates token via GitHub API, stores in
sessionStorage, auto-restores on page load - Repo selection/creation — lists user's repos, or creates new one with sanitized name and
auto_init: true - Cloudflare setup — saves Account ID + API Token as GitHub Actions secrets via NaCl sealed box encryption, pushes deploy workflow to
.github/workflows/deploy.yml, queries GitHub Deployments API for the real CF Pages URL - Dynamic step numbering —
renumberSteps()counts only visible.setup-sectionelements and sets their.step-numbadge text. Called whenever sections show/hide. - Session reset — "Reset session" link clears
sessionStorageand reloads
Editor boot (bootEditor()):
- Fetches existing content from GitHub via
github.fetchContent()(posts, pages, images, optionalsrc/data/menu.json) - Seeds
contentManifestfrom fetched content (path → git blob SHA) so deletions can be detected on first sync - Sets
contentManifest._templatePushed = trueif content already exists - Builds WP Playground Blueprint with: mkdir steps, preview theme files (including pre-made screenshot via base64 decode), plugin files, PHP class files, existing content
.mdfiles, plugin activation, theme activation (wp2astro-previewviarunPHP/switch_theme()), WP config, content import viaAstro_MD_Importer, and menu import frommenu.json(recreates nav menu items with labels, hrefs, targets, title attributes, CSS classes, rel/XFN, and nested children; assigns to theme locations) - After Playground is ready, fetches the deploy URL from GitHub Deployments API and enables the View site button if available
Sync flow (syncToGitHub()):
- Fetches posts, pages, images, and menu from WP via REST API (
playgroundClient.request()) - Compares MD5 hashes against
contentManifestto find additions/changes (includingsrc/data/menu.json) - Detects deletions — paths in
contentManifest(undersrc/content/blog/,src/content/pages/,public/assets/images/) that are not present in WP's current content (menu.jsonis not deleted by this logic) - Commits all additions + deletions in a single GraphQL
createCommitOnBranchmutation - Updates manifest (add new hashes, remove deleted paths)
- Shows "Waiting for deploy..." and polls GitHub Deployments API via
waitForDeploy()until the deployment reaches a terminal state (success/failure/timeout). On success, enables the View site button and displays the live URL as a clickable link
State:
selectedRepo—{ owner, name, full_name }playgroundClient— WP Playground client instancecontentManifest—{ path: hash }map tracking what's in GitHub. Seeded fromfetchContent()on boot, updated after each sync. Internal keys prefixed with_(e.g.,_templatePushed) are skipped during deletion detection.cfPagesUrl— the real Cloudflare Pages URL, persisted insessionStorage. Fetched on boot from GitHub Deployments API (catches deploys from previous sessions) and updated after each sync via deploy polling
GitHub API wrapper using Octokit. Key functions:
connect(token)— Initialize Octokit, return authenticated userlistRepos()— All repos user can push to (paginated, max 100)createRepo(name)— Create withauto_init: true(template files pushed on first sync)detectContentRoot(owner, repo)— Auto-detects the Astro project root within the repo. Uses the git tree API (single request, no 404 noise) to findastro.config.mjs/.tsanywhere in the tree. Returns''for root-level projects or'subdir/'for nested ones (e.g.,'src-astro/'). All content paths are prefixed with this root.fetchContent(owner, repo, contentRoot='')— Fetch existing blog posts, pages, images, andsrc/data/menu.jsonfrom repo, usingcontentRootprefix for all paths. Returns{ posts, pages, images, menu }(menuisnullif the file is missing)fetchTemplateVersion(owner, repo, contentRoot='')— Reads<contentRoot>.astro-wp-versionfrom the repo. Returns{ template, version }or{ template: 'default', version: 0 }if the file is missing (unversioned repo)commitFiles(owner, repo, branch, files, message, deletePaths=[])— Uses GraphQLcreateCommitOnBranchmutation. HEAD OID fetched via GraphQL (not REST — avoids stale cache).filesis{ path: content }for additions.deletePathsisstring[]of paths to delete. Both are passed infileChanges: { additions, deletions }.setRepoSecret(owner, repo, name, value)— Encrypt with NaCl sealed box, set via Actions secrets APIgetDeploymentUrl(owner, repo)— Searches recent GitHub Deployments for any with a.pages.devURL. Strips per-commit hash prefix (e.g.,abc123.my-site.pages.dev→my-site.pages.dev). Returns the production URL ornull.waitForDeploy(owner, repo, afterTimestamp, onStatus, opts)— Polls GitHub Deployments API for a deployment created afterafterTimestamp. CallsonStatus(state, url)on each poll (pending/in_progress/success/failure/error/timeout). Returns{ state, url }. Default interval: 8s, timeout: 5 min.
WP plugin PHP code as JS string exports:
mainPlugin— Plugin entry point. Defines constants (ASTRO_EXPORT_DIR,ASTRO_PAGES_EXPORT_DIR,ASTRO_IMAGES_DIR), ensures export directories exist, includes ALL class files (markdown converter, frontmatter builder, image handler, post exporter, md-to-blocks, md-importer, REST API), registers hooksrestApiClass— REST API class with endpoints:GET /astro-export/v1/posts— All published/draft posts as markdown (returns[{id, slug, filename, markdown, hash}])GET /astro-export/v1/pages— Same for pagesGET /astro-export/v1/images— Images as base64 with metadataGET /astro-export/v1/manifest—{path: md5hash}for change detection (includessrc/data/menu.json)GET /astro-export/v1/menu— Nav menus by theme location:{ locations: { primary: [...], ... }, hash }. Tree nodes:label,href, optionaltarget(e.g._blank), optionaltitle(title attribute), optionalclasses(string array of CSS classes), optionalrel(XFN/link relationship), optional nestedchildren. Internal URLs normalized to site-relative paths (Playground/scope:*prefixes are stripped); external links unchanged.GET /astro-export/v1/post/{id}and/page/{id}— Single item
getPluginFiles()— Returns{ virtualPath: phpContent }map for Blueprint writeFile steps. Only includes the main plugin file and REST API class; core PHP classes are written separately viagetCorePHPSteps()in main.js.
Important: The mainPlugin string must require_once all 6 class files including class-md-to-blocks.php and class-md-importer.php. Without these, the content importer silently fails (class_exists('Astro_MD_Importer') returns false).
getTemplateFiles() returns a { path: content } map of all Astro site scaffold files committed to the user's repo on first sync (and re-pushed when the template version changes). Also exports TEMPLATE_VERSION (integer, bump on any template change) and TEMPLATE_NAME ('default'):
Config files:
package.json— Astro 6 dependency, build/dev/preview scriptsastro.config.mjs— MinimaldefineConfig({})tsconfig.json— Extendsastro/tsconfigs/strict
Content collections (src/content.config.ts):
- Uses Astro 6 Content Layer API with
glob()loader blogcollection —glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' })— schema: title, description, pubDate, updatedDate, author, categories, tags, heroImage, draftpagescollection —glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' })— schema: title, description, updatedDate, menuOrder, heroImage, draftzimported fromastro/zod(Astro 6 change fromastro:content)
Site navigation (src/data/menu.json + components):
- Committed JSON shape:
{ "locations": { "primary": [...], ... } }. Sync writes pretty-printed JSON; the RESThashis derived from WordPress’s compact JSON of the same structure. NavMenu.astro— Recursive list for nested items; renderstarget,title,relattributes on links and CSS classes on<li>elements when present. Submenus use hover dropdown styles in the layout.- WordPress: Use Appearance → Menus. Assign the menu to your theme’s primary (or header) location when available; if no location is assigned, the exporter uses the first menu as
primary. Classic menus only — not the block-only Navigation block in isolation. All WP menu item fields are synced: Navigation Label, URL, Link Target, Title Attribute, CSS Classes, and Link Relationship (XFN).
Layouts:
BaseLayout.astro— Imports../data/menu.json. Iflocations.primary(or the first non-empty location) has items, the header nav uses WordPress. Otherwise it falls back to Home, Blog, and links derived from thepagescollection (previous behavior). Dark gradient header (#1a1a2e→#2d2b55→#3d3580), sticky nav, warm off-white body (#faf9f7), dark footer. Global styles defined in<style is:global>.BlogPost.astro— Article layout with hero image, styled metadata bar (date, author, updated date with·separators), description as italic lead. Scoped styles for content typography (headings, blockquotes, code blocks).PageLayout.astro— Simple article layout with optional hero image.
Pages:
index.astro— Hero section with gradient background, CTA button linking to/blog/. Card-based "Recent Posts" section (latest 5). Empty state with dashed border.blog/index.astro— Header with title + post count. Card-based listing with title, excerpt, date, author, animated arrow. Empty state.blog/[...slug].astro— Dynamic blog post routes. UsesgetStaticPaths()withpost.idas slug (Astro 6 glob loader returns clean IDs without.mdextension).[...slug].astro— Dynamic page routes. Same pattern.
Deploy workflow (.github/workflows/deploy.yml):
See "Deploy Workflow & Site URL Resolution" section for full details.
Placeholder files:
src/content/blog/.gitkeep,src/content/pages/.gitkeep,public/assets/images/.gitkeepsrc/data/menu.json— Default{ "locations": { "primary": [] } }until the first menu sync overwrites it.astro-wp-version—{ "template": "default", "version": <TEMPLATE_VERSION> }— used to detect stale templates and trigger re-push on sync
Minimal classic theme wp2astro-preview (written into Playground at /wordpress/wp-content/themes/wp2astro-preview/). Exported via getWp2AstroPreviewThemeFiles() and activated with a Blueprint runPHP step that calls switch_theme() using WP_CONTENT_DIR (same /wordpress/ layout as other steps — the stock activateTheme step keys off documentRoot and can miss the theme path in some Playground builds). Registers a single primary menu location so Appearance → Menus is available. Includes simple index.php, single.php, page.php, header.php, footer.php, and a short footer note that the public site is Astro. A pre-made screenshot.png is embedded as base64 in src/assets/theme-screenshot-b64.js and written via a runPHP step (getWp2AstroPreviewScreenshotStep()). Not committed to the user’s GitHub Astro repo — only lives in the Playground VM.
Styles organized by section:
- Setup screen — gradient background, card with shadow, header/steps/footer layout
- Step header — flex row with numbered circle badge, h2 title, optional info-tip and badges
- Info tooltip —
::afterpseudo-element positioned below the(i)icon, usesdata-tipattribute for content. Dark background, white text, 260px wide, appears on hover with opacity transition. - Inputs — full-width with focus ring.
.input-rowplaces input + button side-by-side. - Buttons —
.btnbase,.btn-primary(purple),.btn-secondary(gray),.btn-small(compact) - Status badges —
.status.success(green),.status.error(red),.status.working(amber). Hidden when empty via:emptyselector. - Setup footer — centered "Reset session" link, subtle gray, turns red on hover
- Editor header — dark bar, flex layout, repo badge
- Loading overlay — centered spinner with message
Six PHP classes used by the WordPress exporter plugin inside Playground (same logical module as the astro-wp-playground plugin). Kept in-repo so the web app builds standalone.
Custom Vite plugin that reads those 6 PHP files at build time (default directory: php-classes/ next to vite.config.js) and exposes them as virtual:php-classes. Uses JSON.stringify() for safe string escaping. Watches files for HMR.
PHP files (in php-classes/ unless phpDir is overridden):
class-markdown-converter.php— HTML → Markdown conversionclass-frontmatter-builder.php— Builds YAML frontmatter from WP post dataclass-image-handler.php— Processes featured + inline imagesclass-post-exporter.php— Orchestrates per-post exportclass-md-to-blocks.php— Converts Markdown back to WP blocks (for round-trip)class-md-importer.php— Imports existing .md files into WP on boot
Loads phpInlinePlugin() with defaults (php-classes/). Pass { phpDir: '...' } only if you want to load classes from elsewhere (e.g. a sibling astro-wp-playground checkout).
- SPA calls WP Playground REST API via
playgroundClient.request()(NOTfetch()— avoids CORS) - Gets all posts, pages, images, and menu as JSON (images are base64-encoded; menu includes a content
hashforsrc/data/menu.json) - Compares MD5 hashes against in-memory
contentManifestto find additions/changes - Detects deletions: any path in
contentManifestundersrc/content/blog/,src/content/pages/, orpublic/assets/images/that doesn't appear in the current WP content is marked for deletion. Manifest keys starting with_(internal flags) are skipped. - On first sync, also includes Astro template/scaffold files (excluding
.github/workflows) - Commits all additions AND deletions in a single GraphQL
createCommitOnBranchmutation (usesfileChanges: { additions, deletions }) - Updates local manifest — adds new hashes, removes deleted paths (including
src/data/menu.json’s hash from the menu endpoint) - Polls GitHub Deployments API (
waitForDeploy()) until the deployment created after the commit reaches a terminal state. Shows real-time status ("Waiting for deploy..." → "Deployed — URL")
CI: No workflow changes are required for menu.json — npm run build picks it up like any other committed file under src/.
On boot, bootEditor() seeds contentManifest from fetchContent() results:
contentManifest[`src/content/blog/${post.name}`] = post.sha; // git blob SHAThis ensures deletions are detected even on the first sync of a fresh session. The git blob SHA won't match the MD5 hash from WP export, so the first sync will re-push all content (ensuring consistency). Subsequent syncs use MD5 hashes. If menu.json exists in the repo, its blob SHA is seeded the same way; the first sync may re-commit menu JSON to align hashes.
The Git Trees REST API returned persistent 404 errors on newly created repos (GitHub eventual consistency + fine-grained token issues). The GraphQL createCommitOnBranch mutation is a single call that handles tree creation internally. It also natively accepts base64 content, making binary file commits simpler.
Recommendation: Keep GraphQL. It's working, it's simpler (1 call vs 3-4), and it handles edge cases better.
The commitFiles function fetches the branch HEAD SHA via GraphQL (repository.ref.target.oid) instead of the REST git.getRef API. The REST endpoint can return stale/cached SHAs, causing the createCommitOnBranch mutation to fail with "Expected branch to point to X but it did not." Using GraphQL for both the read and the write ensures consistency.
When creating a Cloudflare Pages project, if the requested name (e.g., my-astro-site) is already taken globally, CF silently appends a random suffix (e.g., my-astro-site-60m). This means the actual site URL differs from what the app would naively guess.
Additionally:
- The browser cannot call the CF API directly due to CORS (no
Access-Control-Allow-Originheader) - GitHub Actions
GITHUB_TOKENcannot set repo variables (insufficient permissions) - GitHub Actions
GITHUB_TOKENcan create Deployments (withdeployments: writepermission)
The deploy workflow (deploy.yml) has four steps after build:
- Create CF Pages project —
curlPOST to CF API to ensure the project exists (no-op if already created) - Deploy via wrangler-action — deploys to CF Pages with
id: deploy. The action outputsdeployment-url(e.g.,https://abc123.my-astro-site-60m.pages.dev) - Save site URL as GitHub Deployment — strips the per-commit hash from the wrangler URL using
sed 's|https://[a-f0-9]*\.||'to get the production URL, then creates a GitHub Deployment withenvironment: "production"and the URL asenvironment_url
The app reads the URL back via github.js → getDeploymentUrl():
- Lists recent deployments (up to 10, no environment filter)
- For each, fetches deployment statuses and looks for
environment_urlcontaining.pages.dev - Strips any per-commit hash prefix via regex:
/https?:\/\/[a-f0-9]+\.(.+\.pages\.dev)/ - Returns the clean production URL
| Approach | Why it doesn't work |
|---|---|
| Guess URL from repo name | CF may append a suffix — wrong URL |
| Query CF API from browser | CORS blocks it (no Access-Control-Allow-Origin) |
| Store as GitHub Actions variable | GITHUB_TOKEN can't write variables |
| Store as repo secret | Secrets can't be read back via API |
| Parse workflow run logs | Logs come as zip files — too complex for browser |
| Read from wrangler output directly | Only available during the workflow run |
The workflow requires:
contents: read— to checkout codedeployments: write— to create GitHub Deployments with the site URL
The GITHUB_TOKEN is automatically provided. CF credentials come from repo secrets (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN) set during the CF setup step.
Templates use Astro 6's Content Layer API. Key differences from Astro 5:
- Config location:
src/content.config.ts(wassrc/content/config.ts). Template updates automatically delete the old file. - Glob loader: Collections use
loader: glob({ base, pattern })instead oftype: 'content'. - Zod import:
import { z } from 'astro/zod'(wasfrom 'astro:content'). - Clean IDs:
post.idreturnshello-world(no.mdextension), so no stripping needed. render(): Standalone functionimport { render } from 'astro:content'(already used in Astro 5 templates).
The app can sync content into any Astro site — not just repos created through the app. When a repo already has content but no .astro-wp-version file, the app runs in content-only mode: it pushes blog posts, pages, images, and menus without touching templates, layouts, styles, or config.
Subdirectory support: The Astro project doesn't need to be at the repo root. On boot, detectContentRoot() searches for astro.config.mjs/.ts — if found in a subdirectory (e.g., src-astro/), all content paths are automatically prefixed. No configuration needed.
All paths below are relative to the detected content root (e.g., src-astro/ or repo root).
| Content | Repo path | Format |
|---|---|---|
| Blog posts | src/content/blog/<slug>.md |
Markdown with YAML frontmatter |
| Pages | src/content/pages/<slug>.md |
Markdown with YAML frontmatter |
| Images | public/assets/images/<filename> |
Binary (base64-committed) |
| Navigation menus | src/data/menu.json |
JSON |
Layouts, components, styles, package.json, astro.config.*, src/content.config.ts, .github/workflows/, or any other file outside the content paths above.
Content collections — The site's content config must define blog and pages collections that accept the frontmatter the exporter produces. All fields use .optional().default() so the site can ignore any it doesn't need, but the exporter will always include them.
Blog post frontmatter:
title: string # required
description: string
pubDate: date # required
updatedDate: date
author: string # default: "Admin"
categories: string[]
tags: string[]
heroImage: string # path to image in public/assets/images/
draft: booleanPage frontmatter:
title: string # required
description: string
updatedDate: date
menuOrder: number # for nav ordering
heroImage: string
draft: booleanImages — Referenced in frontmatter as /assets/images/<filename>. The app commits images to public/assets/images/. If your site uses a different image directory (e.g., src/assets/images/), you'll need to adjust your content references or add a redirect.
Navigation menus — The app writes src/data/menu.json with this shape:
{
"locations": {
"primary": [
{
"label": "About",
"href": "/about/",
"target": "_blank",
"title": "About us",
"classes": ["highlight", "cta"],
"rel": "noopener",
"children": []
}
]
}
}To use WP-managed navigation, import this JSON in your header/nav component and render the items. All fields except label and href are optional. The primary location is used when a WP menu is assigned to the theme's primary location; otherwise the first menu becomes primary.
- Ensure
src/content/blog/andsrc/content/pages/directories exist - Add or update content config with compatible
blogandpagescollections (see frontmatter fields above) - Ensure
public/assets/images/exists (or add.gitkeep) - Optionally: import
src/data/menu.jsonin your nav component for WP-managed navigation - Connect the repo through the app — it will detect it as a custom site and sync content only
- If your Astro project is in a subdirectory (e.g.,
src-astro/), the app auto-detects this viaastro.config.*— no extra config needed. However, if using the app's deploy workflow, you'll need to manually setworking-directoryand adjust the wrangler deploy path (see Deploy Workflow Gotchas) - Ensure your deploy workflow uses Node.js 22+ (Astro 6 requirement)
reposcope: Required for all operationsworkflowscope: Required ONLY for pushing.github/workflows/files. Without it, GitHub returns a permissions error on the GraphQL mutation. The app handles this by pushing deploy.yml separately during the CF setup step (not during normal sync).
The app detects three repo states and handles templates accordingly:
| State | .astro-wp-version |
Has content? | Behavior |
|---|---|---|---|
| New empty repo | missing | no | Push default templates + content |
| App-managed repo | present | yes | Push updated templates if version outdated + content |
| Custom repo | missing | yes | Content-only sync — never push templates |
Detection logic (bootEditor()): if fetchContent() finds existing posts/pages/images and fetchTemplateVersion() returns version 0 (no .astro-wp-version), the repo is marked as custom (_isCustomRepo = true).
App-managed repos:
- Repo is created with
auto_init: true(just a README) - Template files are pushed on FIRST sync (flag:
contentManifest._templatePushed) .github/workflows/deploy.ymlis pushed separately when user saves CF credentials- The
_templatePushedflag is set AFTER commit succeeds (was a bug before — setting it before meant retries would skip templates) - Template versioning: A
.astro-wp-versionfile in the repo tracks{ "template": "<name>", "version": <int> }. On boot,fetchTemplateVersion()reads this file. During sync, if the repo's version is lower than the app'sTEMPLATE_VERSION(exported fromtemplate.js), all template files are re-pushed. Thetemplatefield identifies which template set was used (currently only"default"); this future-proofs for multiple template choices without structural changes. - IMPORTANT — when changing any template file in
src/template.js, you MUST bumpTEMPLATE_VERSIONin the same file. This is what triggers existing repos to receive the updated templates on their next sync. Forgetting to bump means stale repos stay stale.
- Node.js 22+ required — Astro 6 needs Node >= 22.12.0. The deploy workflow uses
node-version: 22. Node 20 will fail with "Node.js v20.x is not supported by Astro!" - For repos with Astro in a subdirectory (e.g.,
src-astro/), the workflow needsdefaults: run: working-directory: <subdir>and the wrangler deploy path adjusted to<subdir>/dist - Must use
npm install(notnpm ci) because nopackage-lock.jsonis committed to the repo - The
--create-projectflag does not exist in wrangler v3 — project must be created via the CF API before deploying - wrangler-action v3 is pinned; v4's flags differ
- The
deployment-urloutput from wrangler-action is a per-commit preview URL (e.g.,abc123.my-site.pages.dev), not the production URL — must be stripped - Shell quoting in
-dJSON: the curl commands that create GitHub Deployments must use single-quoted JSON (-d '{"ref":"main",...}'). Double-quoted strings with escaped inner quotes (-d "{\"ref\":\"main\"}") break because bash strips the escaping, producing invalid JSON and silently failing to create deployment statuses - Deployment ID extraction: use
jq -r '.id'(notgrep) to extract the deployment ID from the GitHub API response. The API returns pretty-printed JSON with spaces ("id": 123), which breaksgrep -o '"id":[0-9]*'
writeFileBlueprint step does NOT auto-create parent directories. Must usemkdirsteps first.- REST API calls from the parent page are blocked by CORS. Use
playgroundClient.request()which routes through the iframe's service worker. - Blueprint
runPHPsteps execute in the WP context (require wp-load.phpto access WP functions).
- The plugin's
mainPluginmustrequire_oncebothclass-md-to-blocks.phpandclass-md-importer.phpfor the importer to work. These were originally missing and the import silently failed becauseclass_exists('Astro_MD_Importer')returned false. - Existing content is fetched from GitHub, written to WP Playground's virtual filesystem, then imported into WP via the
Astro_MD_Importerclass in arunPHPBlueprint step. - Nav menus from
src/data/menu.jsonare recreated on boot: menu items are created with labels, hrefs, targets, title attributes, CSS classes, rel/XFN, and nested children. Internal links are matched to WP pages/posts by slug (aspost_typeitems). Menus are assigned to the corresponding theme locations.
- Playground adds a
/scope:0.xxx/prefix to all internal URLs. The menu exporter strips this prefix viastrip_playground_scope()so exported hrefs are clean site-relative paths (e.g.,/about/instead of/scope:0.123/about/).
- The WP REST API only queries
post_status: ['publish', 'draft']. Trashed and permanently deleted posts are excluded from the response. - During sync, any path in
contentManifestthat no longer appears in the WP response is added todeletePathsand included in the commit'sfileChanges.deletions. - The manifest must be seeded on boot (from
fetchContent()) for deletions to work on the first sync of a fresh session.
- PAT, CF Pages URL, and CF configured flag are stored in
sessionStorage(lost on tab close) - "Reset session" link in the setup footer clears all session data and reloads
- The content manifest is NOT persisted — it's rebuilt from
fetchContent()on each boot and updated during sync
- PHP exporter sources are committed under
php-classes/; no sibling repo is required unless you overridephpDirinvite.config.js. libsodium-wrappersdoesn't work with Vite's ESM bundler. That's why we usetweetnacl+blakejsfor GitHub secret encryption.
- Error handling — Many error paths just show
Error: ${e.message}. Add more helpful messages, especially for PAT permission errors (suggest addingworkflowscope). - Image round-trip — Images are exported to GitHub but not re-imported when loading existing content. The
fetchContent()function fetches image metadata but doesn't download/inject them into WP Playground. - Session persistence — PAT is stored in
sessionStorage(lost on tab close). Consider offeringlocalStorageoption with a warning. - Multiple branch support — Everything targets
main. Some users may wantdevelopor feature branches. - Multiple template sets — Currently only one template (
"default"). The.astro-wp-versionfile already includes atemplatefield to identify the set. To add a new template: create itsgetTemplateFiles()variant, give it its own version counter, and route by thetemplatename read from the repo.
- Progress indicator for sync — Show file-by-file progress during large syncs
- Diff preview before sync — Show what changed before committing
- Selective sync — Let users choose which posts/pages to sync
- Custom domain setup — Guide users through CF Pages custom domain configuration
cd astro-wp-web-app
npm install
npm run dev # → http://localhost:5173
npm run build # → dist/PHP classes are read from php-classes/ in this repo (see vite-plugin-php-inline.js).
- GitHub username:
ilicfilip - Test repo:
my-astro-site(CF Pages project:my-astro-site-60mdue to name collision) - PAT scopes needed:
repo+workflow(classic token)