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
23 changes: 23 additions & 0 deletions dev/test/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { build, SHARE, EXAMPLES, videoChapters, wrapPost, footer, exampleCard, siteFonts } from "../../site/build.mjs";
import { drawing, NAMES } from "../../site/iso.mjs";
import { CLUSTERS, CUTS, drawnWidths } from "../../site/assets/hero/layout.js";

test("the site builds: markers filled, files copied, every command copyable", () => {
const out = mkdtempSync(join(tmpdir(), "buildnotes-site-"));
Expand Down Expand Up @@ -98,6 +99,28 @@ test("the site builds: markers filled, files copied, every command copyable", ()
}
});

test("the hero's table: its scene, model, videos, cut images, posters and three.js are all served", () => {
const out = mkdtempSync(join(tmpdir(), "buildnotes-site-"));
try {
const { html } = build({ out });
const served = (p) => assert.ok(existsSync(join(out, p)), p);
// three.js is the site's own copy, named by an import map that hero.js's imports go through
const map = JSON.parse(html.match(/<script type="importmap">([^<]+)<\/script>/)[1]).imports;
served(map.three);
served(`${map["three/addons/"]}loaders/GLTFLoader.js`);
assert.match(html, /<script type="module" src="assets\/hero\/hero\.js"><\/script>/);
for (const f of ["hero.js", "layout.js", "room.glb", "poster.jpg", "poster-phone.jpg"]) served(`assets/hero/${f}`);
for (const c of CLUSTERS) served(`assets/hero/${c.video}`);
for (const cut of Object.keys(CUTS)) for (const src of drawnWidths().keys()) served(`assets/hero/${cut}/${src}`);
// the chip starts on the first cluster's request, and the pager names every cluster in order
assert.match(html, new RegExp(`<span class="typed">${CLUSTERS[0].brief}</span>`));
const stops = [...html.match(/<nav class="stops"[\s\S]*?<\/nav>/)[0].matchAll(/<button type="button" aria-label="([^"]+)"/g)].map((m) => m[1]);
assert.deepEqual(stops, CLUSTERS.map((c) => c.name));
} finally {
rmSync(out, { recursive: true, force: true });
}
});

test("fonts: every page uses the site's own, with a matched fallback after each family", () => {
const out = mkdtempSync(join(tmpdir(), "buildnotes-site-"));
try {
Expand Down
5 changes: 4 additions & 1 deletion docs/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ To keep a public run as a committed example, check it, then copy its page, plan
node site/build.mjs # or: npm run site
node site/build.mjs --url https://buildnotes.releases.sh # canonical link, og:url, og:image there, sitemap.xml and robots.txt
node site/card.mjs # re-render the link-preview images in site/assets (needs Chrome)
node site/hero.mjs # re-cut the hero's images and re-bake its posters in site/assets/hero (needs Chrome)
open site/dist/index.html
npm run site:dev # build, then serve it as the Worker does at http://localhost:8787
# or, in Claude Code desktop, the "site" entry in .claude/launch.json does the same (autoPort: it moves if 8787 is taken)
npm run deploy # build, then deploy (wrangler login, Build Internet account)
```

Hosting: [`wrangler.jsonc`](../wrangler.jsonc) is a Cloudflare Worker that serves `site/dist` as static assets on the `buildnotes.releases.sh` custom domain. Its only script, [`site/worker.mjs`](../site/worker.mjs), runs first for the example videos alone (`run_worker_first`) to answer HTTP Range requests, which the asset layer ignores; without it a browser cannot scrub a video. The `releases.sh` zone is on the same Cloudflare account, so the domain needs no DNS records by hand. `.html` URLs redirect to the extensionless path (`post.html` → `post`), and relative links still resolve.
Hosting: [`wrangler.jsonc`](../wrangler.jsonc) is a Cloudflare Worker that serves `site/dist` as static assets on the `buildnotes.releases.sh` custom domain. Its only script, [`site/worker.mjs`](../site/worker.mjs), runs first for the videos alone, the examples' and the hero's (`run_worker_first`), to answer HTTP Range requests, which the asset layer ignores; without it a browser cannot scrub a video, and Safari won't play one. The `releases.sh` zone is on the same Cloudflare account, so the domain needs no DNS records by hand. `.html` URLs redirect to the extensionless path (`post.html` → `post`), and relative links still resolve.

Workers Builds deploys it on every push to `main`. The `buildnotes-site` Worker is connected to this repo in the Cloudflare dashboard (Workers & Pages → buildnotes-site → Settings → Build) with:

Expand All @@ -61,6 +62,8 @@ Then it copies every local file the page points at: `site/assets/` from there, `

Link previews: the page's description, Open Graph and Twitter tags come from `SHARE` in `build.mjs`. `og:image` must be an absolute URL, so without `--url` it points at `site/assets/og.png` on GitHub's `main` branch. That image is `site/src/card.html` rendered by `site/card.mjs`, which also renders one `site/assets/og-<slug>.png` per example from `site/src/card-example.html` (the product, the post's headline and the example's `image`): render them again and commit them when a headline or a picture changes. The favicon is `site/assets/favicon.svg`. Each example page gets its own tags from its `EXAMPLES` entry (an article titled by product and name, the blurb as description, its card as the picture), with a canonical link and `og:url` when built with `--url`.

The hero is a tilted table with one cluster per example (the post as a tall page, two of its figures with leaders, its deck, its video, and Sunny's room as a 3D model), drawn as one three.js scene by [`site/assets/hero/hero.js`](../site/assets/hero/hero.js) from the layout in [`layout.js`](../site/assets/hero/layout.js). The build copies `site/assets/hero/` whole, since the script loads its own files: three.js r160 (the site's own copy, named by the page's import map), `room.glb` (exported from `site/hero/room.blend`), the videos at 640 × 360, and the images. Those are cut to the width they're drawn at, in two sizes (`d/` for desktop, `m/` for phones), because the GPU holds every image decoded whatever its format: `site/hero.mjs` makes them from `site/hero/src/` and bakes the two posters (`poster.jpg`, `poster-phone.jpg`) from the built page. Run it again and commit the results when a picture, the layout or the scene's look changes. The posts on the table are greeked (text drawn as bars) by `site/hero/greek.js`. The scene renders only while the hero is on screen, at most 1.25× the screen's density into a 2× multisampled target; until it's ready, and for good under reduced motion or without WebGL, the stage shows the poster.

`site/assets/sunny-post-full.webp` is the whole sunny-walkthrough-videos post, rendered by headless Chrome at 1100 px wide in light mode and trimmed to its column. Re-make it if that post changes.

### Promo video
Expand Down
Binary file added site/assets/hero/d/releases/digest-card.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/frame-01.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/frame-03.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/frame-05.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/frame-07.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/post.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/releases/replay-tue.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/boardroom.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/frame-01.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/frame-02.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/frame-05.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/frame-07.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/lamp-after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/sunny/post.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/comment-pair.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/file-page-compare.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/frame-01.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/frame-02.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/frame-03.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/frame-06.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero/d/uploads/post.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading