From 8c8cb972b70cdbe188c33e69c11006c84d1a6127 Mon Sep 17 00:00:00 2001 From: harlan Date: Tue, 14 Jan 2025 15:29:25 +1100 Subject: [PATCH] fix: support `ital` font families Fixes #300 --- docs/content/3.guides/5.custom-fonts.md | 3 ++- src/module.ts | 16 +++++++++------- src/pure.ts | 17 ++++++++++++++--- src/runtime/pure.ts | 17 ++++++++++++++--- src/runtime/server/og-image/templates/html.ts | 6 +++--- src/runtime/types.ts | 4 ++-- src/util.ts | 6 +++--- 7 files changed, 47 insertions(+), 22 deletions(-) diff --git a/docs/content/3.guides/5.custom-fonts.md b/docs/content/3.guides/5.custom-fonts.md index 973ad62a..661d20eb 100644 --- a/docs/content/3.guides/5.custom-fonts.md +++ b/docs/content/3.guides/5.custom-fonts.md @@ -25,7 +25,8 @@ export default defineNuxtConfig({ fonts: [ // will load the Noto Sans font from Google fonts 'Noto+Sans:400', - 'Noto+Sans:700' + 'Noto+Sans:700', + 'Work+Sans:ital:400' ] } }) diff --git a/src/module.ts b/src/module.ts index 60dd2ddb..de110e13 100644 --- a/src/module.ts +++ b/src/module.ts @@ -323,17 +323,19 @@ export default defineNuxtModule({ }) config.fonts = (await Promise.all(normaliseFontInput(config.fonts) .map(async (f) => { + const fontKey = `${f.name}:${f.style}:${f.weight}` + const fontFileBase = fontKey.replaceAll(':', '-') if (!f.key && !f.path) { if (preset === 'stackblitz') { - logger.warn(`The ${f.name}:${f.weight} font was skipped because remote fonts are not available in StackBlitz, please use a local font.`) + logger.warn(`The ${fontKey} font was skipped because remote fonts are not available in StackBlitz, please use a local font.`) return false } if (await downloadFont(f, fontStorage, config.googleFontMirror)) { // move file to serverFontsDir - f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.ttf.base64` + f.key = `nuxt-og-image:fonts:${fontFileBase}.ttf.base64` } else { - logger.warn(`Failed to download font ${f.name}:${f.weight}. You may be offline or behind a firewall blocking Google. Consider setting \`googleFontMirror: true\`.`) + logger.warn(`Failed to download font ${fontKey}. You may be offline or behind a firewall blocking Google. Consider setting \`googleFontMirror: true\`.`) return false } } @@ -341,7 +343,7 @@ export default defineNuxtModule({ // validate the extension, can only be woff, ttf or otf const extension = basename(f.path.replace('.base64', '')).split('.').pop()! if (!['woff', 'ttf', 'otf'].includes(extension)) { - logger.warn(`The ${f.name}:${f.weight} font was skipped because the file extension ${extension} is not supported. Only woff, ttf and otf are supported.`) + logger.warn(`The ${fontKey} font was skipped because the file extension ${extension} is not supported. Only woff, ttf and otf are supported.`) return false } // resolve relative paths from public dir @@ -349,12 +351,12 @@ export default defineNuxtModule({ if (!f.absolutePath) f.path = resolve(publicDirAbs, withoutLeadingSlash(f.path)) if (!existsSync(f.path)) { - logger.warn(`The ${f.name}:${f.weight} font was skipped because the file does not exist at path ${f.path}.`) + logger.warn(`The ${fontKey} font was skipped because the file does not exist at path ${f.path}.`) return false } const fontData = await readFile(f.path, f.path.endsWith('.base64') ? 'utf-8' : 'base64') - f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.${extension}.base64` - await fontStorage.setItem(`${f.name}-${f.weight}.${extension}.base64`, fontData) + f.key = `nuxt-og-image:fonts:${fontFileBase}.${extension}.base64` + await fontStorage.setItem(`${fontFileBase}.${extension}.base64`, fontData) delete f.path delete f.absolutePath } diff --git a/src/pure.ts b/src/pure.ts index 0d0bcece..54f284bf 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -4,17 +4,28 @@ import type { InputFontConfig, ResolvedFontConfig } from './runtime/types' export function normaliseFontInput(fonts: InputFontConfig[]): ResolvedFontConfig[] { return fonts.map((f) => { if (typeof f === 'string') { - const [name, weight] = f.split(':') + const vals = f.split(':') + const includesStyle = vals.length === 3 + let name, weight, style + if (includesStyle) { + name = vals[0] + style = vals[1] + weight = vals[2] + } + else { + name = vals[0] + weight = vals[1] + } return { cacheKey: f, name, weight: weight || 400, - style: 'normal', + style: style || 'normal', path: undefined, } } return { - cacheKey: f.key || `${f.name}:${f.weight}`, + cacheKey: f.key || `${f.name}:${f.style}:${f.weight}`, style: 'normal', weight: 400, ...f, diff --git a/src/runtime/pure.ts b/src/runtime/pure.ts index 2a568dd5..cc8585ec 100644 --- a/src/runtime/pure.ts +++ b/src/runtime/pure.ts @@ -79,17 +79,28 @@ export function separateProps(options: OgImageOptions | undefined, ignoreKeys: s export function normaliseFontInput(fonts: InputFontConfig[]): ResolvedFontConfig[] { return fonts.map((f) => { if (typeof f === 'string') { - const [name, weight] = f.split(':') + const vals = f.split(':') + const includesStyle = vals.length === 3 + let name, weight, style + if (includesStyle) { + name = vals[0] + style = vals[1] + weight = vals[2] + } + else { + name = vals[0] + weight = vals[1] + } return { cacheKey: f, name, weight: weight || 400, - style: 'normal', + style: style || 'normal', path: undefined, } } return { - cacheKey: f.key || `${f.name}:${f.weight}`, + cacheKey: f.key || `${f.name}:${f.style}:${f.weight}`, style: 'normal', weight: 400, ...f, diff --git a/src/runtime/server/og-image/templates/html.ts b/src/runtime/server/og-image/templates/html.ts index aaa6c28c..88b6475a 100644 --- a/src/runtime/server/og-image/templates/html.ts +++ b/src/runtime/server/og-image/templates/html.ts @@ -28,7 +28,7 @@ export async function html(ctx: OgImageRenderEventContext) { const normalisedFonts = normaliseFontInput([...options.fonts || [], ...fonts]) const firstFont = normalisedFonts[0] as FontConfig if (firstFont) - defaultFontFamily = firstFont.name + defaultFontFamily = firstFont.name.replaceAll('+', ' ') await applyEmojis(ctx, island) let html = island.html @@ -37,7 +37,7 @@ export async function html(ctx: OgImageRenderEventContext) { style: [ { // default font is the first font family - innerHTML: `body { font-family: \'${defaultFontFamily.replace('+', ' ')}\', sans-serif; }`, + innerHTML: `body { font-family: \'${defaultFontFamily}\', sans-serif; }`, }, { innerHTML: `body { @@ -73,7 +73,7 @@ svg[data-emoji] { .map((font) => { return ` @font-face { - font-family: '${font.name}'; + font-family: '${font.name.replaceAll('+', ' ')}'; font-style: normal; font-weight: ${font.weight}; src: url('/__og-image__/font/${font.key}') format('truetype'); diff --git a/src/runtime/types.ts b/src/runtime/types.ts index e2da395f..ee4f0aa0 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -139,7 +139,7 @@ export interface OgImageOptions { export interface FontConfig { name: string - style?: string + style?: 'normal' | 'ital' weight?: string | number path?: string key?: string @@ -147,7 +147,7 @@ export interface FontConfig { } export interface ResolvedFontConfig extends FontConfig { cacheKey: string, data?: BufferSource } -export type InputFontConfig = (`${string}:${number}` | string | FontConfig) +export type InputFontConfig = (`${string}:${number}` | `${string}:${'normal' | 'ital'}:${number}` | string | FontConfig) export interface RuntimeCompatibilitySchema { chromium: 'chrome-launcher' | 'on-demand' | 'playwright' | false diff --git a/src/util.ts b/src/util.ts index 0a6f07a7..a6289a36 100644 --- a/src/util.ts +++ b/src/util.ts @@ -25,14 +25,14 @@ export async function checkPlaywrightDependency() { } export async function downloadFont(font: ResolvedFontConfig, storage: Storage, mirror?: true | string) { - const { name, weight } = font - const key = `${name}-${weight}.ttf.base64` + const { name, weight, style } = font + const key = `${name}-${style}-${weight}.ttf.base64` if (await storage.hasItem(key)) return true const host = typeof mirror === 'undefined' ? 'fonts.googleapis.com' : mirror === true ? 'fonts.font.im' : mirror // using H3Event $fetch will cause the request headers not to be sent - const css = await $fetch(`https://${host}/css2?family=${name}:wght@${weight}`, { + const css = await $fetch(`https://${host}/css2?family=${name}:${style === 'ital' ? `ital,wght@1,${weight}` : `wght@${weight}`}`, { timeout: 10 * 1000, // 10 second timeout headers: { // Make sure it returns TTF.