Skip to content

Commit

Permalink
fix: use server storage for fonts (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Jan 29, 2024
1 parent 7dd25b6 commit 123ab97
Show file tree
Hide file tree
Showing 17 changed files with 100 additions and 54 deletions.
8 changes: 8 additions & 0 deletions .playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ export default defineNuxtConfig({
},

ogImage: {
fonts: [
{
name: 'optieinstein',
weight: 800,
// path must point to a public font file
path: '/OPTIEinstein-Black.otf',
},
],
// compatibility: {
// runtime: {
// resvg: 'wasm',
Expand Down
2 changes: 0 additions & 2 deletions src/build/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import type { ModuleOptions } from '../module'
export function setupPrerenderHandler(options: ModuleOptions, resolve: Resolver['resolve'], nuxt: Nuxt = useNuxt()) {
nuxt.hooks.hook('nitro:init', async (nitro) => {
nitro.hooks.hook('prerender:config', async (nitroConfig) => {
nitroConfig.serverAssets = nitroConfig.serverAssets || []
nitroConfig.serverAssets!.push({ baseName: 'nuxt-og-image:fonts', dir: resolve('./runtime/server/assets') })
// bindings
applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve })
// avoid wasm handling while prerendering
Expand Down
57 changes: 36 additions & 21 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import {
type AddComponentOptions,
addComponent,
Expand All @@ -18,7 +19,7 @@ import type { SatoriOptions } from 'satori'
import { installNuxtSiteConfig } from 'nuxt-site-config-kit'
import { isDevelopment } from 'std-env'
import { hash } from 'ohash'
import { relative } from 'pathe'
import { basename, join, relative } from 'pathe'
import type { ResvgRenderOptions } from '@resvg/resvg-js'
import type { SharpOptions } from 'sharp'
import { defu } from 'defu'
Expand All @@ -43,7 +44,7 @@ import { setupDevHandler } from './build/dev'
import { setupGenerateHandler } from './build/generate'
import { setupPrerenderHandler } from './build/prerender'
import { setupBuildHandler } from './build/build'
import { checkLocalChrome, checkPlaywrightDependency, isUndefinedOrTruthy } from './util'
import { checkLocalChrome, checkPlaywrightDependency, downloadFont, isUndefinedOrTruthy } from './util'
import { normaliseFontInput } from './runtime/utils.pure'

export interface ModuleOptions {
Expand Down Expand Up @@ -119,6 +120,12 @@ export interface ModuleOptions {
* Manually modify the compatibility.
*/
compatibility?: CompatibilityFlagEnvOverrides
/**
* Use an alternative host for downloading Google Fonts. This is used to support China where Google Fonts is blocked.
*
* When `true` is set will use `fonts.font.im`, otherwise will use a string as the host.
*/
googleFontMirror?: true | string
}

export interface ModuleHooks {
Expand Down Expand Up @@ -259,35 +266,43 @@ export default defineNuxtModule<ModuleOptions>({
if (preset === 'cloudflare' || preset === 'cloudflare-module') {
config.fonts = config.fonts.filter((f) => {
if (typeof f !== 'string' && f.path) {
logger.warn(`The ${f.name}:${f.weight} font was skipped because remote fonts are not available in Cloudflare Workers, please use a Google font.`)
logger.warn(`The ${f.name}:${f.weight} font was skipped because remote fonts are not available in Cloudflare Workers, please use a Google Font.`)
return false
}
return true
})
}
if (preset === 'stackblitz') {
// TODO maybe only for stackblitz, this will increase server bundle size
config.fonts = config.fonts.map((f) => {
if (typeof f === 'string' && f.startsWith('Inter:')) {
const [_, weight] = f.split(':')
return {
name: 'Inter',
weight,
// nuxt server assets
key: `nuxt-og-image:fonts:inter-latin-ext-${weight}-normal.woff`,
const serverFontsDir = resolve('./runtime/nitro/fonts')
config.fonts = await Promise.all(normaliseFontInput(config.fonts)
.map(async (f) => {
if (!f.key && !f.path) {
if (await downloadFont(f, serverFontsDir, config.googleFontMirror)) {
// move file to serverFontsDir
f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.ttf.base64`
}
}
else if (f.path) {
// move to assets folder as base64 and set key
const fontPath = join(nuxt.options.rootDir, nuxt.options.dir.public, f.path)
const fontData = await readFile(fontPath, 'base64')
f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.ttf.base64`
await writeFile(resolve(serverFontsDir, `${basename(f.path)}.base64`), fontData)
}
return f
}))
config.fonts = config.fonts.map((f) => {
if (preset === 'stackblitz') {
if (typeof f === 'string' || (!f.path && !f.key)) {
logger.warn(`The ${typeof f === 'string' ? f : `${f.name}:${f.weight}`} font was skipped because remote fonts are not available in StackBlitz, please use a local font.`)
return false
}
return f
}).filter(Boolean) as InputFontConfig[]
nuxt.hooks.hook('nitro:config', (nitroConfig) => {
nitroConfig.serverAssets = nitroConfig.serverAssets || []
nitroConfig.serverAssets!.push({ baseName: 'nuxt-og-image:fonts', dir: resolve('./runtime/server/assets') })
})
}
}
return f
}).filter(Boolean) as InputFontConfig[]

// bundle fonts within nitro runtime
nuxt.options.nitro.serverAssets = nuxt.options.nitro.serverAssets || []
nuxt.options.nitro.serverAssets!.push({ baseName: 'nuxt-og-image:fonts', dir: serverFontsDir })

nuxt.options.experimental.componentIslands = true

Expand Down Expand Up @@ -420,7 +435,7 @@ export default defineNuxtModule<ModuleOptions>({
// need to sort by longest first so we don't replace the wrong part of the string
.sort((a, b) => b.length - a.length)
.reduce((name, dir) => {
// only replace from the start of the string
// only replace from the start of the string
return name.replace(new RegExp(`^${dir}`), '')
}, component.pascalName)
return ` '${name}': typeof import('${relativeComponentPath}')['default']`
Expand Down
1 change: 1 addition & 0 deletions src/runtime/nitro/fonts/Inter-400.ttf.base64

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/runtime/nitro/fonts/Inter-700.ttf.base64

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createError, defineEventHandler, getQuery, proxyRequest, sendRedirect, setHeader } from 'h3'
import { parseURL } from 'ufo'
import { prefixStorage } from 'unstorage'
import { getExtension, normaliseFontInput, useOgImageRuntimeConfig } from '../../../utils'
import type { ResolvedFontConfig } from '../../../types'
import { assets } from '#internal/nitro/virtual/server-assets'
import { useStorage } from '#imports'

const assets = prefixStorage(useStorage(), '/assets')

// /__og-image__/font/<name>/<weight>.ttf
export default defineEventHandler(async (e) => {
Expand Down Expand Up @@ -35,12 +38,12 @@ export default defineEventHandler(async (e) => {
})
}

if (import.meta.dev || import.meta.prerender) {
// check cache first, this uses Nuxt server assets
if (font.key && await assets.hasItem(font.key)) {
setHeader(e, 'Content-Type', `font/${getExtension(font.path!)}`)
return assets.getItemRaw<ArrayBuffer>(font.key)
}
// check cache first, this uses Nuxt server assets
if (font.key && await assets.hasItem(font.key)) {
setHeader(e, 'Content-Type', `font/${getExtension(font.path!)}`)
const fontData = await assets.getItemRaw<string>(font.key)
// buf is a string need to convert it to a buffer
return Buffer.from(fontData!, 'base64')
}

// using H3Event $fetch will cause the request headers not to be sent
Expand Down
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { writeFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { Launcher } from 'chrome-launcher'
import { tryResolveModule } from '@nuxt/kit'
import { isCI } from 'std-env'
import { $fetch } from 'ofetch'
import { join } from 'pathe'
import type { ResolvedFontConfig } from './runtime/types'

export const isUndefinedOrTruthy = (v?: any) => typeof v === 'undefined' || v !== false

Expand All @@ -20,3 +25,33 @@ export function checkLocalChrome() {
export async function checkPlaywrightDependency() {
return !!(await tryResolveModule('playwright'))
}

export async function downloadFont(font: ResolvedFontConfig, outputPath: string, mirror?: true | string) {
const { name, weight } = font
const fontPath = join(outputPath, `${name}-${weight}.ttf.base64`)
if (existsSync(fontPath))
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}`, {
headers: {
// Make sure it returns TTF.
'User-Agent':
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
},
})
if (!css)
return false

const ttfResource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
if (ttfResource?.[1]) {
const buf = await $fetch(ttfResource[1], { baseURL: host, responseType: 'arrayBuffer' })
// need to base 64 the buf
const base64Font = Buffer.from(buf).toString('base64')
// output to outputPath
await writeFile(fontPath, base64Font)
return true
}
return false
}
8 changes: 4 additions & 4 deletions test/integration/endpoints/debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ describe('debug', () => {
"fonts": [
{
"cacheKey": "Inter:400",
"key": "nuxt-og-image:fonts:inter-latin-ext-400-normal.woff",
"key": "nuxt-og-image:fonts:Inter-400.ttf.base64",
"name": "Inter",
"path": "/__og-image__/font/Inter/400.ttf",
"path": "",
"style": "normal",
"weight": "400",
},
{
"cacheKey": "Inter:700",
"key": "nuxt-og-image:fonts:inter-latin-ext-700-normal.woff",
"key": "nuxt-og-image:fonts:Inter-700.ttf.base64",
"name": "Inter",
"path": "/__og-image__/font/Inter/700.ttf",
"path": "",
"style": "normal",
"weight": "700",
},
Expand Down
2 changes: 1 addition & 1 deletion test/integration/endpoints/fonts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('fonts', () => {
Blob {
Symbol(kHandle): Blob {},
Symbol(kLength): 310808,
Symbol(kType): "font/ttf",
Symbol(kType): "",
}
`)
}, 60000)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 2 additions & 17 deletions test/integration/endpoints/satori/html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,6 @@ describe('html', () => {
display: inline-block;
}
</style>
<style>
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('/__og-image__/font/Inter/400.ttf') format('truetype');
}
</style>
<style>
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url('/__og-image__/font/Inter/700.ttf') format('truetype');
}
</style>
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime/preset-wind.global.js"></script>
<script>
window.__unocss = {
Expand All @@ -59,7 +43,8 @@ describe('html', () => {
}
</script>
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime/core.global.js"></script>
<link href="https://cdn.jsdelivr.net/npm/gardevoir" rel="stylesheet"></head>
<link href="https://cdn.jsdelivr.net/npm/gardevoir" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"></head>
<body ><div data-v-inspector-ignore="true" style="position: relative; display: flex; margin: 0 auto; width: 1200px; height: 600px; overflow: hidden;"><div class="w-full h-full flex justify-between relative p-[60px] bg-white text-gray-900" data-island-uid><div class="flex absolute top-0 right-[-100%]" style="width:200%;height:200%;background-image:radial-gradient(circle, rgba(0, 220, 130, 0.5) 0%, rgba(255, 255, 255, 0.7) 50%, rgba(255, 255, 255, 0) 70%);"></div><div class="h-full w-full justify-between relative"><div class="flex flex-row justify-between items-start"><div class="flex flex-col w-full max-w-[65%]"><h1 class="m-0 font-bold mb-[30px] text-[75px]">Hello World</h1><!----></div><!----></div><div class="flex flex-row justify-center items-center text-left w-full"><!--[--><svg height="50" width="50" class="mr-3" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path fill="#00dc82" d="M62.3,-53.9C74.4,-34.5,73.5,-9,67.1,13.8C60.6,36.5,48.7,56.5,30.7,66.1C12.7,75.7,-11.4,74.8,-31.6,65.2C-51.8,55.7,-67.9,37.4,-73.8,15.7C-79.6,-6,-75.1,-31.2,-61.1,-51C-47.1,-70.9,-23.6,-85.4,0.8,-86C25.1,-86.7,50.2,-73.4,62.3,-53.9Z" transform="translate(100 100)"></path></svg><p style="font-size:25px;" class="font-bold">nuxt-og-image</p><!--]--></div></div></div></div></body>
</html>"
`)
Expand Down
Loading

0 comments on commit 123ab97

Please sign in to comment.