A cake-recipe site where one page serves four surfaces — HTML for people,
Markdown + JSON + llms.txt for agents — and adds an interactive layer (morph
navigation + a live feed) only where it earns one. Built with
June, the agent-ready React framework, and deployed on the
Vercel Node runtime (Fluid compute).
Live:
chocolate ·
carrot ·
.md ·
.json ·
llms.txt ·
/mcp
The loader runs once; each surface is a projection of the same data. The default export is the view; named exports configure the rest:
// app/recipes/[slug]/page.tsx
export const loader = (ctx) => ({ recipe: RECIPES[ctx.params.slug] });
export default function Page({ recipe }) { // → HTML
return <RecipePage recipe={recipe} />;
}
export const json = ({ recipe }) => recipe; // → .json
export const md = ({ recipe }) => `# ${recipe.title}\n…`; // → .mdThree small files, one per concern:
recipes.ts (data) ·
RecipePage.tsx (view) ·
page.tsx (the route).
llms.txt, the sitemap, and an MCP endpoint derive from the routes
automatically — there's no second codebase for the machine surface, and
nothing to keep in sync by hand.
The recipe content is zero-JS: semantic HTML, no hydration, no serialized state. The interactive layer is opt-in, and this site turns on three pieces to show them off:
- Morph navigation (
clientRouter: true) — same-origin clicks swap the page in place over the same HTML the server already serves; no router, no Flight payload, history just works. - Persist islands — the header carries two (
SaveButton,StatusFeed) markedpersist, so a morph navigation carries their live nodes across instead of tearing them down. Click ♥, navigate between recipes, the count holds. - A live feed over SSE —
StatusFeedopens anEventSourceto/api/activity(a streaming endpoint mounted via theapp/_extra.tsxescape hatch). Because the island ispersist, the connection keeps streaming across navigation without reconnecting — the event count climbs straight through every page change.
That last one is the Live updates how-to made real: a server-push connection that survives navigation, written to stay cheap (it parks between ticks, closes before the host's streaming cap so the browser reconnects, and tears down the moment the client disconnects).
npm install
npm run dev # the june CLI runs on Bun (https://bun.sh)
curl localhost:3000/recipes/chocolate-cake.md
curl localhost:3000/recipes/chocolate-cake.json
curl localhost:3000/llms.txt
curl -N localhost:3000/api/activity # the live feed, rawjune deploy # uses the adapter from june.config.tsThe target is the adapter, not a rewrite. This site sets
deploy.adapter: vercel() (from @junejs/server), which emits the Vercel Build
Output API straight from the same june build bundle. vercel() targets the
Node runtime on Fluid compute by default (vercel({ runtime: "edge" }) opts
into an Edge Function instead). Drop the adapter entirely and june deploy ships
to Cloudflare Workers from the identical bundle — the framework core is just
fetch(Request) → Response, so the same worker runs on all three.
app/global.css is auto-linked by June — no import, no <style> block. It's
plain Tailwind v4 (@import "tailwindcss"); june build content-hashes and
minifies it into one immutable /_june/global.<hash>.css, and june dev
recompiles it on reload.
Like the idea? A star on the framework helps a tiny 0.0.x project: github.com/junebuild/june