a tool for shared writing and social publishing
at main 105 lines 3.9 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 getGoogleFontsUrl, 10 getFontFamilyValue, 11 getFontBaseSize, 12 defaultFontId, 13} from "src/fonts"; 14 15type FontLoaderProps = { 16 headingFontId: string | undefined; 17 bodyFontId: string | undefined; 18}; 19 20export function FontLoader({ headingFontId, bodyFontId }: FontLoaderProps) { 21 const headingFont = getFontConfig(headingFontId); 22 const bodyFont = getFontConfig(bodyFontId); 23 24 // Don't load the default font (Quattro) here — it's already loaded via 25 // next/font/local in layout.tsx under --font-quattro. Loading it again with 26 // a different family name ('iA Writer Quattro V') causes issues when the 27 // @font-face from this component isn't available (e.g., client-side navigation). 28 const isDefaultHeading = headingFont.id === defaultFontId; 29 const isDefaultBody = bodyFont.id === defaultFontId; 30 31 // Collect Google Fonts URLs (deduplicated) 32 const fontsToLoad = [headingFont, bodyFont].filter( 33 (f, i, arr) => f.id !== defaultFontId && arr.findIndex((o) => o.id === f.id) === i 34 ); 35 const googleFontsUrls = [...new Set( 36 fontsToLoad 37 .map((font) => getGoogleFontsUrl(font)) 38 .filter((url): url is string => url !== null) 39 )]; 40 41 const headingFontValue = isDefaultHeading 42 ? (isDefaultBody ? null : "var(--font-quattro)") 43 : getFontFamilyValue(headingFont); 44 const bodyFontValue = isDefaultBody 45 ? (isDefaultHeading ? null : "var(--font-quattro)") 46 : getFontFamilyValue(bodyFont); 47 const bodyFontBaseSize = isDefaultBody ? null : getFontBaseSize(bodyFont); 48 49 // Set font CSS variables scoped to .leafletWrapper so they don't affect app UI 50 // Don't set variables for the default font — let CSS fallback to var(--font-quattro) 51 const fontVariableLines = [ 52 headingFontValue && ` --theme-heading-font: ${headingFontValue};`, 53 bodyFontValue && ` --theme-font: ${bodyFontValue};`, 54 bodyFontBaseSize && ` --theme-font-base-size: ${bodyFontBaseSize}px;`, 55 ].filter(Boolean); 56 57 const fontVariableCSS = fontVariableLines.length > 0 58 ? `.leafletWrapper {\n${fontVariableLines.join("\n")}\n}` 59 : ""; 60 61 return ( 62 <> 63 {/* 64 Google Fonts best practice: preconnect to both origins 65 - fonts.googleapis.com serves the CSS 66 - fonts.gstatic.com serves the font files (needs crossorigin for CORS) 67 Place these as early as possible in <head> 68 */} 69 {googleFontsUrls.length > 0 && ( 70 <> 71 <link rel="preconnect" href="https://fonts.googleapis.com" /> 72 <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> 73 {googleFontsUrls.map((url) => ( 74 <link key={url} rel="stylesheet" href={url} /> 75 ))} 76 </> 77 )} 78 {/* CSS variables scoped to .leafletWrapper for SSR (before client hydration) */} 79 {fontVariableCSS && ( 80 <style 81 dangerouslySetInnerHTML={{ 82 __html: fontVariableCSS, 83 }} 84 /> 85 )} 86 </> 87 ); 88} 89 90// Helper to extract fonts from facts array (for server-side use) 91export function extractFontsFromFacts( 92 facts: Array<{ entity: string; attribute: string; data: { value: string } }>, 93 rootEntity: string 94): { headingFontId: string | undefined; bodyFontId: string | undefined } { 95 const headingFontFact = facts.find( 96 (f) => f.entity === rootEntity && f.attribute === "theme/heading-font" 97 ); 98 const bodyFontFact = facts.find( 99 (f) => f.entity === rootEntity && f.attribute === "theme/body-font" 100 ); 101 return { 102 headingFontId: headingFontFact?.data?.value, 103 bodyFontId: bodyFontFact?.data?.value, 104 }; 105}