Part 1 — asset versioning: every local <script src> and <link rel=stylesheet> on the
tracked pages now carries ?v=<sha256-of-the-file> (10 hex), computed at BUILD time from
the file's bytes by work/build-asset-versions.py — never hand-typed, and idempotent
(re-running with no file change rewrites nothing). This is what makes an edited
focus-rule.js / photo-upload.js a new URL instead of a stale cache hit.
Part 2 — language facet: a language now counts for the Language filter only when its
0-5 self-assessment is >= LANG_FACET_MIN (3 = 'works in it'). The constant sits beside
the 15-point rule in the page script and its mirror in build-directory.py. Nothing
stored changes; the card's languages chart still prints every language above 0, the
search box is unchanged, and the facet stays an exact membership test against the
card's value. A '1' language no longer offers the person under that language.
Two user decisions (2026-11-03). The page is live.
1. The anonymous-contact modal is removed in full, not just its gate: its markup and
comment, the .contact-modal and .dev-anon-btn CSS, the JS (modalEls, openContactModal,
closeContactModal, initContactModal, enhanceContact), the two init-time calls and the
per-card render call. ODOO_EXPERTS_QUOTE_WEBHOOK is deleted — the only readers were the
two gates (initContactModal, enhanceContact) and the postTo inside the modal's own
submit handler; nothing else read it. The modal's lede ("We pass your request on ...")
goes with it, so no relay copy survives. Kept on purpose: the invite relay form and
ODOO_EXPERTS_INVITE_WEBHOOK, the OCA filter + badge, ODOO_EXPERTS_WEBHOOK, and the
card's direct "Email this freelancer" chip.
2. hello@odoo4projects.com is retired site-wide: 24 occurrences (mailto + visible text)
become support@odoo4projects.com, and the 12 support@ODOO4projects.com case variants
are normalised to lower case. oliver@odoo4projects.com is a freelancer's own published
contact address on their card and is left untouched.
Tests run: verify-html.py PROBLEMS 0; check-inline-js.py OK; competitor QC exit 3 with 74
pre-existing hits and 0 added; impeccable detect 1 -> 1 (identical, 0 new).
Same defect the bootcamp carried (2aa9b19), fixed site-wide in one mechanical pass.
P0 (17 pages): /style.css was deferred via media="print" onload=... -> unstyled
first paint -> a BODY layout shift. Now a normal render-blocking
<link rel="stylesheet">; the now-redundant no-JS <noscript> for style.css is
dropped. Web fonts + Font Awesome keep their legitimate deferral.
Pages: index.html, odoo-black-friday/, odoo-hosting-offers/, de/index.html,
es/index.html, odoo-hosting-2027.html (+de/+es), odoo-community-upgrade.html
(+de/+es), odoo-staging-environment/, odoo-community-ai/,
odoo-n8n-masterclass/, australia.html, workshops/, what-can-i-use-odoo-for/.
P1 (1 page): /odoo-black-friday/ footer form action mailto: -> CRM webhook
(Lighthouse is-on-https). Changed only because the page already loads a handler
for #footerContactForm (static/black-friday.js). The other 13 mailto-form pages
load no handler and are left alone on purpose (no drive-by handler copy-in).
Added the no-JS <noscript> fallback so a JS-off visitor keeps a path.
P2 (35 pages): add a single <main> landmark wrapping the primary content; top
bar and footer stay outside. Additive only, no redesign.
Guards held: bootcamp EN/DE/ES and every JS/static file untouched; Buy forms
product_id 4/17 intact; one [data-bc-seats]; ads-consent gate intact.
verify-html.py PROBLEMS 0; competitor QC 74 pre-existing, 0 added.
The four 'version upgrades are run for you' lines (black-friday, hosting-offers,
hosting-2027 body + table row, llms.txt) and their JSON-LD descriptions now say
updates and in-version upgrades are included, and name a major-version migration
as a separate, quoted service. terms.html: migration = separate service, quoted
separately; release list no longer reads stale (names Odoo 20 as current).
No offer, price, page structure or ad change.
- terms.html: cancel any time + refund unused time (pro rata); delete the
no-refund sentence and the 'no refund promise beyond' clause
- terms.html: prices are tax included (was 'plus tax where it applies')
- index.html: order widget #orderPlanTax text '+ tax' -> 'tax included'
(id/class kept; static text, no JS reads or writes it)
- support.html: add the AI-agent-urgent + 24h email support promise,
bot still scoped to quick fixes
- odoo-erp-automation-bootcamp/index.html: seat transferable any time,
cancellable up to two weeks before the 21 Jan 2027 start
The snippet was on 10 of 40 pages, so the Trial / FAQ / Buy / Custom Quote /
search posts / Project Plan goals were inert everywhere else. It now sits
verbatim, in the same <head> position as index.html, on all 39 visitor-facing
pages. email/trial-confirmation.html is deliberately left out (email template).
FAQ: the five <details class="bc-faq"> <summary> elements on the bootcamp page
and its DE/ES twins now carry plausible-event-name=FAQ, and the 15
.faq-question buttons on de/es/support.html get the marking they were believed
to already have. No goal renamed: one FAQ goal across all languages.
No markup, copy or behaviour change, and no third-party request beyond the
snippet itself. verify-html.py PROBLEMS 0; check-links.py OK; event checker
59 PASS / 0 FAIL; impeccable detector +0/-0 findings on the changed files.
Two new root-level pages, assembled from legal.html so they inherit the existing
head, fonts, style block and .legal-* cards. The trial email's /legal/privacy and
/legal/terms links 404 on the live site and are repointed here.
Both are factual drafts built from product facts (data held, the six regions, Stripe,
cancel-anytime, backup slots) and are NOT lawyer-authored. Decision 12 marks them for
legal review; they must not be treated as final before that review.