Product
Docs
Pricing Blog Install free

Docs / Guide

Build an Astro site on WordPress.

Astro for the front-end, WordPress for the CMS, auth, AI, commerce, and media library. One bundle, one domain, one deploy. This page is the reference for getting an Astro project running as a DSGo App: manifest shape, base path, the bridge, dynamic routes from live WP data, and root-mount mode. For the broader DSGo surface (CLI, permissions, all bridge methods), see the main docs.

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

shellapps init --astro
npx @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.

astro.config.mjsdefineConfig
import { 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 flipping mount.mode between "prefixed" and "root" doesn’t require a config edit. Astro rewrites internal asset URLs to match.
  • build.format: 'directory'src/pages/about.astro emits at dist/about/index.html, which is what the manifest’s file field 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".

dsgo-app.jsonroutes[]
{
  "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 &mdash; just Astro.

src/pages/about.astroAstro
---
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.

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:

Layout.astroBASE_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.

src/pages/index.astrobridge
---
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.

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

dsgo-app.jsoncontent.blockStyles
"content": { "blockStyles": ["core", "auto"] }
app codeapplyBlockStyles
import { 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 &mdash; 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.

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

sourceResolves to
wp:postsPublished posts ( post_type=post)
wp:pagesPublished pages ( post_type=page)
wp:cpt:<slug>A registered custom post type
wc:productsPublished 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

shellnpm install
npm 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

astro.config.mjsdsgoAstro()
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

src/pages/cart.astrocomponent 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):

ComponentWhat it doesReads
SmartCartUpsellRecommends 3 to 4 products from the visitor’s current-cart categories.commerce, abilities
AbandonedCartRecoveryOne re-engagement email per 24h to the logged-in customer who abandons checkout.commerce, abilities, email, user
BundleBuilderMulti-product bundle composer with live total; one-click add-to-cart.commerce, abilities
CheckoutFieldsConditional gift message, delivery date, and age verification fields above WC checkout.commerce, abilities, user
FitRecommenderThree-question fit quiz; remembers the answer per visitor.commerce, storage
GiftcardBalancePublic balance lookup against the merchant’s gift-card provider, credentials in the secret vault.http
LoyaltyDashboardLogged-in customer’s points and tier; merchant-side point logic stays in abilities.commerce, abilities, user
PickupSchedulerCalendar widget for storefront pickup; one admin email per booking.email (admin)
PostPurchaseSurveyThree-question survey on the WC thank-you page; digest email to admin.email (admin)
PreorderSignupEmail-signup gate on out-of-stock product pages; batched admin notification.commerce, email (admin)
ProductQaVisitor asks a question; AI answers using only the product description and admin-curated facts.commerce, ai, user
StoreLocatorMulti-location directory with hours, address, phone, one-tap directions.storage.app
SubscriptionFaqAI-powered FAQ for WooCommerce Subscriptions merchants; per-member plan context.ai, user
WholesaleRequestB2B 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:

astro.config.mjsmode
dsgoAstro({
  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:

dsgo-app.jsonmount.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 &mdash; root mode adds, it doesn’t shadow &mdash; 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

shelllogin + deploy
npx @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 set max-age to 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 sends Cache-Control: no-store; don’t override it with a CDN &ldquo;Cache Everything&rdquo; rule.
  • Root-mount asset URLs are rewritten to /wp-content/uploads/designsetgo-apps/<id>/... so nginx serves them direct, bypassing PHP.

Full guidance: caching &amp; 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

Something missing or unclear? Email us.