Opt-In Astro FoldKit Server Rendering Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Add an opt-in definePage path to @opsydyn/astro-foldkit that renders FoldKit HTML during Astro SSG/request SSR and hydrates that exact handoff in the browser, while leaving defineApp and lazyApp client islands unchanged.
Architecture: Keep Astro responsible for request and island lifecycle facts. Keep the page marker, synchronous public Flags factory, build identity, server render handoff, metadata resolver, and hydration dispatch inside @opsydyn/astro-foldkit. Use FoldKit 0.148.x Server.renderToString on the server and Runtime.hydrate in the browser. Keep @opsydyn/foldkit-viz framework-free and change only its FoldKit peer compatibility and deterministic fixtures.
Tech Stack: Bun workspaces, Astro 7.1.1, TypeScript 6, FoldKit 0.148.x, @foldkit/vite-plugin 0.16.x, Effect V4 beta, Astro Cloudflare adapter, Vitest/Bun tests, Oxlint, Oxfmt, release-please.
Spec: docs/superpowers/specs/2026-08-18-astro-foldkit-opt-in-server-rendering-design.md
Global Constraints
defineAppandlazyAppretain their current marker shape, client-only server shell,noMeta, navigation, and one-shot disposal behavior.definePageis a separate opt-in. Do not make existing Astro components hydratable or server-rendered by changing the default integration behavior.- The page Flags factory is synchronous and returns public JSON-serializable data. It must not run FoldKit Commands, subscriptions, remote loads, or browser resources on the server.
- Use static imports for package-owned runtime dependencies. The only lazy module loading allowed is the existing user-declared literal
() => import('./main')loader contract on an app/page marker. - Pass one explicit build ID through the FoldKit Vite plugin, server render, and client hydrate paths. Missing or mismatched identity must fail closed.
- Do not add Astro,
Request, or Effect runtime ownership to@opsydyn/foldkit-viz. - Keep server-only exports out of the client entry and do not import
@opsydyn/astro-foldkit/serverfrom browser code. - Do not hand-edit generated changelogs or release versions; use the repository’s release-please flow after the package manifests and documentation are correct.
Task 1: Pin the compatibility and build surfaces
Files: packages/astro-foldkit/package.json, packages/astro-foldkit/tsdown.config.ts, packages/foldkit-viz/package.json, apps/web/package.json, bun.lock
- Update
packages/astro-foldkittofoldkit: ^0.148.0,@foldkit/vite-plugin: ^0.16.0, and a directeffectruntime dependency matching the repository’s Effect V4 beta (4.0.0-beta.102at the current checkout). - Narrow
@opsydyn/astro-foldkit’s FoldKit peer range to>=0.148.0 <0.149.0; retain the existing Astro peer range. - Update
@opsydyn/foldkit-viz’s dev dependency and peer range from the0.136.xline to>=0.148.0 <0.149.0, leaving its Effect peer policy and no-Astro boundary intact. - Update the web app’s direct
foldkitand@foldkit/vite-pluginversions to the same compatibility line so the demo does not resolve a stale transitive plugin. - Add
define-pageand a separate public server entry totsdown.config.ts. Keep the Astro renderer output asdist/server.mjs; emit the public./serverAPI from a different source entry so the package export never points consumers at Astro’s renderer module. - Add package exports and
typesVersionsentries for./define-pageand./server, with./servermapped to the public resolver entry and not to the renderer entry. - Run
bun installand inspect the lockfile to confirm one FoldKit 0.148.x line and one matching Vite plugin line are selected. - Verify the package builds before any web-app consumer command:
bun run --filter @opsydyn/astro-foldkit build.
Task 2: Add the explicit definePage marker and types
Files: packages/astro-foldkit/src/define-page.ts, packages/astro-foldkit/src/types.ts, packages/astro-foldkit/src/define-app.ts, packages/astro-foldkit/test/unit/define-page.test.ts
- Define
PageFlagsContext<Props>withrequest: Request,url: URL,params: Readonly<Record<string, string | undefined>>, and validatedprops: Props. - Define a page configuration type that exposes the FoldKit server/client contract:
FlagsSchema value,init(flags),update, andview(model, h). KeepFlagsrequired because FoldKit uses it to decode the serialized handoff. - Implement
definePage<Props, Flags>(load, { flags })with the same literal loader shape as the existing app marker and a distinct marker discriminant such as__foldkitPage: true. Preserve the loader without invoking it at marker-construction time. - Keep
defineApp/lazyAppimplementation and exported types independent from page types. Do not widen the existing app marker into a union that would let client-only apps accidentally enter the server path. - Export
definePageand its public types from./define-page; export only server-safe resolver types/functions from./server. - Add unit coverage for marker identity, loader preservation, synchronous Flags factory typing, and rejection of a page configuration without a Flags codec.
Task 3: Implement the FoldKit server handoff and build identity
Files: packages/astro-foldkit/src/build-id.ts, packages/astro-foldkit/src/server-render.ts, packages/astro-foldkit/src/server.ts, packages/astro-foldkit/src/index.ts, packages/astro-foldkit/tsdown.config.ts, packages/astro-foldkit/test/unit/server.test.ts, packages/astro-foldkit/test/unit/server-render.test.ts
- Add one internal build-ID reader that consumes the Vite-injected
import.meta.env.FOLDKIT_BUILD_ID, rejects an absent/empty value, and exposes the same value to server and client entry code. Keep the defaultdevelopmentvalue in integration configuration, not as a hidden renderer fallback. - Add a shared server render helper that statically imports
EffectandrenderToStringfromfoldkit/experimental/server, callsrenderToString({ Flags, init, view }, { flags, buildId }), and unwraps the Effect withEffect.runPromise. - Extend the Astro renderer’s
checkto accept either the existing__foldkitmarker or the new__foldkitPagemarker. - For
definePage, readrequest,url,params, and component props from Astro’s renderer result, call the synchronous Flags factory, load the page module, and return the FoldKit-rendered HTML. Do not callupdate,Command,Subscription, or the existing client shell path. - Preserve the current deterministic
<div data-foldkit-island="true"></div>result fordefineAppand all invalid/non-FoldKit components. - Track page ownership per Astro result so a single document cannot render two page-owner markers. Reject the second owner before emitting HTML; do not introduce a request-scoped render cache.
- Update
foldkit()integration options to accept{ server: { buildId: string } }and pass that value tofoldkitVitePlugin({ buildId }). Keep Astro’s host SSR configuration in charge; do not enable the Vite plugin’s standalone server mode. - Add tests for marker dispatch, request context forwarding, HTML handoff markers, missing build ID, invalid Flags, duplicate page owners, and unchanged
defineAppshell output.
Task 4: Expose the server document resolver
Files: packages/astro-foldkit/src/server-document.ts, packages/astro-foldkit/src/server-public.ts, packages/astro-foldkit/test/unit/server-document.test.ts, packages/astro-foldkit/package.json, packages/astro-foldkit/tsdown.config.ts
- Implement
resolvePageDocument(page, { request, url, params, props })as the public server-only API. It must load the page module, derive Flags with the same context and factory used by the renderer, execute the same deterministic FoldKit render pipeline, and returnRenderedApplicationmetadata (title, optionallang,dir,canonical,ogUrl). - Keep the public resolver call to two user-facing arguments; obtain the configured build ID through the package’s internal build-ID module rather than requiring layouts to repeat deployment configuration.
- Export
resolvePageDocumentfrom@opsydyn/astro-foldkit/serverthrough the public server entry. Ensure its emitted module is not reachable throughdist/client.mjsor the root browser renderer import. - Make metadata mapping preserve FoldKit’s
dirvalues (ltr,rtl,auto) and distinguish absent optional fields from empty values. - Add tests proving the resolver passes request/URL/params/props to Flags, returns the page Document metadata, uses the same build ID, and does not alter the existing
noMetaisland behavior.
Task 5: Add the page-owner hydration branch
Files: packages/astro-foldkit/src/client.ts, packages/astro-foldkit/src/client-helpers.ts, packages/astro-foldkit/test/unit/client.test.ts, packages/astro-foldkit/test/unit/client-helpers.test.ts
- Extend the client marker type and island test doubles with the DOM query operation needed to locate stamped FoldKit roots.
- Keep the existing
defineAppbranch onRuntime.makeApplicationplusRuntime.embed, including its props-to-init override, navigation ports,noMetahandling, and one-shot dispose onastro:unmount. - For
definePage, load the module, callRuntime.makeApplicationwith the page config unchanged so FoldKit owns Flags decoding, require exactly one stamped FoldKit root inside the Astro island, and callRuntime.hydrate(application, { buildId }). - Do not call
Runtime.embed, create a second application, or install thedefineAppdisposal path for a page owner. Let hydration refusal surface without falling back to a fresh client Model over server HTML. - Keep static imports for
Runtimeand build identity. Do not add a runtime module-name lookup or dynamic import of the package’s server API. - Test app/page dispatch, exact
hydratearguments, root-count failure, build-ID forwarding, and the unchanged navigation/disposal expectations for existing app islands.
Task 6: Migrate the greeting example to prove request SSR and SSG
Files: apps/web/src/apps/greeting/page.ts, apps/web/src/apps/greeting/main.ts, apps/web/src/apps/greeting/model.ts, apps/web/src/apps/greeting/view.ts, apps/web/src/pages/greeting.astro, apps/web/src/pages/greeting-static.astro, apps/web/src/layouts/Layout.astro, apps/web/astro.config.ts
- Add a
GreetingPagemarker usingdefinePageand a literal loader. Its Flags factory must derivenamefrom validated props andlocalefrom the request URL, accepting only the existingen/arvalues and defaulting safely toen. - Export a
FlagsSchema/value from the greeting module and changeinitto consume Flags directly. Keepupdateand the FoldKit view pure; do not move request parsing into the app update loop. - Change the greeting Document metadata to use lowercase FoldKit direction values and provide deterministic
title,lang,dir, and optional canonical/Open Graph URL fields. - Update
greeting.astroto callresolvePageDocumentwithAstro.request,Astro.url,Astro.params, and the same props passed toGreetingPage, then render<GreetingPage client:load ... />inside the layout. Keep it request-rendered. - Add
greeting-static.astrowithexport const prerender = trueand fixed universal props/Flags to prove the SSG path without request-only inputs. - Extend
Layout.astroto accept resolver metadata and own only the outer HTML structure; renderlang,dir, title, canonical, and Open Graph tags from the resolver values. - Pass
{ server: { buildId: process.env.FOLDKIT_BUILD_ID ?? 'development' } }inapps/web/astro.config.tsand leave existing chart/diagnostics routes ondefineApp. - Ensure the app’s build/typecheck scripts continue to build
@opsydyn/astro-foldkitbefore Astro evaluates the package exports.
Task 7: Prove package exports, Astro output, and browser hydration
Files: packages/astro-foldkit/test/integration/package-import-smoke.test.ts, packages/astro-foldkit/test/integration/astro-page-smoke.test.ts, packages/astro-foldkit/test/fixtures/page-app/, apps/web/test/, packages/foldkit-viz/test/
- Extend the packed-consumer fixture to import the root,
./define-app,./define-page, and./serverexports under both Bun and Node, and compile a consumer that usesdefinePageandresolvePageDocumenttypes. - Add a built Astro fixture that renders one request-SSR page and one prerendered page. Assert that each response contains greeting text, exactly one FoldKit stamped root, the build marker, and the serialized Flags handoff before JavaScript runs.
- Assert that a page Document’s metadata reaches the outer layout while existing
defineAppoutput remains the client-only shell. - Add a browser smoke test for reload/hydration: observe the existing server DOM being adopted, then perform one locale interaction and verify the live FoldKit update loop. Treat static output as insufficient evidence for this claim.
- Add negative fixture coverage for missing build identity, duplicate page owners, non-serializable Flags, and a page configured without a server Flags codec.
- Add one deterministic FoldKit 0.148-compatible fixture to
@opsydyn/foldkit-viztests without adding Astro or server runtime imports; retain D3 parity assertions for any changed chart math.
Task 8: Update documentation and release metadata
Files: packages/astro-foldkit/README.md, packages/foldkit-viz/README.md, README.md, .release-please-manifest.json, release-please-config.json
- Document the distinction between
defineApp/lazyAppclient islands and opt-indefinePagepage owners. - Add the complete
definePage,Flags, Astroclient:load,foldkit({ server: { buildId } }), andresolvePageDocumentexamples, including the rule that server Flags are public and synchronous. - Document the SSG/request SSR failure policy, one-page-owner constraint, hydration boundary, and explicit exclusion of server Commands/data loading.
- Update the FoldKit compatibility tables and viz package notes to
0.148.x. Do not claim server rendering support for@opsydyn/foldkit-viz. - Keep CHANGELOG edits release-please-owned; use a conventional package-scoped commit and confirm the release configuration will produce a minor
@opsydyn/astro-foldkitrelease for the new opt-in API.
Task 9: Run the release gates and review the diff
Files: repository-wide verification only; no additional source files
- Run focused tests first:
bun run --filter @opsydyn/astro-foldkit test:unit,bun run --filter @opsydyn/astro-foldkit test:integration, andbun run --filter @opsydyn/foldkit-viz test. - Run
bun run checkand resolve all Oxlint errors and Oxfmt changes without suppressions or Biome commands. - Run
bun typecheckand confirm the package build ordering resolves@opsydyn/astro-foldkitbefore@opsydyn/webAstro config loading. - Run
bun testandbun run build, then inspect the generated package tarball with the packed-consumer fixture. - Run the browser smoke against the built Astro app and record the route, build ID, server HTML, and hydration interaction as direct evidence.
- Review
git diff --check, package export maps, generateddistentries, and the release-please diff before committing implementation work.
Completion Criteria
- Existing
defineApp/lazyApproutes and tests retain their current behavior. - A
definePageroute emits FoldKit HTML plus serialized Flags during both request SSR and SSG. - The browser calls
Runtime.hydrateagainst the existing stamped root with the same build ID; it does not start a replacement app. - FoldKit Document metadata reaches Astro’s outer document through the explicit server resolver.
- Packed Bun/Node consumers resolve every new public subpath, and the full check/typecheck/test/build gates pass.
- The release notes describe the opt-in boundary and the package is ready for the release-please minor release.