Skip to content

Commit 992b3c7

Browse files
authored
Avoid contacting Google Fonts directly (#9)
* Avoid contacting Google Fonts directly * Remove logging * Limit proxied headers
1 parent 71fc3b4 commit 992b3c7

File tree

4 files changed

+83
-1
lines changed

4 files changed

+83
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/public/
22
/resources/_gen/
33
/.hugo_build.lock
4+
5+
# Local Netlify folder
6+
.netlify

layouts/_default/baseof.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta charset="utf-8">
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
88
<meta name="google" content="notranslate" />
9-
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet">
9+
<link href="/fonts/css?family=Muli:400,400i,700,700i" rel="stylesheet">
1010
<link rel="stylesheet" href="{{ (resources.Get "sass/main.scss" | resources.ExecuteAsTemplate "assets/css/main.css" . | resources.ToCSS (dict "indentWidth" 4 "outputStyle" "nested" "precision" 10)).Permalink }}">
1111
{{ block "head" . }}{{ end }}
1212
</head>

netlify.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ command = "hugo"
2020

2121
[headers.values]
2222
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
23+
24+
[[redirects]]
25+
from = "/fonts/file/*"
26+
to = "https://fonts.gstatic.com/:splat"
27+
status = 200
28+
force = true
29+
30+
[[edge_functions]]
31+
path = "/fonts/css"
32+
function = "fonts"

netlify/edge-functions/fonts.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Context } from "https://edge.netlify.com";
2+
import * as css from "https://esm.sh/[email protected]";
3+
import * as contentType from "https://deno.land/x/[email protected]/mod.ts";
4+
5+
export default async (
6+
request: Request,
7+
_context: Context
8+
): Promise<Response> => {
9+
const { searchParams } = new URL(request.url);
10+
const url = `https://fonts.googleapis.com/css?${searchParams.toString()}`;
11+
const resp = await fetch(url, {
12+
headers: {
13+
// User-Agent is used to serve the right font format
14+
"User-Agent": request.headers.get("User-Agent") || "",
15+
},
16+
});
17+
if (!resp.ok) {
18+
return new Response("Request to upstream failed", { status: 503 });
19+
}
20+
const ct = contentType.parse(resp.headers.get("Content-Type") ?? "");
21+
if (ct.type != "text/css") {
22+
return new Response(`Response was not CSS, but was: ${ct}`, {
23+
status: 503,
24+
});
25+
}
26+
27+
try {
28+
const newCSS = rewriteURLs(await resp.text());
29+
const headers = new Headers({
30+
"Content-Type": "text/css; charset=utf-8",
31+
"Cache-Control": resp.headers.get("cache-control") ?? "",
32+
Expires: resp.headers.get("expires") ?? "",
33+
});
34+
return new Response(newCSS, { headers });
35+
} catch (e) {
36+
return new Response(e, { status: 500 });
37+
}
38+
};
39+
40+
const urlRegexp = /url\(([^\)]+)\)/;
41+
const rewriteURLs = (text: string): string => {
42+
const sheet = css.parse(text);
43+
if (sheet.stylesheet?.parsingErrors?.length) {
44+
throw new Error(`Invalid CSS: ${sheet.stylesheet?.parsingErrors}`);
45+
}
46+
47+
for (const rule of sheet.stylesheet?.rules || []) {
48+
if (rule.type !== "font-face") {
49+
continue;
50+
}
51+
for (const prop of (rule as css.FontFace).declarations || []) {
52+
const decl = prop as css.Declaration;
53+
if (decl.property !== "src" || !decl.value) {
54+
continue;
55+
}
56+
const matches = urlRegexp.exec(decl.value);
57+
if (!matches || matches.length < 2) {
58+
continue;
59+
}
60+
const url = new URL(matches[1]);
61+
decl.value = decl.value.replace(
62+
urlRegexp,
63+
`url(/fonts/file${url.pathname})`
64+
);
65+
}
66+
}
67+
68+
return css.stringify(sheet);
69+
};

0 commit comments

Comments
 (0)