Louise Sections
Louise Sections are the preconfigured-blocks model: a page is an ordered list of typed items that your own components render, so a bespoke design stays pixel-perfect while editors still add, reorder, and edit it. Where the Louise Builder stores sanitized HTML and inline fields edit one value at a time, sections store structured JSON and render through your components.
The shape
Section titled “The shape”A page carries a sections array — ordered items, each a _type discriminant
plus its field values:
[ { "_type": "hero", "heading": "Louise Toolkit", "tagline": "…", "ctaHref": "/docs" }, { "_type": "featureGrid", "items": [{ "title": "…", "body": "…" }] }]The site owns rendering (a bespoke component per _type); Louise owns
editing only. No markup is ever authored in the editor, so the design can’t
drift.
The catalog
Section titled “The catalog”A SectionCatalog describes each type’s editable fields — schema only, no
markup:
import type { SectionCatalog } from "louise-toolkit/client";
export const SECTIONS: SectionCatalog = { hero: { label: "Hero", fields: { heading: { type: "text" }, tagline: { type: "textarea" }, ctaLabel: { type: "text" }, // No visible text on the page → edited in the dock, not in place. ctaHref: { type: "text", inline: false }, }, }, featureGrid: { label: "Feature grid", fields: { items: { type: "array", itemLabel: "Feature", itemFields: { title: { type: "text" }, body: { type: "textarea" } }, }, }, },};Field types are text, textarea, array (repeatable, with itemFields), and
image. Plain text is edited in place; array and image are edited in the
dock (an image gets Upload + Choose from media + clear controls, so it
always resolves to a media asset,
never a pasted URL), as is any field you mark inline: false (e.g. a link URL
with no visible text). Pass mediaBase to assertValidSections and a section
image that isn’t media-hosted is rejected on write (422).
Rendering + edit markers
Section titled “Rendering + edit markers”Map each item’s _type to its component. In edit mode, stamp a
data-louise-sfield marker on every visible text node so the client can make it
editable in place. The path is "<index>.<field>", or
"<index>.<key>.<itemIndex>.<subField>" for array items:
<h1 data-louise-sfield={`${i}.heading`}>{heading}</h1><p data-louise-sfield={`${i}.tagline`} data-louise-multiline>{tagline}</p>Render empty fields too (in edit mode) so there’s something to click into;
data-louise-multiline keeps newlines for textarea-backed fields.
Editing: mountSections
Section titled “Editing: mountSections”import { mountSections } from "louise-toolkit/client";
mountSections(el, { catalog: SECTIONS, pageId, initial });// Auto-save is on by default; opt out with:mountSections(el, { catalog: SECTIONS, pageId, initial, autoSave: false });el is the wrapper around the server-rendered sections. The UX is hybrid:
- Text is edited in place on the live design — each
data-louise-sfieldnode becomescontenteditable, writing into a shared fine-grained store (a keystroke updates only that leaf, so rows never tear down). - A floating control dock handles what you can’t point at: add / reorder /
remove sections, array-item add/remove, and any
inline: falsefield.
The save contract
Section titled “The save contract”When the page is wired for drafts & publishing (a versions
collection), a save stages a draft version without touching the live page,
and Publish promotes it.
- Text edits stage a draft — no reload (the DOM already shows the change); the live page is unchanged until you Publish. With auto-save on (the default) this happens on an idle debounce, so the dock shows only a live status and Publish — no Save draft button. Auto-save never publishes.
- Structural changes save a draft and then reload, so the server re-renders the new shape (which comes back inline-editable). In edit mode the page resumes your latest draft; view mode always shows the published version.
Opt out with autoSave: false to bring back the manual Save draft button.
Store sections as a JSON column on your pages table and add it to your
pagesRoute fields allowlist (metadata/create/delete) —
the draft/publish surface is versionsRoute.
Validation
Section titled “Validation”The stored JSON is validated server-side before every write. Give pagesRoute a
validate hook that runs assertValidSections against your catalog:
import { assertValidSections } from "louise-toolkit/content";import { SECTIONS } from "./sections/catalog";
pagesRoute({ table: pages, resolveEditor, fields: [...DEFAULT_PAGE_FIELDS, "sections"], validate: async (data, ctx) => { if ("sections" in data) await assertValidSections(SECTIONS, data.sections, ctx); },});validateSections (the non-throwing form) checks that the value is an array, that
every item’s _type is a known catalog entry, and that each field matches its
declared shape (text/textarea → string; array → objects whose itemFields are
validated in turn). A field can also carry a validation chain — the same
Rule builder collection fields use, e.g.
heading: { type: "text", validation: (r) => r.required().max(80) }.
assertValidSections throws LouiseValidationError on any error-severity
violation, which pagesRoute turns into a 422 { error, violations } — the
on-page dock surfaces the first violation as the save-failure reason.
Search
Section titled “Search”Because sections is a json field, its content is full-text searchable: list
it in the collection’s search.fields and the FTS index flattens every string
leaf (headings, feature text…) into the index. Mount
searchRoute and the Settings’ Pages panel gains a search
box. Only published content is indexed; run POST /api/louise/pages/reindex once
after adding the FTS table to backfill existing rows.