fix(website): self-host the intro video, drop the YouTube iframe - #1441
Conversation
vivek7405
left a comment
There was a problem hiding this comment.
Read the whole diff. The shape is right and the JS-off win is real, so this is close, but it drops something the embed had.
The one that matters is the accessible name. The iframe carried title="WebJs introduction video" and the <video> carries nothing, so the player now has no name at all in the accessibility tree. I checked it against the running page rather than reasoning about it: CDP Accessibility.getPartialAXTree reports no name, and both aria-label and title are null. For a screen reader user that turns a described video into an unlabelled media element, which is a regression this PR introduces rather than something it inherited.
The second one is a comment that says something the layout does not do. Worth fixing because a wrong explanation is worse than no explanation: the next person will believe it.
Everything else I went looking for came back fine. The module-scope html constant does not change the elision verdict (import-only, emitting copy-cmd and like-button, identical to main), which was my main worry about moving markup out of the page function. Keeping the runbook in a JS comment rather than the template is the right call and I would not have thought of it: template comments really are on the wire, 30 of them on this page today.
One thing outside the diff that I would fix before merge, since the PR body is part of the change: it still describes the poster as "71 KB, the video's own first frame" at intro-poster.webp. That key is deleted and 404s now.
|
Closing unmerged. Keeping the YouTube embed for now, so main is unchanged. The reasoning and the measurements are on #1440, and the branch stays in place if this is revived. |
a244f1e to
33d677b
Compare
The embed's every property was a workaround for being cross-origin: a frame hidden until load to cover the white canvas some engines paint before YouTube's stylesheet lands, a plain onload attribute to reveal it because this page never hydrates, and a noscript rule that deleted the whole section for a JS-off reader, since their player needs JS inside the frame and nothing this page does can supply it. That last one is the real cost. WebJs sells progressive enhancement on its own landing page, and the landing page was deleting its video for anyone without JS. A native player needs none of it. The file is served from our own R2 bucket at videos.webjs.dev, with preload="metadata" keeping the bytes off the wire until someone presses play.
The first frame doubled as the poster, so the landing page showed a screenshot of itself. This is a purpose-made thumbnail instead, keyed intro-thumbnail.webp rather than intro-poster.webp to say what it is. 1920 wide at q92: the box is capped at max-w-3xl, about 718 CSS px, so that covers a 2x display and a wider encode buys nothing a viewport can show.
The iframe carried title="WebJs introduction video" and the video that replaced it carried nothing, so the player had no name in the accessibility tree at all. Also corrects the comment above it. The intrinsic width and height do not hold the box shape here, because the wrapper is aspect-video and the element is w-full h-full, so removing both leaves the box unchanged. And scopes the youtube assertion to an embed url, since matching the whole page meant any future link would fail a test about this section.
At 2315 kbps, preload="metadata" starved the thumbnail on a slow link: throttled to 400 kbps the video had pulled 6.2 MB by the time the poster painted, 25.8 seconds in, so the first thing a slow visitor saw was an empty black box. preload="none" fetches nothing until someone presses play. This is a deliberate deviation from rubyonrails.org, which uses metadata on a file running 658 kbps, a quarter of this one. The cost is that the controls cannot show the duration before the first play.
33d677b to
b0233b1
Compare
Closes #1440
Replaces the landing page's YouTube iframe with a self-hosted
<video>, the way rubyonrails.org does it.Every property the embed had was a workaround for being cross-origin: a frame hidden until
loadto cover the white canvas some engines paint before YouTube's stylesheet applies, a plainonloadattribute to reveal it (an@eventhole would be dropped at SSR, since this page never hydrates), and anoscriptrule that deleted the whole section for a JS-off reader, because their player needs JS inside the frame and nothing this page does can supply it.That last one is why I wanted this changed. We sell progressive enhancement on the landing page, and the landing page was deleting its video for anyone without JS. A native player is UA chrome, so it just works.
What it serves now
Cloudflare R2 bucket
webjs-videoson a custom domain. R2 egress is free, so bandwidth is not a cost lever.preload="metadata"keeps the file off the wire until someone presses play, so a page view costs the poster plus a metadata range request.The
max-ageis a day and the header is deliberately notimmutable, because the key carries no version: anyone holding an old cut needs a way to pick up a new one, and a purge clears the edge but never a browser.s-maxagekeeps the edge copy long lived regardless, since the edge is purgeable.Replacing the video later
The thumbnail is 1920 wide because the box measures about 718 CSS px, so that already covers a 2x display and a wider encode buys nothing a viewport can render.
The upload procedure is a comment on
INTRO_VIDEOinwebsite/app/page.ts, in a JS block comment rather than inside the template on purpose: template comments are served to every visitor (30 of them ship on this page today), and an operational runbook should not be on that wire.It covers the three things that are easy to get wrong: R2 metadata cannot be edited after upload so
Cache-Controlis set at write time or not at all, aCLOUDFLARE_API_TOKENin the environment silently overrides wrangler's OAuth credentials and carries no R2 permission, and a purge reaches the edge but never a browser.Test plan
website/test/ssr/intro-video-ssr.test.tsrewritten. Its three old tests pinned theinvisibleclass, theonloadattribute and thenoscriptrule, all of which this removes, so they are replaced rather than deleted. Five tests now pin the src, the poster,preload/playsinline, the absence of every workaround, and that the section survives for a JS-off reader.<video>back to an<iframe>fails 4 of the tests, and dropping thearia-labelreds the accessible-name test alone.npm testinwebsite: 480 node, 88 browser, 0 failures.webjs check: clean.webjs doctor: exit 0, 11 passed / 2 warnings / 0 failed (main carries 3 warnings, both remaining ones pre-existing).createRequestHandler:/200,/docs308,/ui200, no broken modulepreloads. Served markup asserted to contain the<video>and no<iframe>.javaScriptEnabled: false: the player renders at 718x403 with its poster and native controls. Same with JS on.Surfaces
website/only, nopackages/*/src).websiteis not published.Review
Reviewed the whole diff and fixed three things it turned up. The one that mattered: the iframe carried
title="WebJs introduction video"and the<video>carried nothing, so the player had no accessible name at all. Confirmed against the running page with CDP before andgetByLabelafter, and pinned with a test.Also corrected a comment claiming the intrinsic
width/heighthold the box shape, which they do not here (the wrapper isaspect-video, so removing both leaves the box at 718x403), and scoped the youtube assertion to an embed url rather than the whole page.Elision verdict is unchanged by moving the markup into a module-scope constant:
import-only, emitting copy-cmd and like-button, identical to main.