diff --git a/package.json b/package.json index 97bc1432c5e..567102e0440 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@babel/preset-react": "^7.18.6", "@mdx-js/mdx": "^2.1.3", "@resvg/resvg-js": "^2.6.2", + "@shuding/opentype.js": "1.4.0-beta.0", "@types/body-scroll-lock": "^2.6.1", "@types/classnames": "^2.2.10", "@types/debounce": "^1.2.1", diff --git a/scripts/generateOgImages.mjs b/scripts/generateOgImages.mjs index 937727a1cb1..bc805d38633 100644 --- a/scripts/generateOgImages.mjs +++ b/scripts/generateOgImages.mjs @@ -14,6 +14,7 @@ import path from 'path'; import satori from 'satori'; import {Resvg} from '@resvg/resvg-js'; import matter from 'gray-matter'; +import opentype from '@shuding/opentype.js'; const ROOT = process.cwd(); const CONTENT_DIR = path.join(ROOT, 'src', 'content'); @@ -34,6 +35,33 @@ const medium = fs.readFileSync( path.join(ROOT, 'public', 'fonts', 'Optimistic_Display_W_Md.ttf') ); +// Title area width: card width minus the horizontal padding (80px per side). +const TITLE_MAX_WIDTH = 1200 - 80 * 2; +const TITLE_MAX_FONT = 96; +const TITLE_MIN_FONT = 56; +// Small margin so borderline titles don't wrap and orphan a single trailing +// character (e.g. the "p" in "renderToStaticMarkup", which is 1px too wide +// to fit on one line at 96px). +const TITLE_SAFETY = 8; + +const boldFont = opentype.parse( + bold.buffer.slice(bold.byteOffset, bold.byteOffset + bold.byteLength) +); + +// Multi-word titles wrap cleanly at spaces, so a length bucket is fine for +// them. Single-word titles (most API names) can only break mid-word, which +// leaves an orphaned letter on its own line, so instead shrink them just +// enough to fit on a single line. +function titleFontSize(title) { + const trimmed = title.trim(); + if (/\s/.test(trimmed)) { + return trimmed.length > 24 ? 72 : 96; + } + const widthPerPx = boldFont.getAdvanceWidth(trimmed, 1); + const fit = Math.floor((TITLE_MAX_WIDTH - TITLE_SAFETY) / widthPerPx); + return Math.max(TITLE_MIN_FONT, Math.min(TITLE_MAX_FONT, fit)); +} + function el(type, style, children) { return {type, props: {style, children}}; } @@ -103,7 +131,7 @@ function card(title, pagePath) { flexGrow: 1, display: 'flex', alignItems: 'center', - fontSize: title.length > 24 ? 72 : 96, + fontSize: titleFontSize(title), fontFamily: 'Optimistic Display Bold', color: '#f6f7f9', lineHeight: 1.1,