chore(site): serve OG Studio link cards from the files bucket, add docs previews (#63560)

Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
This commit is contained in:
David Hill
2026-11-12 10:00:20 -06:00
committed by GitHub
Unverified
parent 1a85993784
commit 33aefad13c
185 changed files with 129 additions and 51 deletions
+40 -1
View File
@@ -9,7 +9,7 @@ The site's heavy media (product videos, arcade sounds, 3D models and download zi
- The build (`script/build.ts`) drops every `mediaFile` from the Worker's assets, so a local copy in `public/` never ships.
- In development, `vite dev` proxies `/files` to `https://dev.opencode.ai`, so pages play the dev bucket's copies.
- Images (`png`, `jpg`, `webp`, `avif`, `svg`), posters, fonts and the 3D decoders (`arcade3d/draco`, `arcade3d/basis`) stay in git and in the static assets.
- The link-card videos in `og/` (`og:video`, from `script/og.ts`) are the exception: they are small and ship as static assets with their cards.
- OG Studio's link cards are in the bucket too, under `site/og/` (see [Link cards](#link-cards)). `inBucket` leaves `og/` in the static assets only for a version 1 manifest, which copies its files there.
- `videoSources()` offers a video's AV1 and HEVC copies (`media/<base>.<width>.<codec>.<hash>.mp4`, listed in `src/component/site/media-manifest.json`) before the H.264 original. The manifest lists only copies that are in the bucket; a video without copies plays its original.
| Folder | Files |
@@ -34,3 +34,42 @@ curl -sI https://dev.opencode.ai/files/site/2026-11/arcade3d/cabinet.glb # exp
```
A changed file gets a new dated prefix (for example `site/2026-12`) and a new `mediaBase`, so cached copies never go stale.
## Link cards
OG Studio (the local design tool that makes the link cards) uploads every card PNG and `og:video` MP4 to both files buckets under `site/og/`, and commits only `src/asset/og-studio/manifest.json`. File names end in a hash of their content (`home-98637499f6.png`, `home-uk-7dbf00d466.png`, `home-ec8331a4d6.mp4`), so an object never changes and a redesign only adds new objects; nothing is overwritten and nothing grows the repo.
The manifest (version 2) names the files and where they are served:
```json
{
"version": 2,
"generator": "og-studio",
"width": 1200,
"height": 630,
"base": "/files/site/og",
"cards": {
"/": {
"image": "home-98637499f6.png",
"alt": "…",
"video": "home-ec8331a4d6.mp4",
"locales": { "uk": { "image": "home-uk-7dbf00d466.png", "alt": "…" } }
}
}
}
```
`script/og.ts` writes `${base}/<file>` into `src/lib/og-images.json`, and `ShareMeta` prefixes `https://opencode.ai`, so every stage's `og:image` and `og:video` load from the production bucket. Pages that are not in the manifest still get a card rendered by `script/og.ts` into `public/og/`.
Upload new files to both buckets before you merge the manifest that names them. The buckets are in different accounts (`services/files/wrangler.jsonc`): `opencode-dev-files` in the dev account and `opencode-production-files` in the production account. To upload by hand, from a folder without a `cloudflare.config.ts` (so `cf` doesn't load this one):
```sh
CLOUDFLARE_ACCOUNT_ID=d8e944f537ceef3f06a5bd3aec1d7a53 cf r2 objects put site/og/home-98637499f6.png \
--bucket-name opencode-dev-files --file home-98637499f6.png --content-type image/png
CLOUDFLARE_ACCOUNT_ID=15d29c8639fd3733b1b5486a2acfd968 cf r2 objects put site/og/home-98637499f6.png \
--bucket-name opencode-production-files --file home-98637499f6.png --content-type image/png
curl -sI https://dev.opencode.ai/files/site/og/home-98637499f6.png # expect 200 and image/png
curl -sI https://opencode.ai/files/site/og/home-98637499f6.png # expect 200 and image/png
```
Use `--content-type video/mp4` for videos. Never delete an object that a merged manifest names: old link previews keep loading it.
+25 -11
View File
@@ -4,9 +4,13 @@
// title, image or template gives it a new URL and link previews refresh. Cards are English at
// every locale.
//
// OG Studio (a local design tool) can replace any card. It writes finished files and a manifest to
// src/asset/og-studio/: for each page an English PNG, an optional MP4 for og:video and an optional
// PNG for each locale. Pages in that manifest use its files; every other page renders here.
// OG Studio (a local design tool) can replace any card. For each page it makes an English PNG, an
// optional MP4 for og:video and an optional PNG for each locale, and writes a manifest to
// src/asset/og-studio/manifest.json. Pages in that manifest use its files; every other page renders
// here. A version 2 manifest has a `base` (/files/site/og): OG Studio uploads the files to the
// files bucket there (MEDIA.md) and only the manifest is in git, so pages link `${base}/<file>`
// and nothing is copied. A manifest without `base` (version 1) keeps its files beside it, and they
// are copied into public/og/.
//
// This file is the cards' source of truth. The layout is David's template frame in Paper (the
// "OG images" file, frame "/ — dark"): a grid of #2e2e2e lines at x 72, 900 and 1128 and y 72,
@@ -202,19 +206,25 @@ const fonts = [
await mkdir(out, { recursive: true })
const manifest: Record<string, OgEntry> = {}
const studio = await readStudio()
const studioBase = studio?.base?.replace(/\/+$/, "") ?? "/og"
for (const [page, entry] of Object.entries(studio?.cards ?? {})) {
const files = [entry.image, entry.video, ...Object.values(entry.locales ?? {}).map((l) => l.image)].filter(
(file): file is string => Boolean(file),
)
await Promise.all(files.map((file) => Bun.write(path.join(out, file), Bun.file(path.join(root, STUDIO, file)))))
if (!studio?.base) {
const files = [entry.image, entry.video, ...Object.values(entry.locales ?? {}).map((l) => l.image)].filter(
(file): file is string => Boolean(file),
)
await Promise.all(files.map((file) => Bun.write(path.join(out, file), Bun.file(path.join(root, STUDIO, file)))))
}
manifest[page] = {
src: `/og/${entry.image}`,
src: `${studioBase}/${entry.image}`,
alt: entry.alt,
...(entry.video ? { video: `/og/${entry.video}` } : {}),
...(entry.video ? { video: `${studioBase}/${entry.video}` } : {}),
...(entry.locales
? {
locales: Object.fromEntries(
Object.entries(entry.locales).map(([locale, l]) => [locale, { src: `/og/${l.image}`, alt: l.alt }]),
Object.entries(entry.locales).map(([locale, l]) => [
locale,
{ src: `${studioBase}/${l.image}`, alt: l.alt },
]),
),
}
: {}),
@@ -244,11 +254,15 @@ await Promise.all(
(await readdir(out)).filter((file) => !keep.has(file)).map((file) => rm(path.join(out, file), { force: true })),
)
await Bun.write(path.join(root, "src/lib/og-images.json"), JSON.stringify(manifest, null, 2) + "\n")
console.log(`og: ${Object.keys(manifest).length} cards in public/og (${Object.keys(studio?.cards ?? {}).length} from OG Studio)`)
console.log(
`og: ${Object.keys(manifest).length} cards (${Object.keys(studio?.cards ?? {}).length} from OG Studio${studio?.base ? ` at ${studioBase}` : ""})`,
)
type OgEntry = { src: string; alt: string; video?: string; locales?: Record<string, { src: string; alt: string }> }
type Studio = {
/** Version 2: where the files are served (the files bucket's /files/site/og). */
base?: string
cards: Record<string, { image: string; alt: string; video?: string; locales?: Record<string, { image: string; alt: string }> }>
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

@@ -1,8 +1,9 @@
{
"version": 1,
"version": 2,
"generator": "og-studio",
"width": 1200,
"height": 630,
"base": "/files/site/og",
"cards": {
"/": {
"image": "home-98637499f6.png",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

Some files were not shown because too many files have changed in this diff Show More