grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add CJK and emoji font fallbacks to OG images

Add Noto Sans JP and Noto Emoji as fallback fonts so display names and
gallery titles with Japanese characters or emoji render correctly in
OpenGraph images instead of showing tofu.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+38 -10
server/og/NotoEmoji-Regular.ttf

This is a binary file and will not be displayed.

server/og/NotoSansJP-Regular.ttf

This is a binary file and will not be displayed.

+32 -6
server/og/fonts.ts
··· 1 1 import { readFileSync } from "node:fs"; 2 2 import { resolve } from "node:path"; 3 3 4 - let syneFontData: ArrayBuffer | null = null; 4 + const cache = new Map<string, ArrayBuffer>(); 5 + 6 + function loadFont(filename: string) { 7 + let data = cache.get(filename); 8 + if (!data) { 9 + data = readFileSync(resolve(import.meta.dirname, filename)).buffer; 10 + cache.set(filename, data); 11 + } 12 + return data; 13 + } 5 14 6 15 export function syneBrandFont() { 7 - if (!syneFontData) { 8 - const path = resolve(import.meta.dirname, "Syne-ExtraBold.ttf"); 9 - syneFontData = readFileSync(path).buffer; 10 - } 11 16 return { 12 17 name: "Syne", 13 - data: syneFontData, 18 + data: loadFont("Syne-ExtraBold.ttf"), 14 19 weight: 800 as const, 15 20 style: "normal" as const, 16 21 }; 17 22 } 23 + 24 + export function fallbackFonts() { 25 + return [ 26 + { 27 + name: "Noto Sans JP", 28 + data: loadFont("NotoSansJP-Regular.ttf"), 29 + weight: 400 as const, 30 + style: "normal" as const, 31 + }, 32 + { 33 + name: "Noto Emoji", 34 + data: loadFont("NotoEmoji-Regular.ttf"), 35 + weight: 400 as const, 36 + style: "normal" as const, 37 + }, 38 + ]; 39 + } 40 + 41 + export function allFonts() { 42 + return [syneBrandFont(), ...fallbackFonts()]; 43 + }
+2 -2
server/og/gallery.ts
··· 1 1 import { defineOG } from "$hatk"; 2 2 import type { GrainActorProfile, Photo } from "$hatk"; 3 - import { syneBrandFont } from "./fonts.ts"; 3 + import { allFonts } from "./fonts.ts"; 4 4 5 5 export default defineOG("/og/profile/:did/gallery/:rkey", async (ctx) => { 6 6 const { db, params, fetchImage, lookup, blobUrl } = ctx; ··· 241 241 ], 242 242 }, 243 243 }, 244 - options: { fonts: [syneBrandFont()] }, 244 + options: { fonts: allFonts() }, 245 245 meta: { 246 246 title: `${gallery.title} by @${author?.handle || did.slice(0, 24)} — Grain`, 247 247 description: gallery.description || `Photo gallery on Grain`,
+2
server/og/profile.ts
··· 1 1 import { defineOG } from "$hatk"; 2 2 import type { GrainActorProfile } from "$hatk"; 3 + import { fallbackFonts } from "./fonts.ts"; 3 4 4 5 export default defineOG("/og/profile/:did", async (ctx) => { 5 6 const { db, params, fetchImage, lookup, blobUrl } = ctx; ··· 195 196 ], 196 197 }, 197 198 }, 199 + options: { fonts: fallbackFonts() }, 198 200 meta: { 199 201 title: `${displayName} (@${handle}) — Grain`, 200 202 description: description || `@${handle} on Grain`,
+2 -2
server/og/story.ts
··· 1 1 import { defineOG } from "$hatk"; 2 2 import type { GrainActorProfile } from "$hatk"; 3 - import { syneBrandFont } from "./fonts.ts"; 3 + import { allFonts } from "./fonts.ts"; 4 4 5 5 export default defineOG("/og/profile/:did/story/:rkey", async (ctx) => { 6 6 const { db, params, fetchImage, lookup, blobUrl } = ctx; ··· 138 138 ], 139 139 }, 140 140 }, 141 - options: { fonts: [syneBrandFont()] }, 141 + options: { fonts: allFonts() }, 142 142 meta: { 143 143 title: `Story by @${author?.handle || did.slice(0, 24)} — Grain`, 144 144 description: "Photo story on Grain",