Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
3.8 kB · 121 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122// Server-side font loading component// Following Google's best practices: https://web.dev/articles/font-best-practices// - Preconnect to font origins for early connection// - Use font-display: swap (shows fallback immediately, swaps when ready)// - Don't block rendering - some FOUT is acceptable and better UX than invisible text
import { getFontConfig, generateFontFaceCSS, getFontPreloadLinks, getGoogleFontsUrl, getFontFamilyValue, getFontBaseSize,} from "src/fonts";
type FontLoaderProps = { headingFontId: string | undefined; bodyFontId: string | undefined;};
export function FontLoader({ headingFontId, bodyFontId }: FontLoaderProps) { const headingFont = getFontConfig(headingFontId); const bodyFont = getFontConfig(bodyFontId);
// Collect all unique fonts to load const fontsToLoad = headingFont.id === bodyFont.id ? [headingFont] : [headingFont, bodyFont];
// Collect preload links (deduplicated) const preloadLinksSet = new Set<string>(); const preloadLinks: { href: string; type: string }[] = []; for (const font of fontsToLoad) { for (const link of getFontPreloadLinks(font)) { if (!preloadLinksSet.has(link.href)) { preloadLinksSet.add(link.href); preloadLinks.push(link); } } }
// Collect font-face CSS const fontFaceCSS = fontsToLoad .map((font) => generateFontFaceCSS(font)) .filter(Boolean) .join("\n\n");
// Collect Google Fonts URLs (deduplicated) const googleFontsUrls = [...new Set( fontsToLoad .map((font) => getGoogleFontsUrl(font)) .filter((url): url is string => url !== null) )];
const headingFontValue = getFontFamilyValue(headingFont); const bodyFontValue = getFontFamilyValue(bodyFont); const bodyFontBaseSize = getFontBaseSize(bodyFont);
// Set font CSS variables scoped to .leafletWrapper so they don't affect app UI const fontVariableCSS = `.leafletWrapper { --theme-heading-font: ${headingFontValue}; --theme-font: ${bodyFontValue}; --theme-font-base-size: ${bodyFontBaseSize}px;}`.trim();
return ( <> {/* Google Fonts best practice: preconnect to both origins - fonts.googleapis.com serves the CSS - fonts.gstatic.com serves the font files (needs crossorigin for CORS) Place these as early as possible in <head> */} {googleFontsUrls.length > 0 && ( <> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> {googleFontsUrls.map((url) => ( <link key={url} rel="stylesheet" href={url} /> ))} </> )} {/* Preload local font files for early discovery */} {preloadLinks.map((link) => ( <link key={link.href} rel="preload" href={link.href} as="font" type={link.type} crossOrigin="anonymous" /> ))} {/* @font-face declarations (for local fonts) and CSS variable */} <style dangerouslySetInnerHTML={{ __html: `${fontFaceCSS}\n\n${fontVariableCSS}`, }} /> </> );}
// Helper to extract fonts from facts array (for server-side use)export function extractFontsFromFacts( facts: Array<{ entity: string; attribute: string; data: { value: string } }>, rootEntity: string): { headingFontId: string | undefined; bodyFontId: string | undefined } { const headingFontFact = facts.find( (f) => f.entity === rootEntity && f.attribute === "theme/heading-font" ); const bodyFontFact = facts.find( (f) => f.entity === rootEntity && f.attribute === "theme/body-font" ); return { headingFontId: headingFontFact?.data?.value, bodyFontId: bodyFontFact?.data?.value, };}