Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 87 additions & 39 deletions website/app/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,92 @@ function sourceWindow(title: string, sample: string) {
`;
}

/**
* The landing page's intro video, self-hosted rather than embedded.
*
* The player is the browser's own, so it works with scripting disabled. The
* YouTube iframe this replaced could not, because its player needs JS inside
* the frame, so the section used to hide itself from a JS-off reader rather
* than show a broken embed.
*
* REPLACING THE VIDEO, for whoever does it next.
*
* The bytes live in the Cloudflare R2 bucket `webjs-videos`, served from
* videos.webjs.dev. The key carries no version, so a new cut overwrites
* `intro.mp4` in place and nothing in this file changes:
*
* env -u CLOUDFLARE_API_TOKEN npx wrangler r2 object put \
* webjs-videos/intro.mp4 --file <new-cut.mp4> \
* --content-type video/mp4 \
* --cache-control "public, max-age=86400, s-maxage=31536000" \
* --remote
*
* Then purge that URL at the edge (Caching, then Purge Cache, then the custom
* single-file purge), or the old bytes keep serving for the `s-maxage` year.
*
* Three things about this are easy to get wrong.
*
* 1. R2 object metadata cannot be edited after upload. `Cache-Control` is set
* at write time or not at all, so every upload has to pass it again. There
* is no bucket-level setting, and the dashboard uploader has no field for
* it, which is why an upload made there serves no header of its own.
*
* 2. The `env -u` is load bearing. A `CLOUDFLARE_API_TOKEN` in the environment
* overrides wrangler's OAuth credentials, and that token carries no R2
* permission, so the upload fails with an authentication error that names
* nothing. Unsetting it for the one call falls back to the OAuth login.
*
* 3. A purge clears the edge and never a browser. That is why `max-age` is a
* day rather than a year, and why the header is deliberately not
* `immutable`: anyone holding the old file needs a way to pick the new one
* up, and 24 hours is that way. `s-maxage` keeps the edge copy long lived
* regardless, since the edge can be purged and a browser cannot.
*
* A versioned key (`intro-2026-08-21.mp4`) is the other valid shape. Take it
* and the tradeoff inverts: put `immutable` back, raise `max-age` to a year,
* and update the src below on every cut.
*
* The thumbnail at `intro-thumbnail.webp` is uploaded the same way, with
* `--content-type image/webp`. It is 1920 wide because the box is capped at
* max-w-3xl (about 718 CSS px), so that already covers a 2x display and a
* wider encode buys nothing a viewport can show.
*/
const INTRO_VIDEO = html`
<section class="intro-video pb-16">
<div class="max-w-3xl mx-auto px-6">
<!-- preload="none" fetches nothing but the thumbnail until someone
presses play. It is deliberately not "metadata", which is the
usual choice and what rubyonrails.org uses. Their file is 658
kbps and this one is 2315, and at 400 kbps the metadata read
starved the poster: the video had pulled 6.2 MB by the time the
thumbnail painted, 25.8 seconds in. The cost is that the controls
cannot show the duration until the first play.

playsinline stops iOS Safari taking the video fullscreen. The
width and height state the intrinsic size, which
this layout does not lean on (the wrapper is aspect-video and the
element is w-full h-full, so CSS fixes the ratio), but which keeps
the shape right if the stylesheet ever fails. The src is absolute and
cross-origin, so it must not be wrapped in asset(), which resolves
a public/ path and would mangle it. -->
<div class="aspect-video overflow-hidden border border-border-strong shadow-[var(--shadow)] bg-black">
<video
class="intro-video-player w-full h-full"
src="https://videos.webjs.dev/intro.mp4"
poster="https://videos.webjs.dev/intro-thumbnail.webp"
aria-label="WebJs introduction video"
width="1920"
height="1080"
preload="none"
playsinline
controls
Comment thread
vivek7405 marked this conversation as resolved.
></video>
</div>
</div>
</section>
`;


export default function LandingPage() {
return html`
<style>
Expand Down Expand Up @@ -470,45 +556,7 @@ export default function LandingPage() {
</div>
</section>

<section class="intro-video pb-16">
<div class="max-w-3xl mx-auto px-6">
<!-- The frame is hidden until it fires load, over a black box.
A cross-origin iframe paints its OWN canvas, and YouTube's embed
sets its black background on body with no color-scheme declared,
so the black only lands once that stylesheet applies. Until then
some engines paint an opaque white canvas, which no background on
the iframe ELEMENT can cover, since the element background sits
behind that canvas. Hiding the frame sidesteps the whole question:
what it paints early is simply not on screen, and the box under it
is already the black the player settles on, so the reveal is
invisible. onload is a plain HTML attribute rather than a template
hole, because an @event drops at SSR and this page never hydrates. -->
<div class="aspect-video overflow-hidden border border-border-strong shadow-[var(--shadow)] bg-black">
<iframe
class="intro-video-frame w-full h-full invisible"
onload="this.classList.remove('invisible')"
src="https://www.youtube-nocookie.com/embed/XghCghezod4?rel=0"
title="WebJs introduction video"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
<!-- With JS off the whole section goes away. Revealing the frame
instead would show YouTube's own noscript error ("An error
occurred. Unable to execute JavaScript."), because their player
needs JS INSIDE the frame and nothing this page does can supply
it. An empty space reads better than a broken player.

The rule still applies from in here: a style element takes effect
wherever it sits, including inside the subtree it hides. And it
is safe in a PAGE, which never hydrates, so the client never
rebuilds this noscript body as live nodes. The same construct
inside a COMPONENT would apply on every JS-enabled visit. -->
<noscript><style>.intro-video { display: none }</style></noscript>
</div>
</section>
${INTRO_VIDEO}

<section class="py-16">
<div class="max-w-6xl mx-auto px-6">
Expand Down
70 changes: 47 additions & 23 deletions website/test/ssr/intro-video-ssr.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/**
* The landing page's intro video is hidden until its frame fires load.
* The landing page's intro video is self-hosted, not embedded.
*
* A cross-origin iframe paints its own canvas before the embedded stylesheet
* applies, and on some engines that canvas is opaque white, which no
* background on the iframe element can cover. Hiding the frame until load
* removes the question: what it paints early is off screen, and the box under
* it is already black.
* It replaced a YouTube iframe whose every property was a workaround for
* being cross-origin: a frame hidden until load (an iframe paints its own
* canvas, opaque white on some engines, before the embedded stylesheet
* lands), a plain onload attribute to reveal it (this page never hydrates, so
* an event hole would be dropped at SSR), and a noscript rule that deleted
* the whole section for a JS-off reader (their player needs JS inside the
* frame, so revealing it would have shown their own error).
*
* Each assertion here is a piece that silently breaks the whole thing if it
* goes missing, which is why they are pinned rather than left to review.
* A native player needs none of that, and the assertions below pin the pieces
* that silently break it if they go missing.
*/
import test from 'node:test';
import assert from 'node:assert/strict';
Expand All @@ -17,26 +19,48 @@ import LandingPage from '#app/page.ts';

const render = () => renderToString(LandingPage());

test('the intro frame ships hidden, over a black box', async () => {
test('the intro video is a native player pointed at our own host', async () => {
const out = await render();
assert.match(out, /class="intro-video-frame [^"]*\binvisible\b/, 'the frame must start hidden');
assert.match(out, /aspect-video[^"]*\bbg-black\b/, 'the box under it must be black');
assert.match(out, /<video[^>]*\ssrc="https:\/\/videos\.webjs\.dev\/intro\.mp4"/);
assert.match(out, /<video[^>]*\scontrols/, 'the controls are the whole no-JS story');
});

test('the frame reveals itself with a plain onload attribute', async () => {
test('the player keeps the accessible name the embed had', async () => {
const out = await render();
// A plain HTML attribute, not an @event hole: this page never hydrates, so
// a template event binding would be dropped at SSR and the frame would stay
// hidden forever.
assert.match(out, /onload="this\.classList\.remove\('invisible'\)"/);
// The iframe this replaced carried title="WebJs introduction video". Without
// a replacement the player is an unnamed media element to a screen reader.
assert.match(out, /<video[^>]*\saria-label="WebJs introduction video"/);
});

test('a JS-off reader gets no embed at all', async () => {
test('the first paint shows a poster rather than a black box', async () => {
const out = await render();
// Without JS the load handler never runs AND YouTube's player cannot run
// inside the frame either, so revealing it would show their own noscript
// error rather than a video. Hide the whole section instead. The rule must
// live in noscript, which a browser with scripting on parses as inert text.
assert.match(out, /<noscript><style>\.intro-video \{ display: none \}<\/style><\/noscript>/);
assert.match(out, /<section class="intro-video /, 'the rule needs its hook on the section');
// Without this the box is empty until someone presses play, which is worse
// than the embed it replaced.
assert.match(out, /<video[^>]*\sposter="https:\/\/videos\.webjs\.dev\/intro-thumbnail\.webp"/);
});

test('the bytes stay off the wire until someone plays it', async () => {
const out = await render();
// The file is 85 MB at 2315 kbps. Under preload=metadata it starved the
// poster on a 400 kbps link: 6.2 MB pulled before the thumbnail painted at
// 25.8s. "none" is the deliberate deviation from what rubyonrails.org does.
assert.match(out, /<video[^>]*\spreload="none"/);
// iOS Safari otherwise takes the video fullscreen the moment it plays.
assert.match(out, /<video[^>]*\splaysinline/);
});

test('none of the cross-origin workarounds survive', async () => {
const out = await render();
assert.doesNotMatch(out, /<iframe/, 'no embed of any kind');
assert.doesNotMatch(out, /youtube-nocookie|youtube\.com\/embed/i, 'no embed url');
assert.doesNotMatch(out, /intro-video-frame/);
assert.doesNotMatch(out, /onload="this\.classList\.remove/);
});

test('a JS-off reader gets the video, not a deleted section', async () => {
const out = await render();
// The old markup hid the section outright. A native player works with
// scripting disabled, so the rule that removed it has to be gone.
assert.doesNotMatch(out, /\.intro-video \{ display: none \}/);
assert.match(out, /<section class="intro-video /, 'the section still renders');
});
Loading