Install the plugin
Download the free plugin zip, then in wp-admin go to Plugins → Add New → Upload Plugin, choose the zip, and activate. (A WordPress.org listing is in review and this page will update once it is live.) The plugin works on WordPress 6.9 and up; WordPress 7.0 (May 20, 2026) is required for dsgo.ai.prompt() and the in-admin Riff builder. Ability consumption depends on the site's Abilities API availability and degrades when the API or a named ability is absent.
Once activated you’ll see DSGo Apps in the wp-admin sidebar.
Which paths work on which plan?
| Path / feature | Free | Pro ($149.99/yr) | Studio ($499/yr) |
|---|---|---|---|
| Path A, upload an HTML artifact in wp-admin | yes, 1 active app | yes, unlimited active apps | yes, unlimited active apps |
| Path B, upload a packaged bundle in wp-admin | yes, 1 active app | yes, unlimited active apps | yes, unlimited active apps |
| Path C, CLI scaffold + deploy from your editor | init only; deploy is Pro | yes, unlimited active apps | yes, unlimited active apps |
| Riff, in-admin AI builder | no | included | included |
Apps-as-Abilities publishing ( abilities.publishes) | installs, inactive | active | active |
| Scheduled jobs / webhook endpoints | installs, inactive | active | active |
Live dynamic routes ( wp:posts, wc:products) | installs, inactive | active | active |
Multi-site CLI deploy ( --sites=a,b,c) | no | Studio tier | included |
| White-label | no | no | included |
Free installs as many bundles as you want via wp-admin upload but only one is active at a time (switchable per app in the apps list). Pro-only runtime declarations can be installed on Free, but the gated behavior stays inactive until a Pro license or trial is active. See pricing →
Path A, Upload an artifact (no terminal)
You downloaded a Claude, ChatGPT, v0, or static HTML artifact. Install it without leaving wp-admin:
- WordPress admin → DSGo Apps.
- Use the first-run chooser to install the starter app, or stay on Upload artifact and drop your
.htmlor static.zip. - Confirm the app ID and display name, then install.
- Use the success actions to open the app, copy the URL, embed it in a post, or make it your site’s home page.
To uninstall, hover the row in Installed apps and click Delete.
Path B, Upload a packaged bundle
If you have a built bundle with a dsgo-app.json manifest, choose Upload bundle on the DSGo Apps screen, drop the zip, review the capability list, rendering mode, and network policy, then approve the install.
Path C, Use the CLI
Recommended for iteration speed. You’ll use @designsetgo/cli. The CLI is open and apps init works locally, but CLI deploy to a site is a Pro feature; the server gates Application-Password installs at the plan boundary. Free covers 1 active app via the wp-admin upload paths (Path A and Path B).
Want your AI agent to know all this without reading the page?
npx skills add DesignSetGo/skills drops five Agent Skills (scaffold, manifest, bridge, CLI, abilities) into Claude Code, Cursor, Codex, or Claude Desktop. The agent loads them on demand, so it can answer bridge and manifest questions before apps init runs. See DesignSetGo/skills.
One-time setup on your site
- Activate the DesignSetGo Apps plugin.
- Generate a WordPress Application Password:
https://your-site.com/wp-admin/profile.php#application-passwords-section - Copy the 24-character value, you’ll see it once.
Scaffold an app
npxnpx @designsetgo/cli apps init my-app
cd my-app Creates a single-file starter ( index.html + dsgo-app.json + CLAUDE.md) with no build step. Add --astro if you want the multi-page Astro project with file-based routing instead, then npm install inside it.
Authenticate
loginnpx @designsetgo/cli apps login You’ll be prompted for the site URL, your WordPress username, and the Application Password you copied. Credentials are stored at ~/.config/designsetgo/credentials.json with 0600 permissions. Run logout to remove them.
Iterate locally
devnpx @designsetgo/cli apps dev Starts a local dev server at http://localhost:3838 with hot reload and a mocked bridge. No WP site needed. Add --bridge proxy --site=https://your-site.com (Pro) to route bridge calls to your real site. File saves reflect in the iframe within 500ms.
Deploy
deploynpx @designsetgo/cli apps deploy --build Runs npm run build, shows you the capabilities the app is asking for, pushes the bundle, and prints the live URL. Re-running deploy updates the existing install, same app id means atomic update.
Preview before going live (Pro): Add --preview to get a shareable URL at /dsgo-preview/slug/token/ without touching the installed-apps catalog. Share with clients, then run --promote <id> to install when they approve. Preview URLs expire after 7 days (configurable with --ttl).
Manifest, dsgo-app.json
Every app has a manifest at the bundle root. v1 fields:
manifest_version: 1{
"manifest_version": 1,
"id": "recipe-leaderboard",
"name": "Recipe leaderboard",
"version": "0.1.0",
"isolation": "inline",
"entry": "index.html",
"routes": [
{ "path": "/", "file": "index.html" },
{ "path": "/about", "file": "about.html" }
],
"display": {
"modes": ["page"],
"default": "page"
},
"permissions": {
"read": ["posts", "user"],
"write": [],
"post_meta": ["price", "rating", "listing_*"]
},
"runtime": {
"sandbox": "strict",
"csp": {
"script_src": ["self"],
"style_src": ["self"],
"img_src": ["self", "data:"],
"connect_src": ["self", "https://cdn.example.com"]
}
}
} The full schema, validation rules, and additive-v1.x fields are in MANIFEST.md. Key things that changed from earlier drafts: the app key is id, routes use file, and permissions live under permissions.read/permissions.write. Bundles may also ship WebAssembly ( .wasm/.wat/.data) and spawn Web Workers; the optional runtime.uses_wasm / runtime.uses_workers flags surface an informational line in the install dialog.
Inheriting Site Kit / Google Analytics
Inline-mode apps that own the full document ( theme.wrap: "none") bypass wp_head, which is where Site Kit, MonsterInsights, and most analytics plugins inject their tags. Set runtime.wp_head: true in the manifest to opt back in. The runtime fires wp_head + wp_footer, filters the captures to head-safe tag types ( <script>, <link>, <meta>, <style>, <noscript>), nonce-stamps each tag under the per-request CSP, and injects head capture before </head> / footer capture before </body>. Origins from captured <script src> / <link href> auto-widen script_src / style_src / img_src; the Google Analytics collect endpoints ( *.google-analytics.com, *.analytics.google.com) auto-widen connect_src / img_src so gtag’s runtime measurement calls aren’t blocked. Only valid on isolation: "inline" with theme.wrap: "none"; rejected on iframe and on theme.wrap: "header_footer" (where wp_head already fires via get_header()).
Claiming routes over WordPress content
A root-mounted app serves at the site root /, but by default it only fills in for paths WordPress would 404 on. A real WP page, post, or archive at the same URL still wins. That’s the safe default, but it gets in the way when an app deliberately wants to own a URL space (for example, an app blog that pulls posts from wp:posts would otherwise collide with WP’s native blog archive). Add claim: "always" to a route to flip that priority:
routes[].claim{
"mount": { "mode": "root" },
"routes": [
{ "path": "/", "file": "index.html" },
{ "path": "/blog", "file": "blog/index.html", "claim": "always" },
{ "path": "/blog/:slug", "file": "blog-template.html",
"dataset": { "source": "wp:posts", "id_field": "slug" },
"claim": "always" }
]
} With claim: "always", the dispatcher serves the app at that path even when WP has a real object for it. The field is opt-in per route, only legal on mount.mode === "root" inline apps, and rejects any value other than "always" (the string-not-boolean shape leaves room for future modes like "if-authenticated" without a breaking change). Paths the app doesn’t actually list still fall through to WP as normal.
Bridge API
Apps import @designsetgo/app-client and get a typed bridge. The surface is read-first, with deliberate additive v1.x methods for storage, abilities, email, AI, and commerce. Same wire format whether the app is in an iframe or running inline.
| Method | What it returns | Permission |
|---|---|---|
| dsgo.site.info() | Site name, URL, language, timezone | site_info |
| dsgo.posts.list(query) | Posts the current user can see | posts |
| dsgo.posts.get(id) | A single post | posts |
| dsgo.pages.list(query) | Pages | pages |
| dsgo.pages.get(id) | A single page | pages |
| dsgo.user.current() | Logged-in user or null | user |
| dsgo.user.can(cap) | boolean | user |
| dsgo.storage.app.get/set | Per-app shared state | none |
| dsgo.storage.user.get/set | Per-app, per-user state | none |
| dsgo.abilities.list() | Available abilities | abilities |
| dsgo.abilities.invoke(name, args) | Ability result | abilities |
| dsgo.abilities.implement(name, handler) | Register a Pro-gated handler for an ability the app declares in abilities.publishes | declared per ability |
| dsgo.ai.prompt(params) | LLM via the site’s Connector; supports messages, max_tokens, and ability tools | ai |
| dsgo.email.send(params) | Email via wp_mail() to admin or current_user | |
| dsgo.media.upload(file, opts?) | Promote a Blob to the WP Media Library | core, opt-out |
| dsgo.commerce.products.list/get | WooCommerce catalog (live) | commerce |
| dsgo.commerce.cart.get/add/update/remove | Visitor cart, session-backed | commerce |
| dsgo.commerce.checkout.openHostedPage | Hand off to the WC checkout | commerce |
| dsgo.router.navigate(path, opts?) | Programmatic navigation inside the app mount | none |
| dsgo.router.subscribe(handler) | Observe app path/search/hash changes | none |
| dsgo.bridge.ping() | Round-trip health check | none |
| dsgo.bridge.requestResize(height) | Ask block embeds with auto-resize enabled to resize their iframe | none |
| dsgo.help.method(name) | Lookup for a bridge method’s signature, description, and errors | none |
| dsgo.http.fetch(url, init?) | App-bound outbound HTTPS to allowlisted hosts; substitutes {{ALIAS}} from the per-app secret vault server-side so the iframe never sees the credential | permissions.http |
Full schema and error codes documented above. bridge_version: 1 is frozen.
dsgo.content.applyBlockStyles() is an SDK helper, not a permissioned bridge method. Use it after rendering post or page content when your manifest opts into content.blockStyles or content.themeStyles.
Posts and pages returned by dsgo.posts.* / dsgo.pages.* include an optional Post.meta map carrying custom fields the app declared in permissions.post_meta (and that the site exposed via register_post_meta(..., show_in_rest => true), which includes ACF Pro field groups with “Show in REST” enabled, Pods, and Meta Box). Filtering happens server-side in the plugin’s REST filter; the app sees null when it did not opt in, {} when it opted in but no matching key was REST-exposed, and the filtered map otherwise.
Routing context and URL state
After await dsgo.ready, apps can read dsgo.context.path, search, hash, and routeParams. Dynamic inline routes like /items/:id expose decoded params at dsgo.context.routeParams. Use dsgo.router.navigate(path, opts?) instead of calling history.pushState directly; the parent validates that the new URL stays inside the app mount. dsgo.router.subscribe(handler) observes programmatic navigation and browser back/forward events.
In inline page mode and iframe page mode, router navigation updates the browser URL. In block and admin iframe contexts, navigation is internal-only: subscribers fire and dsgo.context.path updates, but the parent post or admin URL does not change. Block embeds with autoResize enabled can call dsgo.bridge.requestResize(height); the host clamps the height and ignores the request outside auto-resizing block mode.
Rendering WordPress block markup
Apps that pull in posts or pages get the post body as block-formatted HTML on post.content. Drop it into the DOM and the markup is there, but the styling WordPress itself would emit on the front end is not, the Cover block has no min-height, Columns aren’t flex, and so on. Opt in via the manifest’s content object and the runtime ships the matching stylesheets alongside each post:
opt-in{
"permissions": { "read": ["posts"], "write": [] },
"content": {
"blockStyles": ["core", "auto"]
}
} Then call the SDK helper after rendering. It’s idempotent, safe to call on every route change.
JavaScriptconst post = await dsgo.posts.get(id);
// render post.content into your container however you normally would
dsgo.content.applyBlockStyles(post); // returns 0 if already injected or content_styles is null "core" ships the typography/color/spacing baseline; "auto" resolves the per-block CSS handles the post actually contains (cover, columns, group, media-text, third-party blocks). Add "designsetgo" for partner-plugin styles or "themeStyles": "global" to ship theme.json compiled CSS. Combined payload is capped at 256 KB per post. Full reference: MANIFEST.md → content.
CLI commands
| Command | Does | Notes |
|---|---|---|
| apps init [name] | Scaffold a starter app (minimal by default; --astro for the multi-page Astro starter) | writes dsgo-app.json + CLAUDE.md |
| apps dev | Local dev server with hot reload, mock or proxied bridge | free; --bridge proxy requires Pro on the connected site |
| apps dev --bridge proxy --site=... | Dev server forwarding bridge calls to a real WP site | --proxy-url for a hosted @designsetgo/proxy instance |
| apps login | Authenticate against a site | WordPress App Passwords |
| apps logout | Remove stored credentials | clears ~/.config/designsetgo |
| apps deploy | Bundle and push the current dir | --build runs npm run build |
| apps deploy --preview | Upload as a shareable time-boxed preview URL without installing | Pro; --password, --require-login, --ttl flags |
| apps deploy --promote <id> | Promote a preview to an installed app | Pro; no re-upload; preview URL stays valid until TTL |
| apps deploy --from-artifact url | Deploy a Claude artifact directly | skip the download step |
| apps deploy --site=... | Deploy to one specific site | overrides .dsgorc.json and DSGO_SITE |
| apps deploy --sites=a,b,c | Deploy to several sites in one run | Studio tier; prints a per-site summary, exits non-zero on any failure |
| apps deploy --dry-run --json | Print the resolved plan without uploading | useful in CI and debugging |
| apps preview list | List your active previews on the configured site | Pro; --json for scripting |
| apps preview delete <id> | Delete a preview before its TTL | Pro; prompts unless --yes |
| apps preview extend <id> --ttl <days> | Extend a preview’s TTL | Pro; capped at 30 days from creation |
| apps preview promote <id> | Promote a preview to an installed app | Pro |
| apps list | List installed apps | queries the active site |
| apps status | Show resolved site, auth source, and reachability | --json for scripting |
| apps uninstall <id> | Remove an installed app | prompts unless --yes is passed |
| apps open [id] | Open the site, app URL, or admin page | --print emits the URL instead |
| apps logs <id> | Tail recent app log entries | requires a plugin version exposing the logs endpoint |
| apps doctor | Run preflight checks (project config, credentials, REST reachability) | first stop when deploys fail |
| apps registry ... | Manage private starter registries | Studio tier |
| completion <shell> | Print shell completions | bash, zsh, fish |
For scripted deploys, pass --json and use DSGO_SITE, DSGO_USER, and DSGO_APP_PASSWORD instead of a saved credentials file. DSGO_USER and DSGO_APP_PASSWORD must be set together; setting only one fails fast so CI misconfiguration is visible.
Permissions
Most bridge permissions are declared under permissions.read and surfaced to the site admin at install time. A few v1.x surfaces use sibling gates: permissions.http for outbound HTTPS and permissions.run for scheduled jobs and webhooks. Storage is built in and does not require a manifest permission. v1 permissions:
permissions.http is a hostname allow-list for app-bound dsgo.http.fetch(). permissions.run: ["scheduled"] activates scheduled.jobs[]; permissions.run: ["webhooks"] activates webhooks.endpoints[]. The run surfaces dispatch through abilities.publishes[].execute_php because cron and webhook requests happen when no iframe is running.
permissions.post_meta is a meta-key allowlist that exposes a filtered Post.meta map on every dsgo.posts.* / dsgo.pages.* response. Entries are exact keys ( price) or trailing-wildcard patterns ( listing_*); cap of 32 per app; requires posts or pages in permissions.read. ACF Pro, Pods, and Meta Box all reach this surface when their field groups are registered with WP REST exposure. The install dialog renders the literal patterns under “Custom fields requested” so the admin sees exactly which fields the app wants.
dsgo.storage.* is always available. dsgo.media.upload is also a special case: every app gets it by default with no permission required, but the runtime gate is the visitor’s WordPress upload_files capability (only Authors+ can actually upload). Apps that don’t want their visitors uploading at all can opt out with "media": { "uploads": false } in the manifest.
Sharing app images with the Media Library
Apps that ship images they want the site’s editorial team to reuse (logos, OG art, illustrations) can list glob paths under media.publish in the manifest. At install time the plugin promotes matching files to real WordPress Media Library attachments, so they appear in the block editor’s media picker, can be set as featured images, and can be referenced by other plugins. Re-deploys with unchanged content are no-ops; updated content replaces the file backing the same attachment so posts that already use the image stay in sync. Spec: media.publish in MANIFEST.md.
What the admin sees at install time
The install dialog groups every permission an app declares into seven labeled buckets: read content, write content, external services, send messages, AI, run automatically, commerce. The admin reads bucket labels and a one-sentence justification per bucket (you supply these via permissions.justifications), not a flat list of permission strings. Reinstalling a newer version highlights only the buckets that changed; unchanged buckets collapse to a single “previously approved” line.
The seven-bucket cap is a v1 governance commitment. Any new bridge surface added in v1.x lands in one of these buckets, not an eighth. Background: Capability budget in MANIFEST.md.
Apps that need credentials (the Secrets tab)
An app that calls third-party APIs through dsgo.http.fetch references credentials by {{ALIAS}} tokens in its outbound requests. The admin sets those values on the per-app Secrets tab at DSGo Apps → [app row] → Secrets. Values are stored sodium-encrypted in wp_options with autoload off; the iframe never sees a resolved value. When the manifest declares required_secrets and any of them are unset at install time, the admin is redirected straight to the Secrets tab on install completion, so a credential-gated app cannot quietly land as “installed” while being non-functional. Field reference: secrets / required_secrets in MANIFEST.md.
Apps don’t see your tokens. The runtime calls REST on their behalf, with permission checks in one place. For the full contract, treat MANIFEST.md and BRIDGE-API.md as canonical.
Compatibility
The plugin works on WordPress 6.9 and up. WordPress 7.0 is required for dsgo.ai.prompt() and Riff. Commerce features require WooCommerce. Static routes work on Free; dynamic routes backed by live sources are Pro-gated. When Pro is active, wc:products resolves to an empty list if WooCommerce is inactive.
Trust model matters too: use iframe mode for untrusted bundles or saved artifacts; use inline mode when the app is reviewed, trusted, and needs native SEO, root-mount behavior, or multi-page crawlable routes.
Live data sources
Dynamic routes ( /blog/:slug, /shop/:slug, etc.) substitute fields from a dataset into a single template at request time. Bundle JSON datasets are static. Live sources resolve from the host site’s own content and are Pro-gated; on Free the app installs, but those live-source routes are marked inactive until a Pro license or trial is active. When active, new posts and products appear without redeploying.
routes[].dataset.source{
"routes": [
{
"path": "/blog/:slug",
"file": "blog-template/index.html",
"dataset": { "source": "wp:posts", "id_field": "slug" }
},
{
"path": "/shop/:slug",
"file": "product-template/index.html",
"dataset": { "source": "wc:products", "id_field": "slug" }
}
]
} source | Resolves to |
|---|---|
| wp:posts | All published posts ( post_type=post). |
| wp:pages | All published pages ( post_type=page). |
| wp:cpt:<slug> | A registered custom post type (e.g. wp:cpt:recipe). |
| wc:products | Published WooCommerce products. Returns [] when WC is inactive. |
id_field for built-in live sources must be either "slug" or "id". Plugins can register additional schemes via the dsgo_apps_dataset_resolver filter; see the MANIFEST.md live-source section for the resolver contract and cache TTL filters.
Results are cached per app+route+version for one hour by default. Saves and deletes on the underlying post type invalidate the cache automatically, so editorial changes go live within seconds.
Bundle constraints
Bundles are static web assets, not PHP plugins. The installer rejects server-executable or unsafe filesystem shapes: no symlinks, no executable bits, no .php, .exe, .sh, or dotfiles other than approved .well-known/ assets. Paths must stay inside the bundle root.
- WebAssembly is allowed. Bundles may ship
.wasm,.wat, and.data. Inline mode serves.wasmwith the correct MIME type; iframe mode depends on the web server’s static MIME mapping, so useWebAssembly.instantiate()with anArrayBufferfallback if streaming compilation fails. Threaded WASM and Service Workers are out of scope for v1. - Web Workers are allowed.
runtime.uses_workersis informational; it surfaces a line in the install dialog. Service Workers remain rejected in v1. - Agent discovery files are supported at the root. Root-mounted apps may ship
.well-known/api-catalog; the runtime emits the matchingLinkheader. Static routes can also ship a same-path.mdsibling, and clients that requestAccept: text/markdownreceive the Markdown version. Dynamic routes are not Markdown-negotiated.
Caching & CDNs
DSGo Apps is built to play well with every layer of the typical WordPress caching stack, host page caches (GoDaddy MWP gateway, WP Engine, Kinsta), edge CDNs (Cloudflare, Fastly), and WP plugins like WP Rocket or W3 Total Cache. The short version: assets get edge-cached for free, HTML routes are safe to cache, bridge calls must not be cached.
Static assets, cache aggressively
Bundle assets (CSS, JS, fonts, images, SVGs) live at /wp-content/uploads/designsetgo-apps/<id>/..., the canonical WordPress uploads path. Every host CDN already has rules for it. You don’t need to do anything; assets edge-cache automatically.
- Astro & Vite emit content-hashed filenames (e.g.
developers.BYFSNV21.css). New build = new hash = automatic cache bust.max-agecan safely be set to a year. - Root-mount apps benefit twice. The plugin rewrites asset URLs to the upload path so they bypass PHP entirely, nginx (or your CDN edge) serves them direct.
- Don’t add asset-rewriting plugins like Autoptimize or Async JavaScript over a DSGo bundle. The HTML sanitizer rejects unknown injected scripts; let the host CDN do its work and skip the plugin layer.
HTML routes, safe to cache for anonymous visitors
The HTML for a route ( /, /docs/, etc.) is mostly static, what Astro produced at build time, plus a small JSON hydration block ( appId, routePath, locale) and DSGo’s bridge bootstrap. None of that is user-specific. Per-user data comes from bridge calls at runtime, after the page loads.
- Logged-in users bypass the cache automatically. Every managed host (GoDaddy, WPE, Kinsta) ships rules that bypass page cache when a WP authentication cookie is present. So admins/editors always see fresh PHP, while anonymous visitors hit the cache. Don’t override this.
- Cache CSP nonces only when the full response is cached as a unit. The nonce lives in both the HTML body and the response header; full-page caches must store and replay both together. If your cache rewrites either side independently, exclude DSGo HTML routes from that rule.
- For page-rendered apps, assume the route is publicly cacheable. If your app does something user-specific in the bundle (capability gating, personalization), do it via bridge calls at runtime, not in the SSR’d HTML.
Bridge calls, never cache
Bridge endpoints under /wp-json/dsgo/v1/... serve per-user data. dsgo.posts.list() returns drafts to editors and only published posts to anonymous visitors; dsgo.storage.user.get() is keyed to the current user. Caching them at any layer is a security bug.
- Hosts default to no-cache on
/wp-json/*. Leave that alone. - If you have a Cloudflare “Cache Everything” page rule, exclude
/wp-json/*explicitly. The single rule that bites people is “Cache Everything for the entire site.” - The bridge sends
Cache-Control: no-storeon responses; if a CDN rule overrides that, you’ll see stale data and (worse) cross-user data leaks.
Redeploys and cache invalidation
When you re-deploy a bundle, the plugin clears its own caches and bumps the host’s gateway cache marker (where supported, you’ll see x-gateway-cache-key change on GoDaddy MWP and WP Engine). Most edge CDNs see the new asset hashes and self-invalidate, but for HTML routes:
- Managed hosts (WPE, GoDaddy MWP, Kinsta). Auto-bust on plugin install/update. No action needed.
- Cloudflare with “Cache Everything” rule. Manual purge after redeploy, or use a versioned URL pattern. Best: leave Cloudflare in default WP mode (caches static assets, passes HTML through).
- WP Rocket / W3 Total Cache. Both clear page cache on plugin update; usually fine.
What about visitor-specific HTML?
v1 doesn’t support server-side personalization, the dispatcher serves the same HTML to every anonymous visitor, and per-user data flows through the bridge. This is intentional: it makes caching trivially safe and keeps the runtime predictable.
If you need per-user content in v1, fetch it via the bridge after the page loads. Astro’s hydration plus a single await dsgo.posts.list() at boot is plenty fast on a cached page (the cache hit serves HTML in <50ms; the bridge call adds ~100ms).
Troubleshooting
Most deploy issues come from the WordPress host’s edge, not from DSGo itself. Below are the failure modes we’ve seen most often, with the diagnostic that confirms each one.
401 from /wp-json/, before you even authenticate
Symptom: curl https://your-site.com/wp-json/ returns 401 with an empty text/html body (no JSON). The CLI’s login command reports “Authentication failed” even with a fresh Application Password.
Cause: something upstream of WordPress is rejecting REST requests entirely. WordPress’ own REST handler returns JSON for both success and errors, an empty HTML body with a 401 means the request never reached PHP. Common culprits, in rough order of frequency:
- A “coming soon” or maintenance-mode plugin. SeedProd, WP Maintenance, Coming Soon Page Pro, and similar plugins gate every front-end and REST request behind a single landing page. Fix: publish the site (or pause the plugin) and try again.
- A security plugin’s REST lockdown. Wordfence, Solid Security (formerly iThemes), All In One WP Security, etc. all ship a “Disable REST API” or “Restrict REST to logged-in users” toggle. Fix: allow the REST API for authenticated requests.
- Managed-host edge gating. WP Engine and GoDaddy Managed WordPress in particular run a gateway in front of WordPress that can reject
/wp-json/*with a blank401on certain plans. Fix: open a support ticket asking them to allow REST access, or check the host’s control panel for a REST toggle. - Cloudflare WAF managed rules. The “WordPress” ruleset has rules (e.g.
100053) that fire on/wp-json/*and can return blank401s. Fix: in Cloudflare, allow that rule to skip for authenticated REST traffic, or whitelist the relevant paths.
Diagnostic: an unauthenticated GET /wp-json/ on a healthy site returns 200 with a JSON namespace listing. Anything else means the REST namespace itself is unreachable, and no fix on the WordPress side, including reissuing the App Password, will help.
“Authentication failed” from the CLI when REST is reachable
Symptom: /wp-json/ returns JSON, but designsetgo apps login still fails.
Cause: most often, the host strips the Authorization header at the edge before it reaches PHP. Bluehost, GoDaddy MWP, and some shared cPanel hosts do this. The fix is to forward the header explicitly in .htaccess:
Authorization passthroughRewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] Add the snippet near the top of .htaccess (above the WordPress block). Reissue the App Password if it’s old, then retry designsetgo apps login.
If the username has spaces or special characters, copy the App Password value with the spaces shown by WordPress, the CLI normalizes them. If you removed the spaces and pasted as one word, that also works.
Sanitizer rejects scripts at deploy time
Symptom: designsetgo apps deploy fails preflight with a remote_script, inline_script, or script_src error.
Cause: the DSGo runtime’s sanitizer enforces the manifest’s CSP allowlist at install time. Common fixes:
- Inline
<script>bodies. All JS must be an external asset referenced via<script src="...">. In Vite/Astro projects, setvite.build.assetsInlineLimit: 0to prevent inlining. - Scripts from a third-party origin. Add the origin to
runtime.csp.script_srcindsgo-app.json(e.g."https://cdn.example.com"). - Asset paths that resolve outside the bundle. Inline-mode apps use Astro’s
base, in yourastro.config, setbaseto/apps/<your-id>/for prefixed-mount apps or/for root-mount.
Root-mount app loads, but _astro/*.css and favicon.svg 404
Symptom: a root-mounted app’s HTML pages render fine, but every static asset (CSS, JS, SVG, images) returns 404 in the browser’s network tab.
Cause: managed-host nginx layers (GoDaddy Managed WordPress, WP Engine, etc.) match URLs ending in known static extensions ( .css, .js, .svg, .png, .woff2) and try to serve them from disk before WordPress runs. If the file isn’t at that exact path on disk, the host returns a fast-path 404, which means DSGo’s root-mount dispatcher never gets a chance to stream the file from the bundle.
Fix: upgrade to DSGo Apps 0.1.1 or later. The plugin now rewrites bundle asset URLs to point at the bundle’s actual upload path ( /wp-content/uploads/designsetgo-apps/<id>/_astro/...) for root-mount apps, so nginx serves them directly without going through PHP. After upgrading, re-deploy the bundle (the rewrite happens at render time, not install time, but a fresh deploy clears any cached HTML).
Diagnostic: curl -I https://your-site.com/_astro/foo.css returns 404 with content-type: text/html and an empty body. The HTML body would be present if WP’s 404 template ran. Empty body + cache-control headers = host fast-path.
Live page returns 404 after deploy
Symptom: / works but /some-route/ returns 404.
Cause: the route isn’t in the manifest, or its file path doesn’t exist in the built bundle.
- Every URL the site can serve must be declared in
dsgo-app.jsonunderroutes. - The
filepath is bundle-relative, i.e. relative todist/, not the project root. For Astro’s defaultformat: directoryoutput, that’s typicallysome-route/index.html. routes[0]must havepath: "/". The home route is mandatory.- The
dsgo-app.jsonfile itself must be present inside the bundle directory you deploy. Put it in Astro’spublic/folder, or copy it post-build with a script inpackage.json:"build": "astro build && cp dsgo-app.json dist/".
Something missing? Email us. The bridge spec is public; the bridge client and CLI are MIT-licensed on GitHub.