Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ jobs:
- migrate-from-cloudflare-pages
- migrate-from-vercel
- migrate-from-netlify
- wp-zero
- astro-zero
- wp-astro
- wp-react
- zero-perfect
- unfurl
- trailhead
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
- name: Install Spacefast CLI
run: bun add -g spacefast@0.4.1
run: bun add -g spacefast@0.8.0
- name: Run app tests
working-directory: ${{ matrix.app }}
run: |
Expand All @@ -63,11 +70,19 @@ jobs:
run: |
artifact="$RUNNER_TEMP/${{ matrix.app }}.tgz"
case "${{ matrix.app }}" in
zero|kitchen-sink)
zero|kitchen-sink|zero-perfect)
cd "${{ matrix.app }}"
sf build --json
artifact="$PWD/.spacefast/zero/artifact.json"
;;
astro-zero)
# Astro builds into dist/, the capsule is staged beside it, and
# the package build script already runs `sf build dist`.
cd astro-zero
bun install --frozen-lockfile
bun run build
artifact="$PWD/dist/.spacefast/zero/public/index.html"
;;
migrate-from-netlify)
sf publish migrate-from-netlify --dry-run --allow-unsupported-platform-features --json
cd migrate-from-netlify
Expand Down Expand Up @@ -141,8 +156,10 @@ jobs:
# A pure rename (R100) moves a file without changing what it serves.
changed=$(git diff --name-status -M "$BEFORE" "${{ github.sha }}" \
| awk -F'\t' '$1 != "R100" {print $NF}')
# A recipe directory that no longer exists has nothing to publish.
slugs=$(printf '%s\n' "$changed" | sed -n 's#^recipes/\([^/]*\)/site/.*#\1#p' \
| sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
| sort -u | while read -r slug; do if [ -d "recipes/$slug" ]; then echo "$slug"; fi; done \
| jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "slugs=$slugs" >> "$GITHUB_OUTPUT"

publish-recipes:
Expand All @@ -158,7 +175,7 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install Spacefast CLI
run: bun add -g spacefast@0.4.1
run: bun add -g spacefast@0.8.0
- name: Build
working-directory: recipes/${{ matrix.slug }}
run: |
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ sf publish
```

Use `sf build` when you only want to prove the deployable artifact locally.
`sf dev` is the local server for Zero apps (`zero/`, `kitchen-sink/`); for
everything else, use the framework's dev server.
`sf dev` is the local server for Zero apps (`zero/`, `kitchen-sink/`,
`zero-perfect/`, `wp-zero/`); for everything else, use the framework's dev
server. `astro-zero/` needs both: Astro's dev server for the pages and
`sf dev --dir dist` for the capsule.

## Directory map

Expand All @@ -29,6 +31,13 @@ everything else, use the framework's dev server.
| [`nextjs/`](./nextjs/) | Server-rendered Next.js, route handlers, and automatic OpenNext packaging |
| [`astro-with-wordpress/`](./astro-with-wordpress/) | Astro built from WordPress through `@spacefast/wordpress` |
| [`functions-php/`](./functions-php/) | Native PHP Functions with JSON, form bodies, validation, and verified auth context |
| [`zero-perfect/`](./zero-perfect/) | A minimal Zero app: queries, mutations, guest and Google sign-in, realtime, routes, and an HTTP endpoint |
| [`wp-zero/`](./wp-zero/) | WordPress rendered to static pages at build time with a Zero capsule for realtime reactions and search |
| [`astro-zero/`](./astro-zero/) | Astro pages in front of a server-only Zero capsule, staged into `dist/` as one project root |
| [`wp-astro/`](./wp-astro/) | Headless WordPress pulled from the public REST API and rendered statically with Astro |
| [`wp-react/`](./wp-react/) | A Vite + React SPA reading WordPress posts in the browser, with the `_redirects` SPA fallback |
| [`unfurl/`](./unfurl/) | Next.js SSR, a route handler, and middleware on the Functions runtime |
| [`trailhead/`](./trailhead/) | A Next.js static export with `@spacefast/image` Site Accelerator images, `_redirects`, and `_headers` |
| [`recipes/`](./recipes/) | Canonical prompts, metadata, live outputs, and the public recipe feed |
| [`kitchen-sink/`](./kitchen-sink/) | Zero, Pages, PHP, JavaScript, TypeScript, auth, mail, and routing in one project |
| [`migrate-from-cloudflare-pages/`](./migrate-from-cloudflare-pages/) | Wrangler build settings and Pages convention files |
Expand Down
File renamed without changes.
File renamed without changes.
54 changes: 54 additions & 0 deletions astro-zero/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Astro + Spacefast Zero

An Astro blog whose every post carries a realtime comment thread and emoji
reactions, published as one Space. Astro renders the essays at build time; a
Spacefast Zero capsule owns the conversation underneath them.

```sh
bun install
bun run dev # astro dev on :4321; pages, layout, redirect parity
bun run build # astro build → stage capsule → sf build dist
bun run runtime # sf dev --dir dist; the capsule's database and endpoints
sf publish dist --wait
```

`sf publish` is given `dist`, not the project root; see below.

## The composition

A Zero Space publishes a project root: the CLI walks it, compiles `server/`
into the version's runtime artifact, and publishes everything else as content.
Astro refuses to build into its own project root. So the two are stacked:
**`dist/` is both Astro's build output and the Zero project root.**
`astro build` fills it, `scripts/stage-capsule.mjs` copies `sf.jsonc`,
`server/`, and `shared/` in beside the pages, and `sf build dist` compiles the
capsule and assembles the publish tree under `dist/.spacefast/zero/public/`.

The trick that makes this work is in `sf.jsonc`: `runtime.client` names
`client/index.tsx` and that file does not exist. A capsule with no client entry
is server-only, generates no app shell, and so nothing claims `index.html`;
Astro's homepage survives. The live half of the page is a Preact island Astro
bundles itself, importing `useQuery`/`useMutation`/`useAuth` from
`@spacefast/zero/client`. It takes exactly one prop: the post slug.

## Important files

| Surface | Where |
| --- | --- |
| Astro static build, content collection, island | `src/` |
| `@spacefast/astro` integration, `mode: "static"`, inline redirects | `astro.config.mjs` |
| On-disk `_headers`, merged with the inline rules into `dist/` | `_headers` |
| Zero schema, indexes, queries, mutations, `GET`/`POST /api/comments` | `server/index.ts` |
| Pure TypeScript both halves import | `shared/comments.ts` |
| Space config and the server-only runtime declaration | `sf.jsonc` |
| Copies the capsule into `dist/` after `astro build` | `scripts/stage-capsule.mjs` |

## Notes

- `sf dev` serves the capsule and its endpoints, not the Astro pages; the two
local servers are separate. Its output prints a private URL with a capability
token that doubles as a bearer token for `curl` against `/api/comments`.
- `astro build` empties `dist/`, which also removes the `.spacefast/` link state
the CLI writes there. Publish with `--space` after a rebuild.
- Every build ships the Zero platform bundle even though the pages never load
it; the island bundles its own copy of the client through Vite.
File renamed without changes.
File renamed without changes.
28 changes: 19 additions & 9 deletions recipes/astro-zero/site/bun.lock → astro-zero/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
},
"dependencies": {
"@astrojs/preact": "^6.0.4",
"@spacefast/astro": "0.4.1",
"@spacefast/zero": "0.4.1",
"@spacefast/astro": "0.8.0",
"@spacefast/zero": "0.8.0",
"astro": "^7.2.9",
"preact": "^10.29.8"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default capsule({
* The thread without a browser. Same rows the island subscribes to, so an
* agent, a feed reader, or a curl one-liner sees exactly what a reader does.
*/
listComments: endpoint({ method: "GET", path: "/api/comments", mode: "read" }, async (ctx, req) => {
listComments: endpoint({ method: "GET", path: "/api/comments" }, async (ctx, req) => {
const slug = cleanSlug(req.query.get("post") ?? "");
if (!slug) {
return json({ error: { code: "post_required", message: "Pass ?post=<slug>." } }, { status: 400 });
Expand All @@ -137,7 +137,7 @@ export default capsule({
});
}),

createComment: endpoint({ method: "POST", path: "/api/comments", mode: "write" }, async (ctx, req) => {
createComment: endpoint({ method: "POST", path: "/api/comments" }, async (ctx, req) => {
const payload = await req.json<{ post?: string; author?: string; body?: string }>().catch(() => null);
const slug = cleanSlug(payload?.post ?? "");
const text = cleanBody(payload?.body ?? "");
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading