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

microsites

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_idbackend/db/schema.sql:734)。页面需要从实例拿的一切——语料、agent、素材池里的资源、自己的键值存储、访客的 access code——都经由 SDK 的 widget 和 hook 进来,页面只负责把自己拥有不了的东西拼装起来。这个面在 2026-09-05 之前叫"自定义页面"(改名 e7fe80e91 + f6ffcaaa7,migration 2026-09-05-microsites-rename.sql)。

服务路径

  • 一个服务核心,两个地址。 MicrositeHandlers.Mountroutes/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 homeHomepageSlugowner/usecase/microsite.go:34;2026-09-04 的 a5e1cada9,模板 77aee7dc6)。应用中间件探测 GET /api/v1/homepage,有 live 的 home 构建时把 / 重写过去(app/src/middleware.ts:27);404 时落到带码路由,其 fallback 分支从代码里渲染 DefaultHomeapp/src/app/visitor-root.tsx:48app/src/app/default-home.tsx)。DefaultHome 本身也是由同一套 SDK widget 拼的(AgentWidgetCorpusWidgetGateWidgetPageNavWidget),所以没改过的实例和改过的实例走同一套词汇渲染(28f750caf)。自 185c4321b(2026-09-07)起 claim 时什么都不再物化——backend/cmd/server/boot_http.go:248InstallHomepage: nil——不会再有一份存下来的 starter 被后来的 SDK 改动冻在原地。中间件永远不重写 /?code=/?q=:带码访客必须到名字选择器,无码提问必须交给 /gate。admin 里首页有自己的编辑入口和卡片,不在页面表格里(38bfe3a2f、06ab844de)。

编辑器:一个永远在构建的迷你 IDE

每个页面有自己的编辑器路由(app/src/app/admin/microsites/page.tsxcomponents/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 是编辑器头部的一等操作(RenamePageowner/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 过滤的共享表,所以没有可以忘写的 WHEREDeletePage 直接 drop 掉这个 schema(owner/usecase/microsite_store.go,a94909aa9)。公开路由:GET/POST /api/v1/pages/{slug}/storeroutes/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/pagessdk/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/micrositesmicrosites.go:81)。
  • CorpusWidget——语料卡片,带一个小查询语言:query="path:math/** sort:title limit:5"(子树 · 按 recent 或 title 排序 · 上限;query 压过裸的 limitsdk/packages/react/src/widgets/CorpusWidget.tsx:7,113ba6a1a)。读取走和别处一样的 access-control
  • AssetWidget——素材池里的资源以同源 /api/v1/assets/{id} 提供,只放行该 microsite 引用的资源;构建完成回调重算页面的资源引用(RebuildMicrositeAssetRefscorpus/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_imageschema.sql:746;migration 2026-09-06-microsite-seo.sql2026-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:107IndexedMicrositesowner/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_idschema.sql:344,外键在第 758 行,migration 2026-08-23-code-page-binding.sql)把一张 code 绑到一个页面。会话响应带 microsite_slugroutes/public/sessions.go:62),应用把访客落到 /p/<slug>landOnRenderingapp/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。micrositesschema.sql:728)和 microsite_buildsschema.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-renamemicrosite-preview-before-publishmicrosite-preview-follows-the-agentmicrosite-store-admin + microsite-store-isolation(安全场景)、microsite-per-page-seomicrosite-code-binding + microsite-is-the-codes-renderingmicrosite-design-systemmicrosite-asset-widgetmicrosite-starter-actually-chatsmicrosites-linked-on-public-surfaces、编辑器一组(microsite-admin-uimicrosite-admin-authoringmicrosite-editor-entrymicrosite-editor-live-followmicrosite-editor-view-toggle),以及首页一组(homepage-served-at-roothomepage-served-at-site-roothomepage-auto-goes-live-at-claimhomepage-view-live-links-to-rootowner-homepage-edit-entrydefault-home-lookdefault-homepage-cards-expand-inline)。

来源:2026-09-07 读自 standmeet-new main 的 36789537d(v0.1.31);docs/design/ 下的设计文档只当种子,未引用。

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 →