Inline editing
Inline editing is the heart of Louise: in edit mode, each editable region on the page becomes editable in place, and only the fields that actually change are saved.
Field markers
Section titled “Field markers”An editable region carries a data-louise-field marker whose value is
"<collection>:<key>:<field>":
<h1 data-louise-field="settings:1:heroHeadline">…</h1><div data-louise-field="settings:1:heroIntro" data-louise-type="richtext">…</div>- Plain text (no
data-louise-type) becomes a single-linecontenteditable="plaintext-only"region. - Rich text (
data-louise-type="richtext") mounts the ProseKit editor — see Rich text.
List items are patched by index with a dotted field: aboutParagraphs.2.
Mounting the client
Section titled “Mounting the client”import { mountLouise } from "louise-toolkit/client";
// Safe to call on every page: if there are no markers (the page wasn't rendered// in edit mode), mountLouise does nothing — so you can lazy-import it.mountLouise();mountLouise finds the markers, attaches the right editor to each, and mounts
the edit bar for the page: a live-status line, Settings (dispatches a
louise:open-settings event), and Done (leaves edit mode). Edits auto-save
by default (see below), so there is no manual Save button unless you opt out.
The client also re-exports the pieces a host app’s own panels reuse, so the Settings renders the same editor and icon set as inline editing:
import { RichText, mountRichText, Icon } from "louise-toolkit/client";See the client reference for the full export list.
The save contract
Section titled “The save contract”When a save runs, the client sends one PATCH per changed field to your save
endpoint (e.g. POST /api/louise/save). Unchanged fields are never sent.
Your endpoint owns two responsibilities Louise cannot do for you:
- Authorize the write from the session — not from the page’s edit mode.
- Allowlist every writable field. A forged request must not reach a column you didn’t list.
// Sketch of a save endpoint.const EDITABLE_SETTINGS = new Set(["heroHeadline", "heroIntro", "footerBlurb"]);const RICH_FIELDS = new Set(["heroIntro"]); // sanitized as HTML
export async function POST({ request, locals, env }) { if (!locals.editor) return new Response("Forbidden", { status: 403 }); const { field, value } = await request.json(); if (!EDITABLE_SETTINGS.has(field)) return new Response("Unknown field", { status: 400 }); const clean = RICH_FIELDS.has(field) ? sanitize(value) : String(value); // …persist `clean` to your D1 row via db(env.DB)… return new Response(null, { status: 204 });}Auto-save
Section titled “Auto-save”Edits persist automatically, on a short idle debounce — there is no Save button to remember. It’s on by default:
mountLouise(); // auto-save on (800ms debounce)mountLouise({ autoSave: { debounceMs: 1500 } }); // tune the delaymountLouise({ autoSave: false }); // opt out → a manual Save button returns- Auto-save reuses the same save as a manual click: a live field write, or a draft on a versioned page. It never publishes — Publish stays an explicit action.
- Pending edits are flushed when you tab out of a field, hide the tab, or navigate
away (the save
fetchuseskeepaliveso it survives the unload), and the browser warns if you try to leave with a save still in flight. - The edit bar’s status line reflects it live: Saving… → Saved (or Draft saved). A failed save keeps the field dirty and retries on your next edit.
On a page with no inline fields (a sections-only page), the sections dock owns its own auto-save instead.
Adding an inline-editable field
Section titled “Adding an inline-editable field”The pattern, generalised:
- Add the field to your content type and give it a seed/default value.
- Render it with the marker (plain or
data-louise-type="richtext"). - Allowlist it in the save endpoint (and in your rich-field set if it’s HTML).
That’s the whole loop — no schema migration on the content side, because the schema is yours.