Skip to content

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

Description

@vivek7405

Problem

The landing page's intro video is a cross-origin YouTube iframe, and every property it has is a workaround for that fact:

  • the frame ships with an invisible class over a black box, because a cross-origin iframe paints its own canvas and some engines paint it opaque white before YouTube's stylesheet lands (fix(website): refresh the intro video and stop its white flash #1434);
  • it reveals itself through a plain onload HTML attribute, because the page never hydrates so an @event hole would be dropped at SSR;
  • the whole section is hidden from a JS-off reader by a noscript rule, because YouTube's 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 currently deletes its video for anyone without JS. It also loads a third party on first paint and gives us no control over what paints first.

rubyonrails.org has no embed at all. It self-hosts a plain <video> with a poster and a caption track, served from videos.37signals.com, with no player library and no JS:

<video poster="…rails-8-demo.webp" src="…rails-8-demo.mp4"
       width="1920" height="1080" preload="metadata" controls crossorigin>
  <track src="…rails-8-demo-en.vtt" kind="captions" label="English" srclang="en" default>
</video>

Design / approach

Take the same approach. The asset is already hosted and live:

https://videos.webjs.dev/intro.mp4
  content-type: video/mp4
  content-length: 85171620          (3840x2160@60, 4:54, faststart)
  cache-control: public, max-age=86400, s-maxage=31536000
  accept-ranges: bytes              (206 on a Range request, so seeking works)

Cloudflare R2 bucket webjs-videos, custom domain videos.webjs.dev. R2 egress is free, so bandwidth is not a cost lever here.

preload="metadata" keeps the 85 MB off the wire until a click: a page view costs the poster plus a metadata range request. controls is UA chrome, so it works with scripting fully disabled, which is what lets the noscript hide go away.

Every workaround listed above is deleted by this change. Nothing replaces them.

Implementation notes (for the implementing agent)

Where to edit

  • website/app/page.ts, the <section class="intro-video pb-16"> block (around L474 to L510 at time of filing, between the hero section and the "The first paint is the whole page" section). Replace the whole <div class="aspect-video …"> wrapper contents and delete the trailing <noscript><style>…</style></noscript>.
  • website/test/ssr/intro-video-ssr.test.ts must be rewritten, not deleted. All three of its current tests pin behaviour that this change deliberately removes (the invisible class, the onload attribute, the noscript rule), so they will fail, and deleting them would leave the new markup unpinned. Replace with assertions on the new shape: a <video> with controls, preload="metadata", playsinline, the videos.webjs.dev src, a poster, and NO <iframe> anywhere in the output.

Landmines

  • Do NOT wrap the URL in asset(). That helper is public/-relative and prod-only, and this is a cross-origin absolute URL. asset() would mangle it.
  • The poster is not uploaded yet. https://videos.webjs.dev/intro-poster.webp currently 404s. A <video> with no poster shows a black box until play, which is worse than the current state. A generated 1080p poster (71 KB webp, from frame 0, which is the webjs.dev hero) is at ~/Videos/web-export/intro-poster.webp on the owner's machine; upload it to the same bucket before shipping, with the same --cache-control string as the video. Verify it returns 200 before merging.
  • Captions are deferred and must not be added blind. ~/Videos/WebJs Demo.srt is from an older cut: it runs to 5:18 against a 4:54 video, so it drifts about 24 seconds by the end, and it spells the brand WebJS rather than WebJs. Ship without a <track> and re-time it separately.
  • A <track> will need CORS when it is added. The video is cross-origin to the page (videos.webjs.dev vs webjs.dev), so a caption track requires crossorigin on the <video> AND a CORS policy on the R2 bucket. Without both the track silently fails to load. Do not add crossorigin before the bucket policy exists, since it can affect the video load too.
  • playsinline is required or iOS Safari takes the video fullscreen on play.
  • Set width / height (1920 / 1080) so the box reserves its aspect ratio before metadata arrives, matching what the existing aspect-video wrapper does today.
  • The page never hydrates, so no @event holes and no component. This stays plain markup in the page.

Maintenance comments (an explicit requirement of this task)

The new block must carry a comment telling a future agent how to replace the video at the same key, since the key is unversioned and browsers cache it for 24 hours. The procedure, verified in practice:

env -u CLOUDFLARE_API_TOKEN npx wrangler r2 object put webjs-videos/intro.mp4 \
  --file <new-file> --content-type video/mp4 \
  --cache-control "public, max-age=86400, s-maxage=31536000" --remote
# then purge the edge, or the old bytes serve for up to a year
curl -X POST -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://videos.webjs.dev/intro.mp4"]}' \
  "https://api.cloudflare.com/client/v4/zones/<zone-id>/purge_cache"

The comment must state the three facts that make this non-obvious: R2 object metadata cannot be edited after upload (the header is set at write time or not at all), CLOUDFLARE_API_TOKEN in the environment overrides wrangler's OAuth credentials and lacks R2 permission (hence env -u), and a purge clears the edge but never a browser, so a stale copy can persist for the 24 hour max-age. Also state why the header is NOT immutable: that directive was removed precisely so an unversioned re-cut stays recoverable.

Invariants to respect

  • AGENTS.md invariant 11 applies to the comments themselves: no em-dashes, no space-surrounded hyphen or semicolon as pause punctuation, and WebJs capitalised in prose while webjs stays lowercase only as a code token.
  • AGENTS.md invariant 8: this is a page, so no shell tags.
  • The house comment style in website/app/page.ts explains why, and the new comment should match that register rather than narrating the diff.

Tests and docs

  • website/test/ssr/intro-video-ssr.test.ts rewritten as above. Run with ( cd website && npm test ).
  • Add a counterfactual check: revert the <video> to an <iframe> and confirm the new assertions fail.
  • Run ( cd website && npx webjs check ) and ( cd website && npx webjs doctor ). webjs check refuses to run from the repo root, and doctor gates UNMARKED_ASSET_LINKS at error for this app.
  • No framework doc surface changes, since this touches no packages/*/src and no public API.

Acceptance criteria

  • The landing page renders a <video controls preload="metadata" playsinline> pointing at https://videos.webjs.dev/intro.mp4, with a working poster
  • No <iframe>, no invisible class, no onload attribute, and no noscript rule remain in the intro-video section
  • The video is visible and playable with JavaScript fully disabled
  • https://videos.webjs.dev/intro-poster.webp returns 200 before merge
  • website/test/ssr/intro-video-ssr.test.ts pins the new markup, and a counterfactual proves the assertions fire
  • The block carries the replace-the-video comment, covering re-PUT, purge, env -u, and why the header is not immutable
  • webjs check and webjs doctor are clean for website

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions