All nine Buy forms on the bootcamp pages (EN/DE/ES, three each) posted
product_id 12, which the shop resolves to 'Bootcamp - Q1-2026-eng'. The
advertised cohort is Q1 2027 (starts Thu 21 Jan 2027), so a customer
buying the 2027 bootcamp received the 2026 cohort. Both are 595.00 and
neither cart was empty, so it went unnoticed; the form's info field reads
'Q1 2027, English' while the product was 2026.
product_id 12 -> 17 in every bootcamp Buy form, all three languages.
The info field is kept (it renders as the cart line description via
sale.order.line.name, so it is read downstream); it is truthful.
static/buy.js doc comment updated to match. Nothing else changed:
plausible-event-name=Buy class, target=_blank form post, prices and
layout untouched.
Move the homepage's Server Location lockup (label + select) out of section#pricing
and into the HOST27 deal card's CTA row, next to the Buy button. There is still
exactly ONE element with id pricingServerLocation on the page, so buyNow() — which
resolves the selector by id — and every pricing button keep working untouched.
The pricing block keeps only the Odoo Version selector and reads the same global
location selection.
- markup: new .bf-cta-row in the deal form (selector left, brass Buy right);
location lockup removed from .pricing-server-row
- CSS (page-scoped): .bf-cta-row / .bf-cta-location, and the light-on-ivory twin
of .pricing-server-select (style.css carries neither; the homepage styles it
inline for its dark section). Wraps at 375px with no horizontal overflow; the
Buy stays the one dominant action.
- static/black-friday.js: on submit of the native post_basket forms, write the
chosen city + version into the basket line's `info`, exactly as buyNow() does.
Additive only — with JavaScript off the POST is unchanged (no info) and the
selector is empty because its options are built here (pre-existing behaviour).
Untouched: the hidden redirect=/coupon/HOST27, the single plausible-event-name=Buy
mark, the price, the countdown, #trialForm, the booking link and the consent gate.
Verified: verify-html.py PROBLEMS 0; competitor QC exit 0 on the page (74
pre-existing site-wide, 0 added); node --check OK; impeccable detect 35 findings,
identical set to HEAD (0 introduced); live browser: fastest server auto-selected,
wire body basket=[{"product_id":4,"qty":1,"info":"boston|20.0"}] and it tracks the
selectors on repeat submits; 375px no overflow.
On /odoo-black-friday/ the offer row is now two columns at >=901px: the deal (left,
unchanged and still the single dominant primary) with the free trial and a booking
link beside it (right, deliberately smaller and quieter).
- Trial: the existing #trialCard/#trialForm moves verbatim from the features grid into
the row's aside. Same ids, same static/black-friday.js handler, same consent gate and
label (AW-18065522009/AtAPCIjjvrAdENn6p6ZD) — no second trial mechanism.
- Booking: a plain <a target=_blank rel=noopener> to Oliver's verified Google
appointment page (no iframe). Its click fires AW-18065522009/P0j5CPiCv7AdENn6p6ZD
through the same gate via window.o4pAds.bookConversion; the click is never
preventDefault-ed, so a blocked tag can never stop the link opening.
- ads-consent.js gains the booking label (from the page, not hard-coded) and the
bookConversion hook; the buy path is untouched.
Deal buy stays the dominant action; ONE plausible Buy mark; one price; countdown pair
unchanged.
Adds the second Google Ads label to /odoo-black-friday/: the free-trial
form (#trialForm) now fires AW-18065522009/AtAPCIjjvrAdENn6p6ZD (action
7886320008 "Registrierung") when it is submitted.
Reuses the existing gate in static/ads-consent.js rather than a second
mechanism: the label is read from the page (data-ads-trial-send-to on
#adsConsent), so the script stays generic. The page hands the gate its
real submit function via window.o4pAds.trialConversion; the gate fires
the conversion and runs the submit from the event callback, with the same
700 ms hard timeout that runs it anyway. Nothing fires and no cookie is
set before consent (identical default-deny gate).
The buy conversion is untouched (comments only).
Tag id + conversion label now live as data-ads-id / data-ads-send-to on the banner
element, so the served HTML carries the tag config and the script stays reusable
(missing config -> inert). Re-verified: no cookie/request before consent, cookies +
tag after Accept, conversion + form submit on click, 700 ms fallback submit with the
tag blocked.
/odoo-black-friday/ only, page-side. No Odoo change, no ad-account change.
- static/ads-consent.js (new): default-DENY consent; nothing loads and no request
fires until Accept. On Decline (or switching to Decline later) the _gcl_au/_gcl_aw
cookies are cleared. A footer "Cookie settings" link reopens the banner.
- conversion: on submit of either /shop/post_basket buy form (offer block + On the
Rise card), gtag('event','conversion',{send_to: AW-18065522009/vnDUCPDxt7AdENn6p6ZD})
then form.submit() from event_callback, with a 700 ms hard-timeout fallback so a
blocked tag can never swallow the buy. No consent / no tag -> native POST untouched.
- privacy.html: new "Advertising cookies" disclosure (what, why, how to decline).
- Plausible Buy marks and the checkout-side Purchase event untouched.
Fix 1: the On the Rise pricing card quoted $395 while the deal block said $295 first
year — a price contradiction within one scroll. On this page only the card now reads
$295.00 / 'for your first year, then $395 a year'. Other pages keep $395.
JSON-LD Offer price was already 295.00 and still matches the visible price.
Fix 2: the three list-price Buy Now buttons (data-id 6/4/5) competed with the deal.
Side Hustle and Powerhouse buy buttons removed on this page. The On the Rise card's
buy is now the deal purchase — the same one-click checkout as the offer block
(product 4, /coupon/HOST27?r=/shop/checkout), native POST so it works with JS off.
Tracking: the offer-block form is the single element carrying the Plausible Buy class;
the Bootcamp CTA and the two list-price buttons no longer carry it.
Fix 10: a time-remaining element near the offer and near the buy, computed client-side
from one documented constant (2026-11-30T23:59:59-05:00, US/Eastern). The static
sentence 'Ends Monday 30 November 2026.' still reads with JS off; past the instant the
text switches to the past tense. No visibility animation.
- Hero is the homepage's (same markup/assets): slide-in city image + the server
location selector below it that slides a chosen city in; headline adapted to the offer.
- Order now: hero > location selector > deal (HOST27, $295 first year / $395 after,
one-click product-4 buy with redirect=/coupon/HOST27?r=/shop/checkout) > trust bar >
Black Friday FAQ > features + trial card > FAQ > pricing (buy section) > Bootcamp > footer.
- Bootcamp box restored from webmaster/out/2026-11-02-bootcamp-box-removed-archive.md:
heading, USD 595.00, mechanism line, deadline, CTA, its meta/og contribution, its JSON-LD
Product entry and the FAQ answer are all back and consistent.
- Excluded (no blog, no OCA modules, no Project Plan); no dangling refs.
- New /static/black-friday.js = the homepage behaviours this page needs (root-relative
image paths), with the blog/OCA/project-plan blocks removed.
- Canonical URL unchanged; competitor QC 0 on the page.
The three CTAs (hero, mid-page, §7) were <a class="btn-buy"> that only worked
through JavaScript. They are now native <form method="post" target="_blank">
elements posting basket=[{"product_id":12,"qty":1}] + clear=1 to
https://20.odoo4projects.com/shop/post_basket, so the buy works with JavaScript
disabled; the button keeps the brass styling and the same size classes.
static/buy.js no longer builds a form on click. It is now pure progressive
enhancement: a capture-phase submit listener appends the utm_source/medium/
campaign hidden inputs from the cookies and lets the native submit proceed.
One page-scoped declaration keeps the button height at 55px: a <button> takes
line-height:normal from the UA font shorthand where the <a>s inherited the body
line-height (measured 49.19px vs 54.78px without it).
The three Reserve-your-seat CTAs posted to /support.html#contact. They now
run the same checkout path as the main page's Buy Now: a new page-local
static/buy.js mirrors script.js buyNow() (POST basket to
20.odoo4projects.com/shop/post_basket, clear=1, UTM cookies appended) and
binds .btn-buy via one delegated click handler. CTAs become
.btn-buy[data-id=12]; label unchanged. script.js and the homepage untouched.
The Price-and-booking section keeps a second, smaller link to
/support.html#contact so readers who have questions still have a fallback.
The switcher links were plain anchors: clicking one navigated but stored nothing, so
the auto-detect on the EN homepage bounced the visitor straight back. A de-DE browser
on /de/ that clicked EN landed on / and was replaced back to /de/ — the 'an explicit
choice always wins' promise in the plan and in llms.txt was only true for ?lang=.
A click now calls remember() with the link's own hreflang, so the choice is stored in
localStorage + the o4p_lang cookie and survives the next session. Selector tests
27 -> 31 (switcher click stores EN, sets the cookie, stores DE, chosen EN survives a
new session).
Homepage pilot of the website i18n project. Adds the translated homepage for DE and
ES, the shared selector at /static/lang.js, and the hreflang cluster on all three
language versions (EN included, or the cluster is asymmetric and ignored).
- de/index.html, es/index.html: translator output plus the i18n build (html lang,
self-referencing canonical, hreflang cluster, lang.js tag, switcher slot in the footer).
- index.html: same cluster + lang.js tag + switcher slot; canonical and og:url
normalised to the lowercase host.
- static/lang.js: the tested selector (client-side language choice, auto-detect on the
EN homepage only, once per session; a stored choice or ?lang= always wins).
- sitemap.xml: /de/ and /es/ (priority 1.0, lastmod 2026-10-24).
- llms.txt: new '## Languages' section.
Two gaps in the build recipe were closed while building: a mirrored page cannot use
root-relative source paths (style.css, images/* would resolve to /de/style.css -> 404),
so the build now re-points assets at the site root and keeps page links on the EN route
until the language page exists; and og:url is now this page's own URL instead of the
EN one inherited from the source. Not built yet: support.html, odoo-hosting-2027.html,
the blog, the bootcamp page, the legal pages.
- Dark text (#171c26) for all form fields in modal on ivory background
- Explicit colors for labels, inputs, placeholders
- Close button (X) now visible with midnight background