Product
Docs
Pricing Blog Install free

Documentation

Five minutes from installed to shipped.

Three paths to a live app. Drop a saved HTML page in wp-admin (Path A). Upload a packaged bundle with a manifest (Path B). Or scaffold and deploy from your editor with the CLI (Path C). The Riff AI builder adds the in-admin Pro path, and Connect lets claude.ai, chatgpt.com, Cursor, or Claude Desktop build apps over MCP.

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.

Free vs Pro

Which paths work on which plan?

Path / featureFreePro ($149.99/yr)Studio ($499/yr)
Path A, upload an HTML artifact in wp-adminyes, 1 active appyes, unlimited active appsyes, unlimited active apps
Path B, upload a packaged bundle in wp-adminyes, 1 active appyes, unlimited active appsyes, unlimited active apps
Path C, CLI scaffold + deploy from your editorinit only; deploy is Proyes, unlimited active appsyes, unlimited active apps
Riff, in-admin AI buildernoincludedincluded
Apps-as-Abilities publishing ( abilities.publishes)installs, inactiveactiveactive
Scheduled jobs / webhook endpointsinstalls, inactiveactiveactive
Live dynamic routes ( wp:posts, wc:products)installs, inactiveactiveactive
Multi-site CLI deploy ( --sites=a,b,c)noStudio tierincluded
White-labelnonoincluded

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:

  1. WordPress admin → DSGo Apps.
  2. Use the first-run chooser to install the starter app, or stay on Upload artifact and drop your .html or static .zip.
  3. Confirm the app ID and display name, then install.
  4. 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).

Agent users

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

  1. Activate the DesignSetGo Apps plugin.
  2. Generate a WordPress Application Password: https://your-site.com/wp-admin/profile.php#application-passwords-section
  3. Copy the 24-character value, you’ll see it once.

Scaffold an app

shellnpx
npx @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

shelllogin
npx @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

shelldev
npx @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

shelldeploy
npx @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:

dsgo-app.jsonmanifest_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:

dsgo-app.jsonroutes[].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.

MethodWhat it returnsPermission
dsgo.site.info()Site name, URL, language, timezonesite_info
dsgo.posts.list(query)Posts the current user can seeposts
dsgo.posts.get(id)A single postposts
dsgo.pages.list(query)Pagespages
dsgo.pages.get(id)A single pagepages
dsgo.user.current()Logged-in user or nulluser
dsgo.user.can(cap)booleanuser
dsgo.storage.app.get/setPer-app shared statenone
dsgo.storage.user.get/setPer-app, per-user statenone
dsgo.abilities.list()Available abilitiesabilities
dsgo.abilities.invoke(name, args)Ability resultabilities
dsgo.abilities.implement(name, handler)Register a Pro-gated handler for an ability the app declares in abilities.publishesdeclared per ability
dsgo.ai.prompt(params)LLM via the site’s Connector; supports messages, max_tokens, and ability toolsai
dsgo.email.send(params)Email via wp_mail() to admin or current_useremail
dsgo.media.upload(file, opts?)Promote a Blob to the WP Media Librarycore, opt-out
dsgo.commerce.products.list/getWooCommerce catalog (live)commerce
dsgo.commerce.cart.get/add/update/removeVisitor cart, session-backedcommerce
dsgo.commerce.checkout.openHostedPageHand off to the WC checkoutcommerce
dsgo.router.navigate(path, opts?)Programmatic navigation inside the app mountnone
dsgo.router.subscribe(handler)Observe app path/search/hash changesnone
dsgo.bridge.ping()Round-trip health checknone
dsgo.bridge.requestResize(height)Ask block embeds with auto-resize enabled to resize their iframenone
dsgo.help.method(name)Lookup for a bridge method’s signature, description, and errorsnone
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 credentialpermissions.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:

dsgo-app.jsonopt-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.

app codeJavaScript
const 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

CommandDoesNotes
apps init [name]Scaffold a starter app (minimal by default; --astro for the multi-page Astro starter)writes dsgo-app.json + CLAUDE.md
apps devLocal dev server with hot reload, mock or proxied bridgefree; --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 loginAuthenticate against a siteWordPress App Passwords
apps logoutRemove stored credentialsclears ~/.config/designsetgo
apps deployBundle and push the current dir--build runs npm run build
apps deploy --previewUpload as a shareable time-boxed preview URL without installingPro; --password, --require-login, --ttl flags
apps deploy --promote <id>Promote a preview to an installed appPro; no re-upload; preview URL stays valid until TTL
apps deploy --from-artifact urlDeploy a Claude artifact directlyskip the download step
apps deploy --site=...Deploy to one specific siteoverrides .dsgorc.json and DSGO_SITE
apps deploy --sites=a,b,cDeploy to several sites in one runStudio tier; prints a per-site summary, exits non-zero on any failure
apps deploy --dry-run --jsonPrint the resolved plan without uploadinguseful in CI and debugging
apps preview listList your active previews on the configured sitePro; --json for scripting
apps preview delete <id>Delete a preview before its TTLPro; prompts unless --yes
apps preview extend <id> --ttl <days>Extend a preview’s TTLPro; capped at 30 days from creation
apps preview promote <id>Promote a preview to an installed appPro
apps listList installed appsqueries the active site
apps statusShow resolved site, auth source, and reachability--json for scripting
apps uninstall <id>Remove an installed appprompts 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 entriesrequires a plugin version exposing the logs endpoint
apps doctorRun preflight checks (project config, credentials, REST reachability)first stop when deploys fail
apps registry ...Manage private starter registriesStudio tier
completion <shell>Print shell completionsbash, 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:

  • site_info, read site name, URL, locale, and timezone via dsgo.site.info().
  • posts, read posts via dsgo.posts.*. Filtered by current user’s caps.
  • pages, read pages via dsgo.pages.*.
  • user, current user identity and capability checks.
  • abilities, discover and call abilities other plugins register. Requires an abilities.consumes allow-list in the manifest.
  • ai, reach the site’s configured Connector. Requires WP 7.0.
  • email, send to a closed recipient set like admin or current_user. Requires an email block in the manifest.
  • commerce, read the WooCommerce catalog and operate on the visitor’s cart through dsgo.commerce.*. The manifest’s commerce block also has to declare a provider (v1: "woocommerce") and the endpoint groups the app uses ( products, cart, checkout).

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.

dsgo-app.jsonroutes[].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" }
    }
  ]
}
sourceResolves to
wp:postsAll published posts ( post_type=post).
wp:pagesAll published pages ( post_type=page).
wp:cpt:<slug>A registered custom post type (e.g. wp:cpt:recipe).
wc:productsPublished 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 .wasm with the correct MIME type; iframe mode depends on the web server’s static MIME mapping, so use WebAssembly.instantiate() with an ArrayBuffer fallback if streaming compilation fails. Threaded WASM and Service Workers are out of scope for v1.
  • Web Workers are allowed. runtime.uses_workers is 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 matching Link header. Static routes can also ship a same-path .md sibling, and clients that request Accept: text/markdown receive 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-age can 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-store on 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:

  1. 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.
  2. 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.
  3. 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 blank 401 on certain plans. Fix: open a support ticket asking them to allow REST access, or check the host’s control panel for a REST toggle.
  4. Cloudflare WAF managed rules. The “WordPress” ruleset has rules (e.g. 100053) that fire on /wp-json/* and can return blank 401s. 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:

.htaccessAuthorization passthrough
RewriteEngine 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, set vite.build.assetsInlineLimit: 0 to prevent inlining.
  • Scripts from a third-party origin. Add the origin to runtime.csp.script_src in dsgo-app.json (e.g. "https://cdn.example.com").
  • Asset paths that resolve outside the bundle. Inline-mode apps use Astro’s base, in your astro.config, set base to /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.json under routes.
  • The file path is bundle-relative, i.e. relative to dist/, not the project root. For Astro’s default format: directory output, that’s typically some-route/index.html.
  • routes[0] must have path: "/". The home route is mandatory.
  • The dsgo-app.json file itself must be present inside the bundle directory you deploy. Put it in Astro’s public/ folder, or copy it post-build with a script in package.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.