a tool for shared writing and social publishing

Simplify font handling code

+83 -354
+34 -50
components/FontLoader.tsx
··· 6 6 7 7 import { 8 8 getFontConfig, 9 - generateFontFaceCSS, 10 - getFontPreloadLinks, 11 9 getGoogleFontsUrl, 12 10 getFontFamilyValue, 13 11 getFontBaseSize, 12 + defaultFontId, 14 13 } from "src/fonts"; 15 14 16 15 type FontLoaderProps = { ··· 22 21 const headingFont = getFontConfig(headingFontId); 23 22 const bodyFont = getFontConfig(bodyFontId); 24 23 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"); 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; 47 30 48 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 + ); 49 35 const googleFontsUrls = [...new Set( 50 36 fontsToLoad 51 37 .map((font) => getGoogleFontsUrl(font)) 52 38 .filter((url): url is string => url !== null) 53 39 )]; 54 40 55 - const headingFontValue = getFontFamilyValue(headingFont); 56 - const bodyFontValue = getFontFamilyValue(bodyFont); 57 - const bodyFontBaseSize = getFontBaseSize(bodyFont); 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); 58 48 59 49 // 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(); 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 + : ""; 67 60 68 61 return ( 69 62 <> ··· 82 75 ))} 83 76 </> 84 77 )} 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" 78 + {/* CSS variables scoped to .leafletWrapper for SSR (before client hydration) */} 79 + {fontVariableCSS && ( 80 + <style 81 + dangerouslySetInnerHTML={{ 82 + __html: fontVariableCSS, 83 + }} 94 84 /> 95 - ))} 96 - {/* @font-face declarations (for local fonts) and CSS variable */} 97 - <style 98 - dangerouslySetInnerHTML={{ 99 - __html: `${fontFaceCSS}\n\n${fontVariableCSS}`, 100 - }} 101 - /> 85 + )} 102 86 </> 103 87 ); 104 88 }
+12 -2
components/ThemeManager/Pickers/PageThemePickers.tsx
··· 42 42 43 43 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 44 44 let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 45 + let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value; 46 + let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value; 45 47 46 48 return ( 47 49 <div ··· 62 64 /> 63 65 {!props.home && !props.hideFonts && ( 64 66 <> 65 - <FontPicker label="Heading" entityID={props.entityID} attribute="theme/heading-font" /> 66 - <FontPicker label="Body" entityID={props.entityID} attribute="theme/body-font" /> 67 + <FontPicker 68 + label="Heading" 69 + value={headingFontId} 70 + onChange={(fontId) => rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/heading-font", data: { type: "string", value: fontId } })} 71 + /> 72 + <FontPicker 73 + label="Body" 74 + value={bodyFontId} 75 + onChange={(fontId) => rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/body-font", data: { type: "string", value: fontId } })} 76 + /> 67 77 </> 68 78 )} 69 79 </div>
+5 -40
components/ThemeManager/Pickers/TextPickers.tsx
··· 1 1 "use client"; 2 2 3 - import { Color } from "react-aria-components"; 4 3 import { Input } from "components/Input"; 5 4 import { useState } from "react"; 6 - import { useEntity, useReplicache } from "src/replicache"; 7 5 import { Menu } from "components/Menu"; 8 - import { pickers } from "../ThemeSetter"; 9 - import { ColorPicker } from "./ColorPicker"; 10 6 import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 11 7 import { useIsMobile } from "src/hooks/isMobile"; 12 8 import { ··· 19 15 getFontConfig, 20 16 } from "src/fonts"; 21 17 22 - export const TextColorPicker = (props: { 23 - openPicker: pickers; 24 - setOpenPicker: (thisPicker: pickers) => void; 25 - value: Color; 26 - setValue: (c: Color) => void; 27 - }) => { 28 - return ( 29 - <ColorPicker 30 - label="Text" 31 - value={props.value} 32 - setValue={props.setValue} 33 - thisPicker={"text"} 34 - openPicker={props.openPicker} 35 - setOpenPicker={props.setOpenPicker} 36 - closePicker={() => props.setOpenPicker("null")} 37 - /> 38 - ); 39 - }; 40 - 41 - type FontAttribute = "theme/heading-font" | "theme/body-font"; 42 - 43 18 export const FontPicker = (props: { 44 19 label: string; 45 - entityID: string; 46 - attribute: FontAttribute; 20 + value: string | undefined; 21 + onChange: (fontId: string) => void; 47 22 }) => { 48 23 let isMobile = useIsMobile(); 49 - let { rep } = useReplicache(); 50 24 let [showCustomInput, setShowCustomInput] = useState(false); 51 25 let [customFontValue, setCustomFontValue] = useState(""); 52 - let currentFont = useEntity(props.entityID, props.attribute); 53 - let fontId = currentFont?.data.value || defaultFontId; 26 + let fontId = props.value || defaultFontId; 54 27 let font = getFontConfig(fontId); 55 28 let isCustom = isCustomFontId(fontId); 56 29 ··· 65 38 parsed.fontName, 66 39 parsed.googleFontsFamily, 67 40 ); 68 - rep?.mutate.assertFact({ 69 - entity: props.entityID, 70 - attribute: props.attribute, 71 - data: { type: "string", value: customId }, 72 - }); 41 + props.onChange(customId); 73 42 setShowCustomInput(false); 74 43 setCustomFontValue(""); 75 44 } ··· 141 110 <FontOption 142 111 key={fontOption.id} 143 112 onSelect={() => { 144 - rep?.mutate.assertFact({ 145 - entity: props.entityID, 146 - attribute: props.attribute, 147 - data: { type: "string", value: fontOption.id }, 148 - }); 113 + props.onChange(fontOption.id); 149 114 }} 150 115 font={fontOption} 151 116 selected={fontOption.id === fontId}
-179
components/ThemeManager/PubPickers/PubFontPicker.tsx
··· 1 - "use client"; 2 - 3 - import { useState } from "react"; 4 - import { Menu } from "components/Menu"; 5 - import { Input } from "components/Input"; 6 - import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 7 - import { useIsMobile } from "src/hooks/isMobile"; 8 - import { 9 - fonts, 10 - defaultFontId, 11 - FontConfig, 12 - isCustomFontId, 13 - parseGoogleFontInput, 14 - createCustomFontId, 15 - getFontConfig, 16 - } from "src/fonts"; 17 - 18 - export const PubFontPicker = (props: { 19 - label: string; 20 - value: string | undefined; 21 - onChange: (fontId: string) => void; 22 - }) => { 23 - let isMobile = useIsMobile(); 24 - let [showCustomInput, setShowCustomInput] = useState(false); 25 - let [customFontValue, setCustomFontValue] = useState(""); 26 - let fontId = props.value || defaultFontId; 27 - let font = getFontConfig(fontId); 28 - let isCustom = isCustomFontId(fontId); 29 - 30 - let fontList = Object.values(fonts).sort((a, b) => 31 - a.displayName.localeCompare(b.displayName), 32 - ); 33 - 34 - const handleCustomSubmit = () => { 35 - const parsed = parseGoogleFontInput(customFontValue); 36 - if (parsed) { 37 - const customId = createCustomFontId( 38 - parsed.fontName, 39 - parsed.googleFontsFamily, 40 - ); 41 - props.onChange(customId); 42 - setShowCustomInput(false); 43 - setCustomFontValue(""); 44 - } 45 - }; 46 - 47 - return ( 48 - <Menu 49 - asChild 50 - trigger={ 51 - <button className="flex gap-2 items-center w-full !outline-none min-w-0"> 52 - <div 53 - className={`w-6 h-6 rounded-md border border-border relative text-sm bg-bg-page shrink-0 ${props.label === "Heading" ? "font-bold" : "text-secondary"}`} 54 - > 55 - <div className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 "> 56 - Aa 57 - </div> 58 - </div> 59 - <div className="font-bold shrink-0">{props.label}</div> 60 - <div className="truncate">{font.displayName}</div> 61 - </button> 62 - } 63 - side={isMobile ? "bottom" : "right"} 64 - align="start" 65 - className="w-[250px] !gap-0 !outline-none max-h-72 " 66 - > 67 - {showCustomInput ? ( 68 - <div className="p-2 flex flex-col gap-2"> 69 - <div className="text-sm text-secondary"> 70 - Paste a Google Font name 71 - </div> 72 - <Input 73 - value={customFontValue} 74 - className="w-full" 75 - placeholder="e.g. Roboto, Open Sans, Playfair Display" 76 - autoFocus 77 - onChange={(e) => setCustomFontValue(e.currentTarget.value)} 78 - onKeyDown={(e) => { 79 - if (e.key === "Enter") { 80 - e.preventDefault(); 81 - handleCustomSubmit(); 82 - } else if (e.key === "Escape") { 83 - setShowCustomInput(false); 84 - setCustomFontValue(""); 85 - } 86 - }} 87 - /> 88 - <div className="flex gap-2"> 89 - <button 90 - className="flex-1 px-2 py-1 text-sm rounded-md bg-accent-1 text-accent-2 hover:opacity-80" 91 - onClick={handleCustomSubmit} 92 - > 93 - Add Font 94 - </button> 95 - <button 96 - className="px-2 py-1 text-sm rounded-md text-secondary hover:bg-border-light" 97 - onClick={() => { 98 - setShowCustomInput(false); 99 - setCustomFontValue(""); 100 - }} 101 - > 102 - Cancel 103 - </button> 104 - </div> 105 - </div> 106 - ) : ( 107 - <div className="flex flex-col h-full overflow-auto gap-0 py-1"> 108 - {fontList.map((fontOption) => { 109 - return ( 110 - <FontOption 111 - key={fontOption.id} 112 - onSelect={() => { 113 - props.onChange(fontOption.id); 114 - }} 115 - font={fontOption} 116 - selected={fontOption.id === fontId} 117 - /> 118 - ); 119 - })} 120 - {isCustom && ( 121 - <FontOption 122 - key={fontId} 123 - onSelect={() => {}} 124 - font={font} 125 - selected={true} 126 - /> 127 - )} 128 - <hr className="mx-2 my-1 border-border" /> 129 - <DropdownMenu.Item 130 - onSelect={(e) => { 131 - e.preventDefault(); 132 - setShowCustomInput(true); 133 - }} 134 - className={` 135 - fontOption 136 - z-10 px-1 py-0.5 137 - text-left text-secondary 138 - data-[highlighted]:bg-border-light data-[highlighted]:text-secondary 139 - hover:bg-border-light hover:text-secondary 140 - outline-none 141 - cursor-pointer 142 - `} 143 - > 144 - <div className="px-2 py-0 rounded-md">Custom Google Font...</div> 145 - </DropdownMenu.Item> 146 - </div> 147 - )} 148 - </Menu> 149 - ); 150 - }; 151 - 152 - const FontOption = (props: { 153 - onSelect: () => void; 154 - font: FontConfig; 155 - selected: boolean; 156 - }) => { 157 - return ( 158 - <DropdownMenu.RadioItem 159 - value={props.font.id} 160 - onSelect={props.onSelect} 161 - className={` 162 - fontOption 163 - z-10 px-1 py-0.5 164 - text-left text-secondary 165 - data-[highlighted]:bg-border-light data-[highlighted]:text-secondary 166 - hover:bg-border-light hover:text-secondary 167 - outline-none 168 - cursor-pointer 169 - 170 - `} 171 - > 172 - <div 173 - className={`px-2 py-0 rounded-md ${props.selected && "bg-accent-1 text-accent-2"}`} 174 - > 175 - {props.font.displayName} 176 - </div> 177 - </DropdownMenu.RadioItem> 178 - ); 179 - };
+3 -3
components/ThemeManager/PubThemeSetter.tsx
··· 20 20 import { useToaster } from "components/Toast"; 21 21 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 22 22 import { PubPageWidthSetter } from "./PubPickers/PubPageWidthSetter"; 23 - import { PubFontPicker } from "./PubPickers/PubFontPicker"; 23 + import { FontPicker } from "./Pickers/TextPickers"; 24 24 25 25 export type ImageState = { 26 26 src: string; ··· 202 202 hasPageBackground={showPageBackground} 203 203 /> 204 204 <div className="bg-bg-page p-2 rounded-md border border-primary shadow-[0_0_0_1px_rgb(var(--bg-page))] flex flex-col gap-1"> 205 - <PubFontPicker 205 + <FontPicker 206 206 label="Heading" 207 207 value={headingFont} 208 208 onChange={setHeadingFont} 209 209 /> 210 - <PubFontPicker 210 + <FontPicker 211 211 label="Body" 212 212 value={bodyFont} 213 213 onChange={setBodyFont}
+17 -6
components/ThemeManager/ThemeProvider.tsx
··· 29 29 PublicationThemeProvider, 30 30 } from "./PublicationThemeProvider"; 31 31 import { getColorDifference } from "./themeUtils"; 32 - import { getFontConfig, getGoogleFontsUrl, getFontFamilyValue, generateFontFaceCSS, getFontBaseSize } from "src/fonts"; 32 + import { getFontConfig, getGoogleFontsUrl, getFontFamilyValue, getFontBaseSize, defaultFontId } from "src/fonts"; 33 33 34 34 // define a function to set an Aria Color to a CSS Variable in RGB 35 35 function setCSSVariableToColor( ··· 192 192 accentContrast = sortedAccents[0]; 193 193 } 194 194 195 - // Get font configs for CSS variables 195 + // Get font configs for CSS variables. 196 + // When using the default font (Quattro), use var(--font-quattro) which is 197 + // always available via next/font/local in layout.tsx, rather than the raw 198 + // font-family name 'iA Writer Quattro V' which depends on a dynamic @font-face. 199 + // When both heading and body are default, omit the variables entirely so the 200 + // CSS fallback var(--theme-font, var(--font-quattro)) resolves naturally. 201 + const isDefaultBody = !bodyFontId || bodyFontId === defaultFontId; 202 + const isDefaultHeading = !headingFontId || headingFontId === defaultFontId; 196 203 const headingFontConfig = getFontConfig(headingFontId); 197 204 const bodyFontConfig = getFontConfig(bodyFontId); 198 - const headingFontValue = getFontFamilyValue(headingFontConfig); 199 - const bodyFontValue = getFontFamilyValue(bodyFontConfig); 200 - const bodyFontBaseSize = getFontBaseSize(bodyFontConfig); 205 + const headingFontValue = isDefaultHeading 206 + ? (isDefaultBody ? undefined : "var(--font-quattro)") 207 + : getFontFamilyValue(headingFontConfig); 208 + const bodyFontValue = isDefaultBody 209 + ? (isDefaultHeading ? undefined : "var(--font-quattro)") 210 + : getFontFamilyValue(bodyFontConfig); 211 + const bodyFontBaseSize = isDefaultBody ? undefined : getFontBaseSize(bodyFontConfig); 201 212 const headingGoogleFontsUrl = getGoogleFontsUrl(headingFontConfig); 202 213 const bodyGoogleFontsUrl = getGoogleFontsUrl(bodyFontConfig); 203 214 ··· 325 336 "--page-width-units": `min(${pageWidth || 624}px, calc(100vw - 12px))`, 326 337 "--theme-heading-font": headingFontValue, 327 338 "--theme-font": bodyFontValue, 328 - "--theme-font-base-size": `${bodyFontBaseSize}px`, 339 + "--theme-font-base-size": bodyFontBaseSize ? `${bodyFontBaseSize}px` : undefined, 329 340 } as CSSProperties 330 341 } 331 342 >
+1 -1
next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 - import "./.next/dev/types/routes.d.ts"; 3 + import "./.next/types/routes.d.ts"; 4 4 5 5 // NOTE: This file should not be edited 6 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+4 -4
package-lock.json
··· 85 85 "uuid": "^10.0.0", 86 86 "y-prosemirror": "^1.2.5", 87 87 "yjs": "^13.6.15", 88 - "zustand": "^5.0.4" 88 + "zustand": "^5.0.11" 89 89 }, 90 90 "devDependencies": { 91 91 "@atproto/lex-cli": "^0.9.5", ··· 19745 19745 } 19746 19746 }, 19747 19747 "node_modules/zustand": { 19748 - "version": "5.0.4", 19749 - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", 19750 - "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", 19748 + "version": "5.0.11", 19749 + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", 19750 + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", 19751 19751 "license": "MIT", 19752 19752 "engines": { 19753 19753 "node": ">=12.20.0"
+1 -1
package.json
··· 96 96 "uuid": "^10.0.0", 97 97 "y-prosemirror": "^1.2.5", 98 98 "yjs": "^13.6.15", 99 - "zustand": "^5.0.4" 99 + "zustand": "^5.0.11" 100 100 }, 101 101 "devDependencies": { 102 102 "@atproto/lex-cli": "^0.9.5",
public/fonts/Lora-Italic-Variable.woff2

This is a binary file and will not be displayed.

public/fonts/Lora-Variable.woff2

This is a binary file and will not be displayed.

public/fonts/NotoSans-Italic-Variable.woff2

This is a binary file and will not be displayed.

public/fonts/NotoSans-Variable.woff2

This is a binary file and will not be displayed.

public/fonts/SourceSans3-Italic-Variable.woff2

This is a binary file and will not be displayed.

public/fonts/SourceSans3-Variable.woff2

This is a binary file and will not be displayed.

+6 -68
src/fonts.ts
··· 1 - // Font configuration for self-hosted and Google Fonts 2 - // This replicates what next/font does but allows dynamic selection per-leaflet 1 + // Font configuration for Google Fonts and the default system font 2 + // Allows dynamic font selection per-leaflet 3 3 4 4 export type FontConfig = { 5 5 id: string; ··· 9 9 baseSize?: number; // base font size in px for document content 10 10 } & ( 11 11 | { 12 - // Self-hosted fonts with local files 13 - type: "local"; 14 - files: { 15 - path: string; 16 - style: "normal" | "italic"; 17 - weight?: string; 18 - }[]; 19 - } 20 - | { 21 12 // Google Fonts loaded via CDN 22 13 type: "google"; 23 14 googleFontsFamily: string; // e.g., "Open+Sans:ital,wght@0,400;0,700;1,400;1,700" 24 15 } 25 16 | { 26 - // System fonts (no loading required) 17 + // System fonts or fonts loaded elsewhere (e.g. next/font/local) 27 18 type: "system"; 28 19 } 29 20 ); 30 21 31 22 export const fonts: Record<string, FontConfig> = { 32 - // Self-hosted variable fonts (WOFF2) 33 23 quattro: { 34 24 id: "quattro", 35 25 displayName: "iA Writer Quattro", 36 26 fontFamily: "iA Writer Quattro V", 37 27 baseSize: 16, 38 - type: "local", 39 - files: [ 40 - { 41 - path: "/fonts/iaw-quattro-vf.woff2", 42 - style: "normal", 43 - weight: "400 700", 44 - }, 45 - { 46 - path: "/fonts/iaw-quattro-vf-Italic.woff2", 47 - style: "italic", 48 - weight: "400 700", 49 - }, 50 - ], 28 + type: "system", // Loaded via next/font/local in layout.tsx 51 29 fallback: ["system-ui", "sans-serif"], 52 30 }, 53 31 lora: { ··· 55 33 displayName: "Lora", 56 34 fontFamily: "Lora", 57 35 baseSize: 17, 58 - type: "local", 59 - files: [ 60 - { 61 - path: "/fonts/Lora-Variable.woff2", 62 - style: "normal", 63 - weight: "400 700", 64 - }, 65 - { 66 - path: "/fonts/Lora-Italic-Variable.woff2", 67 - style: "italic", 68 - weight: "400 700", 69 - }, 70 - ], 36 + type: "google", 37 + googleFontsFamily: "Lora:ital,wght@0,400..700;1,400..700", 71 38 fallback: ["Georgia", "serif"], 72 39 }, 73 40 "atkinson-hyperlegible": { ··· 199 166 } 200 167 201 168 return fonts[fontId] || fonts[defaultFontId]; 202 - } 203 - 204 - // Generate @font-face CSS for a local font 205 - export function generateFontFaceCSS(font: FontConfig): string { 206 - if (font.type !== "local") return ""; 207 - return font.files 208 - .map((file) => { 209 - const format = file.path.endsWith(".woff2") ? "woff2" : "truetype"; 210 - return ` 211 - @font-face { 212 - font-family: '${font.fontFamily}'; 213 - src: url('${file.path}') format('${format}'); 214 - font-style: ${file.style}; 215 - font-weight: ${file.weight || "normal"}; 216 - font-display: swap; 217 - }`.trim(); 218 - }) 219 - .join("\n\n"); 220 - } 221 - 222 - // Generate preload link attributes for a local font 223 - export function getFontPreloadLinks( 224 - font: FontConfig, 225 - ): { href: string; type: string }[] { 226 - if (font.type !== "local") return []; 227 - return font.files.map((file) => ({ 228 - href: file.path, 229 - type: file.path.endsWith(".woff2") ? "font/woff2" : "font/ttf", 230 - })); 231 169 } 232 170 233 171 // Get Google Fonts URL for a font