1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-04-17 10:28:46 -04:00

fix: load OG image fonts from @fontsource/inter instead of Google Fonts

Vercel's build infra was intermittently hitting ETIMEDOUT against
fonts.googleapis.com, causing OG image generation errors during
prerender. Ship the Inter .woff files with the function via
outputFileTracingIncludes so the build is network-free. Also add
turbopackIgnore hints on process.cwd() calls to silence an NFT warning
that was over-tracing next.config.ts into the route bundle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 15:44:40 -04:00
parent 5a3c7b9613
commit e59aee63c5
5 changed files with 33 additions and 38 deletions

View File

@@ -5,9 +5,24 @@ import { notFound } from "next/navigation";
import { ImageResponse } from "next/og";
import siteConfig from "@/lib/config/site";
import { loadGoogleFont } from "@/lib/og-utils";
import { getFrontMatter, getSlugs, POSTS_DIR } from "@/lib/posts";
// Reading Inter fonts from the local @fontsource/inter package (instead of
// fetching Google Fonts at build time) avoids flaky network timeouts on
// Vercel's build infra that caused OG image generation to fail intermittently.
// Satori supports .woff but not .woff2. The two file paths are listed explicitly
// in next.config.ts under `outputFileTracingIncludes` so NFT ships them with
// the function output.
const loadInterFont = async (weight: 400 | 600): Promise<ArrayBuffer> => {
const fontPath = path.join(
/* turbopackIgnore: true */ process.cwd(),
"node_modules/@fontsource/inter/files",
`inter-latin-${weight}-normal.woff`,
);
const buffer = await fs.promises.readFile(fontPath);
return Uint8Array.from(buffer).buffer;
};
export const contentType = "image/png";
export const size = {
// https://developers.facebook.com/docs/sharing/webmasters/images/
@@ -28,7 +43,7 @@ const getLocalImage = async (src: string): Promise<ArrayBuffer | string> => {
// https://stackoverflow.com/questions/5775469/whats-the-valid-way-to-include-an-image-with-no-src/14115340#14115340
const NO_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
const imagePath = path.join(process.cwd(), src);
const imagePath = path.join(/* turbopackIgnore: true */ process.cwd(), src);
try {
if (!fs.existsSync(imagePath)) {
@@ -67,10 +82,7 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> })
getLocalImage("app/avatar.jpg"),
]);
const [fontRegular, fontSemibold] = await Promise.all([
loadGoogleFont("Inter", 400),
loadGoogleFont("Inter", 600),
]);
const [fontRegular, fontSemibold] = await Promise.all([loadInterFont(400), loadInterFont(600)]);
// template is HEAVILY inspired by https://og-new.clerkstage.dev/
return new ImageResponse(

View File

@@ -1,31 +0,0 @@
import { cacheLife } from "next/cache";
// Load a Google Font from the Google Fonts API
// Adapted from https://github.com/brianlovin/briOS/blob/f72dc33a11194de45c80337b22be4560da62ad7e/src/lib/og-utils.tsx#L32
export async function loadGoogleFont(font: string, weight: number): Promise<ArrayBuffer> {
"use cache";
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
const cssResponse = await fetch(url, {
next: {
revalidate: 31_536_000, // 1 year
},
});
const css = await cssResponse.text();
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
if (resource) {
const fontResponse = await fetch(resource[1], {
next: {
revalidate: 31_536_000, // 1 year
},
});
if (fontResponse.status === 200) {
cacheLife("max"); // cache indefinitely if successful
return fontResponse.arrayBuffer();
}
}
throw new Error(`Failed to load font: ${font} ${weight}`);
}

View File

@@ -18,7 +18,12 @@ const nextConfig = {
],
},
outputFileTracingIncludes: {
"/notes/[slug]/opengraph-image": ["./notes/**/*", "./app/opengraph-image.jpg"],
"/notes/[slug]/opengraph-image": [
"./notes/**/*",
"./app/opengraph-image.jpg",
"./node_modules/**/@fontsource/inter/files/inter-latin-400-normal.woff",
"./node_modules/**/@fontsource/inter/files/inter-latin-600-normal.woff",
],
},
productionBrowserSourceMaps: true,
experimental: {

View File

@@ -23,6 +23,7 @@
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@fontsource/inter": "^5.2.8",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "16.2.3",

8
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@base-ui/react':
specifier: ^1.3.0
version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@fontsource/inter':
specifier: ^5.2.8
version: 5.2.8
'@mdx-js/loader':
specifier: ^3.1.1
version: 3.1.1
@@ -949,6 +952,9 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@fontsource/inter@5.2.8':
resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
'@hono/node-server@1.19.13':
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
engines: {node: '>=18.14.1'}
@@ -4702,6 +4708,8 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
'@fontsource/inter@5.2.8': {}
'@hono/node-server@1.19.13(hono@4.12.12)':
dependencies:
hono: 4.12.12