Skip to content

fix(website): self-host the intro video, drop the YouTube iframe - #1441

Merged
vivek7405 merged 4 commits into
mainfrom
feat/self-host-intro-video
Aug 21, 2026
Merged

fix(website): self-host the intro video, drop the YouTube iframe#1441
vivek7405 merged 4 commits into
mainfrom
feat/self-host-intro-video

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

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 load to cover the white canvas some engines paint before YouTube's stylesheet applies, a plain onload attribute to reveal it (an @event hole would be dropped at SSR, since this page never hydrates), and a noscript rule 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

https://videos.webjs.dev/intro.mp4            85 MB, 3840x2160@60, 4:54, faststart
https://videos.webjs.dev/intro-thumbnail.webp  266 KB, 1920 wide, q92
cache-control: public, max-age=86400, s-maxage=31536000

Cloudflare R2 bucket webjs-videos on 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-age is a day and the header is deliberately not immutable, 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-maxage keeps 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_VIDEO in website/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-Control is set at write time or not at all, a CLOUDFLARE_API_TOKEN in 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.ts rewritten. Its three old tests pinned the invisible class, the onload attribute and the noscript rule, 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.
  • Counterfactual: swapping the <video> back to an <iframe> fails 4 of the tests, and dropping the aria-label reds the accessible-name test alone.
  • npm test in website: 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).
  • Prod boot through createRequestHandler: / 200, /docs 308, /ui 200, no broken modulepreloads. Served markup asserted to contain the <video> and no <iframe>.
  • Real Chromium at 1280x900 with javaScriptEnabled: false: the player renders at 718x403 with its poster and native controls. Same with JS on.

Surfaces

  • Docs site, AGENTS.md, skill references, scaffold, MCP, editor plugins: N/A, no framework surface changed (website/ only, no packages/*/src).
  • Marketing copy: this is the marketing page, and no claim on it changed.
  • Version bump: N/A, website is 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 and getByLabel after, and pinned with a test.

Also corrected a comment claiming the intrinsic width/height hold the box shape, which they do not here (the wrapper is aspect-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.

@vivek7405 vivek7405 self-assigned this Aug 20, 2026

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread website/app/page.ts
Comment thread website/app/page.ts Outdated
Comment thread website/test/ssr/intro-video-ssr.test.ts Outdated
@vivek7405

Copy link
Copy Markdown
Collaborator Author

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.

@vivek7405 vivek7405 closed this Aug 20, 2026
@vivek7405 vivek7405 reopened this Aug 21, 2026
@vivek7405
vivek7405 marked this pull request as ready for review August 21, 2026 18:57
@vivek7405
vivek7405 force-pushed the feat/self-host-intro-video branch from a244f1e to 33d677b Compare August 21, 2026 18:57
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.
@vivek7405
vivek7405 force-pushed the feat/self-host-intro-video branch from 33d677b to b0233b1 Compare August 21, 2026 19:04
@vivek7405
vivek7405 merged commit 5bfa596 into main Aug 21, 2026
10 checks passed
@vivek7405
vivek7405 deleted the feat/self-host-intro-video branch August 21, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(website): self-host the intro video, drop the YouTube iframe

1 participant