diff --git a/docs/features/cms-native-forms.md b/docs/features/cms-native-forms.md
index b318ef2cc..968ee1786 100644
--- a/docs/features/cms-native-forms.md
+++ b/docs/features/cms-native-forms.md
@@ -54,7 +54,7 @@ CMS-native submission is a two-step public flow:
The submit handler reloads the latest published site snapshot, derives the form snapshot from the published page tree, requires the target `DataTable` to be a non-system `data` table, validates fields against that table, and creates a `data_rows` record with `createDataRow`. After persistence, `server/forms/handler.ts` calls `emitContentEntryCreated` from `server/publish/contentEvents.ts` with the `system` actor, so plugins can react to successful submissions through the standard `content.entry.created` channel.
-Validation lives in `src/core/forms/validation.ts`. It rejects unknown fields, enforces required fields, coerces table field types, applies email/url/number/select checks, applies control min/max/pattern constraints, and caps payload size.
+Validation lives in `src/core/forms/validation.ts`. It rejects unknown fields, enforces required fields, coerces table field types, applies email/url/number/select checks, applies control min/max/pattern constraints, and caps payload size. A `select` or `multiSelect` submission may carry an option's `value` (what `base.option` nodes inserted from the table hold) or its `id`; the stored cell is always the option id.
## Security
diff --git a/docs/features/content-storage.md b/docs/features/content-storage.md
index a025f283d..f10d8cd43 100644
--- a/docs/features/content-storage.md
+++ b/docs/features/content-storage.md
@@ -242,7 +242,7 @@ For postType rows, `publishDataRow` does the same but incrementally: writes the
See [docs/features/publisher.md](publisher.md) for the full pipeline.
-For **post-types**, public row routes require an explicitly authored entry template (a `pages` row with `template.target = { kind: 'postTypes', tableSlugs: [table.slug] }`). When you publish a post, the renderer resolves the template chain for the entry route (`resolveTemplateChain`) and renders the merged tree with the post row pushed onto the entry stack as `currentEntry`. Dynamic bindings on the template nodes resolve `currentEntry.title`, `currentEntry.body`, etc. Without a matching entry template, the published row exists in the CMS but its public detail URL returns 404. See [docs/features/templates.md](templates.md) for the full template model.
+For **post-types**, public row routes require an explicitly authored entry template (a `pages` row with `template.target = { kind: 'postTypes', tableSlugs: [table.slug] }`). When you publish a post, the renderer resolves the template chain for the entry route (`resolveTemplateChain`) and renders the merged tree with the post row pushed onto the entry stack as `currentEntry`. Dynamic bindings on the template nodes resolve `currentEntry.title`, `currentEntry.body`, etc. Media cells — `featuredMedia` and every schema-declared `media` field, including repeater media — resolve to public `/uploads/…` paths exactly as they do inside a `data.rows` loop (`publishedDataRowToEntryItem` in `server/publish/loopPrefetch.ts`), so the same binding renders the same URL on a detail page and in a list. Without a matching entry template, the published row exists in the CMS but its public detail URL returns 404. See [docs/features/templates.md](templates.md) for the full template model.
### Scheduled publishing
diff --git a/docs/features/html-import.md b/docs/features/html-import.md
index 7cb7bf493..6dc7c17a4 100644
--- a/docs/features/html-import.md
+++ b/docs/features/html-import.md
@@ -158,7 +158,7 @@ The importer is "approximate by construction". Several inputs do not survive the
| Input | What happens | Why |
|---|---|---|
| `alt=""` on `` | Reported in `imageAlts`, not stored on the node | `base.image` has no `alt` prop — alt text lives on the media library asset, which Site Import creates with the authored value |
-| Safe HTML attributes not modeled by the matched module (`id`, ARIA attrs, `role`, custom attrs, `data-*`, etc.) | Preserved in `props.htmlAttributes` on base container/text/link/button/image nodes and editable in the Properties panel Attributes view. `class` names become registry classes, inline `style` declarations become `node.inlineStyles`, event handlers are stripped, reserved editor/runtime `data-*` names are not imported, and attributes already owned by the module (for example `href` on links and `src` on images) stay in their first-class module props. | The module schema owns modeled props; `htmlAttributes` is the safe escape hatch for extra authored attributes |
+| Safe HTML attributes not modeled by the matched module (`id`, ARIA attrs, `role`, custom attrs, `data-*`, etc.) | Preserved in `props.htmlAttributes` on base container/text/link/button/image nodes and editable in the Properties panel Attributes view. `class` names become registry classes, inline `style` declarations become `node.inlineStyles`, event handlers are stripped, reserved editor/runtime `data-*` names are not imported, and attributes already owned by the module (for example `href` on links and `src` on images) stay in their first-class module props. An authored `rel` on a link or button anchor is kept in the bag; at render time the module merges it with the `noopener noreferrer` it adds for `target="_blank"` and emits one `rel` attribute. | The module schema owns modeled props; `htmlAttributes` is the safe escape hatch for extra authored attributes |
| Exact inline whitespace around mixed content (`
` | Normal-flow indentation carries no rendered content, while `` whitespace is content and must retain its literal DOM text-node shape for CSS and runtime scripts. | | Void elements (`
`, `
`, etc.) | Imported as a childless `base.container` node with `tag:'custom'` and the real tag name as `customTag`. No children, no empty-container placeholder. `` imports as a form primitive instead. | React throws if children are rendered inside void element tags; the dedicated void-element rule (before the catch-all) sets `recurse:false` and the canvas renderer skips children entirely for void tags. | diff --git a/docs/features/loops.md b/docs/features/loops.md index b18364eda..f4183bb40 100644 --- a/docs/features/loops.md +++ b/docs/features/loops.md @@ -116,7 +116,7 @@ Iterates rows in any `data_table`. The user picks the table in the Properties pa **Filtering by a cell.** Three `filters` keys carry the condition: `cellField` (a field id from the selected table), `cellOperator`, and `cellValue`. The operator set is closed — `is`, `isNot`, `isTrue`, `isFalse`, `isSet`, `isEmpty` — and `parseCellFilter` in `src/core/loops/cellFilter.ts` returns `null` for anything absent or half-configured, so a loop mid-edit keeps listing everything rather than silently emptying. -**Sorting by a cell.** `orderBy` also accepts `cell:`, read by `parseCellOrder`. Riding on `orderBy` rather than a separate prop means every caller that already threads it — the publisher, the canvas preview endpoint, imported `data-order-by` attributes — supports cell sorting without further plumbing. Values compare as text in both dialects: ISO dates sort chronologically, numbers sort lexicographically (`'10' < '9'`). +**Sorting by a cell.** `orderBy` also accepts `cell: `, read by `parseCellOrder`. Riding on `orderBy` rather than a separate prop means every caller that already threads it — the publisher, the canvas preview endpoint, imported `data-order-by` attributes — supports cell sorting without further plumbing. Number fields use numeric comparison in both dialects; missing, null and non-number values sort last in either direction. Other scalar fields compare as text, so ISO dates sort chronologically. Field metadata selects the comparison type: authors keep actual numbers in their cells rather than formatting values to influence ordering. Both read `cells_json`, the one place the two dialects genuinely differ (`#>> array[$n]` on Postgres, `json_extract` on SQLite). **The field name binds as a parameter, never as SQL text.** `cellFilterSql` and `cellOrderSql` own that switch; `loop-source-sql-safety.test.ts` scans the whole `src/core/loops/` tree for Postgres-isms. diff --git a/docs/features/mcp-connectors.md b/docs/features/mcp-connectors.md index bb504abba..ee8c19d93 100644 --- a/docs/features/mcp-connectors.md +++ b/docs/features/mcp-connectors.md @@ -144,7 +144,7 @@ executeAiTool(...) / live editor bridge | `contentAuthorization.ts` | Before a document tool is relayed to the browser, resolves the target row through `server/auth/dataAccess.ts` with the connection's own identity and capabilities (table family plus own-vs-any ownership), so the owner's admin cookie cannot widen the delegated grant. | | `transports/http.ts` | Authenticated, Origin-validated `createMcpHandler` entry for MCP 2026-07-28 plus the stateless 2025 fallback. | | `server.ts` / `registry.ts` | Low-level SDK server, TypeBox input schemas, catalog deduplication, and capability filtering. | -| `editorBridge.ts` | Per-user, per-scope live workspace bridge. The stream carries an **idle lease** (120s, re-armed by every relayed tool request) so an active batch is never cut mid-flight; only quiet streams recycle. The workspace's reconnect loop (`useMcpWorkspaceBridge`) reopens a recycled healthy stream immediately off the stream-end network event — deliberately timer-free, because hidden webviews (backgrounded browser tabs) clamp timers to minutes while network events still fire — and a tab becoming visible short-circuits any pending retry delay. | +| `editorBridge.ts` | Per-user, per-scope live workspace bridge. Tool requests are delivered **serially** (the next is pushed only once the previous settles, matching the editor's one-at-a-time execution), so each call's 90s timeout measures the editor's actual work, not time queued behind slower calls. The stream carries an **idle lease** (120s, re-armed by every relayed tool request and never expiring while a call is pending) so an active batch is never cut mid-flight; only quiet streams recycle. The workspace's reconnect loop (`useMcpWorkspaceBridge`) reopens a recycled healthy stream immediately off the stream-end network event — deliberately timer-free, because hidden webviews (backgrounded browser tabs) clamp timers to minutes while network events still fire — and a tab becoming visible short-circuits any pending retry delay. | | `tools/publishTool.ts` | Explicit canonical full-site publish with MCP audit metadata. | | `tools/uploadMediaTool.ts` | Server-resolved image upload (`media_upload`) — inline base64 or SSRF-guarded `sourceUrl` download, through the shared media pipeline. | diff --git a/docs/features/modules.md b/docs/features/modules.md index b61509d49..9b59b0099 100644 --- a/docs/features/modules.md +++ b/docs/features/modules.md @@ -51,7 +51,7 @@ src/modules/base/ ├── slotOutlet/ — base.slot-outlet (VC author side) ├── slotInstance/ — base.slot-instance (VC consumer side) ├── shared/ -│ └── anchorTarget.ts — AnchorTargetSchema, ANCHOR_TARGET_OPTIONS, anchorRel() (button + link) +│ └── anchorTarget.ts — AnchorTargetSchema, ANCHOR_TARGET_OPTIONS, anchorRel(), mergeAnchorRel(), anchorHtmlAttributes() (button + link) ├── utils/ │ ├── escape.ts — escapeHtml, safeUrl, sanitiseCssValue, buildStyle (re-exports publisher utils) │ ├── htmlTag.ts — htmlTagControl, customHtmlTagControl (resolution lives in @core/htmlAttributes) diff --git a/docs/features/publisher.md b/docs/features/publisher.md index c31f99cf6..89fef0b68 100644 --- a/docs/features/publisher.md +++ b/docs/features/publisher.md @@ -320,7 +320,10 @@ The exclusive namespaces `/_instatic/css/*` (`serveSiteCss`) and `/_instatic/ass (`tryServeRuntimeAsset`) are served **disk-first**, falling back to a rebuild (`serveSiteCss`) or the DB (`published_runtime_assets`) only for preview or a publish whose disk write failed. Unknown paths under either prefix 404 rather -than falling through. +than falling through. Each full publish prunes the stored +`published_runtime_assets` of every page version except the one it activates +and the one it supersedes (`pruneSupersededRuntimeAssets` in +`server/repositories/publish.ts`); version rows themselves are kept. --- diff --git a/docs/features/site-import.md b/docs/features/site-import.md index de6093e12..041964b7a 100644 --- a/docs/features/site-import.md +++ b/docs/features/site-import.md @@ -15,7 +15,7 @@ CSS declarations containing `var()`, `env()`, `clamp()`, `min()`, or `max()` pas - **Per-stylesheet import modes:** each top-level linked stylesheet either converts to editable style rules (default) or imports verbatim as a page-scoped `SiteFile` stylesheet (`options.stylesheetModes`, picked in the Review step). There are no generated scope classes — page isolation comes from the kept file's runtime scope. - `commitImportPlan(plan, adapter)` — uploads assets, then wraps all store writes in a single `adapter.commit` call → one Cmd+Z reverts the whole import. - Static imports load the current CMS draft into the editor store on demand when launched outside `/admin/site`; if no draft exists, the modal creates an empty site before analysis. -- Conflict resolution: rename with a numeric suffix (default), overwrite, skip, or custom-rename — per page slug, per class name, per design token (colour / font CSS variable), and per divergent cross-stylesheet class definition, with category-level bulk actions. Token renames rewrite `var(--x)` references so imports stay faithful. +- Conflict resolution: rename with a numeric suffix (default), overwrite, skip, or custom-rename — per page slug, per class name, per design token (colour / font CSS variable), and per divergent cross-stylesheet class definition, with category-level bulk actions. Token renames rewrite `var(--x)` references, and declarations of the same variable in other rules, so imports stay faithful. - What imports: pages, linked CSS plus unconditional local CSS `@import` graphs, `kind:'class'` and `kind:'ambient'` style rules, stylesheets kept as page-scoped files, `@keyframes`, uploadable media/font files, root CSS color tokens, root CSS font tokens, `@font-face` families, known external font stylesheet imports, safe extra HTML attributes on base modules, body-level classes/attributes/style metadata, bare DOM text nodes in mixed content, and executable HTML scripts as page-scoped runtime scripts. Module-script imports from known npm CDNs are rewritten to bare package imports and added to the site dependency manifest. - CMS bundle import preserves selected exported tables, rows, optional site shell, media, folders, and redirects using the same merge strategies as site transfer (`replace`, `merge-add`, `merge-overwrite`). - HTML forms import through the shared HTML importer as first-class form primitives (`base.form`, controls, labels, submit buttons), not as custom containers. @@ -46,6 +46,8 @@ src/core/siteImport/ ├── applyAssetRewrites.ts — patch fragment props + CSS/@keyframes url() with new media URLs (post-upload) ├── linkRewrite.ts — rewrite intra-site to cms:page: refs ├── conflicts.ts — detect page-slug + class-name + design-token collisions; apply resolutions (incl. var(--x) rewrites) +├── tokenNames.ts — settle the emitted name of every imported colour + font token (authored → emitted renames) +├── varRefs.ts — rename a custom property across imported rules, inline styles and @keyframes (var(--x) refs + --x declarations) ├── adapter.ts — SiteImportAdapter + SiteImportTransaction interfaces (the ONE transaction contract; the editor store implements it directly) ├── paths.ts — dirname/joinPaths for FileMap-relative path resolution ├── planCss.ts — single CSS-source parse path (external sheets + per-page inline