Fieldwork · Astro example
A complete, statically built website using @storypane/sdk: Astro routes, typed
React block components, pulled schema JSON, and generated TypeScript block props.
Published pages render at build time; only the dedicated preview route hydrates React.
The Next.js version implements the same content contract and can be selected as another Storypane preview application. Each example owns its renderer, styles, schema snapshot, generated types, and publication helpers.
Configuration uses server-only STORYPANE_* variables.
Run it
From the monorepo root, first configure local .env and start the current Node
installation as described in self-hosting:
pnpm db:start
node --env-file=.env --import tsx scripts/admin.ts migrate
node --env-file=.env --import tsx scripts/admin.ts bootstrap
pnpm dev
In another terminal:
pnpm --filter @storypane/example-astro setup
pnpm --filter @storypane/example-astro dev
Open http://127.0.0.1:3132/home. The setup command creates a separate
Fieldwork · Astro example space, five schemas, a published Home story with
Default and French locales, and an unpublished Work in progress story. It creates
scoped API keys and writes them to an ignored, owner-readable .env. It only
seeds a local CMS and preserves an existing .env and content on subsequent runs.
Set STORYPANE_SESSION_COOKIE from your signed-in local editor session when
running pnpm --filter @storypane/example-astro setup; Clerk installs can use the private
STORYPANE_SESSION_TOKEN. Do not commit or share either value.
The French hero is translated; the other blocks deliberately start as copies.
Manage locale names and URL prefixes in Space settings → Locales. The build
discovers them with client.listLocales(); no STORYPANE_LOCALES environment variable
is needed. A locale gets a public route only when that story locale is published.
After adding or renaming locales, rebuild the site (or restart the dev server).
For the configured default locale, call client.listStories() for one page or
client.listAllStories() for every publication; no options object is needed.
Read one story with client.getStory({ slug: 'home' }). Add locale: 'fr' to
select another locale. An explicit locale: '' selects the root URL locale,
which may differ from the configured default.
This example paginates publications for every configured locale, using
listAllStories() for the default and listAllStories({ locale: slug }) for the
others. Route generation rejects
invalid or duplicate paths, the reserved preview, media, and _astro prefixes,
and Default slugs that occupy a configured locale prefix, such as fr/home.
To use an existing space instead, copy .env.example to .env, configure its
URL/space/tokens, and run pnpm --filter @storypane/example-astro storypane:generate.
Adjust the block components to match that space’s schemas. Do not put either
token in a PUBLIC_ variable. Preview tokens are used only by schema tooling;
published tokens are used only by the Astro build/server.
Try live editing
Sign in to the CMS, open Fieldwork · Astro example → Home → Preview, then edit the Hero title or a nested Feature. Updates appear after autosave. Click a block in the iframe to select its content editor; select a block in the editor to outline it in the iframe. Focusable blocks also support Enter/Space.
The seeded preview URL is:
http://127.0.0.1:3132/preview?slug={slug}&locale={locale}
This static shell loads a story through the authenticated editor bridge, including
unpublished stories/locales with no built route. Open Work in progress → Preview
to try it. Opening /preview outside the CMS shows instructions, not draft data.
Use this /preview URL for existing integrations too. Published routes such as
/home and /fr/home always show the built publication, including inside an iframe;
preview query parameters, cookies, and the old PUBLIC_STORYPANE_PREVIEW* settings
no longer activate drafts on those routes. Standalone preview needs your own
authenticated session endpoint (see the SDK guide).
PreviewShell.tsx uses useStorypanePreview and the current URL’s story and locale.
Changing the URL without reloading clears the old draft and disconnects its live
subscription before connecting the new story. References arrive with the authorized
preview payload, so the shell does not need a build-time copy of published stories.
To prove preview works after a static build, stop the dev server and run:
pnpm --filter @storypane/sdk build
pnpm --filter @storypane/example-astro build
pnpm --filter @storypane/example-astro preview
Now edit in the CMS again. The public HTML remains the published snapshot; /preview
updates through its React island. Publishing requires rebuilding/redeploying the
public site. Published pages ship no React runtime or preview subscription; a small
script restores the decorative artwork if a hero image fails to load.
Pull schemas and generate TypeScript
pnpm --filter @storypane/example-astro storypane:pull # CMS → storypane/components.json
pnpm --filter @storypane/example-astro storypane:types # local JSON → src/types/storypane.ts
pnpm --filter @storypane/example-astro storypane:generate # both steps
storypane:pull loads this app’s .env and uses a Preview · server only token.
The schema endpoint is space-scoped, rejects published-only/revoked tokens, and
exports deployed schema metadata without defaults, draft values, or data-source
URLs. storypane:types works offline. Commit both files; regenerate after deploying
schema migrations. The checked-in schema is a sample; setup replaces its IDs with those in
your own local space.
The SDK’s storypane executable can be used in other projects too:
storypane pull-components --out storypane/components.json
storypane generate-types --input storypane/components.json --out src/types/storypane.ts
Pull reads STORYPANE_URL, STORYPANE_SPACE_ID, and STORYPANE_PREVIEW_TOKEN from the process
environment. With Node 24 you can load a file explicitly using
node --env-file=.env node_modules/@storypane/sdk/bin/storypane.mjs pull-components --out storypane/components.json.
Invalid input leaves the previous output untouched.
Generated types include HeroBlock, PageBlock, StorypaneComponentMap, and unions for
all blocks, nestable blocks, and content types. Each interface has a literal
component discriminant and _uid. Nested fields honor allowed definitions;
inline choices become literal unions; dynamic choices remain string IDs. Asset,
link, rich text, number, boolean, date, and text formats have matching value types.
Optional fields account for empty values; required fields describe valid
publications. Dates remain strings, markdown remains text, and story options
remain IDs—the SDK does not silently resolve references.
For example:
import { editable } from '@storypane/sdk';
import type { HeroBlock } from '../types/storypane';
function Hero({ block }: { block: HeroBlock }) {
return (
<section {...editable(block)} tabIndex={0}>
<h1>{block.title || 'Untitled'}</h1>
<p>{block.description}</p>
</section>
);
}
The switch in src/components/blocks/block.tsx maps each schema name to a typed
component in its own file. Add a case when adding a schema. Page composes the
header, PageBody, and selected footer; PageBody groups adjacent feature cards.
References and preview settings are explicit props, matching the Next.js example.
Drafts can be incomplete; renderers should tolerate missing required content.
Generated types are compile-time contracts, not runtime validators. They describe
the pulled deployed migration. The delivery client and preview components pass
its generated storypaneMigrationVersion; outdated integrations warn and keep
receiving content. Schema deployment migrates both drafts and live publications.
The current example supports hero images, rich-text descriptions, linked feature cards placed directly in
a page body, and a shared footer selected through page.footer. Story links and
footer references use published stories in the page’s locale, with Default as a
fallback. Publish the referenced footer (separately or with the page during publish
review) and rebuild before it appears on the public site. In authenticated live
preview, the selected footer uses its current draft,
including unpublished footers. Changes made in another tab refresh on the preview
heartbeat (within 15 seconds); changing the selection refreshes with the page save.
Published assets are downloaded through the server-side delivery client and built
into /media/ alongside the static pages. Visitors never receive a CMS token or
need a CMS login. Rebuild after changing or removing published assets. Local CMS
preview uses the signed-in editor’s asset URLs so newly selected draft images can
appear immediately; this relies on the CMS and example using the same site
(such as 127.0.0.1 on different ports).
Opt-in public responsive media
Static export remains the default. To serve responsive variants directly from the CMS, an administrator must enable API & preview → Public media delivery for the space. Use the SDK’s standalone helper in an Astro component or renderer:
---
import { publicAssetUrl, type PublicImageOptions } from '@storypane/sdk';
const { assetId, alt, spaceId, cmsUrl } = Astro.props;
const image = (width: PublicImageOptions['width']) =>
publicAssetUrl({ url: cmsUrl, spaceId, assetId }, { width, aspect: '16:9' });
---
<img
src={image(640)}
srcset={`${image(320)} 320w, ${image(640)} 640w, ${image(1280)} 1280w`}
sizes="(max-width: 640px) 100vw, 50vw"
width="1280"
height="720"
alt={alt ?? ''}
/>
The HTML contains IDs and supported image options, never a token. Public URLs work
only while an active publication uses the asset; draft preview still uses the
authenticated editor route. Saving focal metadata updates cover crops globally.
Public responses are not browser/CDN cached; the Node runtime rechecks eligibility
and computes variants on demand. Disabling the setting or removing the last
live reference returns 404 on the next request. Already exported /media/ files
remain until a rebuild removes them. See the SDK media contract
for supported variants, still-frame animation, and image size limits.
Files and checks
| File | Purpose |
|---|---|
src/lib/cms.ts |
Discover space locales, fetch publications, paginate, build routes |
src/lib/publications.ts |
SDK publication pagination and Astro route validation |
src/pages/[...path].astro |
Static route generation |
src/components/PreviewShell.tsx |
Preview of unpublished stories/locales |
src/components/content.tsx |
Shared entry point for public and preview rendering |
src/components/blocks/ |
Block switch, individual components, and page body layout |
storypane/components.json, src/types/storypane.ts |
Pulled schema and generated types |
From the repository root, pnpm --filter './apps/example-*' lint checks both self-contained examples with ESLint and Prettier. The shared configuration includes recommended
JavaScript/TypeScript/Astro rules, React hook checks, and Next.js rules for the Next
example. pnpm --filter @storypane/example-astro lint checks only this app.
Generated schema types are checked by TypeScript instead. Use pnpm format to
apply the repository’s formatting conventions.
pnpm typecheck includes Astro checking. pnpm --filter @storypane/example-astro build checks and builds
this site; root pnpm build builds the CMS/SDK independently of a configured CMS
connection. With the CMS running and the example built, run:
pnpm exec playwright test tests/astro.e2e.ts
Playwright starts the built Astro preview server. The publication and preview contract test checks static pages, assets and image fallback, locales, unpublished preview, selected footers, URL changes, and absence of API keys in browser assets. It uses the local example’s preview token and requires no saved Clerk session:
pnpm exec playwright test tests/astro.e2e.ts -g 'Astro publication and preview contract'
The editor integration test also verifies live edits and nested selection using a configured Clerk admin session. It restores the edited draft and preview settings. The full browser suite skips this test if the example has not been configured.
The generated dist/ can be hosted as static assets, including on Cloudflare.
Use an HTTPS preview URL when the CMS is HTTPS, permit the CMS origin in
frame-ancestors, and avoid an incompatible X-Frame-Options header. Deployment,
standalone preview authentication, and a public DAM/CDN are outside this example.
For authoring and CI/CD, the SDK also supports storypane migrations pull,
storypane migrations create "description", and storypane migrations push --deploy --yes. These use a separate management token issued by an admin in
Blocks → Schema migrations → CLI access. See the
SDK guide.
Working with linked dev spaces
For a new team, follow the recommended setup: one production space for content editors, with schema protection enabled, and one linked dev space per developer. The seeded Fieldwork example is a local demo; cloning its schema does not copy its sample site content in bulk.
Keep authoring CLI credentials pointed at a linked dev space and provide its
production token separately as STORYPANE_PRODUCTION_MANAGEMENT_TOKEN. Deploy and
test schema changes in dev, then run storypane migrations promote. Use
storypane migrations status and storypane migrations sync when another
developer advances production. Block-library nesting and nested-block restrictions
travel with the migration; content dependencies are reviewed and copied as drafts.
Configure a local/test instance of this frontend with dev’s space ID, tokens, and deployed types; create sample stories there and register its preview URL in dev. Configure the live frontend and its preview with production’s credentials. The preview bridge is bound to one space, so a production-configured instance cannot preview a dev story. Clones do not copy preview targets or tokens.
Before building this example for production, point its existing delivery/preview
configuration at production and run pnpm storypane:generate from this app’s
directory. That pulls production’s deployed schema and migration version. Keep
management tokens server-side and separate from delivery configuration. See the
linked-space workflow
for conflict resolution, CI, and generating from a separately pulled production
migration file.