diff --git a/astro.config.mjs b/astro.config.mjs
index a2e204b..bbbbdba 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -106,6 +106,14 @@ export default defineConfig({
},
vite: {
plugins: [tailwindcss()],
+ build: {
+ // Font files are always emitted as files (src/components/Fonts.astro).
+ // Vite would otherwise inline those under 4 KB, the smaller Unicode
+ // subsets of Geist, into the stylesheet as base64, and every page would
+ // download subsets it never uses before its first paint. Anything else
+ // keeps the default limit.
+ assetsInlineLimit: (file) => (/\.woff2?$/.test(file) ? false : undefined),
+ },
},
build: {
inlineStylesheets: 'auto',
diff --git a/src/components/Fonts.astro b/src/components/Fonts.astro
new file mode 100644
index 0000000..7ca1dbe
--- /dev/null
+++ b/src/components/Fonts.astro
@@ -0,0 +1,34 @@
+---
+// The site's fonts, Geist and Geist Mono, served from this site. They used to
+// come from Google Fonts, whose stylesheet blocks the first paint until it has
+// loaded; where Google cannot be reached, as in mainland China without a
+// proxy, a page then stayed blank for about 30 seconds.
+//
+// The files are the Fontsource builds of the Google Fonts releases, bundled by
+// Vite under hashed names. As on Google Fonts, each weight is split into
+// subsets by unicode-range, so a browser downloads only the subsets a page
+// uses (Latin, for these pages), and every face uses font-display: swap.
+// Chinese text is set in the system's own font (--font-sans in global.css), so
+// no CJK font is downloaded. Until Geist has loaded, text is set in a local
+// font adjusted to Geist's metrics ("Geist Fallback" in global.css), so the
+// swap does not move the layout.
+//
+// The Latin files of the weights on every page are preloaded: 400 for text,
+// 500 for navigation and buttons, 600 for headings, and Geist Mono 400 for the
+// labels above them. The Open Graph images are drawn from the same packages
+// (src/lib/og.ts).
+import "@fontsource/geist/400.css";
+import "@fontsource/geist/500.css";
+import "@fontsource/geist/600.css";
+import "@fontsource/geist/700.css";
+import "@fontsource/geist-mono/400.css";
+import "@fontsource/geist-mono/500.css";
+import geist400 from "@fontsource/geist/files/geist-latin-400-normal.woff2?url";
+import geist500 from "@fontsource/geist/files/geist-latin-500-normal.woff2?url";
+import geist600 from "@fontsource/geist/files/geist-latin-600-normal.woff2?url";
+import geistMono400 from "@fontsource/geist-mono/files/geist-mono-latin-400-normal.woff2?url";
+
+const preload = [geist400, geist500, geist600, geistMono400];
+---
+
+{preload.map((href) => )}
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index 45bb836..b5f2f63 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -2,6 +2,7 @@
import "~/styles/global.css";
import { getLang, t } from "~/i18n";
import Seo from "~/components/Seo.astro";
+import Fonts from "~/components/Fonts.astro";
import { ogImageAlt, ogImagePath, type OgCard } from "~/lib/og";
import type { JsonLd } from "~/lib/structured-data";
@@ -47,14 +48,7 @@ const GA_ID = "G-R8LX8NCHCJ";
-
-
-
+