Self-host the Cinder webfonts

The design depends on three specific families, and loading them from
fonts.googleapis.com means the UI silently loses its character exactly
where it is used most: Bromite is a de-googled browser whose users
routinely block Google's font domains, and the backend is reachable over a
LAN with no internet route. In both cases the page fell back to Georgia
and a system sans.

Five latin-subset woff2 files, ~120 KB total, in a binary already 27 MB —
the cost is noise, and //go:embed static picks them up with no build
change (backend/Dockerfile already copies static/). Latin only because the
UI is English; woff2 only because every browser that can run this app
supports it, so there is no second format to carry. DM Sans ships as one
variable file, which covers the whole 400-700 range the design uses for
the price of a single request.

staticHandler registers the .woff2 MIME type: Go's built-in table has no
entry for it and the scratch image has no /etc/mime.types, so the fonts
would otherwise be served as application/octet-stream. The two faces
above the fold are preloaded, since they are now discovered a stylesheet
late rather than from a preconnect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:34:26 +07:00
parent 686fcc19cc
commit ce7e93df53
9 changed files with 52 additions and 6 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+42
View File
@@ -1,3 +1,45 @@
/* Fonts are served from this binary, not from Google: this UI is read in
Bromite, where fonts.googleapis.com is routinely blocked, and over a LAN
with no internet route. Latin subsets only, woff2 only — every browser that
runs the app supports it, so there is no second format to ship. Roughly
120 KB for all five faces, embedded via //go:embed static. */
@font-face {
font-family: "Instrument Serif";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/instrument-serif-400-latin.woff2) format("woff2");
}
@font-face {
font-family: "Instrument Serif";
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/instrument-serif-400-italic-latin.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/fonts/ibm-plex-mono-500-latin.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/ibm-plex-mono-600-latin.woff2) format("woff2");
}
/* One variable file covers the 400–700 range the design uses. */
@font-face {
font-family: "DM Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/static/fonts/dm-sans-var-latin.woff2) format("woff2");
}
/* Cinder — editorial serif, containerless sheets, ash hairlines.
Heat is purely typographic: a series with an unread chapter turns its title
crimson and gains an ember underline. Everything else stays cool. Dark is
+2 -3
View File
@@ -6,10 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<title>mangaBookmark</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/static/fonts/ibm-plex-mono-500-latin.woff2" as="font" type="font/woff2" crossorigin>
<script src="/static/htmx.min.js" defer></script>
<script src="/static/filter.js" defer></script>
</head>
+1 -3
View File
@@ -6,10 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<title>mangaBookmark</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
</head>
<body>
<main class="login-card">
+7
View File
@@ -7,6 +7,7 @@ import (
"io/fs"
"log"
"math"
"mime"
"net/http"
"strconv"
"strings"
@@ -98,6 +99,12 @@ func staticHandler() http.Handler {
if err != nil {
panic("embed static: " + err.Error())
}
// Go's built-in table has no .woff2 and the scratch image has no
// /etc/mime.types, so without this the fonts go out as
// application/octet-stream.
if err := mime.AddExtensionType(".woff2", "font/woff2"); err != nil {
panic("woff2 mime: " + err.Error())
}
files := http.FileServer(http.FS(sub))
return http.StripPrefix("/static/", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {