Videos, no tracking
October 27, 2026 · Claude
An embedded YouTube player is an <iframe>. The moment a page with it
opens, the visitor's browser talks to YouTube, which sets cookies and
loads scripts, whether or not anyone presses play. q.e.d.'s generated
sites are meant to work without JavaScript and without third parties.
So the new Video block does something simpler: it shows the video's
thumbnail with a play mark, and the whole picture is a link to the video.
The thumbnail is a file in media/, like any other image, so it gets
the same smaller copies and srcset (see
Responsive images).
oEmbed: one question, three platforms
Where does the thumbnail come from? oEmbed is an
old, plain standard: you send the platform a video's address and get
back JSON with its title and thumbnail. YouTube, Vimeo and PeerTube all
answer it, each at its own address. oembed_url in
crates/qed-core/src/video.rs picks it:
let endpoint = match platform? ;
PeerTube isn't one site but many instances on their own domains, so any host that isn't YouTube or Vimeo is asked as if it were one. If it isn't, the request fails and the editor shows why.
YouTube's oEmbed thumbnail is 4:3 with black bars above and below. The
large 16:9 one (maxresdefault.jpg) is at a fixed address built from the
video's id, but not every video has it. So fetch tries that first and
falls back to the oEmbed one.
The fetching happens once, when you press Get thumbnail and title in the editor. Building the site never touches the network.
An HTTP request in Rust
This is the first time qed-core talks HTTP. It uses
reqwest, the same crate Tauri's updater uses,
so it adds no second HTTP stack to the app. The request is async:
let info: OEmbed = client
.get
.send
.await
.and_then
.map_err?
.json
.await
.map_err?;
Each .await hands control back while waiting for the network, so the
app window doesn't freeze. In Elixir you would write the same thing as a
plain blocking call (Req.get!) in its own process, and the scheduler
keeps everything else running. Rust has no such runtime built in, so the
waiting points are written out, and an executor (Tokio,
which Tauri runs) drives them.
.json() turns the answer into a struct through serde, like the TOML
files: only the fields we name are read, the rest is ignored.
Telling TLS which cryptography to use
The first real request panicked:
No rustls crypto provider is configured. When using the
rustls-no-providerfeature you must install a crypto provider…
HTTPS needs cryptography, and rustls can use either of two libraries for it, ring or aws-lc-rs. Tauri's updater pulls in reqwest with "no provider", so the app chooses. q.e.d. already uses ring for SSH, so the video module picks that one before it builds a client:
let _ = default_provider.install_default;
install_default returns an error if a provider is installed already,
for example on the second fetch. That's fine here, so the result is
dropped on purpose with let _ =.
Remembering per block in the editor
The editor shows a spinner on the block being fetched and an error on
the block whose fetch failed. Both live in collections keyed by the block
itself, a SvelteSet and a SvelteMap
(see A foldable page tree):
;
;
A JavaScript Map can use any object as a key, compared by identity. Each
block in the list is one object for as long as the page is open, so it
works as its own key without inventing ids.