Microsite:owner 自己写、实例负责构建与托管的页面
上级:key-designs · 构建器的认领协议在 builder-claim-skip-locked,这里不重复。
结论(2026-09-07): 一个 microsite 是 owner 用 StandMeet SDK(software development kit,软件开发工具包)写的 React 页面:源码存进 Postgres,由独立的构建器服务编译,再由实例在
/p/<slug>/提供服务——slug 是保留的home时就直接在/。owner 从不碰部署基础设施:一个页面就是一行记录加一个构建指针,所以上线、预发、回滚都只是指针交换(live_build_id / staging_build_id / previous_live_build_id,backend/db/schema.sql:734)。页面需要从实例拿的一切——语料、agent、素材池里的资源、自己的键值存储、访客的 access code——都经由 SDK 的 widget 和 hook 进来,页面只负责把自己拥有不了的东西拼装起来。这个面在 2026-09-05 之前叫"自定义页面"(改名 e7fe80e91 + f6ffcaaa7,migration2026-09-05-microsites-rename.sql)。
服务路径
- 一个服务核心,两个地址。
MicrositeHandlers.Mount(routes/public/microsites.go:54)注册/microsites/{slug}和/homepage;两者都调serveSlugAt,只在解析哪个 slug、注入哪个<base href>上不同(第 105 行是/p/<slug>/,第 114 行是/)。Next.js 应用在beforeFiles的 rewrites 里把/p/:slug反代过去(app/next.config.ts:71)。文件服务部分和 admin 预览共用(microsite_serve.go),所以路径逃逸校验只存在一份。 - 注入的 head 每次请求现读。
pageHead.tags()(microsites.go:172)写入 base、SEO 块和<meta name="standmeet-page-byoai">;没有任何东西被快照进构建产物,响应带Cache-Control: no-cache, no-store,所以回滚掉的页面立刻打不开。 - 预发是路径里的凭据。
/p/{slug}只出 live;staging 构建通过预览路由拿(microsite_preview.go),凭据走路径而不是 query,因为<base href>上挂 query 会弄断相对资源。
首页就是一个 microsite
实例根路径是保留 slug home(HomepageSlug,owner/usecase/microsite.go:34;2026-09-04 的 a5e1cada9,模板 77aee7dc6)。应用中间件探测 GET /api/v1/homepage,有 live 的 home 构建时把 / 重写过去(app/src/middleware.ts:27);404 时落到带码路由,其 fallback 分支从代码里渲染 DefaultHome(app/src/app/visitor-root.tsx:48,app/src/app/default-home.tsx)。DefaultHome 本身也是由同一套 SDK widget 拼的(AgentWidget、CorpusWidget、GateWidget、PageNavWidget),所以没改过的实例和改过的实例走同一套词汇渲染(28f750caf)。自 185c4321b(2026-09-07)起 claim 时什么都不再物化——backend/cmd/server/boot_http.go:248 里 InstallHomepage: nil——不会再有一份存下来的 starter 被后来的 SDK 改动冻在原地。中间件永远不重写 /?code= 或 /?q=:带码访客必须到名字选择器,无码提问必须交给 /gate。admin 里首页有自己的编辑入口和卡片,不在页面表格里(38bfe3a2f、06ab844de)。
编辑器:一个永远在构建的迷你 IDE
每个页面有自己的编辑器路由(app/src/app/admin/microsites/page.tsx → components/admin/sections/microsites/PageEditor.tsx,5c5034962 / 10ac91c2e)。一改就自动触发构建(lib/admin/use-auto-build.ts;385c22f85、0140bbbac、bf5ffaa82)——没有单独的编译按钮。等构建结果的长轮询跑在一个通用 long-poll worker 里(lib/long-poll/long-poll.worker.ts + use-long-poll.ts,5ca6055aa),不是每个功能各起一个定时器。一个布局齿轮在代码 / 分栏 / 渲染之间切换,靠 CSS 隐藏一列而不是卸载(EditorViewToggle.tsx)。改 slug 是编辑器头部的一等操作(RenamePage,owner/usecase/microsite.go:62,e1c14b27e)。同一套 authoring 能力也以 microsite.write_file / get_draft / build / get_build / promote_to_staging / promote_to_live / rollback / delete / rename 暴露给 owner 的 AI 客户端——op 按域声明在 owner/ops/microsites*.go,在 dispatcher 汇聚(routes/dispatcher/collect.go:70),microsite.guide 返回写页指南(设计系统、widget、怎么内联展示语料)。owner/ops/microsites.go 的头注释还写着 authoring "刻意只走 MCP"——那是编辑器出现之前写的,已经过时;今天两个面都在。
页面拥有自己的存储
一个 microsite 可以持久化访客输入(投票、报名、留言簿),owner 不用加表。这个存储是 NoSQL 形态——命名集合里的不透明 JSON(JavaScript Object Notation)文档——而且物理隔离:每个页面有自己的 Postgres schema(page_<id>,capstore 的模式),不是按 id 过滤的共享表,所以没有可以忘写的 WHERE;DeletePage 直接 drop 掉这个 schema(owner/usecase/microsite_store.go,a94909aa9)。公开路由:GET/POST /api/v1/pages/{slug}/store(routes/public/microsite_store.go:40)。读不设门;访客写默认关着,直到 owner 打开(microsites.store_writable,默认 false——"模型 C";MCP op microsite.set_store_writable,eb2cf70ce)。上限:每页 500 个文档、每个文档 8 KiB、路由处按 IP 限速。SDK 里的 hook 是 useMicrositeStore(collection)(sdk/packages/react/src/use-microsite-store.ts:26;5ec7263aa 时叫 usePageStore,随整个面一起改名);客户端基路径是 /api/v1/pages(sdk/packages/core/src/client.ts:172)。owner 侧的查看和清理是 microsite.store_docs / store_delete_doc / store_clear。
页面由什么组成:SDK widget
AgentWidget——访客对话,接管 code 发出的那个会话;GateWidget——输码;PageNavWidget——链接到 owner 其他已上线页面(GET /api/v1/microsites,microsites.go:81)。CorpusWidget——语料卡片,带一个小查询语言:query="path:math/** sort:title limit:5"(子树 · 按 recent 或 title 排序 · 上限;query压过裸的limit;sdk/packages/react/src/widgets/CorpusWidget.tsx:7,113ba6a1a)。读取走和别处一样的 access-control。AssetWidget——素材池里的资源以同源/api/v1/assets/{id}提供,只放行该 microsite 引用的资源;构建完成回调重算页面的资源引用(RebuildMicrositeAssetRefs,corpus/usecase/note_asset_refs.go:138,由routes/sys/builds.go:191调用;836af0fea),所以被引用的资源无法从池里删掉。- widget 把自己的垂直布局写在内联里,而不是靠一个消费方可能永远不会编译的 Tailwind class(00a3bf9e4);构建模板声明
@source "../node_modules/@standmeet/sdk"让 widget 的 class 真的被编译(builder/template/src/theme.css:15,6faa580f8),release 构建把 widget 里的data-testid剥掉(sdk/packages/react/tsup.config.ts,bec5394fb)。SDK 是 vendor 进构建器镜像的,不是 npm 装的(builder/Dockerfile:21)。
SEO 跟着页面走,不跟实例
每页的 seo_title / seo_description / seo_image(schema.sql:746;migration 2026-09-06-microsite-seo.sql、2026-09-06-microsite-seo-image.sql)被注入服务出去的 head:<title>、<meta name="description">、Open Graph(OG)和 Twitter Card 标签(routes/public/microsite_seo_head.go;79a276cce、b19d8db39)。为空就什么都不注入,构建产物保留自己的标题。已上线的非首页 microsite 以 /p/ 列进动态 sitemap(routes/public/seo.go:107 → IndexedMicrosites,owner/usecase/seo_microsites.go:15;701bf1648)。全局 SEO 设置页和它的 seo_settings 表同一周被删掉(7037a434e,migration 2026-09-06-drop-seo-settings.sql)——SEO(search engine optimization,搜索引擎优化)不再有实例级的家。MCP op:microsite.set_seo。
一张 code 打开一个页面
access_codes.microsite_id(schema.sql:344,外键在第 758 行,migration 2026-08-23-code-page-binding.sql)把一张 code 绑到一个页面。会话响应带 microsite_slug(routes/public/sessions.go:62),应用把访客落到 /p/<slug>(landOnRendering,app/src/lib/gate/use-absorb-code.ts:71),页面里的 AgentWidget 接管这个会话。页面是 code 的一种呈现,不是对 code 的更改——同样的授权、同样的角色、同样的配额、同样的记账(microsite-is-the-codes-rendering.spec.ts)。/c/<slug> 是 code 兑换后的稳定落地路径(c6c54ce88);slug 是定位符,永远不是凭据(app/src/app/c/[slug]/page.tsx)。microsites.allow_byoai(同一个 migration)决定没人出示授权时这一页给不给读者用自己的 key——编辑器里一个每页的 pill(afb7ee478)加 MCP 的 microsite.set_byoai;code 那一侧是 codes.set_microsite。
诚实的天花板
- 单 owner 服务。
listLive、存储、首页都解析唯一 owner;URL 里不带 handle。多租户服务需要路径里加 handle,不需要重新设计。 - 构建对面板是个黑盒。 编辑器只看到 status 和
error_message;没有逐文件的诊断映射,一个 Tailwind 或类型错误读起来就是一次构建失败。 - 存储有上限、没索引。 500 个文档加 JSONB(binary JSON,二进制 JSON)包含过滤;一个页面要排序、要 join、要计数器超出这个范围,就已经长出了这个存储,该变成一个 connector。
已实现 2026-08-23 → 2026-09-07。 表 microsites(schema.sql:728)和 microsite_builds(schema.sql:790);服务在 routes/public/microsites.go / microsite_serve.go / microsite_preview.go / microsite_seo_head.go / microsite_store.go;usecase 在 owner/usecase/microsite*.go;18 个 microsite.* op 经 dispatcher 暴露(在 e2e/test/norm-outward-toolset.spec.ts 里核对;paritymanifest.micrositeEntries() 刻意为空,因为 reach 写在每个 op 上)。测试: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(安全场景)、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、编辑器一组(microsite-admin-ui、microsite-admin-authoring、microsite-editor-entry、microsite-editor-live-follow、microsite-editor-view-toggle),以及首页一组(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)。
来源:2026-09-07 读自 standmeet-new main 的 36789537d(v0.1.31);docs/design/ 下的设计文档只当种子,未引用。