Awesome
google-webfonts-helper
![GitHub Sponsors](https://img.shields.io/github/sponsors/majodev)
A Hassle-Free Way to Self-Host Google Fonts
Current Sponsors
Help me keep this service alive by sponsoring me. Thank you. ❤️
<img src="https://sponsors.mranftl.com/avatar/0" width="35"> <img src="https://sponsors.mranftl.com/avatar/1" width="35"> <img src="https://sponsors.mranftl.com/avatar/2" width="35"> <img src="https://sponsors.mranftl.com/avatar/3" width="35"> <img src="https://sponsors.mranftl.com/avatar/4" width="35"> <img src="https://sponsors.mranftl.com/avatar/5" width="35"> <img src="https://sponsors.mranftl.com/avatar/6" width="35"> <img src="https://sponsors.mranftl.com/avatar/7" width="35"> <img src="https://sponsors.mranftl.com/avatar/8" width="35"> <img src="https://sponsors.mranftl.com/avatar/9" width="35"> <img src="https://sponsors.mranftl.com/avatar/10" width="35"> <img src="https://sponsors.mranftl.com/avatar/11" width="35"> <img src="https://sponsors.mranftl.com/avatar/12" width="35"> <img src="https://sponsors.mranftl.com/avatar/13" width="35"> <img src="https://sponsors.mranftl.com/avatar/14" width="35"> <img src="https://sponsors.mranftl.com/avatar/15" width="35"> <img src="https://sponsors.mranftl.com/avatar/16" width="35"> <img src="https://sponsors.mranftl.com/avatar/17" width="35"> <img src="https://sponsors.mranftl.com/avatar/18" width="35"> <img src="https://sponsors.mranftl.com/avatar/19" width="35"> <img src="https://sponsors.mranftl.com/avatar/20" width="35"> <img src="https://sponsors.mranftl.com/avatar/21" width="35"> <img src="https://sponsors.mranftl.com/avatar/22" width="35"> <img src="https://sponsors.mranftl.com/avatar/23" width="35"> <img src="https://sponsors.mranftl.com/avatar/24" width="35"> <img src="https://sponsors.mranftl.com/avatar/25" width="35"> <img src="https://sponsors.mranftl.com/avatar/26" width="35"> <img src="https://sponsors.mranftl.com/avatar/27" width="35"> <img src="https://sponsors.mranftl.com/avatar/28" width="35"> <img src="https://sponsors.mranftl.com/avatar/29" width="35"> <img src="https://sponsors.mranftl.com/avatar/30" width="35"> <img src="https://sponsors.mranftl.com/avatar/31" width="35"> <img src="https://sponsors.mranftl.com/avatar/32" width="35"> <img src="https://sponsors.mranftl.com/avatar/33" width="35"> <img src="https://sponsors.mranftl.com/avatar/34" width="35"> <img src="https://sponsors.mranftl.com/avatar/35" width="35"> <img src="https://sponsors.mranftl.com/avatar/36" width="35"> <img src="https://sponsors.mranftl.com/avatar/37" width="35"> <img src="https://sponsors.mranftl.com/avatar/38" width="35"> <img src="https://sponsors.mranftl.com/avatar/39" width="35"> <img src="https://sponsors.mranftl.com/avatar/40" width="35"> <img src="https://sponsors.mranftl.com/avatar/41" width="35"> <img src="https://sponsors.mranftl.com/avatar/42" width="35"> <img src="https://sponsors.mranftl.com/avatar/43" width="35"> <img src="https://sponsors.mranftl.com/avatar/44" width="35"> <img src="https://sponsors.mranftl.com/avatar/45" width="35"> <img src="https://sponsors.mranftl.com/avatar/46" width="35"> <img src="https://sponsors.mranftl.com/avatar/47" width="35"> <img src="https://sponsors.mranftl.com/avatar/48" width="35"> <img src="https://sponsors.mranftl.com/avatar/49" width="35"> <img src="https://sponsors.mranftl.com/avatar/50" width="35"> <img src="https://sponsors.mranftl.com/avatar/51" width="35"> <img src="https://sponsors.mranftl.com/avatar/52" width="35"> <img src="https://sponsors.mranftl.com/avatar/53" width="35"> <img src="https://sponsors.mranftl.com/avatar/54" width="35"> <img src="https://sponsors.mranftl.com/avatar/55" width="35"> <img src="https://sponsors.mranftl.com/avatar/56" width="35"> <img src="https://sponsors.mranftl.com/avatar/57" width="35"> <img src="https://sponsors.mranftl.com/avatar/58" width="35"> <img src="https://sponsors.mranftl.com/avatar/59" width="35"> <img src="https://sponsors.mranftl.com/avatar/60" width="35"> <img src="https://sponsors.mranftl.com/avatar/61" width="35"> <img src="https://sponsors.mranftl.com/avatar/62" width="35"> <img src="https://sponsors.mranftl.com/avatar/63" width="35"> <img src="https://sponsors.mranftl.com/avatar/64" width="35"> <img src="https://sponsors.mranftl.com/avatar/65" width="35"> <img src="https://sponsors.mranftl.com/avatar/66" width="35"> <img src="https://sponsors.mranftl.com/avatar/67" width="35"> <img src="https://sponsors.mranftl.com/avatar/68" width="35">
ToC
Give it a try: https://gwfh.mranftl.com
This service might be handy if you want to host a specific Google font on your own server:
- font style and charset customization
- CSS snippets
.eot
,.woff
,.woff2
,.svg
,.ttf
font file formats download (zipped).
Running gwfh on your own server
I provide prebuilt Docker images via GitHub Packages. You can use them as follows:
# See https://developers.google.com/fonts/docs/developer_api for creating your own API-Key.
docker run -e GOOGLE_FONTS_API_KEY=<YOUR-API-KEY> -p 8080:8080 ghcr.io/majodev/google-webfonts-helper:<TAG>
# Express server listening on 8080, in production mode
Development
Quickstart
Do this to setup a development environment:
# Ensure to set the GOOGLE_FONTS_API_KEY env var inside your own gitignored .env file
# See https://developers.google.com/fonts/docs/developer_api for creating your own API-Key.
echo "GOOGLE_FONTS_API_KEY=<YOUR-API-KEY>" > .env
# Start up the development docker container (multistage Dockerfile, stage 1 only)
./docker-helper.sh --up
# [+] Running 1/0
# ⠿ Container gwfh-service-1 Running
# node@3b506a285f7f:/app$
# within this development container:
node$ yarn --pure-lockfile
node$ ./node_modules/.bin/bower install
# start development server
node$ grunt serve
# [...]
# Express server listening on 9000, in development mode
# The application is now available at http://127.0.0.1:9000 (watching for code changes)
# start production server (same command as within the final docker multistage build)
node$ grunt build
node$ NODE_ENV=production node dist/server/app.js
# Express server listening on 8080, in production mode
Production build
If you want to build and run your own production container locally:
# Build the production docker container (final stage)
docker build . -t <your-image-tag>
# Run it (if you have previously started the development container, halt it!)
./docker-helper.sh --halt
docker run -e GOOGLE_FONTS_API_KEY=<YOUR-API-KEY> -p 8080:8080 <your-image-tag>
# Express server listening on 8080, in production mode
To mitigate security issues especially with the projects' deprecated dependencies, the final image is based on a minimal container image. It runs rootless and has no development dependencies.
JSON API
The API is public, feel free to use it directly (rate-limits may apply).
GET /api/fonts
Returns a list of all fonts, sorted by popularity. E.g. curl https://gwfh.mranftl.com/api/fonts
:
[{
"id": "open-sans",
"family": "Open Sans",
"variants": ["300", "300italic", "regular", "italic", "600", "600italic", "700", "700italic", "800", "800italic"],
"subsets": ["devanagari", "greek", "latin", "cyrillic-ext", "cyrillic", "greek-ext", "vietnamese", "latin-ext"],
"category": "sans-serif",
"version": "v10",
"lastModified": "2014-10-17",
"popularity": 1,
"defSubset": "latin",
"defVariant": "regular"
} [...]
]
GET /api/fonts/[id]?subsets=latin,latin-ext
Returns a font with urls to the actual font files google's servers. subsets
is optional (will serve the defSubset
if unspecified). E.g. curl "https://gwfh.mranftl.com/api/fonts/modern-antiqua?subsets=latin,latin-ext"
(the double quotes are important as query parameters may else be stripped!):
{
"id": "modern-antiqua",
"family": "Modern Antiqua",
"variants": [{
"id": "regular",
"eot": "https://fonts.gstatic.com/s/modernantiqua/v6/8qX_tr6Xzy4t9fvZDXPkhzThM-TJeMvVB0dIsYy4U7E.eot",
"fontFamily": "'Modern Antiqua'",
"fontStyle": "normal",
"fontWeight": "400",
"woff": "https://fonts.gstatic.com/s/modernantiqua/v6/8qX_tr6Xzy4t9fvZDXPkh1bbnkJREviNM815YSrb1io.woff",
"local": ["Modern Antiqua Regular", "ModernAntiqua-Regular"],
"ttf": "https://fonts.gstatic.com/s/modernantiqua/v6/8qX_tr6Xzy4t9fvZDXPkhxr_S_FdaWWVbb1LgBbjq4o.ttf",
"svg": "https://fonts.gstatic.com/l/font?kit=8qX_tr6Xzy4t9fvZDXPkh0sAoW0rAsWAgyWthbXBUKs#ModernAntiqua",
"woff2": "https://fonts.gstatic.com/s/modernantiqua/v6/8qX_tr6Xzy4t9fvZDXPkh08GHjg64nS_BBLu6wRo0k8.woff2"
}],
"subsets": ["latin", "latin-ext"],
"category": "display",
"version": "v6",
"lastModified": "2014-08-28",
"popularity": 522,
"defSubset": "latin",
"defVariant": "regular",
"subsetMap": {
"latin": true,
"latin-ext": true
},
"storeID": "latin-ext_latin"
}
GET /api/fonts/[id]?download=zip&subsets=latin&formats=woff,woff2&variants=regular
Download a zipped archive with all .eot
, .woff
, .woff2
, .svg
, .ttf
files of a specified font. The query parameters formats
and variants
are optional (includes everything if no filtering is applied). is E.g. curl -o fontfiles.zip "https://gwfh.mranftl.com/api/fonts/lato?download=zip&subsets=latin,latin-ext&variants=regular,700&formats=woff"
(the double quotes are important as query parameters may else be stripped!)
History
2023:
- Project upgraded to be compatible with Node.js v18+.
- Automated prebuilt Docker images via GitHub Actions.
/server
was fully refactored/modernized (async/await) and now compiles with TypeScript.- Switch to
node:18
for the final image. /client
can still be considered very legacy Angular code.
2022:
This service was mostly on life-support, most of its code and dependencies can be considered deprecated. The current docker image wrapping node@v0.10.44
runs rootless and is hopefully enough to keep the bandits out. API attack surface should be minimal anyways.
2014:
This service was originally a prototype I've created to get familiar with Angular and Express. All magic by generator-angular-fullstack. See my note here.
Idea originally by Clemens Lang who created an awesome bash script to download Google fonts in all formats.
License
(c) Mario Ranftl MIT License