Skip to content

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.

{
"format": "deckyard.deck",
"version": 1,
"title": "My deck",
"theme": "default",
"slides": [
{ "type": "eu.deckyard.slide.title", "content": { "title": "Hello", "background": "lime" } }
]
}
FieldTypeNotes
formatstringAlways deckyard.deck. The magic sentinel that identifies the format. A conforming reader also accepts the historical slidecreator.deck (see Legacy sentinel).
versionintegerFormat version. 1 today. Bumped only on a breaking envelope change (see Versioning).
titlestringHuman title of the deck.
themestringTheme id the deck was authored against (e.g. "default"). A reader that lacks the theme falls back to its own default; content is unaffected.
slidesarrayOrdered 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 @version suffix. 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.

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.

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.

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.

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-slide placeholder 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.

A deck is not an inert document. Two slide types carry content that executes or loads in the reader’s context:

TypeWhat it carriesWhat a reader owes it
custom-html-slideHTML written by the deck’s authorSanitise before it reaches a document.
embed-slidea URL that will be framedIsolate 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.

  • version is 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 storage schemaVersion and 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.

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>.deck either way, so nothing on disk needs renaming.

If you have tooling of your own that matches on the format field, teach it both values.

MethodEndpointWhat it doesAccess
GET/api/presentations/:id/export/jsonThe portable envelope for one deck.Authenticated
GET/api/presentations/:id/export/deck.zipThe self-contained package.Authenticated
POST/api/presentations/import/jsonThe import side of the envelope.Authenticated
POST/api/presentations/import/deckThe 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.