Share Graft CMS across Astro and Next, remove Sanity, pin Bun 1.4
Summary
Graft is now the CMS for both site versions. Sanity is gone.
- Shared CMS: schema + MDX live in
packages/content(@andy-portfolio/content).graft compilewrites the static SQLite index both apps read. - Astro (
apps/web, port 3000): still the live Vercel site. Blog bodies stay on Astro content collections +@astrojs/mdx. - Next (
apps/web-next, port 3001): restored as a workspace sobun dev/bun lint/bun buildinclude it. Not deployed. Blog uses@usegraft/sdk-nextMdxBody; projects and case studies come from the same Graft collections. - Removed:
apps/studio,packages/sanity-config, Sanity export script, revalidate webhooks, Sanity env on Turbo/CI. - Bun 1.4.0 is pinned (
packageManager, CIsetup-bun). First 1.4 install wroteconfigVersion: 1inbun.lock(still hoisted).
Two versions + Graft
| Version | Path | Port | Deploy |
|---|---|---|---|
| Astro 7 | apps/web | 3000 | Vercel (Root Directory apps/web) |
| Next.js 16.3 | apps/web-next | 3001 | Local archive only |
Author posts, projects, and case studies once under packages/content/content/.
Vercel stays Astro-only: apps/web/vercel.json buildCommand is bun run build (the Astro app script, which compiles Graft then runs astro build). There is no apps/web-next/vercel.json.
Index path (dev + prerender)
The compiled SQLite file lives at packages/content/.graft/index.db. Resolving it via import.meta.url broke in two bundlers:
- Astro prerender rewrote it to
dist/server/.prerender/.graft/index.db - Next Turbopack resolved
@andy-portfolio/contentto a virtual[project]/packages/contentpath that is not on disk
getIndexPath() now confirms the candidate exists (or falls back to cwd / GRAFT_CONTENT_ROOT) at open time.
CI Next build (no secrets)
Lint’s new web-next build step has no Spotify/Resend secrets. Next was throwing while collecting /api/spotify/now-playing because env.ts parsed those keys at import. The archive now matches Astro: lazy getSpotifyEnv / getResendEnv, Resend constructed inside handlers, Spotify idle {isPlaying:false} when env is missing. Verified locally with those variables unset.
Greptile P1: stale static sitemap
Deleted apps/web-next/public/sitemap.xml so src/app/sitemap.ts owns /sitemap.xml (Graft posts + /case-studies/*, no obsolete /case-study/*). Deleted the same file from apps/web/public/ so Vercel does not ship it beside @astrojs/sitemap (sitemap-index.xml / sitemap-0.xml). Confirmed Next :3001/sitemap.xml includes i-built-smol and both case studies.
Bun 1.3.14 vs 1.4.0
Same machine (Linux), warm node_modules + bun cache, 3 runs, median. GNU time for wall + max RSS; bun --version also timed with perf_counter (GNU time rounds sub-10ms to 0.00). Nested scripts used the version under test via PATH.
1.3.14 can still bun install --frozen-lockfile against this 1.4 configVersion: 1 lockfile (no rewrite).
| Command | 1.3.14 median | 1.4.0 median | Notes |
|---|---|---|---|
bun --version startup | 1.00 ms (5.7 MB) | 0.85 ms (4.0 MB) | 20 warm runs after dropping the first |
bun install --frozen-lockfile | 0.02 s (25.2 MB) | 0.02 s (18.6 MB) | Warm graph; RSS is the real 1.4 win here |
content compile | 0.43 s | 0.44 s | Graft CLI; wash |
bun run lint | 1.28 s | 1.27 s | Turbo --force; wash |
Astro web build | 5.89 s (934 MB) | 5.96 s (1178 MB) | Work is Node 22 (Astro/Vite) |
Next web-next build | 3.96 s | 4.02 s | Incremental .next after the first 1.4 run |
Do not read this as live-site TTFB. Vercel’s Astro compile still runs on Node 22. Bun 1.4 only changes local/CI bun install and bun run of those scripts.
Verification
Lint, format, Astro typecheck, Next tsc, Astro production build, and Next production build all passed locally after the index-path fix.
agent-browser walk of Astro :3000 and Next :3001: /, /projects (SEO filter), both blog posts, both case studies, Astro mobile viewport. Same Graft titles, no cdn.sanity.io.
Astro home Selected Works
Next home Selected Works
Astro projects SEO filter
Astro Smol blog post
Next Smol blog post
Astro projects on mobile
To show artifacts inline, enable in settings.


Summary by cubic
Replaces Sanity with a shared Graft CMS for both the live Astro site and the local Next.js archive, so both apps read the same compiled content index from @andy-portfolio/content, and pins Bun to 1.4.0.
Changes
- Moves schema and MDX into
@andy-portfolio/content;graft compilewrites the SQLite index both apps read. - Astro keeps its content collections for blog bodies; the Next app uses
@usegraft/sdk-nextfor MDX and reads the same index for projects and case studies. - Resolves the index path at open time via
@andy-portfolio/content/package.json, with a cwd fallback for Turbopack's virtual paths, so Astro's prerendered build finds it. - Allows 127.0.0.1 as a dev origin in the Next app so agent-browser can reach the dev server.
- Reads Next Spotify and Resend env lazily so
next buildworks in CI without secrets. - Removes
apps/studio,packages/sanity-config, the Sanity export script, and the revalidate webhook routes. - Adds
apps/web-nextback to the Turbo workspace and updates its dev/build scripts to compile Graft first. - Removes both static
public/sitemap.xmlfiles so the generated sitemaps own/sitemap.xml. - Pins Bun to 1.4.0 in
packageManagerand the CI setup.
Migration
- Run
bun installto update the lockfile. - Remove Sanity environment variables and webhook secrets from any deployments or local env files.