fix(web): vendor the web fonts instead of loading them from Google
test-web / test (push) Successful in 42s
test-web / test (push) Successful in 42s
app.html linked its stylesheet straight from fonts.googleapis.com, with preconnects to that host and fonts.gstatic.com. A deployed instance has no outbound network, so those requests never arrive and the whole UI renders in fallback faces — Georgia for the display face, whatever the system has for Inter and JetBrains Mono. This is invisible in development, which is why it survived: the dev machine has internet, so the fonts load and everything looks right. Only a real deployment shows the failure. tools/vendor-fonts.py fetches the three families once and writes them under web/static/fonts with a generated stylesheet. static/ is copied into the SvelteKit build, which Go embeds, so the faces travel inside the binary. 32 woff2 files, 912K. Two details that matter for correctness rather than size: Urls in the generated CSS are relative (./Inter-400-latin.woff2), not absolute. A url() resolves against the stylesheet's own address, so the directory keeps working when the app is served under a base path; /fonts/... would not. Every subset Google slices is kept, with unicode-range intact. The browser still fetches only the ranges a page uses, so this costs repository bytes rather than request bytes — and a library full of Cyrillic or Greek artist names renders instead of falling back mid-list. The guard asserts the property, not the vendor: any absolute url in a resource-loading attribute fails, whoever hosts it, since naming Google would pass the day someone reached for a different CDN. It also checks preconnect separately (those carry no fetch of their own, so the url check misses them), strips HTML comments before asserting an absence so prose describing the forbidden thing cannot satisfy the check, and pins the font families to tokens.json rather than a hardcoded list. Falsified against the pre-change app.html: it trips both the external-url and preconnect assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Download the web fonts from Google Fonts and vendor them into the repo.
|
||||||
|
|
||||||
|
Run by hand when the font set changes, never at build or run time:
|
||||||
|
|
||||||
|
python3 tools/vendor-fonts.py
|
||||||
|
|
||||||
|
The app must render itself with no outbound network, so nothing here may
|
||||||
|
happen while it is running. This script fetches once, writes the woff2 files
|
||||||
|
and a stylesheet that points at them by RELATIVE url, and everything is
|
||||||
|
committed. `web/static/` is copied verbatim into the SvelteKit build, which
|
||||||
|
Go then embeds — so the fonts travel inside the binary.
|
||||||
|
|
||||||
|
Relative urls in the stylesheet (`./Inter-latin.woff2`, not `/fonts/...`)
|
||||||
|
are deliberate: a CSS url() resolves against the stylesheet's own address, so
|
||||||
|
the whole directory keeps working when the app is served under a base path.
|
||||||
|
|
||||||
|
Subsets are kept as Google slices them, with their `unicode-range` intact.
|
||||||
|
That is not a size compromise — the browser downloads only the ranges a page
|
||||||
|
actually uses, so vendoring every subset costs repository bytes rather than
|
||||||
|
request bytes, and a library full of non-English artist names renders instead
|
||||||
|
of falling back mid-list.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
OUT = ROOT / "web/static/fonts"
|
||||||
|
|
||||||
|
# Matches the faces the design system actually permits: two weights only, 400
|
||||||
|
# and 500 (never 600/700), and Fraunces' optical-size axis across its range.
|
||||||
|
FAMILIES = (
|
||||||
|
"family=Fraunces:opsz,wght@9..144,400;9..144,500"
|
||||||
|
"&family=Inter:wght@400;500"
|
||||||
|
"&family=JetBrains+Mono:wght@400;500"
|
||||||
|
)
|
||||||
|
CSS_URL = f"https://fonts.googleapis.com/css2?{FAMILIES}&display=swap"
|
||||||
|
|
||||||
|
# A modern browser UA is required, not cosmetic: Google serves ancient TTF to
|
||||||
|
# unrecognised clients and woff2 only to browsers known to support it.
|
||||||
|
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, timeout=30):
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return r.read()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
css = fetch(CSS_URL).decode()
|
||||||
|
|
||||||
|
# Google emits a comment naming each subset before its @font-face block.
|
||||||
|
blocks = re.split(r"(?=/\*\s*[a-z0-9-]+\s*\*/)", css)
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
for old in OUT.glob("*.woff2"):
|
||||||
|
old.unlink()
|
||||||
|
|
||||||
|
out_css, seen, total = [], {}, 0
|
||||||
|
for block in blocks:
|
||||||
|
subset = re.search(r"/\*\s*([a-z0-9-]+)\s*\*/", block)
|
||||||
|
family = re.search(r"font-family:\s*'([^']+)'", block)
|
||||||
|
weight = re.search(r"font-weight:\s*([^;]+);", block)
|
||||||
|
url = re.search(r"url\((https://[^)]+\.woff2)\)", block)
|
||||||
|
if not (subset and family and url):
|
||||||
|
continue
|
||||||
|
w = weight.group(1).strip().replace(" ", "-") if weight else "400"
|
||||||
|
name = f"{family.group(1).replace(' ', '')}-{w}-{subset.group(1)}.woff2"
|
||||||
|
if name not in seen:
|
||||||
|
data = fetch(url.group(1))
|
||||||
|
(OUT / name).write_bytes(data)
|
||||||
|
seen[name] = len(data)
|
||||||
|
total += len(data)
|
||||||
|
out_css.append(block.replace(url.group(1), f"./{name}"))
|
||||||
|
|
||||||
|
header = (
|
||||||
|
"/* Generated by tools/vendor-fonts.py — do not edit by hand.\n"
|
||||||
|
" *\n"
|
||||||
|
" * Vendored from Google Fonts and served from our own origin: the app\n"
|
||||||
|
" * has to render with no outbound network, so a font provider link is\n"
|
||||||
|
" * not an option. Urls are relative so this keeps working under a base\n"
|
||||||
|
" * path. unicode-range is preserved, so the browser still fetches only\n"
|
||||||
|
" * the subsets a page needs.\n"
|
||||||
|
" */\n"
|
||||||
|
)
|
||||||
|
(OUT / "fonts.css").write_text(header + "".join(out_css))
|
||||||
|
for n, size in sorted(seen.items()):
|
||||||
|
print(f" {n:<44} {size/1024:7.1f} KB")
|
||||||
|
print(f"{len(seen)} files, {total/1024/1024:.2f} MB total -> {OUT.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+6
-6
@@ -30,12 +30,12 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<!-- Fonts are vendored into web/static/fonts by tools/vendor-fonts.py and
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
served from our own origin. A deployed instance has no outbound
|
||||||
<link
|
network, so a provider stylesheet would simply never arrive and the
|
||||||
rel="stylesheet"
|
UI would render in fallback faces — hence no font-provider link and
|
||||||
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500&family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
no preconnect to a host we don't run. -->
|
||||||
/>
|
<link rel="stylesheet" href="%sveltekit.assets%/fonts/fonts.css" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
<!-- MINSTREL_TOKENS -->
|
<!-- MINSTREL_TOKENS -->
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
// A deployed instance has no outbound network, so every asset the app draws
|
||||||
|
// itself with has to come from our own origin. This regressed once already
|
||||||
|
// (the font stylesheet was linked straight from Google), and the failure is
|
||||||
|
// invisible in development — the dev machine HAS internet, so the fonts load
|
||||||
|
// fine and only a real deployment renders in fallback faces.
|
||||||
|
//
|
||||||
|
// The assertion is on the PROPERTY, not on a vendor: any absolute URL in a
|
||||||
|
// resource-loading attribute fails, whoever is hosting it. Naming
|
||||||
|
// `fonts.googleapis.com` would pass the day someone reached for a different
|
||||||
|
// CDN, which is the same bug.
|
||||||
|
|
||||||
|
// fileURLToPath rather than import.meta.dirname: the latter needs Node 20.11+
|
||||||
|
// and this repo's toolchain is still on 18, where it is silently undefined.
|
||||||
|
const WEB = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
|
|
||||||
|
/** Strip comments before asserting an ABSENCE — otherwise prose describing
|
||||||
|
* the forbidden thing satisfies the check that forbids it. */
|
||||||
|
const stripComments = (html: string) => html.replace(/<!--[\s\S]*?-->/g, '');
|
||||||
|
|
||||||
|
/** href/src on anything that makes the browser fetch a subresource. */
|
||||||
|
const RESOURCE_URL = /<(?:link|script|img|source|iframe)\b[^>]*?\b(?:href|src)\s*=\s*["']([^"']+)["']/gi;
|
||||||
|
|
||||||
|
const externalUrlsIn = (html: string) =>
|
||||||
|
[...stripComments(html).matchAll(RESOURCE_URL)]
|
||||||
|
.map((m) => m[1])
|
||||||
|
.filter((u) => /^(?:https?:)?\/\//i.test(u));
|
||||||
|
|
||||||
|
describe('the app ships every asset it draws itself with', () => {
|
||||||
|
test('app.html loads no subresource from a third-party origin', () => {
|
||||||
|
const html = readFileSync(join(WEB, 'src/app.html'), 'utf8');
|
||||||
|
expect(externalUrlsIn(html)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('app.html does not preconnect or dns-prefetch to any host', () => {
|
||||||
|
// preconnect carries no href-fetch of its own, so the check above misses
|
||||||
|
// it — but it is the fingerprint of a third-party asset about to be added.
|
||||||
|
const html = stripComments(readFileSync(join(WEB, 'src/app.html'), 'utf8'));
|
||||||
|
const hints = [...html.matchAll(/<link\b[^>]*\brel\s*=\s*["']([^"']+)["'][^>]*>/gi)]
|
||||||
|
.filter((m) => /\b(?:preconnect|dns-prefetch)\b/i.test(m[1]));
|
||||||
|
expect(hints.map((m) => m[0])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the vendored font stylesheet exists and points only at local files', () => {
|
||||||
|
const dir = join(WEB, 'static/fonts');
|
||||||
|
const css = readFileSync(join(dir, 'fonts.css'), 'utf8');
|
||||||
|
|
||||||
|
const urls = [...css.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)].map((m) => m[1]);
|
||||||
|
expect(urls.length).toBeGreaterThan(0);
|
||||||
|
expect(urls.filter((u) => /^(?:https?:)?\/\//i.test(u))).toEqual([]);
|
||||||
|
|
||||||
|
// Every referenced file is actually present — a stylesheet pointing at a
|
||||||
|
// woff2 nobody committed fails exactly like a CDN link would, offline.
|
||||||
|
const present = new Set(readdirSync(dir));
|
||||||
|
const missing = urls
|
||||||
|
.map((u) => u.replace(/^\.\//, ''))
|
||||||
|
.filter((f) => !present.has(f));
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every font family the tokens name is actually vendored', () => {
|
||||||
|
// Pins the families to the TOKENS rather than to a hardcoded list, so
|
||||||
|
// swapping a typeface in tokens.json fails here until it is vendored.
|
||||||
|
const tokens = JSON.parse(
|
||||||
|
readFileSync(join(WEB, 'src/lib/styles/tokens.json'), 'utf8')
|
||||||
|
) as { fonts: Record<string, string> };
|
||||||
|
const files = readdirSync(join(WEB, 'static/fonts'));
|
||||||
|
for (const family of Object.values(tokens.fonts)) {
|
||||||
|
const prefix = `${family.replace(/\s+/g, '')}-`;
|
||||||
|
expect(
|
||||||
|
files.some((f) => f.startsWith(prefix) && f.endsWith('.woff2')),
|
||||||
|
`no vendored woff2 for "${family}"`
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
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.
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.
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.
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.
@@ -0,0 +1,296 @@
|
|||||||
|
/* Generated by tools/vendor-fonts.py — do not edit by hand.
|
||||||
|
*
|
||||||
|
* Vendored from Google Fonts and served from our own origin: the app
|
||||||
|
* has to render with no outbound network, so a font provider link is
|
||||||
|
* not an option. Urls are relative so this keeps working under a base
|
||||||
|
* path. unicode-range is preserved, so the browser still fetches only
|
||||||
|
* the subsets a page needs.
|
||||||
|
*/
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-400-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-400-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-400-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-500-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-500-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Fraunces-500-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-cyrillic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-cyrillic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-greek-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-400-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-cyrillic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-cyrillic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-greek-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Inter-500-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-cyrillic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-cyrillic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-400-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-cyrillic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-cyrillic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./JetBrainsMono-500-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user