perf(bootcamp): fix CLS/LCP + a11y on EN/DE/ES; add the missing footer-form handler

PageSpeed pass on /odoo-erp-automation-bootcamp/ (EN + DE + ES):

- CLS: /style.css was deferred via media="print" onload=... → unstyled first paint
  shifted BODY 0.31 (live CLS 0.326). Now a normal <link rel="stylesheet">; the two
  hero webp files are preloaded (fetchpriority=high) by viewport.
- a11y: footer <h4> → <h3> (heading-order), a lighter brass + underline for links
  inside on-dark notes (color-contrast, link-in-text-block), .fcopy 3.2:1 → 5.17:1.
- best-practices: the footer contact forms and the waiting-list form now post to the
  CRM webhook instead of action="mailto:", which Lighthouse flags as an insecure
  request (is-on-https 0 → 1). No Google Ads tag on these pages (no consent gate).
- regression fix: these pages do NOT load /script.js, so after the action change the
  footer form had no JS handler and a submit navigated the visitor to the webhook's raw
  JSON reply. static/bootcamp-waitlist.js now carries a footer-form handler mirroring
  /script.js one-for-one (same webhook, payload { email, requirements, area:'message' }
  in UTM.inject, success/failure behaviour); its two strings are read from the form's
  own data-ok / data-err attributes so each language keeps its own text. ENDPOINT
  hoisted to module scope so both handlers share it.
- no-JS path: a <noscript> footer line with support@ODOO4projects.com as a plain
  mailto link. Verified it does not re-trigger is-on-https (unlike a mailto FORM action).

Guards held: Buy forms still post product_id 17; exactly one [data-bc-seats];
waitlist still posts area:'message'; plausible snippet present; competitor QC exit 0.
This commit is contained in:
webmaster committed 2026-11-02 16:07:20 +00:00
1 parent bc942cdb7c
commit 2aa9b19c3c
4 files changed
+203 -32

No files matched your search

+65 -2
View File
@@ -14,6 +14,8 @@
* A 200 from the webhook means the workflow accepted the request, not that a CRM record
* exists (the workflow answers before it runs) — same as the site's other forms.
*/
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/crm';
(function () {
'use strict';
@@ -71,8 +73,6 @@
window.UTM.parse();
/* ---- Waiting list ---- */
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/crm';
function run() {
var form = document.getElementById('bcWaitlistForm');
if (!form) return;
@@ -121,3 +121,66 @@
run();
}
})();
/* ---- Footer contact form ------------------------------------------------------------
* These pages do NOT load /script.js, so its footer-form handler does not run here. This
* mirrors /script.js's handler one-for-one — same webhook, same payload shape
* ({ email, requirements, area:'message' } wrapped in UTM.inject), same success/failure
* behaviour — but reads its two strings from the form's own data-ok / data-err attributes
* so the message is in the page's language. Without it a submit would navigate the visitor
* to the webhook's raw JSON reply.
*/
(function () {
'use strict';
function runFooter() {
var form = document.getElementById('footerContactForm');
if (!form) return;
var emailInput = document.getElementById('footerContactEmail');
var textInput = document.getElementById('footerContactText');
var submitBtn = form.querySelector('.footer-send-btn');
var status = document.getElementById('footerFormStatus');
var okText = form.getAttribute('data-ok') || "Thanks! We'll get back to you shortly.";
var errText = form.getAttribute('data-err') || 'Something went wrong. Please try again.';
function showStatus(message, isError) {
status.textContent = message;
status.classList.toggle('error', !!isError);
status.classList.add('visible');
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var email = emailInput.value.trim();
var text = textInput.value.trim();
if (!email || !text) return;
submitBtn.disabled = true;
status.classList.remove('visible');
fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(window.UTM.inject({
email: email,
requirements: text,
area: 'message'
}))
}).then(function (r) {
if (!r.ok) throw new Error('bad status ' + r.status);
form.reset();
submitBtn.disabled = false;
showStatus(okText, false);
}).catch(function () {
submitBtn.disabled = false;
showStatus(errText, true);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runFooter);
} else {
runFooter();
}
})();