editor
import { saveRoute, settingsRoute, blobSettingsRoute, pagesRoute, mediaRoute, listMediaRoute, inquiriesRoute, seedRoute, runEditorRoute,} from "louisecms/editor";The server-side counterpart to the drawer: framework-generic
api/louise/* request→response handlers. Each factory returns a
WorkerRoute — (request, env, ctx) => Response | undefined
— that composeWorker composes, so a site wires the ones
it needs, passes its own Drizzle tables, and keeps bespoke resource routes
(products, artworks…) per-site. Sites not on composeWorker (Astro, Nitro…) run
the same handlers with runEditorRoute.
The framework drawer panels (Pages/Media/Settings) and the default Inquiries panel
call these endpoints. Peer: drizzle-orm.
Composing the routes
Section titled “Composing the routes”import { composeWorker } from "louisecms/worker";import { pagesRoute, mediaRoute, settingsRoute, saveRoute, inquiriesRoute } from "louisecms/editor";import { getLouiseAuth, resolveEditorSession } from "louisecms/auth";import { pages, media, siteSettings, inquiries } from "./db/schema";
// Bridge the site's auth once; every route reuses it. `getLouiseAuth` and// `resolveEditorSession` come from louisecms/auth — see the auth reference.const resolveEditor = async (request: Request, env: Env) => resolveEditorSession(await getLouiseAuth(env, request.url, authConfig), request);
export default composeWorker({ routes: [ pagesRoute({ table: pages, resolveEditor }), mediaRoute({ table: media, resolveEditor, referenceSources: [/* … */] }), settingsRoute({ table: siteSettings, resolveEditor, columns: ["siteName", "navLinks", /* … */] }), saveRoute({ resolveEditor, collections: { /* … */ } }), inquiriesRoute({ table: inquiries, resolveEditor }), ], // …your bespoke routes + SSR fallthrough.});Auth: resolveEditor + the guard
Section titled “Auth: resolveEditor + the guard”Every route is decoupled from any one auth wiring by a site-supplied
ResolveEditor:
type ResolveEditor<Env> = ( request: Request, env: Env,) => EditorSession | null | Promise<EditorSession | null>;Returning null means “not an editor” and the route answers 401/403. Internally
each route calls guardEditor, which runs requireEditor:
it verifies the session and, on mutations, enforces a same-origin (CSRF)
check. Reads skip the same-origin check; writes require it.
Adapting to Astro (or any non-Worker host)
Section titled “Adapting to Astro (or any non-Worker host)”The handlers are composeWorker WorkerRoutes — (request, env, ctx). A site on
an Astro (or Nitro/Nuxt/…) adapter resolves the editor session in middleware and
has no ExecutionContext to hand the route, so wrap it with runEditorRoute:
it supplies a no-op ctx and turns a path fall-through (undefined) into a 404.
Because the session is already resolved, resolveEditor just hands it back — no
second auth path:
import { inquiriesRoute, runEditorRoute } from "louisecms/editor";import { inquiries } from "../../../db/schema";import { env } from "cloudflare:workers";
export const ALL: APIRoute = (ctx) => runEditorRoute( inquiriesRoute({ table: inquiries, resolveEditor: () => ctx.locals.editor }), ctx.request, env, );runEditorRoute(route, request, env): Promise<Response>.
Routes
Section titled “Routes”| Factory | Endpoint | Methods |
|---|---|---|
pagesRoute |
/api/louise/pages (+ /:id) |
GET list · POST create · GET/PATCH/DELETE one |
mediaRoute |
/api/louise/media |
GET list · POST upload · DELETE (reference-scanned) |
listMediaRoute |
/api/louise/media |
registry-less variant: lists R2 directly, per-request scope |
settingsRoute |
/api/louise/settings |
GET · POST/PATCH (structured base + custom) |
blobSettingsRoute |
/api/louise/settings |
GET · POST/PATCH — single-JSON-blob variant |
saveRoute |
/api/louise/save |
POST (inline field save) |
inquiriesRoute |
/api/louise/inquiries |
GET list · DELETE one |
seedRoute |
/api/louise/seed |
seeds the site_settings singleton (idempotent) |
pagesRoute— CMS pages CRUD. Create/update are allowlisted tofields(defaultsDEFAULT_PAGE_FIELDS) and rich fields (body) are run throughsanitizeRichHtmlbefore store.mediaRoute— wrapslouisecms/media: magic-byte- sniffed uploads, the registry list, and a delete-safety reference scan (a409 in_useunless?force=1). Its env widensEditorRouteEnvwith the R2 bindings (MediaRouteEnv:MEDIA,MEDIA_URL).listMediaRoute— the same GET/POST/DELETE contract asmediaRoutebut with nomediaregistry table: GET lists the R2 bucket directly vialistMedia, and POST reads an allowlisted uploadscopefrom the form (scopes, first is the default) rather than a fixed one. SameMediaRouteEnv(delete-safety still scans D1 content tables).settingsRoute— GET/PATCH thesite_settingssingleton. Extensible, not a closed set: it patches an allowlisted structured base (columns, the frameworksiteSettingsColumns) and merges site-declaredcustomKeysinto thecustomJSON. A key in neither allowlist is ignored, never written — this is what backs the drawer’s Settings extension groups.blobSettingsRoute— the variant for sites that keep all config in a single JSON blob column (not the structuredsiteSettingsColumns), paired with the drawer’ssettingsBaseGroups: []+ render fields.allowis a{ key: sanitize }map (only listed top-level keys are merged into the blob, each through its sanitizer; anything else is ignored); an optionalreadtransforms the blob on GET (e.g. seed-merge). GET returns{ settings: <blob> }.saveRoute— the inline edit-on-the-page save endpoint. Each collection declares its editable fields (SaveCollectionConfig); values are resolved and sanitized before write.inquiriesRoute— read-mostly: list submissions newest-first, delete one by?id=.
Pure helpers
Section titled “Pure helpers”The security-sensitive logic is factored into pure, testable functions you can reuse or unit-test:
pickFields(input, fields, richFields, sanitize)— allowlist + sanitize a create/update payload (pagesRoute).partitionSettingsPatch(patch, columns, customKeys)— split a settings patch into base-column updates,customupdates, and ignored keys (settingsRoute).mergeBlobPatch(blob, patch, allow)— merge an allowlisted{ key: sanitize }patch into a settings blob, returning{ blob, ignored, changed }without mutating the input (blobSettingsRoute).resolveFieldValue(...)— resolve one inline-save field against its collection config (saveRoute).
Also exported: runEditorRoute, guardEditor, json, matchPath, ident,
tableMeta, and the EditorRouteEnv / ResolveEditor types.