a tool for shared writing and social publishing
at feature/fonts 121 lines 3.8 kB view raw
1// Server-side font loading component 2// Following Google's best practices: https://web.dev/articles/font-best-practices 3// - Preconnect to font origins for early connection 4// - Use font-display: swap (shows fallback immediately, swaps when ready) 5// - Don't block rendering - some FOUT is acceptable and better UX than invisible text 6 7import { 8 getFontConfig, 9 generateFontFaceCSS, 10 getFontPreloadLinks, 11 getGoogleFontsUrl, 12 getFontFamilyValue, 13 getFontBaseSize, 14} from "src/fonts"; 15 16type FontLoaderProps = { 17 headingFontId: string | undefined; 18 bodyFontId: string | undefined; 19}; 20 21export function FontLoader({ headingFontId, bodyFontId }: FontLoaderProps) { 22 const headingFont = getFontConfig(headingFontId); 23 const bodyFont = getFontConfig(bodyFontId); 24 25 // Collect all unique fonts to load 26 const fontsToLoad = headingFont.id === bodyFont.id 27 ? [headingFont] 28 : [headingFont, bodyFont]; 29 30 // Collect preload links (deduplicated) 31 const preloadLinksSet = new Set<string>(); 32 const preloadLinks: { href: string; type: string }[] = []; 33 for (const font of fontsToLoad) { 34 for (const link of getFontPreloadLinks(font)) { 35 if (!preloadLinksSet.has(link.href)) { 36 preloadLinksSet.add(link.href); 37 preloadLinks.push(link); 38 } 39 } 40 } 41 42 // Collect font-face CSS 43 const fontFaceCSS = fontsToLoad 44 .map((font) => generateFontFaceCSS(font)) 45 .filter(Boolean) 46 .join("\n\n"); 47 48 // Collect Google Fonts URLs (deduplicated) 49 const googleFontsUrls = [...new Set( 50 fontsToLoad 51 .map((font) => getGoogleFontsUrl(font)) 52 .filter((url): url is string => url !== null) 53 )]; 54 55 const headingFontValue = getFontFamilyValue(headingFont); 56 const bodyFontValue = getFontFamilyValue(bodyFont); 57 const bodyFontBaseSize = getFontBaseSize(bodyFont); 58 59 // Set font CSS variables scoped to .leafletWrapper so they don't affect app UI 60 const fontVariableCSS = ` 61.leafletWrapper { 62 --theme-heading-font: ${headingFontValue}; 63 --theme-font: ${bodyFontValue}; 64 --theme-font-base-size: ${bodyFontBaseSize}px; 65} 66`.trim(); 67 68 return ( 69 <> 70 {/* 71 Google Fonts best practice: preconnect to both origins 72 - fonts.googleapis.com serves the CSS 73 - fonts.gstatic.com serves the font files (needs crossorigin for CORS) 74 Place these as early as possible in <head> 75 */} 76 {googleFontsUrls.length > 0 && ( 77 <> 78 <link rel="preconnect" href="https://fonts.googleapis.com" /> 79 <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> 80 {googleFontsUrls.map((url) => ( 81 <link key={url} rel="stylesheet" href={url} /> 82 ))} 83 </> 84 )} 85 {/* Preload local font files for early discovery */} 86 {preloadLinks.map((link) => ( 87 <link 88 key={link.href} 89 rel="preload" 90 href={link.href} 91 as="font" 92 type={link.type} 93 crossOrigin="anonymous" 94 /> 95 ))} 96 {/* @font-face declarations (for local fonts) and CSS variable */} 97 <style 98 dangerouslySetInnerHTML={{ 99 __html: `${fontFaceCSS}\n\n${fontVariableCSS}`, 100 }} 101 /> 102 </> 103 ); 104} 105 106// Helper to extract fonts from facts array (for server-side use) 107export function extractFontsFromFacts( 108 facts: Array<{ entity: string; attribute: string; data: { value: string } }>, 109 rootEntity: string 110): { headingFontId: string | undefined; bodyFontId: string | undefined } { 111 const headingFontFact = facts.find( 112 (f) => f.entity === rootEntity && f.attribute === "theme/heading-font" 113 ); 114 const bodyFontFact = facts.find( 115 (f) => f.entity === rootEntity && f.attribute === "theme/body-font" 116 ); 117 return { 118 headingFontId: headingFontFact?.data?.value, 119 bodyFontId: bodyFontFact?.data?.value, 120 }; 121}