Self-host your web fonts
Serve fonts from your own server: download the files, write @font-face rules with WOFF2 and unicode-range, then set up preload, font-display, caching, CORS and CSP.
Self-hosting means the font files live on the same server as your website. Visitors’ browsers fetch them from you, and no font host sees the request. For a typical site with one or two families it takes about half an hour. This guide uses Sandhavn as the example; the steps are the same for every family in the catalogue.
Before you start
- You need to be able to upload files to the web server and edit the site’s HTML
<head>or main stylesheet. On WordPress, follow the WordPress guide instead. - Note which styles the design uses: upright only, or upright and italic. Every extra file costs download time, so only self-host what you need.
1. Get the font files
Open the family’s font page and scroll to “Self-host instead”. It lists one full WOFF2 file per style, a ready-made CSS snippet and a link to the ZIP download. The ZIP contains the TTF files, the licence (OFL-1.1) and a README.
Use WOFF2. Every browser in current use supports it, and it is usually less than half the size of the TTF. Our families are variable fonts: one file per style covers every weight from 100 to 900, so Sandhavn needs two files, one upright and one italic.
To build your own WOFF2 from the TTF, or keep only the characters you need, use fontTools:
pip install fonttools brotli
# Convert the whole font
fonttools ttLib.woff2 compress -o sandhavn.woff2 Sandhavn.ttf
# Or keep only Latin characters (smaller file)
pyftsubset Sandhavn.ttf --flavor=woff2 --layout-features='*' \
--unicodes='U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD' \
--output-file=sandhavn-latin.woff2
Use the TTF file names from the ZIP; the names above are examples. Subsetting removes characters. If a visitor’s text contains one you removed, the browser falls back to another font for that character. Keep the subsets your content needs, for example Latin Extended for Polish, Czech or Turkish.
2. Put the files on your server
Create a folder such as /fonts/ in the web root and upload the WOFF2 files and the licence file. Use names that change when the file changes, for example sandhavn-v2.woff2. That lets you cache them for a long time (step 6).
3. Write the @font-face rules
Add one rule per file to your main stylesheet, before anything uses the family. The font page’s snippet already has the right values; this is what it looks like for Sandhavn:
@font-face {
font-family: 'Sandhavn';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/sandhavn.woff2') format('woff2');
}
@font-face {
font-family: 'Sandhavn';
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/sandhavn-italic.woff2') format('woff2');
}
body {
font-family: 'Sandhavn', sans-serif;
}
- One family name, several rules. Give the upright and italic file the same
font-familyand setfont-styleto match the file. Then<em>picks the italic file instead of the browser slanting the upright one. - Weight range.
font-weight: 100 900tells the browser that one file covers all weights, sofont-weight: 650works without another download. - font-display controls what happens while the file downloads.
swapshows a fallback font immediately and swaps when the font arrives;optionaluses the font only if it arrives very quickly, which avoids layout shifts on slow connections. - Paths in
url()are relative to the stylesheet, not the page. Root-relative paths such as/fonts/…avoid surprises.
4. Split by script with unicode-range
If your site uses several scripts, you can split each style into one file per script and tell the browser which characters each file covers. The browser then downloads only the files whose characters appear on the page:
/* latin-ext */
@font-face {
font-family: 'Sandhavn';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/sandhavn-latin-ext.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Sandhavn';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/sandhavn-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Create each file with pyftsubset (step 1), using the same ranges in --unicodes as in unicode-range. To see the ranges our CDN uses, open https://fonts.heymadefonts.com/css2?family=Sandhavn:wght@400 in your browser. For a site in a single Latin-script language, one full file per style is usually simpler and small enough.
5. Preload the critical file
The browser only discovers a font after it has downloaded and parsed the CSS and found text that uses it. For the one file used above the fold, usually the upright body file, you can start the download earlier:
<link rel="preload" href="/fonts/sandhavn.woff2" as="font" type="font/woff2" crossorigin>
- Keep the
crossoriginattribute even though the file is on your own domain. Fonts are always requested in CORS mode; without the attribute the browser downloads the file twice. - Preload one or two files at most. A preloaded file that the page does not use wastes bandwidth and produces a console warning.
- The
hrefmust match theurl()in your CSS exactly.
6. Caching, MIME type and CORS
Font files rarely change. If the file name changes whenever the content does (step 2), let browsers keep them for a year:
location ~* \.woff2$ {
types { font/woff2 woff2; }
add_header Cache-Control "public, max-age=31536000, immutable";
}
AddType font/woff2 .woff2
<FilesMatch "\.woff2$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
- Send
Content-Type: font/woff2. Some older server configurations sendapplication/octet-stream; browsers usually still load the font, but fix it anyway. - CORS: nothing to do when the fonts are on the same origin as the page. If they are on another domain or subdomain, such as
static.example.com, that server must sendAccess-Control-Allow-Originwith your site’s origin (or*), or the browser refuses to use the font.
7. Content Security Policy
If your site sends a Content-Security-Policy header, self-hosted fonts need font-src 'self'. Remove any font hosts you no longer use from style-src and font-src, so that a forgotten reference fails visibly instead of quietly loading.
8. Remove old font requests
If the site loaded fonts from a font host before, find and remove those references. Paste your page’s <head> into the switch tool to list them, or run a website scan to see each request with the line that caused it.
- Delete the
<link rel="stylesheet">to the font host and any@importof it in your CSS. - Delete
preconnectanddns-prefetchhints for the font host. They open a connection even when no font is loaded from there. - Clear any caching or CSS optimisation layer (CDN, page cache, minifier) so it stops serving the old HTML and combined CSS.
9. Check the result
- Open the site in a private window and reload with the developer tools’ Network panel open. Filter by “font”: every request should go to your own domain.
- Embedded services such as maps, videos or captchas may still load their own fonts; those come from the embed, not your stylesheet.
- Check a few different page types (home, article, contact form, shop), because templates can load fonts separately.
- Run a website scan for a record you can keep. A scan covers the page it visits, so scan the pages that matter.
Licence
Every HeyMadeFonts family uses the SIL Open Font License 1.1, which allows self-hosting. Keep the licence file from the ZIP next to the fonts on your server; it is part of the conditions. If you modify the files (subsetting can count as a modification), read the licence’s rules on Reserved Font Names, which may require you to rename a modified version. Each font page shows the licence and any reserved names. More in font licences explained.
What this changes, and what it does not
When a browser loads a font from a third-party host, it connects to that host’s servers. The request transmits the visitor’s IP address and HTTP metadata, such as the browser’s user agent, the requested URL and, depending on the referrer policy, the address of the page, to whoever operates that host. Self-hosted fonts keep those requests on your own server.
Changing how your fonts load changes which third parties your visitors’ browsers contact. It does not by itself decide whether your website as a whole meets data protection requirements. This guide is not legal advice. For compliance questions, ask a qualified lawyer or your data protection officer.
Prefer not to host files yourself? The HeyMadeFonts CDN serves the same families and sets no cookies. It is still a third party.