Skip to content

Deck package

A .deck package is a self-contained, portable archive of a presentation and its assets. Where the JSON export (/export/json) carries only the deck and still points at server-hosted /uploads/… images, the package carries its own pixels — so it renders and round-trips on another machine without the server, and it can enumerate exactly which assets it needs.

The layout is OCF/EPUB-inspired.

mimetype First entry, STORED (uncompressed). Content:
"application/vnd.deckyard.deck". Lets the archive be
identified by magic number.
manifest.json Package metadata + the asset inventory (see below).
deck.json The portable deck (as from presentationToDeck), with
every asset ref rewritten to a bundle ref.
theme.json The deck's database theme, when it is on one (see
Theme). Absent for a file theme.
slide-types/<slug>.json
Each database slide type the deck uses (see Custom
slide types). Absent for core and file-JS types.
assets/<sha256>.<ext> The asset bytes, content-addressed by SHA-256 of the
content: slide images, theme logos and curated font
files. Identical bytes are stored once (dedup).

The media type is application/vnd.deckyard.deck. It is registered with IANA, in the vendor tree, assigned 2026-08-13 after Expert Review: https://www.iana.org/assignments/media-types/application/vnd.deckyard.deck. That records the name and points at the specification; it is not standards-track, and changing the registration goes through the same review. Packages written before 1.7.0 carry application/vnd.slidecreator.deck, which a reader keeps accepting for good; see Legacy sentinel. The file extension is unaffected either way.

{
"format": "deckyard.deck",
"bundleVersion": 3,
"mimetype": "application/vnd.deckyard.deck",
"deck": "deck.json",
"theme": { "ref": "theme.json", "hash": "9c1f…07ab" },
"slideTypes": [
{ "slug": "hero", "ref": "slide-types/hero.json", "hash": "41d0…9e2c" }
],
"assets": [
{
"ref": "assets/e2e9…445a.png",
"id": "sha256-4unkYiBMX+HF…",
"hash": "e2e9…445a",
"mime": "image/png",
"bytes": 1265204,
"sources": ["/uploads/photo-1a2b.png"]
},
{
"ref": "assets/77d0…c3e1.woff2",
"id": "sha256-d9DK…",
"hash": "77d0…c3e1",
"mime": "font/woff2",
"bytes": 48212,
"fontFaces": [
{ "family": "Inter", "weight": 400, "subset": "latin" },
{ "family": "Inter", "weight": 700, "subset": "latin" }
]
}
],
"missingAssets": ["/uploads/gone.png"],
"fontsNotIncluded": [
{ "family": "Brand Sans", "role": "body", "source": "adobe", "reason": "licensed" }
]
}
FieldTypeNotes
formatstringThe same sentinel the deck carries.
bundleVersionintegerPackage-layout version, 3 today. Independent of the envelope version.
mimetypestringMirrors the mimetype entry.
deckstringPath of the deck inside the archive. Always deck.json.
assets[].refstringWhere the bytes live in the archive; also the value used inside deck.json.
assets[].idstringAn SRI-shaped integrity id (sha256-<base64>), the stable, algorithm-tagged identity of the asset.
assets[].hashstringThe hex SHA-256 (the content address; matches the ref name).
assets[].mimestringThe media type of the bytes.
assets[].bytesintegerSize in bytes.
assets[].sourcesarrayOn a slide image or theme logo: the original /uploads/… name(s) that mapped to this asset. This is the separate name layer: human names stay in the manifest so hash churn never leaks into the readable structure. Multiple sources means the same bytes were referenced from several places.
themeobjectOptional. { ref, hash } of theme.json when the deck is on a database theme. The reader re-hashes it like an asset and rejects a mismatch.
slideTypesarrayOptional. One { slug, ref, hash } per database slide type the deck uses; ref is always slide-types/<slug>.json. The reader refuses another path, a hash mismatch, or a file that names another slug.
assets[].fontFacesarrayOn a font file instead of sources: every face (family, weight, subset) the file serves.
missingAssetsarrayOptional. Local refs whose bytes could not be read at export time; these keep their original ref in deck.json (or theme.json).
fontsNotIncludedarrayOptional. Theme fonts that travel by name only, each with its role (heading/body), source and a reason: licensed (an upload, Adobe or Monotype font, licensed to the sending organization) or not-vendored (the sending instance holds no file for it).

The portable deck (presentationToDeck output: format, version, title, theme and slides — see Deck format). Asset refs in slide content are rewritten from /uploads/x.png to the bundle ref assets/<hash>.<ext>. External (http(s)://) image URLs are left untouched — they are already portable and are not fetched into the package.

  • Self-contained: all local slide assets are embedded, and so are a database theme’s logos and curated fonts. What is not: external https:// images, a file theme, fonts that travel by name only (fontsNotIncluded) and assets referenced from inside CSS values. A package whose deck uses any of those still needs the network, or the same install, to render as authored.
  • Content-addressed + verifiable: each asset’s bytes hash to its ref/hash; the reader (readDeckBundle) re-hashes every asset and rejects a mismatch.
  • Deduplicated: identical bytes are stored once regardless of how many slides reference them.
  • Enumerable: the manifest is a complete inventory of the deck’s assets.

POST /api/presentations/import/deck takes a raw .deck body and creates a presentation from it — the mirror of the export. The flow:

  1. readDeckBundle(buffer) — verify the mimetype sentinel and re-hash every asset (integrity), yielding { manifest, deck, assets }.
  2. For each manifest asset, write its bytes back into /uploads/ via saveUploadedFile, using the manifest sources[0] as the human basename. This builds an assets/<hash>.<ext>/uploads/<uuid>.<ext> map.
  3. rewriteBundleRefs(deck, mapFn) — rewrite the deck’s bundle refs to the new upload URLs (the inverse of the export’s rewriteAssetRefs).
  4. deckToPresentationParts + createPresentation/updatePresentation — the same normalization + creation path as the JSON import.

Round-trip: for content-bearing slides, export → import → export is a fixpoint (identical content-addressed refs, since identical bytes hash the same).

Graceful degradation:

  • An asset whose mime is unsupported by saveUploadedFile (or that otherwise fails to write) is skipped — its ref is left in place and reported in a failedAssets field on the response, rather than crashing the import.
  • Unknown slide types become a content-slide placeholder that names the missing type, says whether it was deliberately removed and what replaces it, and carries the original content across as markdown. Import persists rather than renders, so it applies the same archived-slide contract as every render surface.
  • Local refs that were already missing at export time (missingAssets) keep their original /uploads/… ref and import as dangling (harmless) references.

A deck on a database theme carries that theme as theme.json: the fields a theme is created from, with its logos and curated (open-licence) font files going through the same asset walk as slide images. Licensed fonts travel by name only, listed in fontsNotIncluded. A file theme ships with an install and travels by the id in deck.json alone.

Each database slide type the deck uses travels as slide-types/<slug>.json, likewise the fields it is created from. Core and file-JS slide types are code: they travel by the type id on the slide, and an install that lacks them imports the placeholder.

In the core repository:

  • Build: server/export/deck-bundle.jsbuildDeckBundle(repoRoot, pres).
  • Read/validate: readDeckBundle(buffer){ mimetype, manifest, deck, assets }.
  • Import: server/routes/api/presentations/import-deck.jshandlePresentationsImportDeck (route POST /api/presentations/import/deck).
  • Pure ref layer: shared/slide-types/deck-assets.js (collectAssetRefs, rewriteAssetRefs, rewriteBundleRefs, assetRefForHash).
  • Export route: GET /api/presentations/:id/export/deck.zip (downloads <title>.deck).