Skip to content

Embed SDK

The embed SDK places a published Deckyard presentation in an iframe on another website and lets that page drive it: go to a slide, follow along as the viewer navigates, read the current position. It is one dependency-free script served by your Deckyard instance.

This page describes core Deckyard 1.34.0. The source of truth is client/embed-sdk.js (the SDK), server/routes/static/embed.js (the embed route) and server/utils/embed-html/ (the player inside the iframe) in the Deckyard repository. For a task-oriented introduction, and for embedding a deck without a running instance, see Embedding.

<div id="deck"></div>
<script src="https://your-instance.com/client/embed-sdk.js"></script>
<script>
const embed = PresentationSystemEmbed.createDeckEmbed({
el: document.getElementById('deck'),
publishId: '3f9a1c2e',
options: {
baseUrl: 'https://your-instance.com',
},
});
embed.on('slidechange', ({ slideIndex, totalSlides }) => {
console.log(`Slide ${slideIndex + 1} of ${totalSlides}`);
});
</script>

The deck must be published. The Publish dialog in the editor generates this snippet with your instance’s URL, the deck’s publish id and its language filled in.

A published deck’s public URL is /p/<publishId>-<slug>. The publish id is the 8-character hexadecimal part before the first hyphen: in /p/3f9a1c2e-quarterly-review it is 3f9a1c2e. Republishing the deck keeps it.

The embed is public: whoever has the publish id can load the deck, and no login is involved.

PresentationSystemEmbed.createDeckEmbed({ el, publishId, options }) // → controller

The script defines one global, window.PresentationSystemEmbed, with one function.

ArgumentTypeRequiredDescription
elHTMLElementyesThe element the embed is appended to. Throws when it is not an element.
publishIdstringyesThe deck’s publish id. Throws when empty.
optionsobjectnoSee Options.

The function appends a wrapper <div class="ps-embed-wrap"> holding the iframe to el and returns a controller.

OptionTypeDefaultDescription
baseUrlstringThe host page’s originOrigin of the Deckyard instance. Set it whenever the page is not served by Deckyard itself.
slugstringnoneAppended to the embed URL as <publishId>-<slug>. Optional: a missing slug works, a wrong one redirects to the right one.
controlsbooleantrueShow the control bar above the slide: previous, next, slide counter, language buttons and fullscreen.
ui"default" or "min""default""min" hides the control bar entirely, whatever controls says.
startnumber00-based index of the first slide shown. Values past the end show the last slide.
loopbooleanfalseWrap from the last slide to the first and back.
allowFullscreenbooleantrueAllow fullscreen on the iframe and show the fullscreen button.
langstringThe deck’s own languageWhich language version to show, as a deck language code (nl, en-GB, de, …; en means en-GB). An unknown code falls back to the deck’s language.
langSwitchbooleanfalseShow a button per language version in the control bar. Only appears when the deck has more than one version.
allowedOriginsstring[][location.origin]Origins whose commands the embedded player accepts. See Security.
aspectRationumber16 / 9Aspect ratio of the wrapper. Invalid or non-positive values fall back to 16/9.
titlestring"Embedded presentation"The iframe’s title, read out by screen readers.
onReadyfunctionnoneCalled with the ready payload.
onSlideChangefunctionnoneCalled with the slidechange payload.
onErrorfunctionnoneCalled with the error payload.

controls, loop, allowFullscreen, ui, lang, langSwitch, start and allowedOrigins travel to the player as query parameters on the iframe URL; aspectRatio, title and the callbacks stay in the host page.

MemberDescription
next()Go to the next slide. At the last slide this does nothing, unless loop is on.
prev()Go to the previous slide. At the first slide this does nothing, unless loop is on.
goToSlide(index)Go to a 0-based slide index, clamped to the deck.
getState()Returns { slideIndex, slideId, totalSlides, publishId }.
on(event, handler)Subscribe to an event. Returns a function that unsubscribes.
off(event, handler)Unsubscribe a handler.
destroy()Remove the wrapper and iframe from the page and stop listening for messages.

Navigation methods send a message and return immediately; they do not wait for the slide to change. Listen for slidechange to know it happened.

getState() returns the last state the player reported, not a live query: it is correct once ready and the first state event have arrived, and slideIndex is 0 before that.

The controller also carries _iframe, _wrapper and _src (the iframe element, the wrapper element and the iframe URL). The underscore marks them as internals.

EventWhenPayload
readyThe player has loaded{ publishId, totalSlides }
stateRight after ready, with the starting position; and in answer to GET_STATE{ publishId, slideIndex, slideId, totalSlides }
slidechangeThe slide changed, through the controller, the control bar or the keyboard{ publishId, slideIndex, slideId, totalSlides }
errorThe player failed while handling a command from the host page{ message }

slideId is the slide’s stable id in the deck, so it survives reordering, unlike slideIndex. The first slide shown does not trigger slidechange; read the starting position from state.

error does not report a deck that fails to load. An unknown or unpublished publish id makes the iframe show a not-found page, and a render failure shows an error page inside the iframe; neither sends a message.

Every event is delivered three ways:

  • to handlers registered with on() (event names are case-insensitive),
  • to the matching onReady / onSlideChange / onError callback (there is none for state),
  • as a CustomEvent dispatched on the wrapper element, with the payload in event.detail. It does not bubble, so listen on embed._wrapper itself.

Errors thrown inside your handlers are swallowed and do not stop other handlers.

The iframe loads /embed/<publishId> or /embed/<publishId>-<slug> from the instance. You can also put that URL in a plain <iframe> without the SDK and pass the same options as query parameters: booleans as 1/0 or true/false, allowedOrigins as a comma-separated list. slideIndex is accepted as an alias of start.

Inside the iframe:

  • One slide is visible at a time, rendered on a 1600×900 stage that scales to the iframe.
  • The keyboard works once the iframe has focus: arrow keys, Space, Page Up and Page Down navigate, Home and End jump to the first and last slide, F toggles fullscreen.
  • A video on a slide loads when the slide becomes active (autoplaying if the slide is set to) and pauses when the viewer moves on.
  • Slides that only exist in a live session, and slides hidden from the published version, are left out, as on the published page.
  • The page is marked noindex,nofollow and may be framed by any site.

The SDK is a wrapper around a small postMessage protocol. You need it only to control a plain <iframe>, or to send the two commands the controller does not wrap: GET_STATE and SET_OPTIONS.

Every message in both directions has this shape:

{ source: 'presentation-system-embed', type: 'NEXT', payload: {} }

Messages with a different source are ignored.

typepayloadEffect
NEXTnoneNext slide
PREVnonePrevious slide
GOTO{ slideIndex }Go to a 0-based slide index
GET_STATEnonePlayer answers with STATE
SET_OPTIONSAny of controls, loop, allowFullscreen, ui, langSwitch, allowedOrigins, startChanges those options without reloading; start also navigates

SET_OPTIONS leaves an option unchanged when its value is not a boolean (controls, loop, allowFullscreen, langSwitch), not min or default (ui), or not an array (allowedOrigins). Two values are converted rather than checked: every allowedOrigins entry is turned into a string, and empty entries are dropped; any start that is not null goes through Number(), so "2" goes to index 2 and a value that is not a number, such as "bad", goes to index 0. SET_OPTIONS cannot change the language: that needs a new iframe URL.

embed._iframe.contentWindow.postMessage(
{
source: 'presentation-system-embed',
type: 'SET_OPTIONS',
payload: { ui: 'min', loop: true },
},
'https://your-instance.com',
);

READY, STATE, SLIDE_CHANGE and ERROR, with the payloads listed under Events. The SDK only accepts them from its own iframe and from the instance’s origin.

  • allowedOrigins guards the player, not your page. The player only obeys commands from a parent page whose origin is in the list. An empty list, or *, accepts commands from any page that frames the deck. The SDK defaults to the host page’s own origin, so list your other origins (staging, a second domain) explicitly.
  • Events go to any parent. The player posts its events with target origin *, so any page that frames a deck can follow its position. Events carry the publish id and slide positions, nothing else from the deck.
  • The publish id is the authorization. Anyone with it can view the published deck, embedded or not. To stop an embed, unpublish the deck.
  • The iframe gets referrerpolicy="strict-origin-when-cross-origin" and loading="lazy", but no sandbox attribute. It is kept out of the host page’s DOM only when the instance and the host page are on different origins, which is the normal case; an embed served from the same origin as the page can reach its parent.

The wrapper <div class="ps-embed-wrap"> fills the width of el, keeps its height at the aspect ratio (with a padding fallback for browsers without aspect-ratio), and has a black background, 12px rounded corners and hidden overflow. These are inline styles, so override them on embed._wrapper.style, or size the embed by giving el a width.

  • Embedding - embedding a deck, including the standalone HTML export for sites without an instance
  • Public API: Publishing - publish and unpublish from code