Skip to content

feat(cms): redesign article thumbnail generator for new corporate design#35

Merged
jhb-dev merged 1 commit into
mainfrom
claude/update-article-image-design-fuNeB
May 9, 2026
Merged

feat(cms): redesign article thumbnail generator for new corporate design#35
jhb-dev merged 1 commit into
mainfrom
claude/update-article-image-design-fuNeB

Conversation

@jhb-dev
Copy link
Copy Markdown
Contributor

@jhb-dev jhb-dev commented May 9, 2026

Overhauls the article thumbnail generator to align with the JHB corporate design system, replacing the previous gradient/blob background system with a clean, frame-based layout using the design tokens.

Summary

The thumbnail endpoint has been completely redesigned to match the corporate visual identity. Instead of customizable gradient backgrounds with optional blob overlays, thumbnails now feature a consistent hairline frame with corner registration marks, a mono eyebrow label, and the JHB mark anchored in the bottom-left. The design supports two themes (light/dark) that map to the design system's color tokens.

Key Changes

  • Replaced background system: Removed BackgroundInput type with its preset/pattern/colors/accents/noise options. Replaced with simple theme parameter ('light' | 'dark')
  • New color palette system: Introduced ThemePalette type mapping design tokens (background, border, cornerMark, eyebrow, subtitle, title, etc.) to specific colors per theme
  • Updated typography: Switched from Montserrat (400/700) to Geist (400/600) + Geist Mono (500) for better alignment with corporate design
  • New layout structure:
    • Added hairline frame inset from edges with corner registration marks at each corner
    • Eyebrow label with leading backslash (always tertiary) positioned at top of content area
    • Title/subtitle stack hangs from eyebrow with generous breathing room
    • Logo anchored to bottom-left within content padding
  • Removed noise overlay: Eliminated the film-grain noise tile generation and compositing logic
  • Updated logo: Replaced scaled-down SVG paths with full-resolution JHB mark (inlined as LOGO_INNER)
  • Simplified API: Removed background object parameter; now accepts eyebrow (string, defaults to 'ARTICLE') and theme (enum)

Implementation Details

  • Layout uses frame inset (64px) and content padding (80px) constants for consistent spacing
  • Corner marks are 32px crosshairs rendered at each frame corner
  • Eyebrow uses mono font with uppercase text and custom letter-spacing
  • Font caching in /tmp persists across function invocations for performance
  • SVG generation simplified with direct palette color references instead of preset resolution logic

https://claude.ai/code/session_01TYfRa7SiLgA62om3QamM4h

Replaces the old gradient/blob/preset thumbnail with the new design
language from web/design.md: flat surface, hairline frame, tertiary
corner registration marks, mono \ EYEBROW thread, Geist Sans title +
subtitle, JHB primary square mark in the bottom-left.

Endpoint shape narrows to title/subtitle/eyebrow/theme — the obsolete
background.preset/colors/accents/pattern/noise options are dropped
since the new system has no gradients or accent fills.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
website Ignored Ignored May 9, 2026 9:48pm
website-cms Ignored Ignored May 9, 2026 9:48pm

Request Review

@jhb-dev jhb-dev changed the title Redesign thumbnail generator with corporate design system feat(cms): redesign article thumbnail generator for new corporate design May 9, 2026
@claude
Copy link
Copy Markdown

claude Bot commented May 9, 2026

Code Review — PR #35: Redesign thumbnail generator with corporate design system

Overview

Clean, well-scoped redesign. Removing the multi-preset gradient/blob system in favour of two opinionated theme palettes is a clear simplification, and the new frame/eyebrow layout aligns with the site's visual identity. The diff is easy to follow and the deleted code (noise tile, blob SVG filters, preset resolver) is straightforwardly removed without loose ends.


Potential Bugs

1. Font family name resolved from TTF metadata, not cache filename

resvg resolves font-family from the TTF's internal name table, not from the filename used in the cache. The SVG uses:

font-family="Geist Mono"

Geist Mono TTFs from Google Fonts typically report "Geist Mono" as their family name, so this is likely fine — but the eyebrow text silently falls back to defaultFontFamily ("Geist") if the name doesn't match exactly. Worth a quick smoke-test to confirm the mono font is actually applied.

2. Stale rejected promise in ensureFonts

fontPathsPromise is set before the async work begins and is never reset on failure:

fontPathsPromise = (async () => {
  // ... fetch can throw or return !res.ok
})()
return fontPathsPromise

If a font fetch fails (transient network error), the rejected promise is permanently cached for the lifetime of the function instance. All subsequent requests will hit the same rejection without retrying. The previous code had the same issue — but since this endpoint is being touched, it's worth fixing:

fontPathsPromise = (async () => { ... })().catch((err) => {
  fontPathsPromise = null
  throw err
})

3. logoScale is always 1.0

logoSize = 96 and LOGO_INNER contains <rect width="96" height="96">, so logoScale = 96 / 96 = 1. The scale(${logoScale}) in the SVG transform is a no-op. Not a bug, but it's dead generality — either remove the variable or change logoSize to a different value to make the scaling meaningful.


Performance Considerations

  • Removing the noise compositing step and blob SVG filters is a net win — the sharp pipeline is simpler and resvg has less SVG work to do.
  • Font caching in /tmp with promise deduplication is unchanged and correct.
  • The LOGO_INNER path data has extremely high-precision coordinates (many decimal places from the SVG export). This marginally inflates the SVG string size on every render. Could be rounded to 2 decimal places without any visual difference at 1920×1080.

Security Considerations

  • eyebrow user input is correctly passed through escapeXml in the SVG output. No injection risk.
  • title and subtitle go through wrapText then escapeXml — same as before, fine.
  • No new attack surface introduced.
  • Minor: eyebrow has no length guard. A long string won't wrap (unlike title/subtitle) and will overflow the frame. A clamp similar to the filename sanitisation (body.eyebrow.trim().slice(0, N)) would be defensive. The filename already caps at 60 chars — capping eyebrow at ~40 chars would be consistent.

Minor Style Notes

  • The eyebrowSvg and cornerMarks template-literal lines are very long (200+ chars). Extracting them into small helper functions (similar to titleTspans / subtitleTspans) would keep buildThumbnailSvg readable.
  • FONT_URLS is typed Record<string, string> where a union literal key type (e.g. 'geist-400' | 'geist-600' | 'geistmono-500') would catch a typo at compile time, consistent with how PALETTES is typed as Record<Theme, ThemePalette>.

Summary

Correctness ✅ Logic is sound; theme defaulting and eyebrow defaulting are clean
Conventions ✅ Follows CMS endpoint patterns
Performance ✅ Net improvement
Security ✅ No new issues; one minor defensive hardening opportunity (eyebrow length)
Blocking issues The stale-promise bug (#2) is worth fixing before merge — it can cause permanent failures after a transient font-fetch error with no recovery path short of a cold start

🤖 Generated with Claude Code

@jhb-dev jhb-dev merged commit c8ffb91 into main May 9, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants