What's new
Changelog
Every Deckyard release, in plain language. The commit-level detail lives on GitHub; here is what it means for the people using it.
-
v1.13.0 Latest
Deckyard stops asking a CDN for its sanitizer, the bundled fonts turn out to have been the wrong ones all along, and there is now an image source with no licence attached.
Two of the three things in this release are the same thing: an instance should not need anybody else’s server to work correctly. The sanitizer came off a CDN, and the fonts that were meant to be self-hosted turned out to be re-downloaded from Google on every install, without a pin, and with the wrong bytes.
This is a reason to update. If your instance blocks third-party requests, or sits behind a firewall that quietly failed to fetch
cdn.jsdelivr.net, the editor’s sanitizer was not loading at all. And separately: every deck has almost certainly been rendering in a fallback typeface rather than the one its theme names. Both are below.The sanitizer is served by your own instance
The app shell loaded DOMPurify from
cdn.jsdelivr.net, which made a third party a dependency of two things at once: the security posture, and whether the app booted at all. It is now vendored from the npm copy that the server-side sanitizer already used, pinned by the integrity hash inpackage-lock.json.- An air-gapped or CDN-blocked install gets a working sanitizer, where before it got whatever the browser did when the script failed to load.
- The browser and the server now run the same DOMPurify. The CDN tag was stuck on 3.2.2 while the server had moved to 3.4.12.
The fonts were the wrong bytes
This one is worth reading even though it is filed as a fix. Google serves each font family split into per-script subsets and expects the browser to pick between them by
unicode-range. The installer took whichever@font-facecame back first, which wascyrillic-ext. So every curated font on disk contained no Latin glyphs at all, and everything that asked for one, slides, exports, the theme picker and the app’s own interface, silently rendered in a fallback face instead. All four Inter weights were byte-identical to the same Cyrillic file.- Each weight now ships as
latinpluslatin-ext, with a realunicode-rangeon every generated@font-face. Your decks will start rendering in the typeface the theme actually names, which for most installs is a visible change. - The download is pinned.
scripts/google-fonts.lock.jsonrecords the exact URL, SHA-256 and byte size of every file, and the installer refuses anything that does not match. Until now the contents ofassets/fonts/google/were a function of Google’s release schedule rather than of the release you installed. - Exports lose about two thirds of their font weight. Most curated families are variable fonts where every weight resolves to the same file, but each weight was declared separately, and an export base64-inlines every rule. The default theme was shipping roughly 930 KB of fonts where about 253 KB is unique, in every standalone HTML file and every PDF or PNG render.
- Two built-in themes were exporting a fallback face where their CSS asked for a weight the theme did not list: Corporate was missing Archivo 500, Midnight was missing JetBrains Mono 500 and 700. All six themes are regenerated, and a test now fails if a list drifts again.
- Font family names are escaped properly on the way into CSS. A name ending in a backslash could escape the closing quote of the declaration it sat in, and family names reach that path from uploaded fonts and custom theme JSON, so this was reachable input rather than a lint. Five places were interpolating names and URLs into CSS strings, and three of them were doing no escaping at all.
An image source with no licence attached
There is a third image source in the picker: 30 abstract gradients rendered from the six built-in themes’ own palettes. No API key, no attribution, no rate limit, no external request.
The reason it exists is the licence question rather than the aesthetics. Stock images come from a third-party API with terms attached, which is awkward on a public or guest-facing instance where uploads are off and stock is the only source someone has.
- The toggle is
stockMedia.bundled.enabled, and it is off by default. There is an admin checkbox for it. Nothing changes on an instance that leaves it alone. - A picked gradient survives export. The files sit under
assets/gradients/, which is the prefix the exporter inlines, so it works in PDF and PNG exports, published pages and embeds with no special handling.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.13.0 release on GitHub.
-
v1.12.1
Two deprecated slide types leave the registry, a page without video stops loading a video player, and links stay readable on dark theme variants.
A small release on the same day as 1.12.0, with one clean-up worth reading about: the two slide types that had been deprecated for a while are now actually gone. Nothing breaks, and there is nothing to run.
Two slide types are retired
content-columns-slideandcard-stack-slideare out of the built-in set. The built-in slide-type count goes from 36 to 34.- Stored decks do not break. A slide of either type renders as an archived placeholder with all of its content still visible, so nothing is lost and nothing crashes.
- Card stacks have a successor. The placeholder names
icon-card-gridas the rebuild target; it takes the sameitems[]shape, andscripts/migrate-slides.jswill do the conversion for you if you want it done in bulk. - Content columns has no successor. Rebuild those slides with whichever type fits; there is no automatic conversion, because there is no type that means the same thing.
A page without video makes no third-party requests
Bunny’s
player.jswas loaded on every page of the live app, whether or not a deck contained a video. It is lazy-loaded now, and only when a video slide is actually present. For a deck without video that is zero third-party requests, which is both a privacy improvement and a faster first load.Links stay readable on theme variants
A theme variant with its own slide background inherited the base link colour, which could fall away against that background. The link colour is now derived per variant, so an inverted or dark variant gets a readable link without a manual override.
Upgrade notes
- Removing deprecated slide types in a 1.x release is the intended shape. Deckyard stays on 1.x for the whole beta, so a retirement like this one is not held back for a major version.
- Nothing to run. The migration script is optional and only exists for people who would rather convert their card stacks than leave them as placeholders.
For the complete, commit-level list, see the 1.12.1 release on GitHub.
-
v1.12.0
The slide editor was rebuilt on one generic engine, so every type edits the same way, and `?lang=` becomes the only way a URL names a language.
Almost every hand-built slide form in the editor is gone, replaced by one editor that reads each type’s own declaration. You will notice it as consistency rather than as a new feature: adding, removing and reordering items works identically on every slide type now, because it is literally the same code.
If you share links carrying
?locale=, they need to become?lang=. The old alias is removed in this release; see the URL section below.The editor got rebuilt
The per-type forms were the oldest part of the editor and the least consistent: seven different collection forms, each with its own idea of what an add button looks like.
- One generic collection editor replaces all seven. Lists, cards, matrix and the rest now add, remove and reorder the same way.
- Charts and tables become a closed per-field vocabulary. Each field is a declared control instead of a bespoke form, which is why they now behave like the rest of the editor.
- The image forms became declarations too, sharing one element card.
- New items get defaults in the deck’s language. Per-item defaults resolve per deck language, so adding a row to a Dutch deck no longer seeds it with English placeholder text.
- The slide list reads each type’s
labelField, so the label in the left-hand list is the field that actually identifies the slide.
A URL that names a language is obeyed
?lang=is now the only UI-locale URL parameter. The?locale=alias is removed. Nothing inside the app used it, but a link you sent someone might: those links need updating, or they open in the recipient’s own saved language rather than the one you meant.?lang=outranks the saved interface language for that session, so a link that names a language opens in it, even for someone who normally works in another.
Fixed
- Quote slides stop jumping while you type. The editor canvas reflowed when the text field took focus, so the text moved under the cursor as you clicked into it. A delegated control on the same slide also stopped rendering as if it were broken.
For self-hosters
- The retention cleanups are actually scheduled now. All three ran only if something called them, and nothing did. They are wired into a daily job, honouring
ACTIVITY_RETENTION_DAYS(default 180). If you have been running an instance for a while, expect activity data older than the retention window to disappear on the first nightly run after upgrading, which is what the setting was always supposed to do.
The built-in slide-type count is unchanged at 36.
For the complete, commit-level list, see the 1.12.0 release on GitHub.
-
v1.11.0
A slide type now has exactly one spelling on the wire, the write API refuses types it does not know, and PDF export stops producing files nobody wanted to send.
This release settles something the format had been vague about: what a slide’s
typeis actually called. Until now the same type could reach you under two spellings depending on which door it came out of, and a write could put any string at all into that field. Both are fixed here. Around that, organizations became usable for the people in them, and PDF export got a round of repairs.If anything you run reads
slides[].typeoff an export or an API response, read the first section before updating. Stored decks migrate themselves, so an instance needs nothing; an integration that pattern-matches on the old spelling does.One spelling for a slide’s type
Export and every read API now write the canonical reverse-DNS id,
eu.deckyard.slide.title, and nothing else. Before, the bare registry key could also appear on the wire, so a consumer had to know both spellings and guess which one it was looking at.- Anything reading the bare key has to move. There is one spelling now, and it is the reverse-DNS one.
- The
slideTypesmanifest is gone from the deck envelope. It described the types used in the deck, was read by nothing, and duplicated a fact already on every slide.
The write API validates the slide type
An unknown
slides[].typeis a400on every write path. On Postgres installs any string used to pass through silently, which meant a typo in an integration produced a deck full of slides that could never render, and nothing said so until somebody opened it.If you write decks over the API, an unknown type now fails at the point of writing rather than at the point of reading.
Stored decks migrate to schema v4 by themselves
Decks already on disk carrying an old type spelling are folded to the canonical form on the next read or write. There is no script to run and no downtime; an administrator does not have to do anything for this one.
Organizations: inviting people, and a profile screen
1.10.0 gave multi-organization installs their first screens and left two obvious holes. Both are filled here.
- Invite people into an organization from the interface, with the invitations that actually went out reported back to you.
- Members can reach the member list, not just admins, so an ordinary member can see who else is in the organization.
- An organization profile screen, including delete for the owner.
- Administering the organization you are currently in behaves correctly, which it did not always do when you belonged to several.
PDF export gets a round of repairs
Five separate ways a PDF could come out wrong, all fixed:
- Images are capped at their display size instead of their source resolution, which is most of the reason exported PDFs were as large as they were.
- Gradient slide backgrounds are rasterized, and so are gradient pseudo-element layers, so a gradient no longer disappears or bands in the PDF.
- Blurred shadows stop reaching the PDF as luminosity masks, which is what turned a soft shadow into a grey rectangle.
- The slide font is anchored to the theme, so the app’s own interface font stops leaking into the exported file.
- Local
url()assets are inlined, and an image that cannot be fetched is blanked rather than left as a broken box.
Dutch field labels for eight slide types
Comparison, timeline, countdown, custom HTML, embed, lead capture, matrix and process slides had English field labels inside a Dutch interface. They are translated now, so a Dutch editing session no longer switches language halfway down the form.
For implementors
The spec work from this cycle is visible in the reference documentation: the published JSON Schema is open and the
structurefacet is normative, type ids are reverse-DNS with-slidedropped, and the catalogue now carries three tiers plus afallbackfacet that says what a reader does with a type it has never seen.The built-in slide-type count is unchanged at 36.
Upgrade notes
- A breaking change in a minor version is the intended shape. Deckyard stays on 1.x for the whole beta; 2.0.0 is reserved for leaving beta, not for a cleanup that happens to break something. The single-spelling change and the stricter write validation are exactly that kind of cleanup.
- Nothing to run. Stored decks migrate on access; the work is in your integrations, if you have any.
For the complete, commit-level list, see the 1.11.0 release on GitHub.
-
v1.10.0
Organizations get a real interface, an exported deck loses about half its weight, and one slide type is retired - with the rename handled for you on upgrade.
The headline of this release is not on a slide: an install that runs more than one organization finally has an interface for it. Around that, exported decks got dramatically lighter, and a slide type that was only ever a second name for another one is gone.
If you run on the file store, the upgrade has one manual step. Stored decks are renamed automatically by database migration
056, but a file-store install runsnode scripts/migrate-lijstje-slide.jsonce. Details below.Organizations get an interface
Multi-organization installs have had the data model for a while and no way to see it. This release adds the screens.
- An organization switcher in the user menu, so you can tell which organization you are working in and move between them.
- Admin screens now follow your role in the active organization, not the instance-wide admin flag. Someone who is an admin in organization A and an ordinary member in B no longer sees admin screens while working in B.
- The Users tab becomes the member list of the active organization in a multi-org install, and it is now a place where you do things: change a member’s role, remove someone, leave yourself, transfer ownership. The list is paged, so a large organization is workable.
- Several permission checks around organization ownership and settings were tightened along the way.
Still missing, so do not plan around it: inviting people from the interface, and the organization profile screen. Multi-organization remains in development.
One slide type fewer, and the rename is done for you
lijstje-slideis retired. It was a second name forlist-slideand never a second type, which meant the same slide could be stored under either name depending on how it was created.- Stored decks are renamed automatically when you upgrade, by database migration
056. - File-store installs run
node scripts/migrate-lijstje-slide.jsonce, after upgrading. - Anything that slips through both renders as an archived slide pointing at
list-slide, not as a broken slide.
split-partner-title-slidealso leaves the registry in this release. It has been archived for several versions, so it was already unavailable when building a deck; slides that still carry it degrade to archived slides. Between the two, the built-in slide-type count goes from 38 to 36.An exported deck stops carrying the editor
A standalone HTML export shipped the editor’s entire stylesheet: modals, inspectors, the slide-type picker, settings, none of which an exported deck can use. The export now ships viewer CSS only. That takes roughly 92% off the stylesheet inside the file, which on a real deck is about half its total weight.
Nothing about the export changes visually, and
?ui=minstill collapses the chrome the same way.A green and brass default theme
There is a new built-in theme, Forest: forest green with brass, warm paper and sage surfaces, and a chart ramp that alternates hue and lightness so two adjacent categories differ by more than color. It is now the default theme.
Existing decks are unaffected. A deck stores its theme id, so only new decks and decks explicitly set to
defaultmove. The violetdeckyardtheme keeps its id and its name; it is one style among six now rather than the one everybody inherits.Smaller changes
- Video slides: the watch link shown in exports is yours to set. A video cannot play inside a PDF, so the export shows a link instead; you can now say where it points.
- KPI tiles take their colors from the theme through tokens, instead of carrying their own.
- A video slide’s poster image now renders in the PDF and PNG placeholders.
- AI suggestions in comments are recognized by the effective author, so a suggestion made on someone’s behalf is labeled correctly.
For self-hosters
- Maintenance mode for deploys. Putting the instance into maintenance during a deploy means a save that lands mid-deploy fails visibly instead of silently, which is the difference between a retry and lost work.
- A partial write no longer erases deck columns. Writing part of a deck could blank fields it did not touch.
- The published JSON Schema no longer carries legacy fields, so a second implementation validating against it is not told to support fields Deckyard has dropped.
- The editor got quieter. Deck thumbnails are rendered when you save rather than when you look, and the slide list no longer rebuilds itself every time a lock or comment event arrives.
For the complete, commit-level list, see the 1.10.0 release on GitHub.
-
v1.9.0
Deck thumbnails stop turning back into placeholders after every edit, a table can carry one colour across its whole top row, and AI refine accepts the new text-blocks shape.
The second release of the day, after 1.8.0, and the one worth updating for is the fix at the top: the deck overview stops flickering back to grey every time you open a deck.
Thumbnails stop reloading after every edit
The card for a deck on the home screen shows a picture of its first slide. That picture was cached against the deck’s revision number, which changes on every save - so editing slide nine, or just opening a deck and closing it again, threw the picture away. And a thrown-away picture costs about ten seconds of shimmering placeholder before the card fills in again.
The cache now keys on what the picture is actually made of: slide 1 and the resolved theme. An edit anywhere else in the deck no longer invalidates anything. While a new picture is being rendered the previous one is served, so a card is never blank just because slide 1 is one edit stale.
One colour across a table’s top row
The top-left corner cell of a table has always taken the colour of the label column, so a
softtable shows a faintly tinted corner set slightly apart from its header. That is a legitimate look, and now it is a choice: a new Top-left cell setting picks whether the corner belongs to the label column (as before) or to the header row, so the whole top row reads as one band.It composes with every table style rather than being a fourth style, so your existing table keeps whatever style it has. The difference is visible on
soft; onplainandpanelit changes nothing, because their header and first column already share a colour.AI refine accepts four-row text blocks
Since text blocks became an array (
rows[]), the schema behind AI refine still demanded the older numbered fields, so it rejected every array-shaped text-blocks slide - including freshly created ones, and any slide using 1.8.0’s fourth row. Refine now validates the array shape, with the numbered fields optional and passed through untouched.For self-hosters
server/data/deck-thumbs/gets cleaned up. It grew by one file per edit to a first slide and nothing ever pruned it. Older rasters for a deck are now removed once the fresh one lands.- A 500 in the log when two people opened the same slide is gone. Acquiring a slide lock is now atomic; the editor always handled the collision quietly, so this was never visible in the interface, only in the server log.
For the complete, commit-level list, see the 1.9.0 release on GitHub.
-
v1.8.0
A list slide keeps the text size you chose and fills the slide, text blocks take a fourth row, and quote slides stop typing themselves out.
Three changes to how slides look while you are reading them, none of which asks anything of you. Existing decks are not migrated and nothing breaks; two of the three simply make slides you already have look the way you meant them to.
A list slide keeps the size you picked
Choosing “Large” on a list of seven short bullets did nothing: the slide rendered at the default size, in two columns, with the bottom half empty. Two separate causes, both now fixed.
- An explicit text size wins. Past six items the size was thrown away outright, on the assumption that the list was still one column. At seven items it is two, where roughly twice as much text fits. Size and column count are now resolved together against a measured capacity table. The one case that still steps down - a list long and wordy enough to spill across two columns - says so, in the inspector under “Text size”, instead of silently changing nothing you can see.
- The rows spend the leftover height. They used to stack against the top of the list box and leave the rest of the slide blank. They now grow into it, so the dividers spread evenly across the slide rather than crowding into its top half.
Measured across 1512 combinations of item count, title length, text length, subheading, columns and text size: nothing overflows, 628 of them are fuller, and none is emptier.
Decks imported from markdown benefit too.
lijstje-slide, which the importer still emits, was a full copy of the list-slide definition that had drifted to a much older layout; it is now a real alias, so the same content renders the same either way.Text blocks take a fourth row
The cap on a text-blocks slide goes from three rows to four. Nothing changes for a slide with three or fewer - those render byte-for-byte as before - and the layout tightens a notch at four rows so the extra one fits without riding over the title.
The fourth row lives only in the canonical
rows[]shape, which is what the editor writes. The older numbered fields (row1,row2,row3) stay frozen at three on purpose, so hand-written or imported content keeps its meaning.Quote slides stop typing themselves out
A quote slide typed its quote out character by character, and it did so regardless of the deck’s “Builds” setting: turning builds off never reached this one effect. It also jittered, because the quote block centres the whole group - quote, byline and portraits - so the block shifted while the text grew.
The effect is removed rather than adjusted; it can come back properly later. The per-bullet typewriter is a different effect and still works: the “Typewriter” reveal style in the deck settings is untouched.
For the complete, commit-level list, see the 1.8.0 release on GitHub.
-
v1.7.1
One fix: the steps on a horizontal process slide line up along their top edge again.
A single fix, released the same day as 1.7.0. Nothing else changed, and there is nothing to do beyond updating. The reason to be on 1.7.x at all is in 1.7.0, where PPTX export stopped being silently wrong after a PDF import.
Process slides stop looking crooked
On a horizontal process slide, every step was centred vertically against the tallest one, so a step with a shorter description sank as a whole: number, title and all. Descriptions are rarely the same length, so the row usually looked misaligned by a few pixels in a way that was hard to name and easy to see.
The steps now share a top edge, while the group as a whole stays centred in the slide, so a deck that already looked right does not suddenly shift upward. Checked across multiple rows as well, at six steps.
Existing decks simply look better after the update.
For the complete, commit-level list, see the 1.7.1 release on GitHub.
-
v1.7.0
The field list an agent is handed now comes from the code that defines it, PPTX export stops breaking after a PDF import, and the format sentinel takes the product name.
Three changes: one that repairs what an AI writes into a slide, one that repairs a file it turned out you could not trust, and one rename that asks nothing of you.
If you use PPTX export and file import on the same instance, this is a reason to update now. On 1.6.0 and earlier, exporting a PowerPoint file after somebody had imported a PDF produced a deck with rubbish where the slides should be, and it did so without an error. Details below.
The catalogue agents read is generated, not maintained
A slide type declares its fields once, in the registry. The list handed to a model over MCP was a second copy of that, kept by hand, and it had drifted in five places. Two of them were visibly broken:
video-slidewas offered a field that does not exist. Agents were told to fillvideoUrl; the type readssource. So every AI-written video slide fell back, silently, to the demo video.payoff-slidewas offered ataglinethat the type never reads at all.
That catalogue is now derived from the registry, so this class of mistake cannot be introduced again. The same change also tells agents about fields an author could always fill in by hand but the catalogue had never mentioned.
PPTX export was silently wrong after a PDF import
Both exports render through headless Chrome, and after a PDF import had run in the same server process, Chrome started handing back a different kind of bytes. The PPTX builder took them at face value, so the file it wrote had nonsense where the slides should be, with no error anywhere in the chain.
Restarting the server cleared it, which is exactly what lets a bug like this survive: it looks like a fluke, and the next export works. It is fixed, and there is now a smoke test that starts a real browser, so a silent regression of this shape fails in CI rather than in somebody’s deck.
The format sentinel says
deckyard.deckThe
formatfield inside a portable deck, and the media type of a.deckpackage, used to sayslidecreator- a placeholder from December 2025, written before this product had its name. They now saydeckyard.deckandapplication/vnd.deckyard.deck.Nothing to do, and nothing breaks. The old values are accepted on read for good, so a deck exported by an older version still opens. The file extension is unchanged: a package still downloads as
<title>.deck. The one thing worth a look is tooling of your own that matches on theformatfield, which should accept both values.The format spec has been updated to match, and names both.
For the complete, commit-level list, see the 1.7.0 release on GitHub.
-
v1.6.0
A published deck stops requesting things it does not use, and an embedded one can now drop the presenter chrome around it.
Two changes, both about the page you hand to somebody else. A published deck used to load syntax highlighting, math typesetting and a video player whether or not it contained code, math or video; now it loads them only when it does. And a deck in an iframe can be asked for a minimal interface, so the frame is the slide and nothing else.
Fifteen requests to third parties, down to none
Measured on a real nine-slide deck with no code blocks, no math and no video: fifteen requests to third parties before, none now. Fourteen of them went to jsDelivr for Prism (the core, ten hard-coded language packs and a theme stylesheet) plus KaTeX, and one to mediadelivery.net for the Bunny player.
Three things are worth being exact about:
- This is every published page, not just the HTML download.
/p/, the embed route and the exported file are built by the same code, so an operator sharing a link gets the same benefit as one sending a file. - The requests became conditional, not removed. A deck with a code block still loads Prism from jsDelivr, now the core plus the languages that deck actually uses instead of ten fixed ones. Math still loads KaTeX; a Bunny video still loads its player. A deck with a code block and a formula makes five requests.
- Nothing leaked and nothing was repaired. Requests were being made that were not needed. That is a different thing from a vulnerability, and it should not be filed as one.
Why it is the headline rather than a performance footnote: Deckyard is self-hosted software that people run to keep their data in their own hands, and jsDelivr runs on Fastly in the United States. A deck that phoned out to two American CDNs to display nine slides of plain text was an awkward thing to hand a public institution.
?ui=minfor embedded decksAdd
?ui=minto any published or exported deck URL and the title bar and control row disappear, with their layout rows collapsing to zero height. The scaled 1600x900 stage is then the whole frame, which means a host page can size an iframe with a plainaspect-ratio: 16 / 9instead of compensating for a fixed chrome height.- The 3px progress bar stays. It is absolutely positioned, so it costs no layout height and the frame remains exactly 16:9.
- The visible slide counter goes, because it was the part that wrapped on a narrow frame and the host page can show its own. Screen readers still hear “Slide 3 of 9” and the slide title.
- Arrows, space, Home, End and
Ffor fullscreen all keep working. - Without the parameter nothing changes, so existing links are untouched.
The name and meaning match the
uioption the embed API already had, so the two runtimes keep one vocabulary.Along the way, the reference documentation for the standalone export gained a section on URL parameters, which it did not have at all:
loop,autoplayandintervalexisted but were written down nowhere.For the complete, commit-level list, see the 1.6.0 release on GitHub.
- This is every published page, not just the HTML download.
-
v1.5.1
Dependency maintenance, with one thing to check first: the practical Node floor moved to 22.13.
A maintenance release: seven dependency updates and the one code change one of them required. Nothing new, and nothing behaves differently. There is one thing worth doing before you update, and it is not in the release itself.
Check your Node version first. Deckyard has asked for Node 22 for a while and still does, but two dependencies in this release raised their own floor:
jsdom(25 to 29) now wants 22.13 or newer, andpuppeteer-core(24 to 25) wants 22.12. On Node 22.0 through 22.12 you were fine on 1.5.0 and are now under both. npm does not enforce that by default, so it installs with a warning rather than an error, and the parts that would notice are HTML export and sanitisation. That is the kind of thing you find out three weeks later, which is why it is here.What else is in it
- zod moved to 4, and the AI schema validators moved with it: zod 4 dropped
ZodError.errorsand the five validators read that field. Both sit in this release, so nothing about AI generation changes between 1.5.0 and 1.5.1. - Chrome 150 under the export, via
puppeteer-core25. Verified by hand on a real render, because no test starts a browser; the output is unchanged. - jsdom 25 to 29, four majors under the HTML sanitizer. Twenty-one XSS and mXSS vectors through the four sanitise functions produce byte-identical output on both versions. Nothing was repaired here; it was checked that nothing changed.
- The RSS, Atom and JSON feeds are unchanged after
feed5 to 6, plus a group of eight minor and patch updates and two CI actions that self-hosters never see.
For the complete, commit-level list, see the 1.5.1 release on GitHub.
- zod moved to 4, and the AI schema validators moved with it: zod 4 dropped
-
v1.5.0
Deckyard now picks the text color on a colored surface by measuring contrast instead of guessing from brightness, and the theme editor shows you the number while you choose.
Two commits, and one of them changes how decks you already have look. Deckyard puts light or dark text on a colored surface for you; that choice used to run on brightness and now runs on a measured contrast ratio, which flips the text color on nine of the twenty palette colors across the five built-in themes. Alongside it, the theme editor gained a contrast readout, so the number you were previously supposed to check elsewhere now sits next to the picker that produces it.
If you run Deckyard, read the upgrade note before you field the first “did something break?” message. Text that was white yesterday can be dark today, and that is the fix rather than a regression.
The automatic text color is now measured
Cards, accent blocks and background variants get their text color from Deckyard rather than from you. The old rule split on brightness: darker than the middle, use light text. That picks the color that looks logical, not the color that reads. Both candidates are now run through the contrast formula and the higher ratio wins.
Measured across all five built-in themes:
- Nine of the twenty palette colors get a different text color.
- Every flip lifts a failing pair to WCAG AA or better. No pair gets worse.
- Pairs under AA: nine, down to one.
- The sharpest case is
midnight’s icon block, which went from 2.05:1 - white on light blue, effectively invisible - to 8.16:1.
The one pair still under AA is
playful’s accent#ea580cat 4.40:1, where the accent color itself is the limit rather than the choice of text color.Because contrast is not symmetric around the middle of the range, the crossover sits near a relative luminance of 0.21, so mid-light backgrounds get dark text. That asymmetry is exactly why the old rule left so many pairs under AA.
The theme editor shows contrast while you choose
Every color pair in the editor now carries its contrast ratio and a WCAG 2.2 verdict: fail, AA or AAA, judged against the threshold for the text size in that role. Buttons and headings are measured as large text, body copy against the stricter one. An APCA Lc value sits beside it as a second reading.
- It reports, it does not block. A theme with a failing pair still saves. A self-hoster can have a brand reason to ship a low-contrast variant, and a settings panel is not the place to overrule that.
- The ratio is always shown, passing or not, so contrast is a property you can steer toward rather than an alarm that appears once you are already past the line.
- WCAG is the verdict; APCA stands next to it. WCAG 2.2 is what EN 301 549 and the European Accessibility Act reference, so it is the claim anyone can lean on. APCA, candidate work for WCAG 3, models perceived contrast better - particularly light text on dark, which is what
midnightis built on - but it never decides.
The readout covers the four main colors and the background variants. The brand palette carries no verdicts yet, deliberately: those tokens are about to be redefined, and badges on them would be obsolete on arrival.
Upgrade notes
- Existing decks will look different, and that is the point. Nothing to configure and nothing to migrate: the text color is derived at render time, so opening a deck is enough. Expect the change to be most visible on
midnightand anywhere an icon block or accent card sat on a mid-light color. If someone reports it as a bug, the pair they are looking at almost certainly measured under 4.5:1 before. - A custom theme benefits without being edited. The derivation is shared, so your own colors go through the same measurement. Open the theme editor to see where each pair now lands.
For the complete, commit-level list, see the 1.5.0 release on GitHub.
-
v1.4.0
A slide type can now carry the rules your organization set for filling it, and every registered type is either visible to an agent or withheld on purpose.
What an AI agent knew about a slide type used to come from a hand-written catalog inside Deckyard: editorial copy that says which type to pick. This release opens that up. Every registered type is now visible to agents, including the ones your own instance defines, and each type can carry the rules your organization set for filling it: which sources to draw on, which cut-off date applies, what has to be named. It is the first feature where a self-hoster fills something in, rather than something working better behind the scenes.
Two versions were tagged the same day. 1.4.1 followed half an hour later with a single fix to the streaming API, described under Upgrade notes. Install 1.4.1.
Your house rules travel with the type
- Every registered type reaches an agent, or is withheld on purpose. The
get_slide_typestool used to walk the hand-written catalog, so a type without an entry was invisible to agents and indistinguishable from one held back deliberately. It now reads the runtime registry, and types defined in your own builder UI arrive there too. Of the 38 registered types, 31 are offered and 7 carry an explicit opt-out; none are undocumented, and a test fails the build if that ever changes. - A type can carry its own usage rules. Alongside the copy that says what a type is for, a type can now hold the house rules for filling it in - the source a chart must come from, the reporting date that counts, the caveat that has to be stated. Those rules ship in every
get_slide_typesresponse, after the schema, so an agent reads the shape first and your rule second. - Four places to write them, depending on where the type lives. On a core type’s catalog entry; as an override in a fork, without patching the open-source file; on a file-defined type of your own; or in the builder UI for a type defined in the database. Rules are capped at 1000 characters per type, because they multiply by every visible type in every response.
A removed type keeps its content
The
freeform-slidetype is no longer registered. It had been deprecated for a while and was offered nowhere, so most instances have no such slides; if a stored deck does, nothing is lost. The content stays in the deck JSON, and the slide now renders a placeholder that names the type, says it was deliberately removed and points at the successor, instead of a bare “Unknown slide type” box.That placeholder is a general promise now, not a patch for one type. On canvas - editor, presenter, embed, PNG, PDF - it is bounded and says how many fields it withheld, because a 16:9 frame cannot grow. In the reader and reflow views it shows everything, since that is the recovery surface. Importing a deck with an unresolved type stores the original content as markdown rather than dropping it, which was the one path that actually destroyed anything. And the editor no longer crashes on such a deck: opening one used to produce a white error page instead of a placeholder.
A shorter inspector rail
The “INSPECTOR” title bar is gone; the cross that collapses the rail stays where it was. The background section is split by how often you touch it: colour is a plain visible field, while the image with its crop, focal point and overlay sits in a collapsed block that shows a thumbnail once a background is set. On a title slide the rail went from roughly 1310 to 376 pixels, which means Accessibility now fits on one screen without scrolling.
Fixes
#slide=<n>works again in exported HTML. An exported standalone deck ignored the deep link in the URL and always opened on slide 1, so sharing an export with a link to a particular slide did nothing. The pattern was emitted inside a template literal, where\dcollapses to a literald; it never matched, and had been broken for as long as the export runtime has existed.GET /api/v1/image-libraryanswered 500 on every call. A documented endpoint that had never worked: the route imported a function the storage layer does not export. It is wired up now, with the search, category and pagination parameters actually implemented and the OpenAPI description corrected to match the fields the library really has.- Writing tags answered 400.
PUT /api/presentations/:id/tagsandPOST /api/tagsread the request body one level too deep, so writing tags through the API was dead on arrival.
Upgrade notes
- SSE error events changed shape. This is the whole of 1.4.1. Server-sent events on the import, conversion and AI generation streams now carry their error text in
messageinstead oferror, and no longer include anokfield. Inside the app you will notice nothing; if you have a fork or an integration listening on those streams, this is one field name to rename. - A breaking change in a minor version is the intended shape. Deckyard stays on 1.x for the whole beta, so 2.0.0 is reserved for leaving beta rather than for a cleanup that happens to break something. The
freeform-slideremoval above is that change.
For the complete, commit-level list, see the 1.4.0 and 1.4.1 releases on GitHub.
- Every registered type reaches an agent, or is withheld on purpose. The
-
v1.3.0
Organizations become usable on a single instance, alignment becomes a property of the block, and a cross-user XSS in the slide list is closed.
The organizations track lands, and with it the answer to a question that had been half-built for months: how one Deckyard instance serves several organizations without either of them seeing the other. There is also a security fix that is a reason to update on its own.
Update if you run a multi-user instance. This release closes a DOM XSS in the slide list, described at the bottom of this page. A slide title is written by a person, so in a deck shared with colleagues the payload can come from someone else’s slide, and it fires when you drag one in the slide list. There is no configuration to change: updating is the fix.
Organizations on one instance
Three changes that only make sense together:
- Who you are no longer depends on where you are. Every authentication lookup used to filter on the organization stamped onto your account, while membership allows you to belong to several. Anyone whose home organization was not the one the request ran in resolved to nobody and got a 401. That is why
MULTI_WORKSPACE_ENABLEDwas never wired end to end: switching it on would have logged people out. Identity is now resolved on its own, and membership is a separate question. - A request runs in the organization your session resolved to. Switching workspace used to change the session cookie and nothing else, so reads, writes and lists all stayed in the default organization. The organization now travels with the request, and only ever one you are a verified member of: membership is re-checked per request, because a token outlives a revocation by up to two weeks.
- Authorization knows about organizations too. The permission layer granted workspace-wide reads and writes without mentioning organizations at all, and relied on the storage layer to scope the query. That held in practice, but a layer that only holds because the layer beneath it remembers to scope is not a layer. The four workspace grants now check that both sides are in the same organization; ownership, authorship and collaborator access are untouched.
Single-organization installations, which is most of them, behave exactly as before and pay nothing for this: the organization is answered from configuration without a database lookup, and no query is added anywhere.
Alignment belongs to the block
Aligning a title slide used to mean setting the title, subtitle and meta line each to “center”, which still left them centred on axes far apart, because each box sits somewhere different on the slide. Alignment is now a property of the visual block rather than of every field in it, set once through the layout switcher you already use. Fields that no longer own their own alignment say so instead of showing a control that does nothing.
Fixes
- Nothing outlives its view any more. A lifecycle audit of the client found six leaks of the same shape: something registered on mount that survived the unmount with no handle left to stop it. Pending reconnects kept reopening streams nothing could close, and cleanups registered after teardown vanished into a map nobody drained. The notification bell and both Q&A streams now reconnect with backoff instead of retrying every 1.2 seconds per viewer.
- A new deck starts in the language of your interface. Opening the app in English gave an English interface with the language toggle in “New presentation” still on the workspace default. A language you picked yourself still wins over both.
- A stalled thumbnail always resolves. A card in the deck grid whose thumbnail request never completed shimmered forever. Every card now reaches a real end state, and upgrades to the real image if it arrives later.
- Card stack follows the theme. The last-resort palette was a hardcoded purple belonging to one theme, so a custom theme rendered off-brand cards. It is now derived from the theme’s accent colour, with a neutral ramp as the final fallback.
- Optimistic-lock conflicts speak the standard error shape. Three presentation write routes returned a hand-rolled body when a save collided with someone else’s. They now emit the canonical envelope, with the stable
conflictandlockedcodes and the revision details preserved. Status codes are unchanged and the app is unaffected; if you built against the API directly, theerrorfield now carries a machine code and the human sentence moved tomessage.
Security
A cross-user XSS in the slide list. The drag image built when you pick up a slide interpolated the slide’s title straight into HTML. A slide titled with a fragment of markup would run it the moment someone dragged that slide, in the browser of whoever was dragging. Because slide titles are written by people and decks are shared, on a multi-user instance the code and the victim need not be the same person. The drag ghost now builds its label as text, and two neighbouring sinks in the JSON debug and import panels were rebuilt the same way.
For the complete, commit-level list, see the 1.3.0 release on GitHub.
- Who you are no longer depends on where you are. Every authentication lookup used to filter on the organization stamped onto your account, while membership allows you to belong to several. Anyone whose home organization was not the one the request ran in resolved to nobody and got a 401. That is why
-
v1.2.0
A small release: interface copy that reaches every locale, a translated editor toast, and a round of hardening for publicly reachable instances.
A quiet release between two bigger ones. Nothing changes in how you build a deck; the work sits in the interface language and in the parts of the stack that face the open internet.
Every locale gets the interface, not just English
Deckyard ships in 12 languages, but a label written straight into the code has no key to translate, so it rendered English everywhere without any test noticing. An audit found and translated the strings still on the main path: the slide-library toast, the theme card specimen, the data-source modal’s provider hints and field labels, the autoplay toggle, the focal-point control and the theme logo alt text. A new check in CI now fails the build on new hardcoded copy, so the list can only shrink.
The JSON debug modal’s “Invalid JSON” toast, the last raw string in an otherwise translated modal, now goes through the translation layer as well.
Hardening for instances open to the public
This round targets the anonymous sandbox, and it matters for any install where strangers can create decks:
- Rate limiting behind a reverse proxy. If Deckyard sits behind a proxy without being told so, the limiter keys every request on the proxy’s own address, so all visitors share one bucket and a single abusive guest trips the limit for everyone.
TRUST_PROXYandTRUSTED_PROXY_COUNTmake it key on the real client address instead. - A per-guest quota. Deck count and total bytes are now capped per guest at every point where a deck is minted (create, import, duplicate), with a smaller request-body cap bounding each one. A guest at the cap gets a clear 429 rather than filling the shared volume.
- One more SSRF hole closed. Remote images already went through the guard on export; a CSS
background-image: url()smuggled in through an imported custom-HTML slide did not. It is now inlined or blanked before the HTML reaches the renderer.
For the complete, commit-level list, see the 1.2.0 release on GitHub.
- Rate limiting behind a reverse proxy. If Deckyard sits behind a proxy without being told so, the limiter keys every request on the proxy’s own address, so all visitors share one bucket and a single abusive guest trips the limit for everyone.
-
v1.1.0
The first big release since going open source: real-time collaboration, a reworked editor, self-contained exports, and a hardened self-hosting story.
Deckyard 1.1 is the first substantial release since the code went public. It deepens the parts that make Deckyard a platform rather than a slide editor: people working in the same deck at the same time, exports you can hand to anyone, and a self-hosting path that stands up to real scrutiny.
Real-time collaboration
- Live presence: see who else is in a deck, with gliding name labels and focus chips on the slides they’re editing.
- Conflict-free co-editing built on CRDTs, so two people can type in the same deck without stepping on each other.
- Comments with @mentions and autocomplete, per-user read status, and GitHub-style subscriptions that follow you into the threads you join.
A sharper editor
- Selection-aware inspector that switches between element and slide settings as you work.
- Inline markdown editing straight on the canvas, with a floating toolbar.
- Draggable focal points on cropped images, fully keyboard-navigable.
- Reorder cards inline with overlay grip handles, and open image settings from a chip on the image itself.
- A spreadsheet-style data grid for chart slides, with a CSV toggle for pasting numbers in bulk.
Exports you own
- A unified export modal with color-coded formats, so the whole menu reads at a glance.
- Self-contained
.deckbundles that export and re-import with everything intact. - Standalone HTML with fonts embedded, so a deck renders offline with nothing to phone home for.
- A semantic, reflowable HTML reader export, plus smaller PDFs thanks to image compression before embedding.
Themes and branding
- Richer theme configuration stored on the theme itself, including background presets and title-slide layouts.
- Theme surface tokens now flow through the slide design system, so brand colors land consistently across slide types.
Self-hosting and security
- Single-IdP SSO over OIDC for self-hosted instances.
- A one-command installer with interactive
.envsetup, plus a non-interactive flow for agents and CI. - Security hardening across the board: rate limiting and brute-force throttling on auth, SSRF and CSRF protection, per-resource authorization for live sessions, multi-workspace isolation, and security headers.
Accessibility
- An HTML semantics pass across the render layer: landmarks, native lists, and RTL support.
- A document heading outline for visual export and embed, and an accessibility status panel with collapsible rules.
For the complete, commit-level list, see the 1.1.0 release on GitHub.
-
v1.0.0
Deckyard goes open source. The full platform - create, present, publish, collaborate - lands on GitHub under the MIT license.
The first public release. Deckyard was already a working platform in daily use; 1.0 is the moment its source became yours to read, run and change.
What shipped
- The full end-to-end platform: 35+ slide types, live presenting, one-click web publishing, and collaboration.
- Self-hosting from a single Docker Compose file, Node and Postgres.
- AI on your own terms: bring your own key, tune the prompts, or drive it over MCP.
- Export to PDF, PowerPoint, self-contained HTML, PNG and JSON, so nothing is locked in.
- MIT licensed, GDPR-native, built to run on infrastructure you control.