[OCaml] High Intensity Training Online
feat self-host application fonts
Serve Work Sans and Courier Prime from the app so mobile rendering does not depend on late third-party font requests.
Changed files
lib/presenter/routes.ml
@@ -38,3 +38,4 @@
38
38
let%path record_slot_edit = "/logbook/%s/slots/%d/edit"
39
39
let%path stylesheet = "/assets/hito.css"
40
40
let%path workout_client = "/assets/workout-client.js"
41
Added:
let%path font = "/assets/fonts/%s"
lib/web/assets/fonts/courier-prime-400-latin.woff2
Binary files differ
lib/web/assets/fonts/courier-prime-700-latin.woff2
Binary files differ
lib/web/assets/fonts/work-sans-latin-var.woff2
Binary files differ
lib/web/assets/hito.css
@@ -1,4 +1,30 @@
1
Removed:
@import url("https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Work+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600&display=swap");
1
Added:
/* Self-hosted fonts. The app embeds and serves these under /assets/fonts/, so
2
Added:
the browser never reaches a third party. This removes the render-blocking
3
Added:
Google Fonts @import that left Courier Prime absent on slow mobile networks.
4
Added:
Work Sans ships as one variable WOFF2 covering the 400-800 weight axis.
5
Added:
Courier Prime is static, so it needs one face per weight. Latin subset only;
6
Added:
no italic face is used anywhere in the app. */
7
Added:
@font-face {
8
Added:
font-family: "Work Sans";
9
Added:
font-style: normal;
10
Added:
font-weight: 400 800;
11
Added:
font-display: swap;
12
Added:
src: url("/assets/fonts/work-sans-latin-var.woff2") format("woff2");
13
Added:
}
14
Added:
@font-face {
15
Added:
font-family: "Courier Prime";
16
Added:
font-style: normal;
17
Added:
font-weight: 400;
18
Added:
font-display: swap;
19
Added:
src: url("/assets/fonts/courier-prime-400-latin.woff2") format("woff2");
20
Added:
}
21
Added:
@font-face {
22
Added:
font-family: "Courier Prime";
23
Added:
font-style: normal;
24
Added:
font-weight: 700;
25
Added:
font-display: swap;
26
Added:
src: url("/assets/fonts/courier-prime-700-latin.woff2") format("woff2");
27
Added:
}
2
28
3
29
:root {
4
30
color-scheme: light;
lib/web/handlers_assets.ml
@@ -19,6 +19,24 @@
19
19
let asset_headers content_type =
20
20
[ ("Content-Type", content_type); ("Cache-Control", "no-cache") ]
21
21
22
Added:
(* Fonts are the exception: each filename maps to fixed font bytes that never
23
Added:
change, and they are the largest assets. A long immutable cache lets a
24
Added:
returning visitor skip the download, which is the whole point of self-hosting
25
Added:
them. A new font would ship under a new filename. *)
26
Added:
let font_headers =
27
Added:
[
28
Added:
("Content-Type", "font/woff2");
29
Added:
("Cache-Control", "public, max-age=31536000, immutable");
30
Added:
]
31
Added:
32
Added:
(* Serve an embedded WOFF2 by name. The name is matched against the embedded
33
Added:
set, so an unknown or crafted path yields a plain 404 rather than reaching
34
Added:
the filesystem. *)
35
Added:
let font name =
36
Added:
match Stylesheet.read ("fonts/" ^ name) with
37
Added:
| Some bytes -> Dream.respond ~headers:font_headers bytes
38
Added:
| None -> Dream.respond ~status:`Not_Found ""
39
Added:
22
40
let routes =
23
41
[
24
42
Dream_html.get Routes.stylesheet (fun _ ->
@@ -29,4 +47,5 @@
29
47
Dream.respond
30
48
~headers:(asset_headers "application/javascript; charset=utf-8")
31
49
workout_client);
50
Added:
Dream_html.get Routes.font (fun _ name -> font name);
32
51
]
test/web/workout.ml
@@ -35,6 +35,43 @@
35
35
let asset = get c "/assets/workout-client.js" in
36
36
Alcotest.(check int) "serves the workout client" 200 (status asset)
37
37
);
38
Added:
( "self-hosted fonts are served as woff2, and an unknown name is 404",
39
Added:
`Quick,
40
Added:
fun () ->
41
Added:
(* The fonts are embedded and served from the app's own origin, so a
42
Added:
visitor never reaches a third party. Each known face returns 200
43
Added:
with the woff2 media type. An unknown or crafted name returns a
44
Added:
plain 404 rather than reaching the filesystem. No sign-in is
45
Added:
needed: fonts are public assets. *)
46
Added:
let c = client () in
47
Added:
let faces =
48
Added:
[
49
Added:
"work-sans-latin-var.woff2";
50
Added:
"courier-prime-400-latin.woff2";
51
Added:
"courier-prime-700-latin.woff2";
52
Added:
]
53
Added:
in
54
Added:
List.iter
55
Added:
(fun face ->
56
Added:
let response = get c ("/assets/fonts/" ^ face) in
57
Added:
Alcotest.(check int)
58
Added:
(Printf.sprintf "%s is served" face)
59
Added:
200 (status response);
60
Added:
Alcotest.(check (option string))
61
Added:
(Printf.sprintf "%s is woff2" face)
62
Added:
(Some "font/woff2")
63
Added:
(Dream.header response "Content-Type");
64
Added:
(* The body is the real font: a non-empty payload whose first
65
Added:
four bytes are the WOFF2 signature. *)
66
Added:
let bytes = body response in
67
Added:
Alcotest.(check bool)
68
Added:
(Printf.sprintf "%s has a woff2 signature" face)
69
Added:
true
70
Added:
(String.length bytes >= 4 && String.sub bytes 0 4 = "wOF2"))
71
Added:
faces;
72
Added:
let missing = get c "/assets/fonts/does-not-exist.woff2" in
73
Added:
Alcotest.(check int)
74
Added:
"an unknown font is not found" 404 (status missing) );
38
75
( "beginning before recovery is refused without an override",
39
76
`Quick,
40
77
fun () ->