Skip to content
Wiki— browse docs
On this page

Step 7: Publish it

One frozen edition, served back through your own render — what freezes, and who can see it.

Your wiki works. Now you want a URL you can send to someone with no account.

Note:

No live preview for this step, and it can't have one. Publishing runs your bake in the publisher's real browser against their real world, then serves the result from a real server at a real address — three things this sandboxed page has none of. What follows is what the scaffold already ships working — bake in src/publish-hooks.ts, the baked component and its two frozen* readers in src/app.tsx — condensed to the load-bearing lines.

Two files, two jobs

Publishing is one frozen edition of the world, served back through your own render. Both halves are code you shipped:

  • bake (src/publish-hooks.ts) — reads the live world through a reads-only context and returns one immutable payload. It never sees a database; ctx is the only door, and it has no write path.
  • renderBaked (src/app.tsx) — turns that payload into pixels, in a sandboxed iframe, behind a brokered read-only host.
src/publish-hooks.ts
export async function bake(ctx: PublishBakeContext): Promise<PublishedAppContent> {
  const [world, docs, mediaUrls, appState] = await Promise.all([
    ctx.world(),
    ctx.documents(),
    ctx.mediaUrls(),
    ctx.appState ? ctx.appState() : Promise.resolve(undefined),
  ])

  // Every entry's address, edition-relative — what Step 6's deep links resolve against.
  const routes: Record<string, string> = {}
  for (const d of docs) routes[d.id] = "/" + encodeURIComponent(d.slug ?? d.id)

  // … fill `documents` from ctx.documentContent(d.id), in parallel batches — the
  // scaffold ships that loop (and a refs pass) working; see Reference …
  const documents: Record<string, unknown> = {}

  return { index: docs, routes, mediaUrls, documents, extra: { worldName: world.name, page: appState ?? null } }
}

The two frozen* readers below live in the scaffold's src/app.tsx, right next to the component (the shipped versions defend against malformed input field by field; EMPTY_PAGE is the all-defaults page state):

src/app.tsx
/** The frozen Customize values an edition carries (theme + fonts). */
function frozenCustomization(settings: unknown) {
  const rec = (settings ?? {}) as Record<string, unknown>
  // The site family freezes an envelope ({ v, customization, config });
  // older editions froze the bare values map. Read both.
  return (rec.customization ?? rec) as AppCustomizationValues
}

/** The frozen page state — what bake put under extra.page. */
function frozenPage(snapshot: unknown) {
  const extra = (snapshot as { extra?: { page?: PageState | null } } | null)?.extra
  return extra?.page ?? EMPTY_PAGE
}

export function ChronicleBaked({ snapshot, settings, entryPath }: PublishedAppSurfaceProps) {
  return (
    <WikiPage
      editing={false}
      values={frozenCustomization(settings)}
      page={frozenPage(snapshot)}
      actions={null}
      entryPath={entryPath}
    />
  )
}

Because there's no writing, the published half is genuinely simpler than the live one: same component, editing={false}, actions={null} — which is exactly the shape Step 2 already built src/app.tsx around.

What actually ships

Not everything in the world reaches the edition, and both filters are authored by the world's owner, not by you:

  1. isViewable === false never ships. Step 3's filter keeps your live page honest about it.
  2. Typed cards are gated by their entity type's "site content" setting. Untyped documents — maps, notes, canvases — always ship.

ctx.documents() has already applied both, which is the whole reason Step 3's useWorldQuery filter and this list agree.

The published host is smaller, on purpose

AvailableGone
world · media · refs · nav · embedscollab · search · presence
identity (canEdit() always false)projects · sitePublish · theme · media.pick

Everything on the right resolves to nothing rather than throwing — useHostTheme() returns the platform default, media.pick is undefined. Reference has the full table.

An edition is frozen

Worth saying out loud, because it's the most common surprise: editing the world does not change the published site. The live wiki follows your edits instantly; the published site keeps serving the edition you last published, byte for byte, until you press Publish again. Every publish appends an immutable version — nothing is overwritten, nothing deleted.

Where the button is, and what it isn't

Open the wiki in its world. Publish sits top-right in the wiki chrome. There's no CLI equivalent, and there can't be a purely server-side one — your bake is third-party code, so it runs where the publisher is.

Warning:

Four verbs, and only one of them is this

vvd save uploads a private draft. vvd share points the world at it. vvd publish submits your template to the Workshop, for other people to install. Publish, in the app, puts this world's wiki on the public internet — the only one of the four this page is about. Ship it covers the other three.

Who can see it

Four audiences, enforced at serve time: public (anyone with the address, the default), unlisted (anyone with <address>/u/<token>), password, and entitled (people granted access by email).

Recap

  • Publishing freezes one edition and serves it through your own renderBaked — the same component your live wiki already renders, with actions={null}.
  • Hidden documents and site-content-disabled entity types never ship; filter the live page the same way or the two will disagree.
  • An edition is frozen: editing the world doesn't change the published site until you publish again.
  • The published host is real but smaller — collab, search and theme are gone; guard for it.

Next steps