Build and host anywhere
A built deck is a static site: HTML, JavaScript, CSS and your files, with no server code. Any host that serves files can serve it, and everything works there except the live room, which needs a server (see the last section). If you would rather not host anything, publish to deck.kadal.cc instead.
npx kadal-deck build # or: npm run buildpnpm exec kadal-deck build # or: npm run buildyarn kadal-deck build # or: npm run buildbunx kadal-deck build # or: npm run buildThe site lands in dist/. Every page of the deck (/presenter, /join, /remote, /notes,
/overview, /export, /stats) gets its own index.html, and the build writes 404.html and
_redirects too, so those addresses work on hosts that have no rewrite rules, such as GitHub Pages,
S3 or a plain nginx.
Check the build the way a host will serve it:
npx kadal-deck serve # http://127.0.0.1:4173/pnpm exec kadal-deck serve # http://127.0.0.1:4173/yarn kadal-deck serve # http://127.0.0.1:4173/bunx kadal-deck serve # http://127.0.0.1:4173/Under a sub-path
Section titled “Under a sub-path”A deck served from https://example.com/talks/tides/ must be built for that path:
npx kadal-deck build --base /talks/tides/pnpm exec kadal-deck build --base /talks/tides/yarn kadal-deck build --base /talks/tides/bunx kadal-deck build --base /talks/tides/Every route, the fonts, the workers and the narration follow the base. kadal-deck serve reads it
back from the build, so npx kadal-deck servepnpm exec kadal-deck serveyarn kadal-deck servebunx kadal-deck serve then shows the deck at
http://127.0.0.1:4173/talks/tides/.
Several decks in one site
Section titled “Several decks in one site”npx kadal-deck build intro.mdx deep-dive.mdxpnpm exec kadal-deck build intro.mdx deep-dive.mdxyarn kadal-deck build intro.mdx deep-dive.mdxbunx kadal-deck build intro.mdx deep-dive.mdxEach deck file builds into its own folder, dist/intro/ and dist/deep-dive/, with a page at
dist/index.html listing them. With --base /talks/, they are served at /talks/intro/ and
/talks/deep-dive/.
Leave the notes out
Section titled “Leave the notes out”npx kadal-deck build --without-notespnpm exec kadal-deck build --without-notesyarn kadal-deck build --without-notesbunx kadal-deck build --without-notesSpeaker notes are compiled into modules of their own and never load on a viewer’s screen unless you share them, but they are still files in the build. For a public build of a talk whose notes are private, leave them out altogether; the presenter view then has none.
Deploy
Section titled “Deploy”kadal-deck ci writes a host’s deploy files, fitted to your package manager and to where the deck
sits in the repository:
npx kadal-deck ci github-pagespnpm exec kadal-deck ci github-pagesyarn kadal-deck ci github-pagesbunx kadal-deck ci github-pageswrote .github/workflows/deploy-deck.yml
Next: GitHub: Settings → Pages → Source: GitHub Actions, then push to main.The workflow builds with --base /<repository>/ (or / for a <owner>.github.io repository) and
deploys on every push to main.
| Host | Command | What you get |
|---|---|---|
| GitHub Pages | kadal-deck ci github-pages |
a workflow that builds and deploys on push |
| Netlify | kadal-deck ci netlify |
netlify.toml: build command, dist/, rewrite, cache headers |
| Vercel | kadal-deck ci vercel |
vercel.json, the same for Vercel |
| Cloudflare Workers | kadal-deck ci cloudflare |
wrangler.jsonc for static assets; npx wrangler deploypnpm dlx wrangler deployyarn dlx wrangler deploybunx wrangler deploy after the build |
| A container | kadal-deck ci docker |
Dockerfile, nginx.conf, .dockerignore: nginx on port 8080 |
| Anything else | upload dist/ |
Name several at once (ci netlify,docker), or add them when you create the deck:
npx kadal-deck new my-talk --ci github-pagespnpm exec kadal-deck new my-talk --ci github-pagesyarn kadal-deck new my-talk --ci github-pagesbunx kadal-deck new my-talk --ci github-pages. If the deck’s build needs a browser (below), the
recipe installs Chromium in CI too.
kadal-deck serve also works as the server itself, for example in a container of your own:
npx kadal-deck serve dist --host 0.0.0.0 --port 8080pnpm exec kadal-deck serve dist --host 0.0.0.0 --port 8080yarn kadal-deck serve dist --host 0.0.0.0 --port 8080bunx kadal-deck serve dist --host 0.0.0.0 --port 8080.
Link previews and the page’s head
Section titled “Link previews and the page’s head”The built page’s <head> comes from the headmatter:
---title: Why the tide is lateinfo: Twenty minutes on the Moon, the Sun and Chennai's coast.author: Ana Silvalang: enfavicon: /logo.svgseoMeta: ogImage: auto ogUrl: https://example.com/talks/tides/ twitterCard: summary_large_image---title and info become the page title and description, and the Open Graph and Twitter tags.
ogImage: auto photographs slide 1 at 1200 × 630 into og-image.png during the build; a path or
URL uses that picture instead. Set ogUrl to the deck’s address so the image URL is absolute, which
link previews need. Other keys: ogTitle, ogDescription, twitterSite, canonical,
themeColor, and any other name becomes a <meta> tag of its own.
npx kadal-deck checkpnpm exec kadal-deck checkyarn kadal-deck checkbunx kadal-deck check warns about a missing favicon or og:image file, and an og:image that
won’t be a full URL.
Present offline
Section titled “Present offline”Two settings make a built deck independent of the network:
pwa: truebundleRemoteAssets: truepwa: true(orbuild --pwa) makes the deck an installable app that presents offline: a web manifest, icons in the theme’s colours (or from your favicon), and a service worker that saves the whole build. It says when the deck is ready offline, and when a new version is published it offers a Reload button instead of reloading in the middle of your talk. It never caches the live room or anything under/api, and it never runs underkadal-deck dev. An object sets the app’s{ name, shortName, themeColor, backgroundColor, display }.bundleRemoteAssets: true(orbuild --bundle-remote-assets) downloads the remote pictures, video, Google and Bunny fonts and CSSurl()s your slides name into the build, and points the deck at the copies. Downloads are cached in.kadal-deck/cachebetween builds.
A build with both, served from your laptop with kadal-deck serve, presents with Wi-Fi off. Icons
from an Iconify set you haven’t installed still need the network; install the set
(Icons).
A Download button
Section titled “A Download button”download: true in the headmatter shows a Download button beside the theme menu, and makes
kadal-deck build print the deck to dist/deck.pdf for it. Without a PDF beside the deck, the
button opens the browser exporter at /export instead. See
Export to PDF, PNG and PPTX.
Builds that need a browser
Section titled “Builds that need a browser”download: true, seoMeta.ogImage: auto and pwa: true drive a headless Chromium during the
build. Install it once in the deck (and in CI, which kadal-deck ci does for you):
npm i -D playwrightnpx playwright install chromiumpnpm add -D playwrightpnpm dlx playwright install chromiumyarn add -D playwrightyarn dlx playwright install chromiumbun add -D playwrightbunx playwright install chromiumThe live room and the remote on a static host
Section titled “The live room and the remote on a static host”A static host has no server, so a deck there has no live room: polls, questions, reactions and following show a placeholder. You have three choices:
- Publish to deck.kadal.cc, which gives the deck a room:
npx kadal-deck publishpnpm exec kadal-deck publishyarn kadal-deck publishbunx kadal-deck publish. - Run your own room server and point the deck at it with
live: { api: "https://rooms.example.com/api" }. See Self-hosting the room. - Only need the phone remote? Run
kadal-deck relaysomewhere and setremote: { relay: "wss://…" }; the presenter view pairs your phone through it. Present from your phone shows how.
Embed a deck in a page
Section titled “Embed a deck in a page”A built deck works in an <iframe>. Add ?embedded to its address (or set embedded: true) so it
leaves the page’s keyboard alone:
<iframe src="https://example.com/talks/tides/?embedded" style="width: 100%; aspect-ratio: 16 / 9; border: 0" allow="fullscreen"></iframe>