diff --git a/.eleventy.js b/.eleventy.js index 3b3f1b8..ab32e73 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -21,6 +21,9 @@ module.exports = function (eleventyConfig) { // Current year, for the footer (replaces the deprecated document.write pattern). eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`); + // JSON-encode a value (for safely emitting strings into JSON-LD). + eleventyConfig.addFilter("json", (v) => JSON.stringify(v)); + return { dir: { input: ".", includes: "_includes", data: "_data", output: "_site" }, templateFormats: ["njk"], diff --git a/README.md b/README.md index 2a82c1d..fa9d4a8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ npm run build # build to _site/ | Job: Traveling Maintenance/Electrical Assistant | `/careers/traveling-maintenance-assistant/` | ## Changelog +* 12-JUN-2026: Frontend polish — self-hosted fonts (removed render-blocking Google Fonts), lazy-loaded reCAPTCHA, BreadcrumbList JSON-LD on interior pages + richer LocalBusiness (geo/areaServed/hasMap), removed dead CSS classes and `!important` nav hacks. * 12-JUN-2026: Migrated to a thin Eleventy build — shared chrome (head/nav/footer/JS) deduplicated into one base layout + partials (was hand-copied across 13 pages). Added skip-link + `
` landmark, accessible mobile-menu dialog, `:focus-visible`, WCAG AA footer contrast, 1200×630 `og-cover` social image, hero LCP preload, WebP logos, image `width`/`height` (CLS), and pre-deploy CI validation (htmlhint/JSON-LD/sitemap/links). Deploy now builds and syncs `_site/` with split Cache-Control. * 14-JUL-2025: Initial pull from WordPress site, changed permalinks for proper navigation * 17-JUL-2025: Add GitHub Actions workflow for S3 sync on push, using AWS OIDC diff --git a/_includes/base.njk b/_includes/base.njk index fd808f0..edecaaa 100644 --- a/_includes/base.njk +++ b/_includes/base.njk @@ -21,10 +21,8 @@ - - - - + + {%- if pageCss %} @@ -33,9 +31,7 @@ {%- endif %} {%- block head %}{% endblock %} - {%- if recaptcha %} - - {%- endif %} + {#- reCAPTCHA is lazy-loaded by form.js on first form interaction (see recaptcha pages) #} @@ -50,6 +46,10 @@ {% include "footer.njk" %} + {%- if breadcrumbName %} + {% include "breadcrumb-ld.njk" %} + {%- endif %} + {%- if recaptcha %} diff --git a/_includes/breadcrumb-ld.njk b/_includes/breadcrumb-ld.njk new file mode 100644 index 0000000..462888f --- /dev/null +++ b/_includes/breadcrumb-ld.njk @@ -0,0 +1,13 @@ + diff --git a/about/index.njk b/about/index.njk index ebe978b..4e2733b 100644 --- a/about/index.njk +++ b/about/index.njk @@ -3,6 +3,7 @@ layout: base.njk title: "About — Sea Haven Industries" description: "Learn about Sea Haven Industries — your trusted partner in construction with 20 years of experience, expert technicians, and a commitment to excellence." pageCss: about +breadcrumbName: "About" ---
diff --git a/assets/css/home.css b/assets/css/home.css index 522c5b7..b355aca 100644 --- a/assets/css/home.css +++ b/assets/css/home.css @@ -19,6 +19,14 @@ color: #fff; } .site-header:not(.scrolled) .nav-hamburger { color: #fff; } + /* Hover on the transparent (homepage, not-scrolled) nav — these win on + specificity so main.css's hover rules no longer need !important. */ + .site-header:not(.scrolled) .nav-links a:hover { color: var(--rose); } + .site-header:not(.scrolled) .nav-cta:hover { + background: var(--rose); + border-color: var(--rose); + color: #fff; + } /* ── Hero ── */ .hero { diff --git a/assets/css/main.css b/assets/css/main.css index 3191487..2735d02 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -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; } @@ -65,12 +88,6 @@ a { color: inherit; text-decoration: none; } } /* ── Typography ── */ -.display-xl { - font-family: var(--font-display); - font-size: clamp(2.8rem, 6vw, 5.5rem); - line-height: 1.05; - letter-spacing: -0.02em; -} .display-lg { font-family: var(--font-display); font-size: clamp(2rem, 4vw, 3.5rem); @@ -89,8 +106,6 @@ a { color: inherit; text-decoration: none; } text-transform: uppercase; color: var(--rose); } -.body-lg { font-size: 1.125rem; line-height: 1.7; } -.body-sm { font-size: 0.875rem; line-height: 1.6; color: var(--text-muted); } /* ── Buttons ── */ .btn { @@ -174,7 +189,7 @@ a { color: inherit; text-decoration: none; } color: var(--ink-mid); transition: color 0.15s; } -.nav-links a:hover { color: var(--rose) !important; } +.nav-links a:hover { color: var(--rose); } .nav-cta { font-size: 0.8125rem; @@ -187,9 +202,9 @@ a { color: inherit; text-decoration: none; } transition: all 0.2s; } .nav-cta:hover { - background: var(--rose) !important; - border-color: var(--rose) !important; - color: #fff !important; + background: var(--rose); + border-color: var(--rose); + color: #fff; } .nav-hamburger { diff --git a/assets/fonts/dm-serif-display-italic.woff2 b/assets/fonts/dm-serif-display-italic.woff2 new file mode 100644 index 0000000..45d19db Binary files /dev/null and b/assets/fonts/dm-serif-display-italic.woff2 differ diff --git a/assets/fonts/dm-serif-display.woff2 b/assets/fonts/dm-serif-display.woff2 new file mode 100644 index 0000000..7c9fa83 Binary files /dev/null and b/assets/fonts/dm-serif-display.woff2 differ diff --git a/assets/fonts/inter.woff2 b/assets/fonts/inter.woff2 new file mode 100644 index 0000000..91dc3e8 Binary files /dev/null and b/assets/fonts/inter.woff2 differ diff --git a/assets/js/form.js b/assets/js/form.js index 136829f..7694e8c 100644 --- a/assets/js/form.js +++ b/assets/js/form.js @@ -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
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", diff --git a/careers/dispatcher/index.njk b/careers/dispatcher/index.njk index 94e736a..3b10122 100644 --- a/careers/dispatcher/index.njk +++ b/careers/dispatcher/index.njk @@ -4,6 +4,8 @@ title: "Dispatcher — Sea Haven Industries" description: "Apply for the Dispatcher position at Sea Haven Industries. View job details and submit your application today." pageCss: jobs recaptcha: true +breadcrumbName: "Dispatcher" +breadcrumbParent: { name: "Careers", url: "/careers/" } ---
diff --git a/careers/index.njk b/careers/index.njk index f415559..c572b2b 100644 --- a/careers/index.njk +++ b/careers/index.njk @@ -3,6 +3,7 @@ layout: base.njk title: "Careers — Sea Haven Industries" description: "Join the Sea Haven Industries team. Explore careers opportunities and grow with a company committed to staff development and excellence in construction." pageCss: careers +breadcrumbName: "Careers" ---
diff --git a/careers/scheduling-coordinator/index.njk b/careers/scheduling-coordinator/index.njk index 442d109..6afffa6 100644 --- a/careers/scheduling-coordinator/index.njk +++ b/careers/scheduling-coordinator/index.njk @@ -4,6 +4,8 @@ title: "Scheduling Coordinator — Sea Haven Industries" description: "Apply for the Scheduling Coordinator position at Sea Haven Industries. View job details and submit your application today." pageCss: jobs recaptcha: true +breadcrumbName: "Scheduling Coordinator" +breadcrumbParent: { name: "Careers", url: "/careers/" } ---
diff --git a/careers/traveling-maintenance-assistant/index.njk b/careers/traveling-maintenance-assistant/index.njk index 4280141..ef903c7 100644 --- a/careers/traveling-maintenance-assistant/index.njk +++ b/careers/traveling-maintenance-assistant/index.njk @@ -4,6 +4,8 @@ title: "Traveling Maintenance/Electrical Assistant — Sea Haven Industries" description: "Apply for the Traveling Maintenance/Electrical Assistant position at Sea Haven Industries. View job details and submit your application today." pageCss: jobs recaptcha: true +breadcrumbName: "Traveling Maintenance / Electrical Assistant" +breadcrumbParent: { name: "Careers", url: "/careers/" } ---
diff --git a/contact/index.njk b/contact/index.njk index 5c459b9..88a4eb8 100644 --- a/contact/index.njk +++ b/contact/index.njk @@ -4,6 +4,7 @@ title: "Contact — Sea Haven Industries" description: "Get in touch with Sea Haven Industries. Have a question or project idea? Contact us today to discuss your construction and facilities needs." pageCss: contact recaptcha: true +breadcrumbName: "Contact Us" ---
diff --git a/eula/index.njk b/eula/index.njk index 1561518..a638655 100644 --- a/eula/index.njk +++ b/eula/index.njk @@ -4,6 +4,7 @@ title: "End User License Agreement — Sea Haven Industries" description: "End User License Agreement for Sea Haven Industries QuickBooks application." pageCss: legal robots: "noindex, nofollow" +breadcrumbName: "End User License Agreement" ---
diff --git a/index.njk b/index.njk index bd015e1..5208774 100644 --- a/index.njk +++ b/index.njk @@ -307,6 +307,16 @@ permalink: /index.html "minValue": 120 }, "foundingDate": "2006", + "geo": { + "@type": "GeoCoordinates", + "latitude": 40.8126, + "longitude": -73.1124 + }, + "areaServed": { + "@type": "Country", + "name": "United States" + }, + "hasMap": "https://www.google.com/maps/search/?api=1&query=710+Koehler+Ave+Ronkonkoma+NY+11779", "serviceType": ["Electrical Work", "Plumbing", "HVACR Services", "Handyman Services", "Garage Door Repair"] } diff --git a/privacy-policy/index.njk b/privacy-policy/index.njk index 7353a71..a42b157 100644 --- a/privacy-policy/index.njk +++ b/privacy-policy/index.njk @@ -3,6 +3,7 @@ layout: base.njk title: "Privacy Policy — Sea Haven Industries" description: "Privacy Policy for Sea Haven Industries. Learn how we collect, use, and protect your personal information." pageCss: legal +breadcrumbName: "Privacy Policy" ---
diff --git a/services/index.njk b/services/index.njk index 358b755..cd9eeb0 100644 --- a/services/index.njk +++ b/services/index.njk @@ -4,6 +4,7 @@ title: "Services — Sea Haven Industries" description: "Sea Haven Industries offers reliable electrical, plumbing, HVAC, handyman, and garage door repair services for every need." pageCss: services recaptcha: true +breadcrumbName: "Services" ---
diff --git a/social-accountability/index.njk b/social-accountability/index.njk index 4def0c6..ff285cf 100644 --- a/social-accountability/index.njk +++ b/social-accountability/index.njk @@ -3,6 +3,7 @@ layout: base.njk title: "Social Accountability — Sea Haven Industries" description: "Learn about Sea Haven Industries' commitment to social accountability and responsible business practices in the construction industry." pageCss: social +breadcrumbName: "Social Accountability" ---
diff --git a/terms-of-service/index.njk b/terms-of-service/index.njk index 570cc00..0f36367 100644 --- a/terms-of-service/index.njk +++ b/terms-of-service/index.njk @@ -3,6 +3,7 @@ layout: base.njk title: "Terms of Service — Sea Haven Industries" description: "Terms of Service for Sea Haven Industries. Review the terms and conditions governing your use of the Sea Haven Industries website." pageCss: legal +breadcrumbName: "Terms of Service" ---