Content Management that can withstand the plundering hoards

Authoring content and rendering

Viking supports Markdown source alongside GrapesJS visual pages, raw downloadable objects and installed renderer formats. Use the page guide for editing steps, API reference for requests, and extensions for raw/plugin content fields. Authoring source remains authoritative; publication never becomes the input to the editor. WordPress importing converts WXR exports or a read-only MySQL source into the same Markdown documents.

Storage contract

Pages are atomic JSON documents in project/pages/<id>.json beneath the private root (/cms by default). The JSON envelope owns identity, title, URL path, update time and the content format. For Markdown, source contains ordinary, renderable Markdown:

{
  "id": "example",
  "title": "About us",
  "path": "/about",
  "contentVersion": 1,
  "format": "markdown",
  "source": "# About us\n\nOur **story**.\n",
  "updatedAt": "2026-10-27T12:00:00Z"
}

format: "html" retains the existing html and css fields for GrapesJS. Missing format/version identifies a legacy visual page. Saves normalize these to html and version 1. Markdown pages cannot contain nonempty html or css; HTML pages cannot contain nonempty source. Unsupported formats and versions are rejected. Empty Markdown source is valid.

The source string is stored without rendering or rewriting it. Metadata stays in the JSON envelope: YAML front matter is not interpreted. Downloading .md exports the source without the JSON envelope and includes the current menu directives. The browser textarea normalizes line endings to LF. Raw HTML, scripts, LaTeX math, shortcodes and template directives are not enabled as executable markup. The renderer supports CommonMark plus tables, strikethrough, automatic links and task lists through Goldmark. Task items (- [ ], - [x] and - [X]) render as disabled checkboxes in both preview and publication, including inside themes and nested lists. Change their state in Visual or Source mode; checking a box on the rendered page does not edit the draft.

Ordinary saves cannot change formats. POST /api/content/convert accepts content plus targetFormat (html or markdown) and returns converted content without writing state or rendering the site theme. Markdown uses the publication renderer and retains standalone index directives as inert blocks. HTML uses the existing HTML-to-Markdown converter with tables and strikethrough after sanitization; styles and unsupported layout details are discarded. Conversion does not promise a lossless round trip. The editor warns before converting to Markdown.

To save a conversion, PUT /api/pages/{id} includes convertFrom naming the stored format and the new content format. Only HTML/Markdown pairs are accepted; legacy empty format counts as HTML. A stale origin is rejected. The editor tracks the original saved format across unsaved conversions and resets it after saving. It checks that the selected page and draft remain unchanged during the request. ID, title/path, menu, layout and featured image are retained.

For automation, PUT /api/pages/content explicitly replaces content by URI and permits format changes while retaining the page identity. See CI content uploads for raw Markdown/HTML uploads, menu selection, binary image uploads, branch deletion and publishing.

Publication state

disabled: true on Page / PageContent excludes a page from the next release. Omitted/false preserves the historical enabled default. Page summaries expose the flag while ordinary full saves explicitly carry it. Byte-only URI uploads retain the saved flag. Source conversion preserves it.

Publication skips disabled page rendering and output validation, including their WASI execution, and omits their URLs from sitemap, navigation, indexes and repeaters. Navigation resolves all saved menu metadata before filtering, promoting enabled children of disabled parents to the top level. A requested disabled draft can still be previewed. Disabled pages remain part of the private source revision and snapshots. They do not restrict media URLs or alter retained older releases.

featuredImage: {"assetId":"<library-id>","alt":"description"} is optional on both Page and PageContent. Send null or omit it in an ordinary full-page save to clear it. IDs must resolve to a non-raw image asset; alt text is limited to 2000 bytes. Deleting media referenced by a saved featured-image selection returns 409; clear the page's selection first. URI content uploads preserve an existing selection because they carry only document bytes.

The body wrapper exposes change-property traits, never HTML attributes. Source pages use companion controls. Snapshot/revision and publication use the same metadata as preview. Theme featured-image slots and page data-viking-featured-image containers produce an escaped image or no markup. In a repeater, the block binds to the matched page, not the containing page.

API and UI

POST /api/pages accepts title, path and the content fields above, creating the source and metadata atomically. PUT /api/pages/{id} replaces the editable fields using the same shape. GET /api/pages/{id} returns the source document. Both writes support the existing overwritePageId collision handling.

For example, a Markdown importer can send:

{"title":"About us","path":"/about","contentVersion":1,"format":"markdown","source":"# About us\n\nOur story.\n"}

There is no conversion step or separate importer service for a raw Markdown file. The UI's New → Markdown, Upload .md, Save draft flow reads UTF-8 text into the same source field. Upload replaces only the selected draft after confirmation if it has content. The UI accepts files up to 1 MiB; the JSON API retains its 8 MiB request limit. Download exports the current source, including unsaved edits. Referenced images are not imported with the file; uploaded CMS media can be referenced using /api/assets/<id>/content.

POST /api/content/render accepts content fields and optional id, title, path and menu context for unsaved navigation preview, and returns {"html":"...","css":"..."} for an unsaved preview. It uses the publisher's renderer and sanitizer, requires authentication and a CSRF token for browser sessions, and does not write project state. Markdown publication and preview render from source; generated HTML/CSS is never persisted into a Markdown draft.

Markdown pages open in a visual editor with headings, bold/italic/strike, inline code, quotes, bullet/numbered/task lists, links, image uploads, tables, code blocks, dividers and undo/redo. Task checkboxes are interactive while editing; they remain disabled in rendered previews and publications. Source and Visual tabs share the same source field. Merely switching modes preserves the exact original source; actual visual edits normalize Markdown syntax. Editor history resets between pages to prevent undo from restoring a different page's content. Raw HTML, front matter, footnotes and custom directives stay in Source mode to preserve unsupported syntax; fenced/inline code remains editable visually. The publisher remains the authoritative preview renderer.

GrapesJS provides visual editing for HTML pages and theme templates. Its toolbar uses uniform icon buttons and labelled Blocks, Layers, Styles and Properties tabs. Undo/redo, duplicate, delete, parent selection and HTML/CSS editing are available. Reusable responsive sections include columns, cards, hero and image/text layouts alongside paragraphs, containers, lists, quotations and tables. Font Awesome is bundled locally.

Index blocks

Visual pages offer Site → Index, stored as an inert <ul class="vc-index" data-viking-index=""></ul> marker. Markdown uses a standalone {{index}} paragraph. Preview and publication replace the marker with direct URI children sorted by case-insensitive title, then URI. Menu visibility/parents do not affect membership. Each li contains a linked title and the first nonempty paragraph as plain text inside a p, omitted when none exists. Shared headers/footers/navigation and other Index blocks do not supply summaries. Generated links use canonical trailing-slash URLs, and all generated content passes through the ordinary sanitizer. Indexes resolve from the release snapshot before theme wrapping and never recursively expand or alter source.

Theme boundary and next steps

Replaceable themes, shared page layouts, visual template editing and portable bundles are implemented in Themes. Markdown stays independent of presentation. A selected site theme owns navigation, header/footer, typography and assets; page metadata can override its default layout. Theme source and assets join the publication snapshot and revision.

WordPress imports create drafts through both UI and API; see the import contract and limits. Other CMS importers remain future work. WordPress themes and plugin behaviours do not become Viking templates. Connection details stay outside page content and public releases.