Deck format
The deck format is Deckyard’s portable, versioned deck interchange format — the
durable envelope a presentation serializes to, so a second implementation can
read, render and round-trip it without Deckyard’s server or storage. It is what
GET /api/presentations/:id/export/json returns, and what the
deck package carries as its deck.json.
A deck is data, not a rendering. The format is intentionally readable and
stable: no server-internal UUIDs or timestamps are required, and slides are a
flat array of { type, content }.
The canonical example lives at tests/fixtures/example-deck.json in the core
repository and is exercised by tests/deck-format-spec.test.js, the CI gate
behind this format.
Envelope
Section titled “Envelope”{ "format": "deckyard.deck", "version": 1, "title": "My deck", "theme": "default", "slides": [ { "type": "eu.deckyard.slide.title", "content": { "title": "Hello", "background": "lime" } } ]}| Field | Type | Notes |
|---|---|---|
format | string | Always deckyard.deck. The magic sentinel that identifies the format. A conforming reader also accepts the historical slidecreator.deck (see Legacy sentinel). |
version | integer | Format version. 1 today. Bumped only on a breaking envelope change (see Versioning). |
title | string | Human title of the deck. |
theme | string | Theme id the deck was authored against (e.g. "default"). A reader that lacks the theme falls back to its own default; content is unaffected. |
slides | array | Ordered list of slides, each { type, content }. |
The envelope is lenient: unknown top-level keys are ignored by the importer, not rejected. This keeps forward-compatibility — a newer producer can add fields an older reader simply skips.
Type identity — one spelling on slides[].type
Section titled “Type identity — one spelling on slides[].type”Each slide names its type once, on slides[].type, in the canonical id:
reverse-DNS for a declarant with a domain (eu.deckyard.slide.title),
namespace/name for one without (acme/hero). Core types are published under
eu.deckyard.slide, with the -slide suffix dropped from the canonical name
because slide is already in the authority.
- One type has one id, and comparing ids is comparing strings — after
stripping an optional
@versionsuffix. There is no alias table and no separate manifest: the id itself names the definition (and optionally the version) a slide was written against. - Two older spellings — the bare registry key (
title-slide) and the qualified form (core/title-slide) — are pre-convergence residue, not part of the format. The importer still accepts them and normalizes on ingest, but export and the read APIs emit the canonical id. A second implementation owes the old spellings nothing and may treat them as unknown types. @version(eu.deckyard.slide.title@2) is a compatibility hint about a definition, not a different type. A reader that lacks the named version renders the version it has; it must not treat the id as unknown.
See custom slide types for how a namespace of your own enters the registry.
Slides
Section titled “Slides”Each slide is:
{ "type": "eu.deckyard.slide.content", "content": { "title": "Why", "body": "..." } }type— the slide type’s canonical id (see above).content— an object whose shape is defined by that slide type’s field registry. Absent or""fields mean “unset”; the importer fills type defaults and never blanks a required field.
Every type’s fields are listed in the slide-type reference.
Portable slides carry no id — ids are a storage concern and are
(re)generated on import. A reader must not depend on slide identity across a
round-trip.
Content schema (the single source)
Section titled “Content schema (the single source)”Each slide type’s content shape is described by a generated JSON Schema derived
from the same fields[] registry that drives validation and the editor — one
source, no hand-synced copy. The schemas are served live and are versioned by
$id:
- Per-type:
https://deckyard.eu/schema/v4/slide-types/<type>.schema.json - Whole deck (discriminated by
type):https://deckyard.eu/schema/v4/deck.schema.json - Reflected at runtime alongside
GET /api/v1/slide-types.
Schemas are lenient contracts, not gates: additionalProperties is allowed
so legacy and forward-compatible keys still validate. They document the known
shape; they do not reject history. (Note the generated deck schema describes the
stored deck, which additionally carries id/schemaVersion; the portable
envelope here is the interchange projection of that model.)
See JSON Schemas for how to fetch them.
Asset references
Section titled “Asset references”Images are referenced by string:
- Local uploads —
"/uploads/<name>-<uuid>.<ext>". Server-hosted; portable only while that server is reachable. - External URLs —
"https://…". Already portable; left untouched by every transform.
To make a deck self-contained (assets travel with it), use the
deck package: a ZIP that embeds each local
asset’s bytes content-addressed as assets/<hash>.<ext> and rewrites the deck’s
refs to those bundle refs. Import re-hydrates them back to /uploads/. Bundle
refs (assets/…) never appear in a portable (non-bundled) deck.
Round-trip guarantee
Section titled “Round-trip guarantee”For content-bearing slides, export → import → export is a fixpoint: after
one normalization pass (defaults filled, ids regenerated) the portable
projection is stable, and identical asset bytes hash to identical content
addresses. tests/deck-format-spec.test.js proves this on the example fixture;
tests/import-deck.test.js proves it end-to-end through the bundle importer.
Deliberate lossy edges (they degrade, they do not crash):
- An unknown slide type imports as a
content-slideplaceholder that names the type it could not resolve, says whether the type was deliberately retired and what replaces it, and carries the original content across as markdown. - A missing local asset keeps its
/uploads/…ref and imports as a dangling reference.
Active content and sanitisation
Section titled “Active content and sanitisation”A deck is not an inert document. Two slide types carry content that executes or loads in the reader’s context:
| Type | What it carries | What a reader owes it |
|---|---|---|
custom-html-slide | HTML written by the deck’s author | Sanitise before it reaches a document. |
embed-slide | a URL that will be framed | Isolate in a frame that cannot reach the surrounding page. |
Validating a deck says nothing about whether it is safe to render, and neither does the package layer: content addressing proves the bytes are the bytes that were packed, not that they are safe to run. Treat a deck from someone else the way you would treat any untrusted document.
This matches the security considerations in the application/vnd.deckyard.deck
media-type registration and the active-content section on the spec
page. The same claim is deliberately worded the same way in
all three places.
Versioning
Section titled “Versioning”versionis the envelope version, bumped only for a breaking change to the envelope shape itself. It is 1 today.- Slide content shape is versioned independently by the schema
$id(/v<N>/…), currently version 4, tied to the storageschemaVersionand its migration runner. A reader validates content against the schema version it understands; the lenient contract lets it tolerate newer keys.
The two numbers are different on purpose and have already drifted apart. Do not
read the $id version as the envelope version.
Legacy sentinel
Section titled “Legacy sentinel”Until 1.7.0 the format field was written as slidecreator.deck, and the
package media type as application/vnd.slidecreator.deck. That name predates the
product: it was invented in the commit that first added JSON export, before this
was called Deckyard.
- Producers write only the current sentinel. Nothing emits the old value any more, and nothing needs to.
- Readers accept both. Not because a body of files carrying the old value is known to exist — Deckyard is new enough that there may be none — but because a rename is a poor reason for a file not to open, and accepting one extra constant costs nothing.
- The file extension never changed. A package downloads as
<title>.deckeither way, so nothing on disk needs renaming.
If you have tooling of your own that matches on the format field, teach it both
values.
Producing and consuming a deck
Section titled “Producing and consuming a deck”| Method | Endpoint | What it does | Access |
|---|---|---|---|
GET | /api/presentations/:id/export/json | The portable envelope for one deck. | Authenticated |
GET | /api/presentations/:id/export/deck.zip | The self-contained package. | Authenticated |
POST | /api/presentations/import/json | The import side of the envelope. | Authenticated |
POST | /api/presentations/import/deck | The import side of the package. | Authenticated |
The schema routes are the open ones; see JSON Schemas.
In the core repository:
- Envelope build/parse:
shared/slide-types/deck.js(presentationToDeck,deckToPresentationParts). - Identity manifest:
collectSlideTypeManifest(shared/slide-types/registry.js). - Content schema generation:
shared/slide-types/json-schema.js. - Asset ref layer:
shared/slide-types/deck-assets.js. - Spec fixture + CI gate:
tests/fixtures/example-deck.json,tests/deck-format-spec.test.js.