perf: self-host fonts + lazy-load reCAPTCHA

Self-host DM Serif Display (regular+italic) and Inter (variable) as
latin-subset woff2 (~84KB), with @font-face + font-display:swap and
preloads. Removes render-blocking Google Fonts (2 third-party origins,
extra DNS/preconnect). reCAPTCHA api.js is no longer eager-loaded in
<head>; form.js injects it on first form focus/submit, keeping ~50KB+
of third-party JS off initial load on form pages. CloudFront CSP
font-src updated to allow 'self' (additive; gstatic kept for transition).
This commit is contained in:
Adam Moussa 2026-06-12 17:26:14 -04:00
parent 381f6b6dce
commit 87d5dd5d0a
6 changed files with 61 additions and 11 deletions

View file

@ -21,10 +21,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32.png">
<link rel="apple-touch-icon" sizes="192x192" href="/assets/images/favicon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500;600&display=swap">
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="preload" href="/assets/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/dm-serif-display.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/css/main.css">
{%- if pageCss %}
<link rel="stylesheet" href="/assets/css/{{ pageCss }}.css">
@ -33,9 +31,7 @@
<link rel="preload" as="image" href="/assets/images/hero-bg.webp" fetchpriority="high">
{%- endif %}
{%- block head %}{% endblock %}
{%- if recaptcha %}
<script src="https://www.google.com/recaptcha/api.js?render=6Les66kUAAAAANyLrgkl7iuN4JUpNlB5upaMovI4"></script>
{%- endif %}
{#- reCAPTCHA is lazy-loaded by form.js on first form interaction (see recaptcha pages) #}
</head>
<body>

View file

@ -2,6 +2,29 @@
Sea Haven Industries — Shared Styles
══════════════════════════════════════════════ */
/* ── Self-hosted fonts (latin subset) ── */
@font-face {
font-family: 'DM Serif Display';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/dm-serif-display.woff2') format('woff2');
}
@font-face {
font-family: 'DM Serif Display';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/dm-serif-display-italic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900; /* variable */
font-display: swap;
src: url('/assets/fonts/inter.woff2') format('woff2');
}
/* ── Reset & Tokens ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/inter.woff2 Normal file

Binary file not shown.

View file

@ -1,14 +1,44 @@
// Basin AJAX submission with reCAPTCHA v3 for the contact form and job apply forms.
// Basin AJAX submission with lazily-loaded reCAPTCHA v3 for the contact form
// and job apply forms. The reCAPTCHA script is not loaded until the visitor
// first interacts with a form (or submits), keeping it off the initial page load.
(function () {
"use strict";
const SITE_KEY = "6Les66kUAAAAANyLrgkl7iuN4JUpNlB5upaMovI4";
// Contact page wraps the form in a .contact-form div; services puts the class
// on the <form> itself; job pages use .apply-form. Match all three.
const forms = document.querySelectorAll(".contact-form form, form.contact-form, form.apply-form");
if (!forms.length) return;
// ── Lazy reCAPTCHA loader ──────────────────────────────────────
let loadStarted = false;
function loadRecaptcha() {
if (loadStarted) return;
loadStarted = true;
const s = document.createElement("script");
s.src = "https://www.google.com/recaptcha/api.js?render=" + SITE_KEY;
s.async = true;
document.head.appendChild(s);
}
// Resolves once grecaptcha is ready; loads the script if not already loading.
function readyRecaptcha() {
loadRecaptcha();
return new Promise((resolve, reject) => {
const start = Date.now();
(function check() {
if (window.grecaptcha && window.grecaptcha.execute) {
window.grecaptcha.ready(resolve);
} else if (Date.now() - start > 10000) {
reject(new Error("reCAPTCHA failed to load"));
} else {
setTimeout(check, 100);
}
})();
});
}
forms.forEach((form) => {
// Warm the loader as soon as the user engages with the form.
form.addEventListener("focusin", loadRecaptcha, { once: true });
const success = document.getElementById("form-success");
const statusEl = document.getElementById("form-status"); // optional role="status" region
const tokenField = form.querySelector("#g-recaptcha-response");
@ -29,7 +59,8 @@
announce("");
try {
const token = await grecaptcha.execute(SITE_KEY, { action: "submit" });
await readyRecaptcha();
const token = await window.grecaptcha.execute(SITE_KEY, { action: "submit" });
if (tokenField) tokenField.value = token;
const res = await fetch(form.action, {
method: "POST",