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" } }
]
} 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.