Prerequisites
- Node 20+ and a terminal.
- A WordPress site (6.9+) with the DesignSetGo Apps plugin activated.
- A WordPress Application Password for that site.
Free covers 1 active static app. Live-source dynamic routes ( wp:posts, wc:products, etc.), unlimited active apps, and CLI deploy require Pro.
Scaffold the project
apps init --astronpx @designsetgo/cli apps init my-astro-site --astro
cd my-astro-site
npm install The Astro starter ships an astro.config.mjs already wired to the DSGo manifest, a base Layout.astro, three example pages, a CLAUDE.md documenting the bridge for agents, and a dsgo-app.json with the home route pre-declared.
astro.config.mjs
Astro needs three things to produce a bundle DSGo can serve: a base matching the URL the plugin mounts the app at, static output, and directory-format routes so every page emits as some-route/index.html.
defineConfigimport { defineConfig } from 'astro/config';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const manifest = JSON.parse(
readFileSync(fileURLToPath(new URL('./dsgo-app.json', import.meta.url)), 'utf8'),
);
export default defineConfig({
base: manifest.mount?.mode === 'root' ? '/' : `/apps/${manifest.id}/`,
output: 'static',
trailingSlash: 'always',
build: { format: 'directory' },
vite: { build: { assetsInlineLimit: 0 } },
}); base— reads the manifest so flippingmount.modebetween"prefixed"and"root"doesn’t require a config edit. Astro rewrites internal asset URLs to match.build.format: 'directory'—src/pages/about.astroemits atdist/about/index.html, which is what the manifest’sfilefield references.vite.build.assetsInlineLimit: 0— prevents Vite from inlining small assets as<script>bodies, which the DSGo CSP sanitizer rejects.
The manifest
Every page Astro produces must be declared in dsgo-app.json under routes. The file path is bundle-relative (relative to dist/), so an Astro page at src/pages/pricing.astro with format: 'directory' maps to file: "pricing/index.html".
routes[]{
"manifest_version": 1,
"id": "my-astro-site",
"name": "My Astro site",
"version": "0.1.0",
"isolation": "inline",
"entry": "index.html",
"routes": [
{ "path": "/", "file": "index.html", "title": "Home" },
{ "path": "/about", "file": "about/index.html", "title": "About" },
{ "path": "/pricing", "file": "pricing/index.html", "title": "Pricing" }
],
"permissions": { "read": ["site_info"], "write": [] }
} Make sure the manifest ends up inside the bundle the CLI uploads. The easiest path is to drop it into Astro’s public/ folder (it’s copied verbatim into dist/), or have the build script copy it: "build": "astro build && cp dsgo-app.json dist/".
routes[0] must have path: "/". The home route is mandatory.
Add pages
Write Astro pages as you normally would. There’s no DSGo build plugin, no special component, no required getStaticPaths contract — just Astro.
Astro---
import Layout from '../layouts/Layout.astro';
---
<Layout title="About">
<h1>About this site</h1>
<p>Built with Astro, deployed to WordPress.</p>
</Layout> Add the matching entry under routes in the manifest and the page will resolve at the configured base path.
Link between pages
Astro doesn’t rewrite href values, only asset URLs. For internal links, use import.meta.env.BASE_URL so prefixed and root mounts both work without code changes:
BASE_URL<a href={`${import.meta.env.BASE_URL}pricing`}>Pricing</a>
<a href={`${import.meta.env.BASE_URL}about`}>About</a> For programmatic navigation inside the app, prefer dsgo.router.navigate(path) — the parent validates the new path stays inside the app mount and updates the browser URL safely. Direct history.pushState calls outside the mount are blocked.
Read WordPress data
Import @designsetgo/app-client in any Astro page or component <script>. The bridge runs in the browser at view time, so the page itself stays a static asset crawlers can index.
bridge---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Latest posts">
<h1>From the blog</h1>
<ul id="posts"></ul>
</Layout>
<script>
import { dsgo } from '@designsetgo/app-client';
await dsgo.ready;
const { items } = await dsgo.posts.list({ per_page: 5 });
const ul = document.getElementById('posts');
for (const post of items) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = post.link;
a.textContent = post.title;
li.appendChild(a);
ul.appendChild(li);
}
</script> The full bridge surface (posts, pages, user, media, AI, commerce, storage, abilities, email, HTTP, router) is documented in the bridge reference.
Declare permissions
Bridge methods are gated by what the manifest declares. Add the permission the call needs before deploying or the install will be rejected at preflight.
permissions.read"permissions": {
"read": ["site_info", "posts", "user"],
"write": []
} Permissions surface to the site admin at install time, grouped into seven buckets with a one-sentence justification per bucket. See permissions reference.
Render block markup
Posts and pages returned from the bridge include the body as block-formatted HTML on post.content. Dropping it into the DOM gives you the markup but not the styles WordPress would emit on its front end (Cover has no min-height, Columns aren’t flex, etc.). Opt in via the manifest, then call the SDK helper after rendering:
content.blockStyles"content": { "blockStyles": ["core", "auto"] } applyBlockStylesimport { dsgo } from '@designsetgo/app-client';
const post = await dsgo.posts.get(id);
container.innerHTML = post.content;
dsgo.content.applyBlockStyles(post); // idempotent, safe on every route change Add "designsetgo" for partner-plugin styles, or "themeStyles": "global" to ship the theme’s compiled theme.json CSS. Combined payload is capped at 256 KB per post.
Dynamic routes from live data
For content the WordPress editor owns (posts, products, custom post types), don’t ship one Astro page per item — declare a single template route with a :param placeholder and point it at a live dataset. The plugin resolves it at request time and substitutes fields into the template.
routes[].dataset{
"path": "/posts/:slug",
"file": "post/index.html",
"dataset": { "source": "wp:posts", "id_field": "slug" }
} The template at src/pages/post.astro uses {{title}}, {{excerpt}}, {{content}} placeholders (server-substituted) or, for richer rendering, calls the bridge with dsgo.context.routeParams.slug to fetch the full post object.
source | Resolves to |
|---|---|
| wp:posts | Published posts ( post_type=post) |
| wp:pages | Published pages ( post_type=page) |
| wp:cpt:<slug> | A registered custom post type |
| wc:products | Published WooCommerce products |
Live sources are Pro. On Free, the app installs and static routes still serve; live-source routes stay inactive until Pro is active. Results cache per app+route+version for one hour; saves and deletes on the underlying post type invalidate automatically.
@designsetgo/astro: vertical-pack components
If you are building a WooCommerce front-end (and later: real estate, fitness, food, local services), @designsetgo/astro is a separate npm package with pre-built Astro components for every DSGo vertical pack, plus a tiny Astro integration that auto-merges the components’ required permissions, abilities, and commerce endpoints into your dsgo-app.json at build time. v0.1.0 ships 14 WooCommerce components.
Useful if you are vibe-coding the project with Claude Code, Cursor, or Codex: as the AI adds and removes component imports, the integration keeps dsgo-app.json honest. CI’s build fails if your manifest drifts from what the imports actually need.
Install the package
npm installnpm install @designsetgo/astro Astro 4 or 5 is a peer dependency you already have. The bridge client ( @designsetgo/app-client) is an optional peer; components import it transitively, so you only need it for your own <script> blocks.
Add the integration
dsgoAstro()import { defineConfig } from 'astro/config';
import dsgoAstro from '@designsetgo/astro/integration';
export default defineConfig({
integrations: [
dsgoAstro({ manifest: './dsgo-app.json' }),
],
}); One line in astro.config.mjs. On every build the integration scans your project for @designsetgo/astro/<pack> imports, loads each component’s manifest from node_modules, unions its required permissions into your dsgo-app.json (preserving every other key), and emits a .astro/dsgo-window.d.ts shim so window.dsgo autocompletes against the permissions you actually declared.
Import a component
component import---
import { SmartCartUpsell, ProductQa } from '@designsetgo/astro/woo';
---
<SmartCartUpsell count={4} heading="You might also like" />
<ProductQa productId={42} /> Each component renders its own markup, ships its own scoped CSS, and self-boots a small client bundle inside an IIFE keyed to its data-dsgo-component slug. Two components on the same page are slug-isolated; bundle A’s mount will never fire for bundle B’s roots.
Component catalog (v0.1.0)
WooCommerce pack ( @designsetgo/astro/woo):
| Component | What it does | Reads |
|---|---|---|
SmartCartUpsell | Recommends 3 to 4 products from the visitor’s current-cart categories. | commerce, abilities |
AbandonedCartRecovery | One re-engagement email per 24h to the logged-in customer who abandons checkout. | commerce, abilities, email, user |
BundleBuilder | Multi-product bundle composer with live total; one-click add-to-cart. | commerce, abilities |
CheckoutFields | Conditional gift message, delivery date, and age verification fields above WC checkout. | commerce, abilities, user |
FitRecommender | Three-question fit quiz; remembers the answer per visitor. | commerce, storage |
GiftcardBalance | Public balance lookup against the merchant’s gift-card provider, credentials in the secret vault. | http |
LoyaltyDashboard | Logged-in customer’s points and tier; merchant-side point logic stays in abilities. | commerce, abilities, user |
PickupScheduler | Calendar widget for storefront pickup; one admin email per booking. | email (admin) |
PostPurchaseSurvey | Three-question survey on the WC thank-you page; digest email to admin. | email (admin) |
PreorderSignup | Email-signup gate on out-of-stock product pages; batched admin notification. | commerce, email (admin) |
ProductQa | Visitor asks a question; AI answers using only the product description and admin-curated facts. | commerce, ai, user |
StoreLocator | Multi-location directory with hours, address, phone, one-tap directions. | storage.app |
SubscriptionFaq | AI-powered FAQ for WooCommerce Subscriptions merchants; per-member plan context. | ai, user |
WholesaleRequest | B2B quote request form; multi-product picker; admin notification on submit. | commerce, abilities, email (admin) |
Every component is behavior-preserving with its standalone-bundle counterpart in examples/woo-*/; a parity test and pre-commit hook keep them in sync. Full catalog with descriptions, options, and troubleshooting lives in the package README.
Write vs check mode
mode: 'write' (default) rewrites dsgo-app.json in place when an imported component adds permissions. mode: 'check' exits the build non-zero with a printed diff if the file is stale. Use 'check' in CI so a forgotten manifest re-stage breaks the build:
modedsgoAstro({
manifest: './dsgo-app.json',
mode: process.env.CI ? 'check' : 'write',
}) The merge is additive only; the integration never removes permissions. Local dev mode debounces merges by 200ms so saving a file that adds an import does not thrash the manifest.
Prefixed mount (default)
Without a mount block, the app serves at /apps/<id>/.... Good for adding a tool, microsite, or marketing surface alongside an existing WP site without changing what’s at /.
Root mount (whole site)
To make the Astro app be the site, add to the manifest:
mount.mode"mount": { "mode": "root" } The app now serves at /. The Astro routes you declared own the URLs WordPress would otherwise serve; any path WP would have 404’d on falls through to the route table. Real WP pages, posts, archives, and feeds still win by default — root mode adds, it doesn’t shadow — so the editor can keep publishing in wp-admin.
If an Astro route deliberately needs to override WP (e.g. an app-rendered /blog backed by wp:posts that would otherwise collide with WP’s native blog archive), add claim: "always" to that route. See claiming routes.
Build and deploy
login + deploynpx @designsetgo/cli apps login --site https://yoursite.com
npx @designsetgo/cli apps deploy --build --build runs npm run build first. The CLI then zips dist/, validates the manifest, shows the capability diff, and posts the bundle to the site’s REST endpoint. Re-running deploy updates the existing install atomically at the same app id.
On Free, apps init --astro and local builds work; deploy falls back to a wp-admin bundle upload (Path B). On Pro, deploy from the terminal directly.
Caching
- Static assets self-bust. Astro + Vite emit content-hashed filenames (
app.BYFSNV21.css). A new build = a new hash = automatic edge cache bust. Safe to setmax-ageto a year. - HTML routes are publicly cacheable for anonymous visitors. Per-user data flows through the bridge at runtime, not the SSR’d HTML.
- Bridge calls (
/wp-json/dsgo/v1/*) must not be cached. The bridge sendsCache-Control: no-store; don’t override it with a CDN “Cache Everything” rule. - Root-mount asset URLs are rewritten to
/wp-content/uploads/designsetgo-apps/<id>/...so nginx serves them direct, bypassing PHP.
Full guidance: caching & CDNs in the main docs.
Common pitfalls
Inline scripts rejected at deploy
The CSP sanitizer rejects inline <script> bodies. Set vite.build.assetsInlineLimit: 0 in astro.config.mjs so Vite emits scripts as external files. For third-party origins, add the host to runtime.csp.script_src.
Page returns 404 after deploy
Every URL must be declared in routes. The file path is bundle-relative ( dist/ root); with format: 'directory', src/pages/about.astro maps to about/index.html, not about.html.
Root-mount asset 404s
On managed hosts (GoDaddy MWP, WP Engine), nginx may fast-path .css/.js/.svg requests before WordPress runs. Requires DSGo Apps 0.1.1+, which rewrites bundle asset URLs to the actual upload path. Upgrade, then re-deploy. See troubleshooting.
dsgo-app.json missing from the bundle
The manifest must be inside dist/ when the CLI zips it. Drop it in Astro’s public/ folder, or copy it post-build: "build": "astro build && cp dsgo-app.json dist/".
Links break when switching to root mount
Hard-coded /apps/<id>/ hrefs won’t resolve at /. Always link via ${import.meta.env.BASE_URL}path so both mount modes work without code changes.
Where to go next
- Full bridge API — every method, error code, and permission scope
- Manifest reference — the complete
dsgo-app.jsonschema - CLI commands —
init,deploy,doctor, multi-site flags - Permissions — the seven-bucket install dialog and what each permission grants
- The narrative version — same workflow, walkthrough form
Something missing or unclear? Email us.