Skip to content

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.

Terminal window
npx kadal-deck build # or: npm run build
Terminal window
pnpm exec kadal-deck build # or: npm run build
Terminal window
yarn kadal-deck build # or: npm run build
Terminal window
bunx kadal-deck build # or: npm run build

The 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:

Terminal window
npx kadal-deck serve # http://127.0.0.1:4173/
Terminal window
pnpm exec kadal-deck serve # http://127.0.0.1:4173/
Terminal window
yarn kadal-deck serve # http://127.0.0.1:4173/
Terminal window
bunx kadal-deck serve # http://127.0.0.1:4173/

A deck served from https://example.com/talks/tides/ must be built for that path:

Terminal window
npx kadal-deck build --base /talks/tides/
Terminal window
pnpm exec kadal-deck build --base /talks/tides/
Terminal window
yarn kadal-deck build --base /talks/tides/
Terminal window
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/.

Terminal window
npx kadal-deck build intro.mdx deep-dive.mdx
Terminal window
pnpm exec kadal-deck build intro.mdx deep-dive.mdx
Terminal window
yarn kadal-deck build intro.mdx deep-dive.mdx
Terminal window
bunx kadal-deck build intro.mdx deep-dive.mdx

Each 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/.

Terminal window
npx kadal-deck build --without-notes
Terminal window
pnpm exec kadal-deck build --without-notes
Terminal window
yarn kadal-deck build --without-notes
Terminal window
bunx kadal-deck build --without-notes

Speaker 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.

kadal-deck ci writes a host’s deploy files, fitted to your package manager and to where the deck sits in the repository:

Terminal window
npx kadal-deck ci github-pages
Terminal window
pnpm exec kadal-deck ci github-pages
Terminal window
yarn kadal-deck ci github-pages
Terminal window
bunx kadal-deck ci github-pages
wrote .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.

The built page’s <head> comes from the headmatter:

---
title: Why the tide is late
info: Twenty minutes on the Moon, the Sun and Chennai's coast.
author: Ana Silva
lang: en
favicon: /logo.svg
seoMeta:
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.

Two settings make a built deck independent of the network:

pwa: true
bundleRemoteAssets: true
  • pwa: true (or build --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 under kadal-deck dev. An object sets the app’s { name, shortName, themeColor, backgroundColor, display }.
  • bundleRemoteAssets: true (or build --bundle-remote-assets) downloads the remote pictures, video, Google and Bunny fonts and CSS url()s your slides name into the build, and points the deck at the copies. Downloads are cached in .kadal-deck/cache between 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).

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.

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):

Terminal window
npm i -D playwright
npx playwright install chromium
Terminal window
pnpm add -D playwright
pnpm dlx playwright install chromium
Terminal window
yarn add -D playwright
yarn dlx playwright install chromium
Terminal window
bun add -D playwright
bunx playwright install chromium

The 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 relay somewhere and set remote: { relay: "wss://…" }; the presenter view pairs your phone through it. Present from your phone shows how.

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>

Send feedback