/** * The public site (SPEC.md §57). * * One renderer draws a page, and it is this one. Studio's builder canvas loads this * very bundle inside its iframe, so a preview cannot drift from what a visitor sees — * there is no second implementation for it to drift from (SPEC.md §59). * * `preview.tsx` is the document that mounts what is here and adds the editor's half * of the conversation. Everything the page *looks like* is in this file and in * `app/blocks/`, which is empty because this project declares no block types yet. */ import { AssemoraPage, type BlockViewProps, createBlockRegistry } from '@assemora/react' import { type BlockTree, emptyTree } from '@assemora/schema' /** * Drawn where a block type has no view here. * * A stored page outlives the code that renders it: a block dropped from this project * is still in every tree that used it, and a visitor should never be shown a silent * gap where one used to be. It is also what you will see first after * `assemora make:block hero` — the declaration is registered, the component is not. */ const Missing = ({ block }: BlockViewProps) => (

No view is registered for a “{block.type}” block. Write one in app/blocks/ and add it to the registry in app/main.tsx.

) /** * Where a block declaration meets its component. * * The key is the block's type from `src/blocks/`, and this map is the only thing * joining the two. It is empty because the project declares no blocks: add one with * `assemora make:block hero`, list it in `pages({ blocks: [Hero] })` in `src/app.ts`, * write its view, and add it here — then the builder can place it. */ export const blocks = createBlockRegistry({}, { fallback: Missing }) /** * What a visitor sees before anybody has published anything. * * A blank white page is what an empty tree renders to, and it is indistinguishable * from a broken build. This says which of the two it is, and what the next act is — * and it is the site's own copy rather than the framework's, so deleting it is one * edit rather than a configuration option nobody can find. */ const Nothing = () => (

Nothing is published yet

Sign in at /studio, make a page, and drop a block into it.

The palette is empty until this project declares a block type: run{' '} pnpm assemora make:block hero, list it in{' '} pages({'{ blocks: [Hero] }'}) in src/app.ts, and write its view in{' '} app/blocks/.

) /** * What a visitor gets: the published tree, by slug, with no session at all. * * `src/routes.ts` is the other half, and it says why this is a route rather than a * policy. Nothing here is authenticated, so `/preview` and `/preview?slug=about` are * pages anybody can open. * * A slug nothing is published at answers with an empty tree rather than an error. To * a visitor the two are one thing — there is nothing to read — and on a project this * new the ordinary case is that no page has been made at all, which is not a failure. */ export const readPublished = async (slug: string): Promise => { const response = await fetch(`/api/site/pages/${encodeURIComponent(slug)}`) if (response.status === 404) return emptyTree() if (!response.ok) throw new Error(`The page could not be loaded (${response.status})`) return ((await response.json()) as { tree: BlockTree }).tree } /** * What the builder's canvas gets: one page by id, in whichever mode it asked for, * through the Query Bus the way Studio and an agent reach it (ADR-0014). * * The session cookie goes with it because a draft is not public: reading is denied by * default like every other operation (SPEC.md §50), and the editor looking at the * canvas is the actor being authorized. Signed out, this is a 403 — which is correct, * and is why the visitor's path above does not use it. */ export const readTree = async (parameters: Record): Promise => { const query = new URLSearchParams(parameters) const response = await fetch(`/api/queries/pages.get?${query.toString()}`, { credentials: 'include', }) if (response.status === 403) { throw new Error('Sign in to Studio at /studio: an unpublished draft is not public.') } if (!response.ok) throw new Error(`The page could not be loaded (${response.status})`) return ((await response.json()) as { tree: BlockTree }).tree } export type SiteProps = { readonly tree: BlockTree /** Marks each block in the DOM so the builder can find it. Off for a visitor. */ readonly editing?: boolean } /** * The empty state is the visitor's, never the canvas's. * * An editor looking at a page they have not put anything in yet is being told what an * empty page is by Studio, over the frame, and a second explanation drawn *inside* the * canvas would be a block they cannot select and cannot delete. */ export const Site = ({ tree, editing = false }: SiteProps) => tree.blocks.length === 0 && !editing ? ( ) : ( )