Re-ran the canonical builder (work/build-directory.py) against the live
odoo_experts webhook, which now returns exactly one record (id=2,
"Oliver - The OC Guy"; records 1 and 3-7 are deleted).
The JS-off build-time list shipped two hardcoded cards: the junk record
id=1 ("oliver", focus "vieoem") and the real id=2. The junk card was
compiled markup, not data, so no DB delete could remove it. The rebuild
ships exactly the one card the record set supports today.
Same rebuild also brings the card in line with the current renderer: the
card carries the JS-off-safe "Ask question anonymously" mailto contact row
(the record has email oliver@odoo4projects.com), and no self-declared link
row (the record's Linkedin field is now null).
Runtime fetch, pager, filters, Best Practices, OCA badge and copy untouched.
- Delete the whole "Post a request" section (heading, "Not open yet" note, pick-list/
brief/email form) and the JS+Css that existed only for it: initRequestForm(),
wireRelay("requestFormWrap"…), initRequestForm(), .dir-pickfield/.dir-picklist/.pick.
build-directory.py: drop PICK_START/PICK_END, render_picklist() and its call.
- Rewrite every relay promise to direct contact (trust block, listing note, final CTA);
keep the "we are not a party to the contract" point.
- Trust block: state no fee to the freelancer and no fee to the client.
- Count means client instances: single label "clients on ODOO4projects" (no projects).
- No confirmation gating: comment now states the backend only exposes confirmed profiles.
- Left with a comment, not deleted: ODOO_EXPERTS_QUOTE_WEBHOOK (it gates the dark
anonymous-contact modal, which was out of scope). The modal stays dark (0 buttons).
- Kept: id=1 junk record; the inlined base64 photo (flagged, not changed).
The live record carries placeholder junk in Linkedin/upwork/Image ('1',
'ieivmermi', 'gineoirn'). With type=url the whole form went invalid and the owner
could not save anything. type=text inputmode=url keeps the URL intent and the
mobile keyboard without hard-blocking on data the owner is trying to correct.
- hide the inert client request form; announce it as coming soon with an email
fallback (no reachable error state, no submit that refuses a completed brief)
- soften the contact claim to 'where a freelancer published their details' and
make a card with no contact say so instead of showing an empty space
- move the admin add/edit panel off the public page to its own draft page
(/odoo-community-developers/admin/), unlinked, reachable only by URL
- admin edit: load-and-confirm the record before writing, and read-modify-write
the booleans so an untouched OCA_member/is_individual keeps its current value
- application form: add Country + Odoo versions (the filters' inputs), GitHub and
OCA profile URLs, and an explicit email-publication consent line
- tighten 'How this works' from ~120 words to two short options + one caveat
- build-directory.py: skip records with no Name; render GitHub/OCA links and the
no-contact line; read Country via alias
- 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
Both pages shipped (355c5d0) with a three-item footer; the site footer is a
four-item commitment. The /odoo-erp-automation-bootcamp/ entry is restored on
both pages, matching index.html / odoo-hosting-2027.html.
/odoo-hosting-offers/: the code HOST27 is added inside the date-scoped Black
Friday block only (BF-BLOCK-START..END), so it disappears with the block after
30 Nov. Title, meta description and H1 are untouched — the intent split holds.
verify-html.py: PROBLEMS 0. node --check script.js: OK.
Three stills from the thumbnail-designer render pipeline, copied byte-identical
(md5 verified against the handoff) into images/black-friday/ so the two BF
pipeline records can reference stable public URLs. WebP siblings added to match
how the site serves images. Not referenced from any page yet — hosting only.
bf2026-hosting-og.png 1200x630
bf2026-hosting-linkedin.png 1200x627
bf2026-bootcamp-lastcall-linkedin.png 1200x627
Modelled on odoo-community-upgrade.html (top bar, band rhythm, FAQ details).
Copy published verbatim from the copywriter's 2026-10-28 file: 58-char title,
157-char meta, the six outbound internal links and the WebPage+Article+FAQPage+
BreadcrumbList @graph. Nine FAQ questions marked plausible-event-name=FAQ (no
event fires on page load). Sitemap + llms.txt list the new URL. Inbound links:
one from the homepage FAQ card, one from the support FAQ answer. Nothing else
moves; no footer change.
Mark the six conversion goals on the self-hosted Plausible instance so the
events match the goals to be registered in Plausible. Event names verbatim:
Trial, FAQ, Buy, Custom Quote, search posts, Project Plan.
CSS-class markings (work without JS logic, on the clickable element):
- FAQ -> index/de/es index .faq-card; odoo-hosting-2027 + odoo-community-upgrade
(de/es twins) and australia <summary>; support.html .faq-question
- Buy -> index/de/es index .btn-buy anchors; australia + bootcamp native
post_basket submit buttons (forms stay native POST)
JS call markings in script.js (loaded by index, de/index, es/index):
- Trial on the successful trial-form POST (.then branch)
- Custom Quote on the successful custom-quote POST (.then branch)
- Project Plan on the successful business-wizard submit
- search posts on the FIRST input of #blogSearch only (guard flag, one per
pageview, not on every keystroke, not on #ocaSearch)
Nothing fires on page load; the 'Start the free trial' anchors that only
scroll to #trialCard are deliberately not tagged. One goal per topic across
all languages, no language suffix. No visual, behavioural or request change.
From seo/out/site-seo-ai-search-audit-2026-10-27.md (§6 items 1, 2, 3a, 5).
- canonical: 10 blog posts declared https://odooprojects.com/... — a live parked
third-party domain — as their canonical. Now self-referencing on
https://odoo4projects.com/blog/posts/<slug>.html (1 line each, nothing else).
- sitemap.xml: 27 -> 33 URLs. Added the 6 live pages that were missing
(support.html, legal.html, community-vs-enterprise-cost, 6-tips,
odoocommunity-agents, odoo-20-is-live); corrected 13 lastmod values to the
real last content commit (homepage was 4 months stale); /de/ and /es/
priority 1.0 -> 0.7 so a translation does not outrank its original.
- blog/index.json: 7 -> 15 posts. The 8 orphan posts are now listed, so every
post is reachable from the homepage grid.
- llms.txt: removed the link to /blog/ (that URL returns 403) and added a
"## Blog posts" section listing all 15 posts with date and summary.
Not changed on purpose: FAQPage on support.html, the homepage "price": ">950.00",
the bootcamp start-date conflict, and the blog post chrome (15 files).
New EN landing page for the head term "ODOO Community Upgrade": hero, what an
upgrade is, the 10-step published process, a developer-labelled technical section
with four sourced command blocks and their substitution notes, the ODOO4projects
migration service (new database on our servers), the self-hosting cost honesty
section, an 8-question FAQ and a final CTA. House CI only, page-scoped CSS, no
new blocking resources.
- CTAs are plain links to the real flows: /support.html#contact (opens the
migration ticket) and /index.html#pricing. No basket POST and no product id:
no price is published for the migration service, so wiring a buy would invent one.
- JSON-LD: WebPage + Service + FAQPage (8 questions), no Offer node.
- sitemap.xml: new URL added; llms.txt: page listed under Key pages.
- support.html: the "older version" FAQ answer now links the new page so it is not orphaned.
Copy: copywriter/out/odoo-community-upgrade-page-copy-2026-10-27.md. The 7
NOT PUBLISHED gaps (G1-G7) are not in the build.