Documentation

Five minutes from installed to shipped.

Two paths. No-terminal install for site owners. CLI deploy for developers. Below: getting started, the manifest, the bridge, and the CLI — the only four pages most people ever need.

Install the plugin

Activate DesignSetGo Apps from the WordPress plugin directory. The plugin works on WordPress 6.9 and up; WordPress 7.0 is required for AI features (dsgo.ai.prompt).

Once activated you’ll see DSGo Apps in the wp-admin sidebar.

Path A — Drop a zip (no terminal)

You got a .zip from someone, or downloaded a Claude artifact as HTML. Install it without leaving wp-admin:

  1. WordPress admin → DSGo Apps.
  2. Drop the file onto the install card (or click choose a file).
  3. Review the capability list, rendering mode, and network policy; then approve the install.
  4. The app is reachable at https://your-site.com/apps/{slug} — or promote it from Installed apps to make it your site’s home page (lives at /).

To uninstall, hover the row in Installed apps and click Delete.

Path B — Use the CLI

Recommended for iteration speed. You’ll use @designsetgo/cli.

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 apps init my-app
cd my-app
npm install

Creates a starter Astro project with dsgo-app.json already wired up.

Authenticate

shelllogin
npx designsetgo 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.

Deploy

shelldeploy
npx designsetgo 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.

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": []
  },
  "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.

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.ai.prompt(messages)LLM via the site’s Connectorai
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.bridge.ping()Round-trip health checknone

Full schema and error codes: BRIDGE-API.md. 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.

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 Astro appwrites dsgo-app.json + CLAUDE.md
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 --from-artifact urlDeploy a Claude artifact directlyskip the download step
apps deploy --site=...Deploy to a specific sitepass different sites in separate deploys
apps listList installed appsqueries the active site

Permissions

Permissions are declared under permissions.read and surfaced to the site admin at install time. 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).

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.

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 AI features like dsgo.ai.prompt(). Commerce features require WooCommerce. Dynamic routes using wc:products resolve to an empty list when 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. Datasets normally ship as JSON files inside the bundle, but a route can also pull from live sources that resolve from the host site’s own content. New posts and products then 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.

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/dsgo-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.
  • The CSP nonce is fine to cache. The nonce lives in both the HTML body and the response header; full-page caches store both together so they stay paired. Nonces aren’t secrets — they’re tags that pair scripts with the CSP allowlist.
  • If you set display.modes: ["page"] only, 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/dsgo-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? Open an issue. The runtime, the bridge spec, and the CLI are all open source.