dev --remote engagement is same-network by design — venue Wi-Fi with AP isolation
will block phones from reaching the gateway on your laptop. Bring a travel router or
phone hotspot, or use a hosted gateway so
phones join over the internet.
::: Audience connections are scoped
server-side — the gateway only relays vote, qa/ask, and react from them, so
a modified page still can’t navigate your deck or draw on it.
Declare a poll in MDX; options live in your source, votes live in the room:
question = " How are you presenting today? "
options = { [ " Keynote/PowerPoint " , " A web framework " , " PDF and prayers " ] }
When the poll’s slide is present , it opens for the audience automatically; their
phones show the question and options.
Votes tally live as bars on the slide. One vote per device (anonymous stable
client id), revisable until you press Close voting on the slide.
Results persist across refreshes — like drawings, they save under
.astro-slides/engagement/ while the gateway runs.
Give each poll a stable id; votes are keyed by it.
The audience types questions on their phones; they appear in your presenter view
(P) in a moderation panel:
Show puts the question on the audience-facing deck window as a banner (at most
one at a time — showing another demotes the first).
Hide returns it to the queue; Dismiss removes it.
Six emoji on the audience page float up the deck when tapped. Rate-limited on the
phone and sprite-capped on the deck, so an enthusiastic room stays legible. Inert in
embeds, previews, and print.
Engagement state persists by design — which means a timed rehearsal with live polls
leaves votes and questions behind for the real session. Reset without touching the
gateway host:
All of it : the Reset button in the presenter view’s toolbar (click twice —
it arms, then wipes). Clears every poll and the whole Q&A queue; connected phones
and the deck zero out live, and the cleared state is persisted.
One poll : a Reset button appears on the slide next to Close voting once a
poll has votes (or was closed). The poll reopens fresh if its slide is showing.
Headless / pre-flight : the gateway accepts
POST /__astro-slides/engagement/reset with {"deck":"<name>"} (plus
?token=… when the gateway runs with a password) — curl it from a deploy script
or a pre-talk checklist.
Audience-role connections cannot trigger resets — like navigation, the reset actions
are outside the audience allowlist, server-side.
dev --remote only reaches phones on the same network . To run engagement against
a deck deployed as a static site (GitHub Pages, Netlify, …), host the gateway
anywhere Node runs and point the deck’s build at it:
# 1. On your server (any Node ≥22 host — a $5 VM, Fly.io, Railway…):
npx astro-slides gateway ./state \
--allow-origin " https://you.github.io " \
--password hunter2 # optional: protects presenter-grade access
# 2. Build the deck against it:
ASTRO_SLIDES_GATEWAY = wss://gateway.example.com astro-slides build .
The built deck opens a WebSocket to the gateway on load; the audience page lives on
the gateway itself at /audience?deck=<name> (the command prints the URL + QR).
--allow-origin is required for a deployed site: cross-origin browsers send an
Origin header and the gateway refuses any that aren’t allowlisted (WebSocket and
the engagement-persistence POST both check it; the POST also gets proper CORS
headers). Comma-separate multiple origins; * allows all.
Poll/Q&A state persists under the directory you pass (./state above), exactly like
the dev gateway’s .astro-slides/engagement/.
Use wss:// (behind TLS — put the gateway behind a reverse proxy or a platform
that terminates TLS); an https:// page cannot open a ws:// socket.
--password derives the same token scheme as dev --remote=<password>: presenter
URLs need it, the audience page can be left open.
examples/audience-engagement
is a runnable four-slide deck exercising all three features:
astro-slides dev examples/audience-engagement --remote
packages/client/components/Poll.astro — the <Poll> component
packages/client/src/engagement/ — tallies, reactions overlay, Q&A banner, persistence
packages/client/src/sync/types.ts — poll/question state + actions in SharedState
packages/core/src/server/audience-page.ts — the /audience mobile page
packages/core/src/server/engagement.ts — audience-role scoping + persistence
docs/built/19-audience-engagement.md — the phase write-up