Spec

The deck format

The portable, versioned shape a presentation serializes to, so a second implementation can read, render and round-trip it without Deckyard's server or its storage.

A deck is data, not a rendering. The format is deliberately plain: slides are a flat array of { type, content }, and nothing in it depends on the machine it came from.

The envelope

Five top-level fields. Everything else about a deck lives inside slides.

{
  "format": "deckyard.deck",
  "version": 1,
  "title": "My deck",
  "theme": "default",
  "slides": [
    { "type": "eu.deckyard.slide.title", "content": { "title": "Hello", "background": "lime" } },
    { "type": "eu.deckyard.slide.quote", "content": { "quote": "A deck is data.", "attribution": "Deckyard" } }
  ]
}
Every example on this page is served live by any Deckyard instance.

The envelope is lenient. Unknown top-level keys are ignored by an importer, never rejected, so a newer producer can add a field that an older reader simply skips.

Each of the five fields, its type and what a reader should do with it, is written out in the documentation. Full reference

Slides, and the one spelling of a type id

A slide is a type and a content object whose shape that type defines. An absent or empty field means "unset": an importer fills the type's defaults and never blanks a required field. Portable slides carry no id, because ids are a storage concern and are regenerated on import.

slides[].type is the type's canonical id, and a type has exactly one: reverse-DNS for a declarant with a domain (eu.deckyard.slide.title), namespace/name for one without. The id names the definition the slide was written against and may pin a version (@2), so there is no separate manifest to cross-check. Two older spellings - the bare registry key title-slide and the qualified core/title-slide - are pre-convergence residue, not part of the format: Deckyard still accepts and normalizes them on import, but what it exports is canonical, and a second implementation owes them nothing.

{ "type": "eu.deckyard.slide.content", "content": { "title": "Why", "body": "..." } }

Content schemas

Each slide type's content shape is described by a JSON Schema generated from the same field registry that drives validation and builds the editor form. One declaration, four consumers, and no way for a schema to describe a shape the software does not accept.

They are versioned by their $id, which carries the version of the content shape (4) rather than the envelope version (1). https://deckyard.eu/schema/v4/deck.schema.json is the whole deck, https://deckyard.eu/schema/v4/slide-types/<type>.schema.json is one type, and https://deckyard.eu/schema/v4/index.json hands you the list. All three resolve: a $id is not formally required to be fetchable, and a format offered to other people as a standard should be anyway.

Additional properties are allowed. The schemas document the known shape of a slide; they do not reject history. Nothing under a published version path is ever withdrawn either, because a type that has been retired here is still named by somebody else’s deck.

Asset references

Images are referenced by string. A local upload is a server path, portable only while that server is reachable. An external https:// URL is already portable and is left untouched by every transform.

The package: a deck with its pixels

Where the JSON export still points at images on a server, the package carries its own. It renders and round-trips on a machine that has never seen the instance it came from, and it can enumerate exactly which assets it contains.

The layout is modelled on OCF, the container EPUB uses, for the same reason EPUB uses it: a ZIP whose first entry is an uncompressed media type is identifiable by magic number before anything unpacks it. Four entries, in this order.

mimetype
manifest.json
deck.json
assets/<sha256>.<ext>
mimetype
First entry, stored uncompressed, containing exactly application/vnd.deckyard.deck.
manifest.json
Package metadata and the complete asset inventory. The original filenames stay in it, so hash churn never leaks into the readable structure, and several sources against one hash means the same bytes were referenced from several places.
deck.json
The portable envelope, with every asset reference rewritten to point inside the archive.
assets/<sha256>.<ext>
The asset bytes, addressed by the SHA-256 of their own content. Identical bytes are stored once.
  • Self-contained

    Every local asset is embedded. The package renders offline, with no server in the picture.

  • Content-addressed and verifiable

    Each asset's bytes hash to its own reference. The reader re-hashes everything on the way in and rejects a mismatch, so a corrupted or tampered archive fails loudly.

  • Deduplicated

    Identical bytes are stored once, however many slides point at them.

  • Enumerable

    The manifest lists every asset a deck needs. You can answer "what is in here" without unpacking it.

Every manifest field, and what import does with each one, is written out in the documentation. Full reference

What is guaranteed, and what is lossy

For content-bearing slides, export to import to export is a fixpoint: after one normalization pass the portable projection is stable, and identical asset bytes hash to identical addresses. A test in the core repo proves this against a committed example deck on every run. Two edges are deliberately not lossless, and both are specified so that they degrade rather than crash - which is what makes the format safe to implement against.

Unknown slide type
Imports as a placeholder that names the type it could not resolve, says whether it was deliberately retired and what replaces it, and carries the original content across as text.
Missing local asset
Keeps its original reference and imports as a dangling one. Harmless, and visible rather than silent. Theme assets and external URLs are not embedded by the package either: the first is a known gap, the second is deliberate, since an https:// URL is already portable.

A deck is not inert

Most of the format is data a reader can render without executing anything. Two slide types are not: custom-html-slide carries HTML an author wrote, and embed-slide carries a URL that will be framed. A deck is therefore active content, and treating a file from someone else as safe because it validated is the wrong conclusion.

What follows is a duty on whoever reads the format, not a property of it: sanitise author HTML before it reaches a document, and isolate embedded URLs in a frame that cannot reach the page around it. The package layer does not change this. Content addressing proves the bytes are the bytes that were packed; it says nothing about whether they are safe to run.

Versioning

version is the envelope version. It moves only for a breaking change to the envelope shape, and it has not moved. Slide content is versioned independently, tied to a schema version with its own migration runner. The model is Jupyter's nbformat: a reader validates against the version it understands, and the lenient contract lets it tolerate keys from a newer one.