2026-09-23·by Sijie Wang#software#project#standmeet

microsites

Microsites: owner-written pages the instance builds and hosts

Parent: key-designs · the builder's claim protocol lives in builder-claim-skip-locked and is not repeated here.

Verdict (2026-09-07): a microsite is a React page the owner writes against the StandMeet SDK (software development kit), stored as source in Postgres, compiled by the separate builder service, and served by the instance at /p/<slug>/ — or at / when its slug is the reserved home. The owner never touches deploy infrastructure: a page is a row plus a build pointer, so going live, staging, and rollback are pointer swaps (live_build_id / staging_build_id / previous_live_build_id, backend/db/schema.sql:734). Everything a page needs from the instance — the corpus, the agent, a pool asset, its own key-value store, the visitor's access code — arrives through SDK widgets and hooks, so the page composes what it cannot own. The surface was called "custom pages" until 2026-09-05 (rename e7fe80e91 + f6ffcaaa7, migration 2026-09-05-microsites-rename.sql).

The serve path

  • One serve core, two addresses. MicrositeHandlers.Mount (routes/public/microsites.go:54) registers /microsites/{slug} and /homepage; both call serveSlugAt, which differs only in the slug it resolves and the <base href> it injects (/p/<slug>/ at line 105, / at line 114). The Next.js app reverse-proxies /p/:slug to it in beforeFiles rewrites (app/next.config.ts:71). File serving is shared with the admin preview (microsite_serve.go) so path-escape validation exists exactly once.
  • Injected head, read fresh per request. pageHead.tags() (microsites.go:172) writes the base, the SEO block, and <meta name="standmeet-page-byoai">; nothing is snapshotted into the built page, and the response carries Cache-Control: no-cache, no-store so a rolled-back page stops opening immediately.
  • Staging is a path credential. /p/{slug} is live only; the staging build is reachable through the preview route (microsite_preview.go), where the credential travels in the path because a query string on <base href> would break relative assets.

The homepage is a microsite

The instance root is the reserved slug home (HomepageSlug, owner/usecase/microsite.go:34; a5e1cada9 on 2026-09-04, template 77aee7dc6). The app middleware probes GET /api/v1/homepage and rewrites / to it when a home build is live (app/src/middleware.ts:27); on 404 it falls through to the coded route, whose fallback branch renders DefaultHome from code (app/src/app/visitor-root.tsx:48, app/src/app/default-home.tsx). DefaultHome is itself composed of the same SDK widgets (AgentWidget, CorpusWidget, GateWidget, PageNavWidget), so an unedited instance and an edited one render through one vocabulary (28f750caf). Since 185c4321b (2026-09-07) nothing is materialized at claim — InstallHomepage: nil in backend/cmd/server/boot_http.go:248 — so no stored starter can freeze behind later SDK changes. The middleware never rewrites a /?code= or /?q= view: a coded visitor must reach the name picker, and a codeless question must hand off to /gate. The admin section gives the homepage its own edit entry and card, outside the pages table (38bfe3a2f, 06ab844de).

The editor: a mini-IDE that always builds

Each page has its own editor route (app/src/app/admin/microsites/page.tsxcomponents/admin/sections/microsites/PageEditor.tsx, 5c5034962 / 10ac91c2e). Editing triggers a build automatically (lib/admin/use-auto-build.ts; 385c22f85, 0140bbbac, bf5ffaa82) — there is no separate compile button. The wait for a build result runs in a generic long-poll worker (lib/long-poll/long-poll.worker.ts + use-long-poll.ts, 5ca6055aa) rather than a per-feature timer. A layout gear switches code / split / render by hiding a column with CSS, not by unmounting (EditorViewToggle.tsx). Renaming a slug is a first-class op from the editor header (RenamePage, owner/usecase/microsite.go:62, e1c14b27e). The same authoring set is exposed to the owner's AI client as microsite.write_file / get_draft / build / get_build / promote_to_staging / promote_to_live / rollback / delete / rename — the ops are declared per domain in owner/ops/microsites*.go and aggregated at the dispatcher (routes/dispatcher/collect.go:70), with microsite.guide returning the authoring guide (design system, widgets, how to show corpus inline). The header comment in owner/ops/microsites.go still says authoring is "deliberately MCP-only" — that predates the editor and is stale; both faces exist today.

A page owns its own store

A microsite can persist visitor input (a poll, a sign-up, a guestbook) without the owner adding a table. The store is NoSQL-shaped — opaque JSON (JavaScript Object Notation) documents in named collections — and physically isolated: each page gets its own Postgres schema (page_<id>, the capstore pattern), not a shared table filtered by id, so there is no WHERE clause to forget; DeletePage drops the schema (owner/usecase/microsite_store.go, a94909aa9). Public route: GET/POST /api/v1/pages/{slug}/store (routes/public/microsite_store.go:40). Reads are open; visitor writes are off until the owner opens them (microsites.store_writable, default false — "model C"; MCP op microsite.set_store_writable, eb2cf70ce). Bounds: 500 documents per page, 8 KiB per document, per-IP rate limit at the route. In the SDK the hook is useMicrositeStore(collection) (sdk/packages/react/src/use-microsite-store.ts:26; it was usePageStore in 5ec7263aa and renamed with the surface); the client base is /api/v1/pages (sdk/packages/core/src/client.ts:172). Owner-side inspection and cleanup are microsite.store_docs / store_delete_doc / store_clear.

What a page is made of: SDK widgets

  • AgentWidget — the visitor chat, adopting whatever session the code issued; GateWidget — code entry; PageNavWidget — links to the owner's other live pages (GET /api/v1/microsites, microsites.go:81).
  • CorpusWidget — corpus cards, with a small query language: query="path:math/** sort:title limit:5" (subtree · sort recent or title · cap; query wins over a bare limit; sdk/packages/react/src/widgets/CorpusWidget.tsx:7, 113ba6a1a). Reads go through the same access-control as everything else.
  • AssetWidget — a pool asset served same-origin at /api/v1/assets/{id}, gated to assets the microsite references; the builder's completion callback recomputes the page's asset references (RebuildMicrositeAssetRefs, corpus/usecase/note_asset_refs.go:138, called from routes/sys/builds.go:191; 836af0fea), so a referenced asset cannot be deleted from the pool.
  • Widgets own their vertical layout inline rather than through a Tailwind class the consumer may never compile (00a3bf9e4); the builder template declares @source "../node_modules/@standmeet/sdk" so widget classes do compile (builder/template/src/theme.css:15, 6faa580f8), and the release build strips data-testid from the widgets (sdk/packages/react/tsup.config.ts, bec5394fb). The SDK is vendored into the builder image, not npm-installed (builder/Dockerfile:21).

SEO follows the page, not the instance

Per-page seo_title / seo_description / seo_image (schema.sql:746; migrations 2026-09-06-microsite-seo.sql, 2026-09-06-microsite-seo-image.sql) are injected into the served head as <title>, <meta name="description">, Open Graph (OG) and Twitter Card tags (routes/public/microsite_seo_head.go; 79a276cce, b19d8db39). Null means nothing is injected and the build keeps its own title. Live non-home microsites are listed in the dynamic sitemap under /p/ (routes/public/seo.go:107IndexedMicrosites, owner/usecase/seo_microsites.go:15; 701bf1648). The global SEO settings page and its seo_settings table were removed the same week (7037a434e, migration 2026-09-06-drop-seo-settings.sql) — SEO (search engine optimization) has no instance-wide home any more. MCP op: microsite.set_seo.

A code opens a page

access_codes.microsite_id (schema.sql:344, FK at line 758, migration 2026-08-23-code-page-binding.sql) binds a code to one page. The session response carries microsite_slug (routes/public/sessions.go:62) and the app lands the visitor on /p/<slug> (landOnRendering, app/src/lib/gate/use-absorb-code.ts:71), where the page's AgentWidget adopts the session. A page is a rendering of the code, not a change to it — same grant, same role, same quota, same accounting (microsite-is-the-codes-rendering.spec.ts). /c/<slug> is the code's stable landing path after redemption (c6c54ce88); the slug is a locator, never a credential (app/src/app/c/[slug]/page.tsx). microsites.allow_byoai (same migration) decides whether a page lets a reader bring their own key when no grant is shown — a per-page pill in the editor (afb7ee478) and microsite.set_byoai over MCP; the code side is codes.set_microsite.

Honest ceiling

  • Single-owner serve. listLive, the store, and the homepage resolve the sole owner; the URL carries no handle. Multi-tenant serving needs a handle in the path, not a redesign.
  • Build is a black box to the panel. The editor sees status and error_message; there is no per-file diagnostic mapping, so a Tailwind or type error reads as one build failure.
  • The store is bounded, not indexed. 500 documents and JSONB (binary JSON) containment filters; a page that wants ordering, joins, or counters beyond that has outgrown the store and should become a connector.

Built 2026-08-23 → 2026-09-07. Tables microsites (schema.sql:728) and microsite_builds (schema.sql:790); serve in routes/public/microsites.go / microsite_serve.go / microsite_preview.go / microsite_seo_head.go / microsite_store.go; usecases in owner/usecase/microsite*.go; 18 microsite.* ops through the dispatcher (verified in e2e/test/norm-outward-toolset.spec.ts; paritymanifest.micrositeEntries() is deliberately empty because reach lives on each op). Tests: microsite.spec.ts (create → build → staging → live → rollback), microsite-rename, microsite-preview-before-publish, microsite-preview-follows-the-agent, microsite-store-admin + microsite-store-isolation (the security scenarios), microsite-per-page-seo, microsite-code-binding + microsite-is-the-codes-rendering, microsite-design-system, microsite-asset-widget, microsite-starter-actually-chats, microsites-linked-on-public-surfaces, the editor set (microsite-admin-ui, microsite-admin-authoring, microsite-editor-entry, microsite-editor-live-follow, microsite-editor-view-toggle), and the homepage set (homepage-served-at-root, homepage-served-at-site-root, homepage-auto-goes-live-at-claim, homepage-view-live-links-to-root, owner-homepage-edit-entry, default-home-look, default-homepage-cards-expand-inline).

Origin: read from standmeet-new main at 36789537d (v0.1.31) on 2026-09-07; design docs under docs/design/ were treated as seeds and not cited.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

microsites