Skip to content

Commit cf94236

Browse files
committed
fix(library): re-render covers every run and add a sync check
Covers are derived artifacts, so skipping outputs that already exist left an image showing the old title after a post's frontmatter `title` changed. Every run now re-renders from scratch; rendering is deterministic, so an unchanged title re-encodes to identical bytes and a full run stays a no-op in git. Replaces `--force` (now the default) with `--check`, which renders in memory and compares against the committed bytes without writing, so CI can catch both a stale cover and the missing-cover case that caused the original breakage.
1 parent 074f2a3 commit cf94236

2 files changed

Lines changed: 42 additions & 10 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"mship:generate": "bun run scripts/generate-mship-contracts.ts",
5959
"mship:check": "bun run scripts/generate-mship-contracts.ts --check",
6060
"library:covers": "bun run scripts/generate-library-covers.tsx",
61+
"library:covers:check": "bun run scripts/generate-library-covers.tsx --check",
6162
"skills:sync": "bun run scripts/sync-skills.ts",
6263
"skills:check": "bun run scripts/sync-skills.ts --check",
6364
"setup": "bun run scripts/setup/index.ts setup",

scripts/generate-library-covers.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
* top-left, a diagonal open arrow top-right, and the post title set large at
66
* the bottom-left. The same template is rendered at request time for docs
77
* pages by `apps/docs/app/api/og/route.tsx`; this script is the build-time
8-
* equivalent for library posts, whose covers ship as static assets so
9-
* `next/image` can optimize them and the SEO builders can probe their real
10-
* dimensions.
8+
* equivalent for library posts, whose covers ship as static assets because
9+
* they are rendered with `unoptimized` (see #5528) and the SEO builders probe
10+
* their real dimensions off disk.
11+
*
12+
* Covers are derived artifacts, not source of truth: the title in the image
13+
* comes from frontmatter, so editing a post's `title` makes its committed
14+
* cover stale. Every run therefore re-renders from scratch rather than
15+
* skipping outputs that already exist. Rendering is deterministic — an
16+
* unchanged title re-encodes to identical bytes — so a full run is a no-op in
17+
* git for everything that did not actually change.
1118
*
1219
* Usage, from the repo root:
13-
* bun run library:covers # write covers for posts that lack one
14-
* bun run library:covers --force # rewrite every post's cover
15-
* bun run library:covers <slug>... # rewrite only the named posts
20+
* bun run library:covers # re-render every post's cover
21+
* bun run library:covers <slug>... # re-render only the named posts
22+
* bun run library:covers --check # verify committed covers are in sync
1623
*/
1724

1825
import { existsSync } from 'node:fs'
@@ -218,7 +225,7 @@ function readFrontmatterTitle(source: string): string | null {
218225

219226
async function main() {
220227
const args = process.argv.slice(2)
221-
const force = args.includes('--force')
228+
const check = args.includes('--check')
222229
const only = new Set(args.filter((arg) => !arg.startsWith('--')))
223230

224231
const font = await readFile(FONT_PATH)
@@ -239,25 +246,49 @@ async function main() {
239246
throw new Error(`No library post for: ${unknown.join(', ')}`)
240247
}
241248

249+
const stale: string[] = []
242250
let written = 0
251+
243252
for (const slug of slugs) {
244253
if (only.size > 0 && !only.has(slug)) continue
245254

246255
const outputPath = path.join(OUTPUT_DIR, slug, 'cover.jpg')
247-
if (only.size === 0 && !force && existsSync(outputPath)) continue
248-
249256
const source = await readFile(path.join(CONTENT_DIR, slug, 'index.mdx'), 'utf8')
250257
const title = readFrontmatterTitle(source)
251258
if (!title) {
252259
throw new Error(`Could not read a \`title\` from frontmatter of ${slug}/index.mdx`)
253260
}
254261

262+
const cover = await renderCover(title, fontData, measure)
263+
264+
if (check) {
265+
const committed = existsSync(outputPath) ? await readFile(outputPath) : null
266+
if (committed === null) {
267+
stale.push(`${slug} — no cover committed`)
268+
} else if (!committed.equals(cover)) {
269+
stale.push(`${slug} — committed cover does not match "${title}"`)
270+
}
271+
continue
272+
}
273+
255274
await mkdir(path.dirname(outputPath), { recursive: true })
256-
await writeFile(outputPath, await renderCover(title, fontData, measure))
275+
await writeFile(outputPath, cover)
257276
written += 1
258277
console.log(`✓ ${slug}/cover.jpg — "${title}"`)
259278
}
260279

280+
if (check) {
281+
if (stale.length > 0) {
282+
console.error(`${stale.length} library cover(s) out of sync with frontmatter:\n`)
283+
for (const entry of stale) console.error(` ✗ ${entry}`)
284+
console.error('\nRun `bun run library:covers` and commit the result.')
285+
process.exitCode = 1
286+
return
287+
}
288+
console.log('All library covers are in sync with their frontmatter titles.')
289+
return
290+
}
291+
261292
console.log(`\n${written} cover${written === 1 ? '' : 's'} written to apps/sim/public/library/`)
262293
}
263294

0 commit comments

Comments
 (0)