Raw objects, repeated blocks and package reference
For browser tasks start with downloads and packages. This reference defines object fields, repeater markup and package authoring.
Viking can store downloadable files with descriptive content, repeat a block layout for matching content, and install portable page-type, renderer, block and theme packages. These packages form the installation contract for a future Viking Forge. This release supports local ZIP upload and export; it does not include a hosted forge, catalogue, remote download client, signatures or automatic updates.
Downloadable objects
Create a page and choose Raw downloadable object, then select the file. The object editor provides a Markdown description, a replacement-file upload and related-image uploads. Save the draft and publish as usual.
The object's URI (for example /downloads/linux) identifies its descriptive
page. The file itself stays unchanged at /api/assets/<id>/content. Raw uploads
accept any file type, up to 50 MiB, and are served as
application/octet-stream attachments with their original filename, including
on the public frontend. They never become executable website content.
Descriptions and related images render on the object's page and can supply
repeater fields. Replacing a file creates a new immutable asset; existing
releases retain their original bytes. Remove an object's references before
removing its file or related images from media storage.
The API uses the existing OAuth and CSRF rules. For automation, upload a file
with POST /api/assets?raw=true, multipart field file, then use the returned
asset ID in POST /api/pages:
{
"title": "Linux application",
"path": "/downloads/linux",
"contentVersion": 1,
"format": "raw",
"source": "The **Linux** build. Requires a 64-bit system.",
"object": {
"assetId": "returned-raw-asset-id",
"name": "application-linux.bin",
"images": ["uploaded-image-asset-id"]
}
}
images is optional and holds up to 100 image IDs from normal image uploads.
The main asset must have been uploaded with raw=true. Update the object with
PUT /api/pages/<page-id> and the same fields. URI paths use Viking's existing
page path rules; the original download filename can contain an extension.
For a named raster image, set object.publishImage: true, use an asset from a
normal POST /api/assets upload and leave source, layout and related images
empty. Both the object name and URL extension must match the validated image
type. Publication writes the original bytes directly at the page path, without
a trailing slash or HTML wrapper. Indexes, navigation and repeaters use that
named URL; repeater image and download bindings also use it. The object's
menu and disabled settings still apply. See the
named-image API example.
Repeat a group of blocks
Add Site → Repeat files to a visual page. Its children form the template: add, rearrange and style them in GrapesJS. Select the repeater to configure its URI path, content kind and whether to include all descendants. An empty URI uses the current page's path. The default includes direct children of all kinds. Menu visibility and navigation ancestry do not affect matching.
URI ancestors do not need stored pages. A homepage repeater with an empty URI
and all descendants enabled includes /poetry/lights-out without a /poetry
page. The editor stores the recursive setting as the strings true or false;
the renderer also accepts a present empty/bare recursive attribute as enabled
for compatibility with older GrapesJS saves. An absent attribute defaults to
direct children.
The image field prefers the featured image, falling back to a raw object's
first related image. featured-image and featured-image-alt select only the
cover and its alt text. Bind data-viking-alt="featured-image-alt" on an image,
or use <figure data-viking-featured-image=""></figure> inside the card template.
Missing image bindings produce no img element. Theme layouts use the
featured-image slot; see content metadata.
Preview and publication expand the template once per matching page or raw object. Matches sort by title ignoring case, then by URI. The current page and the selected branch root are excluded; an empty match set produces no cards. Authoring source retains the template. Nested repeaters are rejected, and each page's expansion is limited to 10,000 nodes. Repeated IDs become shared classes; ID selectors in the page stylesheet are rewritten so GrapesJS styling survives. Use the generated field traits to change a text, link or image binding.
The equivalent HTML is:
<section data-viking-repeat="/downloads"
data-viking-kind="raw"
data-viking-recursive="false"
data-viking-summary-unit="paragraphs"
data-viking-summary-count="2">
<article class="download-card">
<h2><a data-viking-href="url" data-viking-field="title">Title</a></h2>
<img data-viking-src="image" alt="">
<p data-viking-field="summary">Description</p>
<a data-viking-href="download">Download file</a>
</article>
</section>
data-viking-kind accepts all, pages or raw.
data-viking-recursive accepts true or false.
data-viking-summary-unit accepts paragraphs (default) or words.
data-viking-summary-count accepts a positive whole number (default 1).
Absent or empty summary attributes use the defaults, preserving older templates.
The limits apply to summary bindings in repeaters and all file gallery layouts.
Summaries take nonempty paragraphs in document order, excluding headers,
navigation and footers, and join their text with spaces. Word limits count
whitespace-separated words across those paragraphs without splitting a word.
Shorter content is returned in full; content without paragraphs has no summary.
| Field | Value |
|---|---|
title |
Title, falling back to URI |
url |
Descriptive page URL |
path |
Canonical URI path |
summary |
Leading paragraphs or words as plain text, according to the repeater's summary settings |
download |
Raw object's file URL; empty for other pages |
image |
Raw object's first related image URL; empty if absent |
name |
Raw object's filename; empty for other pages |
data-viking-field replaces an element's text. data-viking-href fills its
href, and data-viking-src fills its src. Values are escaped as text or
attributes and pass through the normal publication sanitizer. Missing URL
values remove the URL attribute. Index and repeater templates do not supply
summaries for other cards.
File gallery presentations
Gallery blocks retain the ordinary repeater attributes and card field bindings.
Set data-viking-gallery on the repeater's section or div to carousel,
coverflow, ring or masonry. Use one direct child element as the card template;
controls belong to the runtime and must not be included in the template.
The four separate example plugins provide
starting templates, styles and build scripts.
| Attribute | Behaviour |
|---|---|
data-viking-visible |
Carousel's maximum visible cards or masonry's maximum columns; 1–12, default 3 |
data-viking-interval |
Autoplay interval in milliseconds; 0 disables it, otherwise 1000–60000; default 4000 |
data-viking-card-width |
Maximum 3D card width, 160–640 pixels; default 280 |
data-viking-peek |
Cover flow's visible neighbours per side, 1–5; default 2 |
aria-label |
Accessible name identifying the gallery |
Publication expands the repeater before sanitizing the output. Only the four known presentation names and numeric settings survive as gallery attributes. The fixed CMS runtime then enhances the generated cards; it never executes code from a ZIP or template. Ordinary pages contain no gallery script. Gallery pages embed the runtime once regardless of the number of galleries. They are static, self-contained releases and require no CMS requests for navigation/rotation.
The 2D carousel moves existing card nodes between the list ends, cover flow recycles edge positions behind the current card, and the ring rotates through unbounded angles. All maintain circular previous/next selection without making duplicate card links. Off-screen/non-current 3D cards are inert for keyboard and assistive navigation. Visible carousel cards remain interactive.
Preview fetches the runtime from GET /api/content/gallery-runtime using the
parent's authenticated session and embeds those exact bytes in the opaque
preview. The SPA CSP contains its SHA-256 hash; it permits neither arbitrary
inline scripts nor same-origin access by that frame. Card templates and renderer
output still pass through the normal script/event/URL sanitizer.
Install and distribute packages
Open Plugins and packages, select a ZIP and wait for installation. Renderer packages become choices in the new-page format selector. Block packs add categories to the visual editor's block panel. Theme packages appear in Themes, where you can preview and select them for the site. Publish to update the public website. Package names and IDs do not depend on their ZIP filename.
The API supports:
| Operation | Endpoint |
|---|---|
| List installed page-type/renderer/block packages | GET /api/plugins |
| Install a plugin or existing theme ZIP | POST /api/plugins/install (raw ZIP body) |
| Export a page-type/renderer/block package | GET /api/plugins/<id>/bundle |
| Remove a page-type/renderer/block package | DELETE /api/plugins/<id> |
| List/export/remove themes | Existing /api/themes endpoints |
Installation refuses an existing ID with 409. Version 1 has no in-place
upgrade operation. A page-type or renderer package used by a page cannot be removed. Inserted blocks
are ordinary editable page content and remain after removing their package.
Publication snapshots retain installed renderer bytes, and installed packages
contribute to the draft revision hash. Packages are stored on private CMS
storage; executable modules are never copied into public releases.
Plugin ZIPs contain a root plugin.json and, for renderers only, a root
renderer.wasm. Unknown entries, traversal paths, symlinks, duplicate names,
unknown manifest fields and unsupported schemas are rejected. The compressed
ZIP, expanded contents and stored JSON representation each have a 32 MiB limit.
Themes retain their existing 6 MiB bundle limit. IDs start with a lowercase
letter and use lowercase letters, digits, _ or -, up to 64 characters.
Versions use major.minor.patch.
Block packages
A minimal plugin.json:
{
"schemaVersion": 1,
"id": "notice-blocks",
"name": "Notice blocks",
"version": "1.0.0",
"kind": "blocks",
"blocks": [
{
"id": "notice",
"label": "Notice",
"html": "<article class=\"notice\"><h2>Notice</h2><p>Edit me.</p></article>",
"css": ".notice{padding:1rem;border:1px solid #999}"
}
]
}
Block IDs must be unique within the package. Packages support 1–100 blocks,
with HTML templates and optional CSS. Scripts and browser plugin code are not
supported. CSS is sanitized at installation, HTML is sanitized before entering
GrapesJS, and published content uses the normal HTML/CSS sanitizer. Optional
package metadata is author, license and description.
Build the supplied Callouts example from repository root (Python 3):
examples/plugins/callouts/build.sh
Upload dist/examples/callouts-1.0.0.viking-plugin.zip through
Plugins and packages. Every example's build.sh accepts an optional output
directory; the example guide documents all compilers,
shared Docker verification and generic-registry output.
Renderer packages
Renderer packages use the WASI Preview 1 command ABI: _start reads source
bytes from standard input and writes an HTML fragment to standard output.
A nonzero exit, trap or timeout fails preview/publication. Diagnostics belong
on standard error. The CMS supplies no mounted filesystem, inherited environment, network
or process access. When an authenticated editor context is available, the only
environment variable is VIKING_EDITOR, a JSON object containing issuer,
subject and name. It identifies the current editor/publisher, not the page
creator. Background rendering may omit it. Never derive persistent authorship
from the identity of whoever happens to publish; creator fields retain the
identity from creation/conversion. Execution has a 3-second deadline, a 256 MiB linear memory
limit and an 8 MiB limit for each output stream. Output HTML is sanitized by
the same pipeline as built-in page formats.
The manifest has kind: "renderer" and no blocks. Bundle the compiler's
WASI command as renderer.wasm. Source pages store
format: "plugin:<package-id>" and their original source, with no HTML/CSS.
The editor accepts pasted source or a source-file upload. RST or RTF renderers
can implement this ABI; Viking does not bundle an RST or RTF converter.
The example renderers demonstrate different tasks and implementation languages:
| Package | Language | Input and result |
|---|---|---|
source-text |
Go | Escaped source text inside a preformatted block |
checklist-c |
C | - [ ] / - [x] lines become inert task checkboxes; invalid lines fail |
csv-table-rust |
Rust | CSV header/data rows with quoted fields become an escaped HTML table |
From repository root, with the compilers described in the build guide:
examples/plugins/source-text/build.sh
examples/plugins/checklist-c/build.sh
examples/plugins/csv-table-rust/build.sh
Use scripts/build-examples.sh instead to run the pinned-version toolchains in
Docker, verify actual CMS installation/rendering and export all plugin/theme ZIPs.
Install a renderer ZIP, choose its page format, paste/upload source, preview and
publish. A script packages only plugin.json and renderer.wasm; compiler
sources and test fixtures are not installed. Browser-targeted Wasm and native
Go shared libraries do not implement this WASI command ABI.
Page-type packages
A page type is independent of format: it adds metadata and declarative editing
behaviour to HTML/Markdown content or defines a metadata-only navigation object.
The Blog manifest
is a complete working example. Build it with examples/plugins/blog/build.sh.
A page-type ZIP contains only plugin.json, with kind: "page-type", a
pageType definition and optional ordinary blocks. It has no WASI module or
browser script. Blocks use pageType.blockCategory as their editor section and
are available in visual pages and theme templates.
The definition specifies version: 1, supported formats (html/markdown, or
only none for metadata-only types),
defaultFormat, defaultPath, convertLabel, blockCategory, fields and
bindings. Field IDs and binding names follow package-ID rules. At most 32
fields, 64 bindings and 100 blocks are supported. Duplicate/unknown declarations
and unsupported versions are rejected.
| Field type | Value and behaviour |
|---|---|
creator |
Read-only {issuer, subject, name} derived from the authenticated editor on creation/conversion; later saves preserve it and ignore caller-supplied authorship. |
datetime |
Editable RFC3339 timestamp with explicit timezone; defaults to current UTC when first added. |
text |
Editable text, at most 8192 bytes. |
url |
Editable absolute HTTP/HTTPS URL, at most 8192 bytes; no credentials, whitespace or control characters. Validation never fetches the URL. |
Each field declares id, label, type and optional required. Bindings declare
name, field and part: name for creators, date or time for timestamps,
and no part for text or URLs. Creators expose only their display name in rendered
bindings. The generic registry supplies form controls, conversion actions,
repeater filters and validation from the installed definitions; no Blog ID is
special-cased in CMS code.
Metadata-only types use formats: ["none"], defaultFormat: "none" and
navigationUrlField naming a required url field. Their objects use the existing
page API with format: "none", pageType: "plugin:<id>", typeVersion: 1 and
fields. They retain a unique internal path, title, disabled state and menu
settings. Source, HTML, CSS, page layout, featured images and raw assets are
rejected. The destination replaces the internal URL in navigation and is never
marked as the current page. No page, redirect, sitemap entry, index entry or
repeater card is generated. Direct object previews return empty HTML/CSS; preview
an ordinary page with a navigation theme to review the menu. The
External link package is the
complete working example.
Typed pages retain the existing source fields and add:
{
"pageType": "plugin:blog",
"typeVersion": 1,
"format": "markdown",
"source": "By [author] on [date] at [time].",
"path": "/journal/2026-11-03/12-13-14/my-post",
"pathTemplate": "/journal/[date]/[time]/my-post",
"fields": {
"author": {"issuer": "https://identity.example", "subject": "editor-id", "name": "Editor"},
"published-at": "2026-11-03T12:13:14Z"
}
}
Create/update requests can put the template directly in path. Responses retain
pathTemplate alongside a resolved canonical path, so page lists and menus
have usable URLs. If pathTemplate is supplied, it is authoritative; send an
empty string to switch to a literal URL. Only date/time bindings may appear as
URI tokens, and a tokenized URI requires a literal page-name suffix. Resolved
paths pass ordinary path/reserved-name validation and collision checks. Publication
re-evaluates templates from snapshot properties and rejects duplicate output URLs;
it never substitutes the publication wall clock. This does not schedule posts.
Use <span data-viking-type-field="author">[author]</span> for a field block.
Text tokens such as [author] also work in Markdown/HTML text outside code and
pre; substitutions cannot change attributes or introduce markup. Fields inside
repeaters use each entry's type values. Use data-viking-kind="plugin:blog" for
Blog-only selection. The same registry supplies every gallery's type choices.
Builtin pages still includes Blog posts and other non-raw pages. Repeater fields
such as title, url and summary retain their existing meanings.
Core Go modules can read editorcontext.FromContext(ctx) for non-secret current
editor attribution; auth populates it alongside auth.IdentityFrom(ctx). Core
editor modules can use editor.vikingContext.getIdentity(); /api/me remains the
authenticated browser identity endpoint. WASI renderers can read VIKING_EDITOR
as described above. Tokens, session IDs, email and roles are not exposed through
the shared extension context or stored creator fields.
Version 1 supports metadata decoration and generated controls. It rejects unknown content versions rather than guessing migrations; in-place package upgrades and content migrations remain separate work. Snapshot/export include the full installed definition and blocks, while public releases contain only the rendered output.
Theme images
Existing .viking-theme.zip bundles include templates, CSS, image/font bytes
and preview fixtures in one file. In Themes → Shared stylesheet and bundled
assets, upload an image to add it to the current template and bundle.
References use viking-asset:<filename> and resolve to content-addressed URLs
when published. Previews inline those bytes. PNG, JPEG, GIF, WebP and AVIF
images and WOFF/WOFF2 fonts are supported, up to 2 MiB per asset.
Themes must bundle their own media rather than reference a site's asset IDs.
