Skip to content

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.

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.
});

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:

src/pages/api/louise/inquiries.ts
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>.

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 to fields (defaults DEFAULT_PAGE_FIELDS) and rich fields (body) are run through sanitizeRichHtml before store.
  • mediaRoute — wraps louisecms/media: magic-byte- sniffed uploads, the registry list, and a delete-safety reference scan (a 409 in_use unless ?force=1). Its env widens EditorRouteEnv with the R2 bindings (MediaRouteEnv: MEDIA, MEDIA_URL).
  • listMediaRoute — the same GET/POST/DELETE contract as mediaRoute but with no media registry table: GET lists the R2 bucket directly via listMedia, and POST reads an allowlisted upload scope from the form (scopes, first is the default) rather than a fixed one. Same MediaRouteEnv (delete-safety still scans D1 content tables).
  • settingsRoute — GET/PATCH the site_settings singleton. Extensible, not a closed set: it patches an allowlisted structured base (columns, the framework siteSettingsColumns) and merges site-declared customKeys into the custom JSON. 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 structured siteSettingsColumns), paired with the drawer’s settingsBaseGroups: [] + render fields. allow is a { key: sanitize } map (only listed top-level keys are merged into the blob, each through its sanitizer; anything else is ignored); an optional read transforms 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=.

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, custom updates, 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.