feat: download and self-host Google Fonts woff2 files

- Created scripts/download-fonts.py: downloads Google Fonts woff2 files (latin subset only) using urllib.request with Chrome User-Agent header
- Parses CSS response for latin @font-face blocks, extracts family/weight/style/URL metadata
- Downloads to src/fonts/ with naming convention {family}-{weight}{-italic}.woff2
- Created src/styles/fonts.css: 8 @font-face declarations for:
  * Source Serif 4 (400, 600, 400-italic) — prose voice
  * Inter (400, 500, 600, 700) — structure voice
  * JetBrains Mono (400) — evidence voice
- All fonts use format('woff2'), font-display: swap, and relative URLs
- Fonts placed in src/fonts/ for Vite processing at build time

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
Ken
2026-05-26 22:39:10 +00:00
parent b302f8d910
commit e100119e3b
11 changed files with 403 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Download Google Fonts woff2 files (latin subset only) for self-hosting.
Fonts downloaded:
- Source Serif 4: 400, 600, 400-italic
- Inter: 400, 500, 600, 700
- JetBrains Mono: 400
Files are saved to src/fonts/ with naming: {family}-{weight}{-italic}.woff2
"""
import os
import re
import urllib.request
# Chrome User-Agent to get woff2 format from Google Fonts CSS API
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
# Google Fonts CSS2 API URLs
FONT_REQUESTS = [
# Source Serif 4: 400 normal, 600 normal, 400 italic
(
"https://fonts.googleapis.com/css2?"
"family=Source+Serif+4:ital,wght@0,400;0,600;1,400"
"&display=swap"
),
# Inter: 400, 500, 600, 700
(
"https://fonts.googleapis.com/css2?"
"family=Inter:wght@400;500;600;700"
"&display=swap"
),
# JetBrains Mono: 400
(
"https://fonts.googleapis.com/css2?"
"family=JetBrains+Mono:wght@400"
"&display=swap"
),
]
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
FONTS_DIR = os.path.join(PROJECT_ROOT, "src", "fonts")
def fetch_css(url):
"""Fetch CSS from Google Fonts with Chrome User-Agent to get woff2."""
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req) as resp:
return resp.read().decode("utf-8")
def parse_latin_font_faces(css_text):
"""Parse CSS for latin @font-face blocks and extract metadata + URL.
Returns list of dicts with keys: family, weight, style, url
"""
results = []
# Split into blocks by looking for /* latin */ comment followed by @font-face
# Google Fonts CSS2 uses comments like /* latin */ before each @font-face block
parts = re.split(r'/\*\s*([^*]+?)\s*\*/', css_text)
# parts alternates: [text_before, comment1, text_after1, comment2, text_after2, ...]
i = 1
while i < len(parts):
subset_label = parts[i].strip()
block_text = parts[i + 1] if i + 1 < len(parts) else ""
i += 2
if subset_label != "latin":
continue
# Extract the @font-face block
face_match = re.search(r'@font-face\s*\{([^}]+)\}', block_text)
if not face_match:
continue
face_body = face_match.group(1)
# Extract font-family
family_match = re.search(r"font-family:\s*'([^']+)'", face_body)
if not family_match:
continue
family = family_match.group(1)
# Extract font-weight
weight_match = re.search(r'font-weight:\s*(\d+)', face_body)
weight = weight_match.group(1) if weight_match else "400"
# Extract font-style
style_match = re.search(r'font-style:\s*(\w+)', face_body)
style = style_match.group(1) if style_match else "normal"
# Extract woff2 URL
url_match = re.search(r"url\(([^)]+\.woff2[^)]*)\)", face_body)
if not url_match:
continue
font_url = url_match.group(1)
results.append({
"family": family,
"weight": weight,
"style": style,
"url": font_url,
})
return results
def font_filename(family, weight, style):
"""Generate filename: {family}-{weight}{-italic}.woff2"""
# Normalize family name: "Source Serif 4" -> "source-serif-4"
slug = family.lower().replace(" ", "-")
suffix = "-italic" if style == "italic" else ""
return f"{slug}-{weight}{suffix}.woff2"
def download_font(url, dest_path):
"""Download a font file."""
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req) as resp:
data = resp.read()
with open(dest_path, "wb") as f:
f.write(data)
return len(data)
def main():
os.makedirs(FONTS_DIR, exist_ok=True)
downloaded = []
for css_url in FONT_REQUESTS:
print(f"\nFetching CSS: {css_url[:80]}...")
css_text = fetch_css(css_url)
faces = parse_latin_font_faces(css_text)
print(f" Found {len(faces)} latin @font-face block(s)")
for face in faces:
filename = font_filename(face["family"], face["weight"], face["style"])
dest = os.path.join(FONTS_DIR, filename)
print(f" Downloading {filename}...", end=" ")
size = download_font(face["url"], dest)
print(f"({size:,} bytes)")
downloaded.append(filename)
print(f"\nDone! Downloaded {len(downloaded)} font files to src/fonts/:")
for f in sorted(downloaded):
print(f" {f}")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
/* Self-hosted Google Fonts — woff2, latin subset only */
/* === Source Serif 4 (prose voice) === */
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/source-serif-4-400.woff2') format('woff2');
}
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/source-serif-4-600.woff2') format('woff2');
}
@font-face {
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/source-serif-4-400-italic.woff2') format('woff2');
}
/* === Inter (structure voice) === */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/inter-400.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('../fonts/inter-500.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/inter-600.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/inter-700.woff2') format('woff2');
}
/* === JetBrains Mono (evidence voice) === */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/jetbrains-mono-400.woff2') format('woff2');
}
+171
View File
@@ -0,0 +1,171 @@
/**
* Verification test for Self-Hosted Fonts (Task 3)
*
* Checks that:
* 1. 8 woff2 font files exist in src/fonts/
* 2. src/styles/fonts.css exists with 8 @font-face declarations
* 3. Each @font-face has correct font-family, weight, style, src, font-display
* 4. src URLs use relative paths to ../fonts/
*/
import { readFileSync, existsSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
console.log(`${message}`);
passed++;
} else {
console.error(`${message}`);
failed++;
}
}
// === 1. Font files exist in src/fonts/ ===
console.log('\n1. Font files exist in src/fonts/');
const expectedFonts = [
'source-serif-4-400.woff2',
'source-serif-4-600.woff2',
'source-serif-4-400-italic.woff2',
'inter-400.woff2',
'inter-500.woff2',
'inter-600.woff2',
'inter-700.woff2',
'jetbrains-mono-400.woff2',
];
for (const font of expectedFonts) {
const fontPath = join(root, 'src/fonts', font);
const exists = existsSync(fontPath);
assert(exists, `src/fonts/${font} exists`);
if (exists) {
const stat = statSync(fontPath);
assert(stat.size > 1000, `src/fonts/${font} is a real font file (${stat.size} bytes)`);
}
}
// === 2. fonts.css exists ===
console.log('\n2. fonts.css exists');
const fontsCssPath = join(root, 'src/styles/fonts.css');
assert(existsSync(fontsCssPath), 'src/styles/fonts.css exists');
if (!existsSync(fontsCssPath)) {
console.log('\nfonts.css not found, skipping remaining tests.');
console.log(`\n${'='.repeat(40)}`);
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
process.exit(1);
}
const css = readFileSync(fontsCssPath, 'utf8');
// === 3. Has 8 @font-face declarations ===
console.log('\n3. @font-face count');
const fontFaceBlocks = css.match(/@font-face\s*\{[^}]+\}/g);
const fontFaceCount = fontFaceBlocks ? fontFaceBlocks.length : 0;
assert(fontFaceCount === 8, `has exactly 8 @font-face declarations (found ${fontFaceCount})`);
// === 4. Source Serif 4 declarations (prose voice) ===
console.log('\n4. Source Serif 4 declarations (prose voice)');
// Regular 400
assert(
css.includes("font-family: 'Source Serif 4'") || css.includes('font-family: "Source Serif 4"'),
"has font-family: 'Source Serif 4'"
);
// Check for 400 normal
const ss4_400_block = fontFaceBlocks?.find(
(b) =>
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
b.includes('font-weight: 400') &&
b.includes('font-style: normal')
);
assert(!!ss4_400_block, 'Source Serif 4 400 normal declaration exists');
// Check for 600 normal
const ss4_600_block = fontFaceBlocks?.find(
(b) =>
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
b.includes('font-weight: 600') &&
b.includes('font-style: normal')
);
assert(!!ss4_600_block, 'Source Serif 4 600 normal declaration exists');
// Check for 400 italic
const ss4_400i_block = fontFaceBlocks?.find(
(b) =>
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
b.includes('font-weight: 400') &&
b.includes('font-style: italic')
);
assert(!!ss4_400i_block, 'Source Serif 4 400 italic declaration exists');
// === 5. Inter declarations (structure voice) ===
console.log('\n5. Inter declarations (structure voice)');
const interWeights = [400, 500, 600, 700];
for (const weight of interWeights) {
const interBlock = fontFaceBlocks?.find(
(b) =>
(b.includes("'Inter'") || b.includes('"Inter"')) &&
b.includes(`font-weight: ${weight}`) &&
b.includes('font-style: normal')
);
assert(!!interBlock, `Inter ${weight} normal declaration exists`);
}
// === 6. JetBrains Mono declaration (evidence voice) ===
console.log('\n6. JetBrains Mono declaration (evidence voice)');
const jbBlock = fontFaceBlocks?.find(
(b) =>
(b.includes("'JetBrains Mono'") || b.includes('"JetBrains Mono"')) &&
b.includes('font-weight: 400') &&
b.includes('font-style: normal')
);
assert(!!jbBlock, 'JetBrains Mono 400 normal declaration exists');
// === 7. All declarations use format('woff2') ===
console.log("\n7. All use format('woff2')");
if (fontFaceBlocks) {
for (const block of fontFaceBlocks) {
assert(block.includes("format('woff2')"), `@font-face block uses format('woff2')`);
}
}
// === 8. All declarations use font-display: swap ===
console.log('\n8. All use font-display: swap');
if (fontFaceBlocks) {
for (const block of fontFaceBlocks) {
assert(block.includes('font-display: swap'), '@font-face block uses font-display: swap');
}
}
// === 9. All src URLs use relative paths to ../fonts/ ===
console.log('\n9. src URLs use relative ../fonts/ paths');
if (fontFaceBlocks) {
for (const block of fontFaceBlocks) {
assert(block.includes("url('../fonts/"), "@font-face src uses url('../fonts/...')");
}
}
// === 10. download-fonts.py script exists ===
console.log('\n10. download-fonts.py script exists');
const scriptPath = join(root, 'scripts/download-fonts.py');
assert(existsSync(scriptPath), 'scripts/download-fonts.py exists');
if (existsSync(scriptPath)) {
const script = readFileSync(scriptPath, 'utf8');
assert(script.includes('urllib.request'), 'script uses urllib.request');
assert(script.includes('User-Agent') || script.includes('user-agent'), 'script sets User-Agent header');
assert(script.includes('woff2'), 'script references woff2 format');
}
// === Summary ===
console.log(`\n${'='.repeat(40)}`);
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
if (failed > 0) {
process.exit(1);
} else {
console.log('All font verification tests passed!');
}