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.
Featured image metadata
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.
