Skip to content

Commit 8abdbe7

Browse files
committed
feat(website): add release OG image generator
Programmatic generator for per-release Open Graph images. Runs locally via `pnpm gen:og --version <ver>` and writes a 2400x1260 (2x retina) PNG into the design-resources repo. Stack: satori (JSX/HTML -> SVG) + @resvg/resvg-js (SVG -> PNG) + satori-html (template) + sharp 8-bit palette re-encoding (~500 KB -> ~140 KB with no visible loss on this design — TinyPNG-style libimagequant quantization). Logo SVG is fetched from the assets.rspack.rs CDN at generation time so logo updates flow through without re-committing the template. Space Grotesk (SIL OFL) is committed under assets/fonts/. Background is a brick pattern of pass / running / fail / skip status icons (lucide circle-check / circle-x / loader / circle-dashed, color-matched to @rstest/browser-ui's status palette). Grounds the og frame in the test-runner identity rather than a generic gradient. Layout: 7x12 grid with corner icons pinned to canvas corners (1/4 visible), first/last row/col half-cropped at edges, odd rows offset by half a column for brick stagger. Per-icon stroke-opacity jitter (0.06-0.26) gives a hand-stippled feel; only the loader gets random rotation. A radialGradient vignette is baked into the bg SVG so the v{version} hero text reads cleanly through the wallpaper. The bg SVG is rasterized via Resvg and embedded as <img src="data:..."> in the satori tree — satori's `background: url(data:...)` shorthand parser doesn't accept SVG data URIs.
1 parent a541fa3 commit 8abdbe7

12 files changed

Lines changed: 1021 additions & 0 deletions

File tree

pnpm-lock.yaml

Lines changed: 580 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ allowBuilds:
1313
core-js-pure: false
1414
keytar: false
1515
lefthook: true
16+
sharp: true
1617
svelte-preprocess: false
1718

1819
autoInstallPeers: false

scripts/dictionary.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ antd
44
apng
55
apos
66
applescript
7+
artboard
78
Asus
89
atrules
910
autodocs
@@ -54,6 +55,7 @@ fnames
5455
frontends
5556
fullhash
5657
gzipped
58+
Grotesk
5759
icss
5860
idents
5961
iife
@@ -116,7 +118,9 @@ publint
116118
pxtorem
117119
quasis
118120
quxx
121+
rasterizes
119122
rebranded
123+
resvg
120124
rolldown
121125
rootdir
122126
rsbuild

website/AGENTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,36 @@ This is the documentation website for Rstest, built with [Rspress](https://rspre
1414
pnpm dev # Start dev server
1515
pnpm build # Build for production
1616
pnpm preview # Preview production build
17+
pnpm gen:og # Generate a release Open Graph image (see below)
1718
```
1819

20+
## Open Graph image generation
21+
22+
Per-release og images live in [rstackjs/rstack-design-resources](https://github.com/rstackjs/rstack-design-resources) and are served by the `assets.rspack.rs` CDN. The template lives **in this repo** to keep design-resources as a passive PNG store.
23+
24+
- `scripts/og-image/cli.mts` — entry, parses `--version`/`--description`/`--out`
25+
- `scripts/og-image/render.mts` — fetches the Rstest logo SVG → rasterizes → composes with [satori](https://github.com/vercel/satori) → renders with [@resvg/resvg-js](https://github.com/yisibl/resvg-js) at 2x zoom for retina
26+
- `scripts/og-image/template.mts`[satori-html](https://github.com/natemoo-re/satori-html) template, modeled after the `Rsbuild og image 1.0` artboard in design-resources
27+
28+
### Release workflow
29+
30+
1. Run `pnpm gen:og --version <ver> --description "<tagline>"` from `website/`. Use `--out` to write directly into a local clone of the design-resources repo at `rstest/assets/rstest-og-image-v{version-with-hyphens}.png` (e.g. `v0-5.png`).
31+
2. Commit the PNG in the design-resources repo and open a PR — that repo is the only place release PNGs are stored.
32+
3. After CDN deploy, the per-blog-post URL becomes available at `assets.rspack.rs/rstest/assets/rstest-og-image-v0-5.png`. The `head` function hook in `rspress.config.ts` routes each blog `routePath` to its matching URL.
33+
34+
### Do
35+
36+
- Use Space Grotesk (committed under `scripts/og-image/assets/fonts/` with SIL OFL license)
37+
- Render at 2x via `Resvg({ fitTo: { mode: 'zoom', value: 2 } })` so the PNG stays crisp on retina displays
38+
- Re-encode the resvg output as an 8-bit palette PNG via `sharp({ palette: true, quality: 90, ... })` — drops the file from ~500 KB to ~140 KB with no visible loss
39+
- Fetch the logo from the canonical CDN URL at generation time, not from a committed copy
40+
41+
### Don't
42+
43+
- Don't depend on packages like `geist` that pull in framework peer deps (`next>=13.2`); commit raw `.ttf` files directly instead
44+
- Don't write generated PNGs into this repo; they belong in design-resources
45+
- Don't bake the logo into a static asset; always fetch the SVG so logo updates propagate automatically
46+
1947
## Writing style guidelines
2048

2149
When writing or editing documentation, follow these principles:

website/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ The same as Rspack: [Writing style guide](https://github.com/web-infra-dev/rspac
1515
For images you use in the document, it's better to upload them to the [rstackjs/rstack-design-resources](https://github.com/rstackjs/rstack-design-resources) repository, so the size of the current repository doesn't get too big.
1616

1717
After you upload the images there, they will be automatically deployed under the <https://assets.rspack.rs/>.
18+
19+
## Open Graph images
20+
21+
`scripts/og-image/` generates per-release Open Graph images used by the `og:image` and `twitter:image` meta tags on each release blog post. See [AGENTS.md](./AGENTS.md#open-graph-image-generation) for the template architecture and release workflow.

website/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
"build": "rspress build",
1010
"build:agent-install": "node scripts/buildAgentInstall.mjs",
1111
"dev": "rspress dev",
12+
"gen:og": "node --experimental-strip-types scripts/og-image/cli.mts",
1213
"preview": "rspress preview"
1314
},
1415
"devDependencies": {
16+
"@resvg/resvg-js": "^2.6.2",
1517
"@rsbuild/plugin-sass": "^1.5.2",
1618
"@rspress/core": "2.0.11",
1719
"@rspress/plugin-algolia": "2.0.11",
@@ -26,6 +28,9 @@
2628
"rsbuild-plugin-open-graph": "^1.1.2",
2729
"rspress-plugin-font-open-sans": "^1.0.3",
2830
"rspress-plugin-sitemap": "^1.2.1",
31+
"satori": "^0.26.0",
32+
"satori-html": "^0.3.2",
33+
"sharp": "^0.34.5",
2934
"typescript": "^6.0.3"
3035
}
3136
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
2+
3+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4+
This license is copied below, and is also available with a FAQ at:
5+
http://scripts.sil.org/OFL
6+
7+
8+
-----------------------------------------------------------
9+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+
-----------------------------------------------------------
11+
12+
PREAMBLE
13+
The goals of the Open Font License (OFL) are to stimulate worldwide
14+
development of collaborative font projects, to support the font creation
15+
efforts of academic and linguistic communities, and to provide a free and
16+
open framework in which fonts may be shared and improved in partnership
17+
with others.
18+
19+
The OFL allows the licensed fonts to be used, studied, modified and
20+
redistributed freely as long as they are not sold by themselves. The
21+
fonts, including any derivative works, can be bundled, embedded,
22+
redistributed and/or sold with any software provided that any reserved
23+
names are not used by derivative works. The fonts and derivatives,
24+
however, cannot be released under any other type of license. The
25+
requirement for fonts to remain under this license does not apply
26+
to any document created using the fonts or their derivatives.
27+
28+
DEFINITIONS
29+
"Font Software" refers to the set of files released by the Copyright
30+
Holder(s) under this license and clearly marked as such. This may
31+
include source files, build scripts and documentation.
32+
33+
"Reserved Font Name" refers to any names specified as such after the
34+
copyright statement(s).
35+
36+
"Original Version" refers to the collection of Font Software components as
37+
distributed by the Copyright Holder(s).
38+
39+
"Modified Version" refers to any derivative made by adding to, deleting,
40+
or substituting -- in part or in whole -- any of the components of the
41+
Original Version, by changing formats or by porting the Font Software to a
42+
new environment.
43+
44+
"Author" refers to any designer, engineer, programmer, technical
45+
writer or other person who contributed to the Font Software.
46+
47+
PERMISSION & CONDITIONS
48+
Permission is hereby granted, free of charge, to any person obtaining
49+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+
redistribute, and sell modified and unmodified copies of the Font
51+
Software, subject to the following conditions:
52+
53+
1) Neither the Font Software nor any of its individual components,
54+
in Original or Modified Versions, may be sold by itself.
55+
56+
2) Original or Modified Versions of the Font Software may be bundled,
57+
redistributed and/or sold with any software, provided that each copy
58+
contains the above copyright notice and this license. These can be
59+
included either as stand-alone text files, human-readable headers or
60+
in the appropriate machine-readable metadata fields within text or
61+
binary files as long as those fields can be easily viewed by the user.
62+
63+
3) No Modified Version of the Font Software may use the Reserved Font
64+
Name(s) unless explicit written permission is granted by the corresponding
65+
Copyright Holder. This restriction only applies to the primary font name as
66+
presented to the users.
67+
68+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+
Software shall not be used to promote, endorse or advertise any
70+
Modified Version, except to acknowledge the contribution(s) of the
71+
Copyright Holder(s) and the Author(s) or with their explicit written
72+
permission.
73+
74+
5) The Font Software, modified or unmodified, in part or in whole,
75+
must be distributed entirely under this license, and must not be
76+
distributed under any other license. The requirement for fonts to
77+
remain under this license does not apply to any document created
78+
using the Font Software.
79+
80+
TERMINATION
81+
This license becomes null and void if any of the above conditions are
82+
not met.
83+
84+
DISCLAIMER
85+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+
OTHER DEALINGS IN THE FONT SOFTWARE.
113 KB
Binary file not shown.
112 KB
Binary file not shown.

website/scripts/og-image/cli.mts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { mkdir, writeFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { parseArgs } from 'node:util';
4+
import { renderOgImage } from './render.mts';
5+
6+
const { values } = parseArgs({
7+
options: {
8+
version: { type: 'string', short: 'v' },
9+
description: { type: 'string' },
10+
out: { type: 'string', short: 'o' },
11+
help: { type: 'boolean', short: 'h' },
12+
},
13+
});
14+
15+
if (values.help || !values.version) {
16+
console.log(`Usage: pnpm gen:og --version <ver> [options]
17+
18+
Options:
19+
--version, -v Release version, e.g. 0.5 (required)
20+
--description Optional tagline rendered below the version
21+
--out, -o Output PNG path
22+
(default: rstest-og-image-v<ver>.png in cwd)
23+
--help, -h Show this help
24+
25+
After generating, commit the PNG to rstackjs/rstack-design-resources
26+
under rstest/assets/ so the assets.rspack.rs CDN can serve it.`);
27+
process.exit(values.help ? 0 : 1);
28+
}
29+
30+
const versionSlug = values.version.replace(/\./g, '-');
31+
const outPath = path.resolve(
32+
values.out ?? `rstest-og-image-v${versionSlug}.png`,
33+
);
34+
35+
const png = await renderOgImage({
36+
version: values.version,
37+
description: values.description,
38+
});
39+
40+
await mkdir(path.dirname(outPath), { recursive: true });
41+
await writeFile(outPath, png);
42+
43+
console.log(`Wrote ${outPath} (${(png.length / 1024).toFixed(1)} KB)`);

0 commit comments

Comments
 (0)