Content Management that can withstand the plundering hoards

CMS HTTP API reference

The API belongs to one CMS instance/Website. Use the CMS HTTPS origin, not the public site's hostname. The public frontend has files at some API-shaped asset paths but has no editing/publishing API. Routes below are registered in server.go and authentication middleware.

Authentication, requests and errors

Every /api/* route requires a valid encrypted session cookie or a verified Authorization: Bearer <token>. A Bearer header takes precedence over a cookie; an invalid Bearer token does not fall back to the cookie. allowedRoles applies to the whole API. publishRoles, when set, additionally gates publish and rollback. Roles do not restrict editing to a URI subtree.

For browser mutations, first GET /api/publish/status, retain its CSRF cookie and csrfToken/X-CSRF-Token, then send the cookie and matching X-CSRF-Token with POST/PUT/DELETE/PATCH. A supplied Origin (or Referer fallback) must match request host and scheme. Successfully verified Bearer requests are exempt from the CSRF cookie/token requirement; same-origin validation still applies to any supplied Origin/Referer. Logout has independent browser CSRF validation. See authentication and runnable CI examples.

JSON requests use Content-Type: application/json. Page writes/rendering and most JSON bodies are capped at 8 MiB, with specialized limits listed below. Unknown JSON fields in the normal typed request handlers are rejected. Send server-generated IDs/timestamps only where explicitly accepted, not by blindly PUTting an entire GET response. Errors from API handlers normally have {"error":"message"}; routing/HTTP infrastructure errors may have different bodies. Always inspect HTTP status before parsing a success payload.

Status Typical meaning Response
200 / 201 Read/update or creation succeeded Read the route's result
400 Invalid JSON, path, input or rollback target Correct the input; do not retry unchanged
401 Missing, invalid or expired identity Reauthenticate or obtain a new token
403 Role denial, CSRF/origin rejection, protected bundled theme Check the message and permissions
404 Unknown ID/path Refresh the current list
405 Wrong method Use the documented method
409 Path/identifier collision or resource in use Review conflicting resources before replacing/removing
413 Upload exceeded a size bound Reduce upload or split work
415 Unsupported media/content type Use the supported route/type
500 Storage, rendering or internal error Inspect logs/status before retrying mutations
503 Authentication not ready Fix configuration/provider availability

Limits are enforced in route-specific ways: some oversized JSON/import requests return 400 rather than 413. Mutations are not a general multi-request transaction. There is no API version prefix or generic optimistic-lock/ETag write contract; coordinate editors and CI to avoid competing updates.

Identity and build

Method and path Result
GET /api/me {email,name,roles,issuer,subject}; roles is an array
GET /api/version {version}; build version, no-store

issuer plus subject identify an account across session renewal. Never infer identity equivalence from display name or email alone.

Pages and rendering

Method and path Input Success
GET /api/pages none 200 array of page summaries, empty array when none
POST /api/pages Page input 201 complete created page
GET /api/pages/{id} URL ID 200 full page
PUT /api/pages/{id} Page input 200 full updated page
DELETE /api/pages/{id} URL ID 200 {ok:true,id}
POST /api/content/render Content plus optional preview context 200 {html,css} without saving
POST /api/content/convert HTML/Markdown content plus targetFormat 200 converted content without saving; save with convertFrom
PUT /api/pages/content?path=...&title=... Raw Markdown/HTML; optional menu query 201 new or 200 replacement page
DELETE /api/pages/branch?path=... Explicit URI root 200 {count,deleted}

List summaries contain id, title, normalized path, updatedAt and optional menu, disabled and optional pageType, sorted by path. Full page responses add content version/format and the format-specific source/HTML/CSS/object plus optional layout and featured image. GET summaries are not complete page-update documents.

A normal page input contains title, required path, contentVersion:1 and format, with format-specific fields. It may include layout, menu and overwritePageId, plus disabled and featuredImage:{assetId,alt}. Explicit HTML/Markdown format changes also require convertFrom on updates; see conversion. It does not take a client-assigned page ID. Example:

{
  "title": "About",
  "path": "/about",
  "contentVersion": 1,
  "format": "markdown",
  "source": "# About\n\nOur story.\n",
  "menu": {"visible": true, "position": 10}
}

For HTML use format:"html", html and optional css, with no nonempty source. For raw objects, renderer formats and page-type fields (pageType, typeVersion, fields, pathTemplate) use the extension reference. layout is an optional layout ID from the selected theme. A supplied menu is a complete object with visibility and position, plus optional parentId; omitted menu on updates preserves existing settings. Page updates replace content fields: send the complete desired format content rather than expecting a partial patch.

A URI collision returns 409 and conflictingPage. To explicitly replace it, resubmit with its ID as overwritePageId after reviewing data loss. Detailed format rules and Markdown semantics are in content, and menu rules in navigation.

Raw URI uploads use text/markdown or text/html and an 8 MiB body cap. Query parameters are strict; see CI upload semantics. Branch deletion deletes the matching draft root and all URI descendants, regardless of menu ancestry. Empty path is refused; explicit / deletes all saved draft pages. Neither action publishes. Treat branch deletion as a bulk content change; inspect its scope first.

Settings

GET /api/settings returns the current settings (defaults before first save). PUT /api/settings is a partial update: omitted fields remain unchanged. The editable string fields are:

Field Default Meaning
siteTitle My Viking Website Site-title bindings and default presentation
description empty Site description binding
faviconAssetId empty Uploaded raster-image/ICO asset ID for the site favicon; empty removes the favicon link
seoTitle empty Site-level search metadata title fallback
seoDescription empty Site-level search description fallback
themeId empty Selected theme; empty means original page styling
defaultLayout empty (effective default with theme) Layout for pages without an override

The returned object also has server-managed updatedAt. Theme/layout changes are validated against installed themes and page overrides. A page does not have its own SEO-description field in the current Page API; do not assume site SEO fields are per-page controls. The standard Settings modal edits site title, description and favicon; integrations can use the API for the other fields.

faviconAssetId must identify an existing normal uploaded image: PNG, JPEG, GIF, WebP, AVIF, BMP or ICO. A URL, missing asset, text file, SVG or raw download is rejected with 400. The asset cannot be deleted while selected (409); clear or replace the setting first. Partial updates preserve it when omitted. The response includes an empty string when cleared. The publisher writes <link rel="icon" href="/api/assets/<id>/content"> into every page head and copies the image bytes into the release at that path. Preview responses from POST /api/content/render and site-page theme previews include faviconUrl when selected; the editor embeds those image bytes in the preview head. Saving settings changes drafts; publish separately to update the public site.

Assets

Method and path Input Success
GET /api/assets none 200 asset array, newest first
POST /api/assets Multipart form, file part 201 asset metadata with id and url
POST /api/assets?raw=true Same multipart form 201 raw asset, always octet-stream attachment
GET /api/assets/{id}/content URL ID Bytes with validated type; documents are attachments
DELETE /api/assets/{id} URL ID 200 {ok:true,id}; 409 for protected in-use references

Asset metadata includes id, url, name, size, contentType, updatedAt and optional raw. Limit: 50 MiB of file content, plus up to 1 MiB request multipart overhead. No base64 JSON upload route exists. Normal upload extensions/content must pass the allowlist. Authenticated downloads use Cache-Control: private, max-age=300.

Publication

Method and path Input Success
GET /api/publish/status none Status plus CSRF token/cookie
POST /api/publish No body required {releaseId,publishedAt,previousRelease}
POST /api/publish/rollback {"releaseId":"000001"} Same activation result shape

Status fields: state (idle, publishing, error), currentRelease, lastPublishedAt, previousRelease, retainedReleases, csrfToken, and optional error. Retained-release IDs can expire during confirmation. Publishing is synchronous at the HTTP API: the request waits for its result. Only the browser UI saves the open page first; the API publishes already saved state. Rollback changes public files without restoring drafts. Use publisher for crash/error/retention semantics.

Themes, packages and WordPress

These larger contracts have their own references:

OAuth and admin routes

GET /oauth2/start and GET /oauth2/callback are public login routes. POST /oauth2/logout clears a browser session after CSRF validation and returns {logoutURL}; GET returns 405. The CMS serves the authenticated editor at /. The separate admin listener serves /healthz, /readyz and /metrics without authentication. Port numbers and health bodies are in observability. Every handler surface carries the Viking Server header.