I've got a number of XHTML files that represent slide decks for talks I give. The body of each XHTML file consists of a number of DIV elements: each DIV represents one slide. The head of the file contains a to a CSS file.
That's the data format — what do you want the tool to actually do with these decks? A few common directions:
A presenter/viewer app — load an XHTML deck, navigate slides, present fullscreen, maybe with speaker notes
A converter — turn these into PPTX, PDF, or reveal.js-style HTML
A deck manager/library — browse all your talks, search across slides, tag and organize them
An editor — visually add/reorder/edit slides without hand-editing XHTML
Which one (or something else)?
I would like an XHTML+javascript file that asks for a URL for such a file, and then goes into full-screen mode and displays that file, one slide at a time, allowing me to step through the slides with arrow keys, and page up and page down keys.
Created a file, read a file
Created a file, read a file
Stack: plain XHTML + vanilla JS, no build step, no dependencies. This kind of task doesn't need anything heavier — a single file is easier to carry around with your decks than a framework app would be.
How it works
You get a URL entry screen. Type/paste the URL of a deck's XHTML file and click Load (or hit Enter).
It fetch()es that file, parses it with DOMParser as XHTML, grabs the from its
and injects it into the viewer's own so your deck's CSS applies.
It sets a pointing at the deck's directory, so any relative image/asset paths inside your slides resolve correctly even though they're now living inside a different document.
Every direct-child
of the fetched file's becomes one slide; only one is displayed at a time via a CSS class toggle.
It requests fullscreen automatically after loading.
Arrow keys, Page Up/Down, and Space navigate; Home/End jump to first/last slide; Esc drops fullscreen and returns you to the URL screen.
How to run it
Because it uses fetch(), this needs to be served over HTTP, not opened as a file:// URL — browsers block or restrict local file fetches from file:// pages. Two easy options:
From the folder containing deck-viewer.xhtml (and ideally your decks too), run:
python3 -m http.server 8000
Then open http://localhost:8000/deck-viewer.xhtml.
If your decks live on a different origin (another server, a different port, S3, etc.), that server needs to send CORS headers (Access-Control-Allow-Origin) allowing your viewer's origin to fetch it. Simplest fix: serve the viewer from the same origin/folder as your decks.
What I'd extend first
Local file support — add an alongside the URL box so you can pick a deck straight from disk without a URL at all (works around CORS entirely for local use).
Speaker notes — if your slide
s have a notes convention (e.g. an element with class="notes"), render them in a second window or a small overlay only visible to you.
Overview grid — a "g" key that shows all slides as thumbnails for jumping around, handy for Q&A.
Remember last URL — store it (e.g. in localStorage, or as a query param) so reloading/relaunching doesn't require retyping.
Multiple decks / a small launcher — a JSON list of your talks with friendly names, so you pick from a list instead of pasting URLs each time.
Deck viewer
XHTML
I'm afraid that didn't work. I see the page, it asks for a URL, I typed in a valid URL, I see from the server logs that it was served, but the page replies "Could not load deck: Failed to fetch"
Thought for 14s
Thought for 14s
That Failed to fetch message combined with the server log showing the request was actually served is the signature of a CORS block: the browser made the request, the server responded, but the response didn't include an Access-Control-Allow-Origin header, so the browser refuses to hand the response body to your JavaScript. fetch() reports that as a generic "Failed to fetch" rather than a clear CORS error (Chrome/Firefox usually log the real reason in the DevTools console — worth a quick look).
This happens whenever the viewer page and the deck file are on different origins — and "different origin" includes different ports on the same host, so if you served the viewer on :8000 and the deck elsewhere (even :8001 on localhost), that already counts.
Two ways to fix it:
Serve both from the same origin — put deck-viewer.xhtml in the same directory tree you're serving your decks from, so both come from the same host+port.
Turn on CORS on the deck's server (send Access-Control-Allow-Origin: * or your viewer's origin).
Rather than rely on either, let me add a local file picker as a fallback that sidesteps CORS entirely — you pick the XHTML file straight off disk.
Edited 4 files, viewed a file, read a file · 1 note
Edited 4 files, viewed a file, read a file · 1 note
Updated. There's now a file picker under the URL box — click it, choose the deck's .xhtml file straight off disk, and it'll parse and present it without touching fetch() or CORS at all. That should get you unblocked immediately.
Two caveats with the local-file path, both from the browser sandboxing what a picked file is allowed to know about its own folder:
The deck's linked stylesheet and any relative image paths in slides won't resolve unless they happen to be reachable from wherever the viewer page itself is served. If your CSS/images live in the same folder as the deck, the cleanest fix is still the CORS/same-origin route.
If that matters to you, tell me your actual setup — where the decks live (same machine? a web server? which one?) — and I'll tell you the specific one-line fix (e.g. exact flag for python3 -m http.server, or a tiny Node static server with CORS enabled) rather than the generic options above.
Try the file picker on your deck and let me know if slides render correctly (ignoring styling for now) — that'll confirm the DIV-parsing and navigation logic is solid before we chase the CORS/styling piece.
Deck viewer
XHTML
OK, thanks, that explained that problem. I see one remaining problem: in the linked CSS file, there is a section called "@media projection" that also has to be included in the CSS used to display each slide. Can you fix that?
Thought for 48s
Thought for 48s
Ah — that makes sense. Modern browsers only ever match screen, print, speech, or all for the active media type; projection was a real CSS2 media type but nothing renders in it anymore, so browsers just silently skip that whole @media projection { ... } block. It's not a fetch/CORS problem this time — it's genuinely unreachable CSS.
The fix: fetch the CSS text ourselves, pull out just the rules inside @media projection { ... }, and re-inject them as always-active rules (since this viewer is effectively "projecting" your slides).
Edited 2 files, read a file · 1 note
Edited 2 files, read a file · 1 note
Fixed. Here's what changed:
After the normal stylesheet is set up, the viewer now separately fetch()es the same CSS file's text (only when loading by URL — not applicable for the local-file picker path, since a picked file's sibling CSS generally isn't independently fetchable anyway).
It parses that CSS through a throwaway