A filtered-out directory card stayed visible. The card is targeted by an
id-based ready-state selector (`.js #directory[data-dir-state="ready"]
#dirGrid > .dev-card`, two ids), which outranked the old per-element patch
`.dev-card[hidden] { display:none }` — so `card.hidden = true` set the
attribute and every card stayed on screen while the count line correctly
read "Showing 1 of 4".
Fix: one rule, `[hidden] { display:none !important }`, early in /style.css.
A class- or id-based `display` can no longer beat the attribute, so this
cannot recur with the next class that sets `display`. The per-element
`[hidden]` patches are removed so a single mechanism governs:
- odoo-community-developers: cards + filter row + pager + modal
(load-bearing — each sets `display`); focus-sliders/noscript (redundant)
- index/de/es `.hero-notice[hidden]`, odoo-black-friday `.ads-consent[hidden]`
(redundant — those elements set no `display`)
Re-fingerprint local assets (style.css changed): /style.css?v 3332fbba93 ->
061517baef across 51 pages.
Measured in a real browser (local harness, stubbed webhook; no record created,
no probe write) — computed display + geometry, never the `hidden` property:
- filtered to one card: the other three report display:none and height 0;
the visible set (1) equals the count line ("Showing 1 of 4") — 1 == 1
- HEAD (pre-fix) reproduces the defect: 3 cards hidden=true yet display:flex,
height 599.38 each; 4 visible vs the count's 1
- pager: display:none height 0 with one page; display:flex height 66.83 with
12 cards (Page 1 of 2); page 2 hides page-1 cards (display:none)
- modals closed: display:none height 0; load state: cards 0, pager/empty
display:none, filter row visibility:hidden by design
- 375 px: same result, no horizontal overflow; avatar ask-row top identical
across a row (2053.97 / links 2086.91)
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.
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 blog was an island: zero post→page links to hosting/upgrade/support/courses,
and only one page→post link in the whole site. Posts ranked for nothing while the
old blog host ranked for our long-tail queries.
Posts → pages (26 new in-body contextual links across 17 posts):
each post now carries one or two links to the page that answers its reader's next
question — odoo-hosting-2027, odoo-community-upgrade, support, the workshops hub,
the n8n masterclass, and the plan pricing on index.html#pricing.
Pages → posts (7 pages, one contextual link each):
index.html (OCA section), odoo-hosting-2027, odoo-community-upgrade, support,
workshops/, odoo-n8n-masterclass/, odoo-erp-automation-bootcamp/ now link into the
post that fits their own subject. The clearest miss is fixed — the
community-vs-enterprise-cost post and the odoo-community-upgrade page now link
to each other.
Anchors wrap existing sentences where the text already said the thing; four short
linking sentences were added (index OCA section, support first-steps FAQ, masterclass
lessons, bootcamp included section). No new components, no related-posts widget,
in-body anchors only. English pages only; DE/ES untouched (translator follow-up).
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.
Four follow-up defects found beyond the 2026-10-27 SEO audit (no copy changes):
1. legal.html canonical was https://ODOO4projects.com/legal — uppercase host AND a
path that 404s. Now https://odoo4projects.com/legal.html. The file's og:url keeps
its path /legal but its host is lowercased (path fix awaiting a decision).
2. The no-"4" domain typo in reader-visible <pre><code> body text of
blog/posts/{guide-migrate-enterprise,speed-run-deploy,my-first-post}.html:
odooprojects.com -> odoo4projects.com. Domain string only, no other wording.
3. Lowercase host in every machine-read value: the robots.txt Sitemap line, the
homepage + de/ + es/ Organization JSON-LD url / contactPoint.url / offers[].url,
legal.html og:url, and the four absolute self-URL hrefs in blog posts,
odoo/support.html and email/trial-confirmation.html. Display text untouched
(support@ODOO4projects.com stays as written).
4. One BlogPosting per blog post (15), every value taken from the page itself:
headline = its <h1>, datePublished = its visible post-meta date, url = its own
canonical, author/publisher = the Organization the site already publishes,
image = its og:image where one exists (omitted on the 10 posts that have none).
Empire ">950.00", FAQPage on support.html, the blog-chrome change and the bootcamp
date conflict are untouched — decisions pending with the user.
- blog/posts/odoo-19-to-20-invoice-migration-with-n8n-and-mcp.html: in-post image
much.png + og:image
- blog/posts/track-the-oca-modules-you-actually-use.html: in-post image
oca_module_status.png + og:image
- blog/index.json: optional "image" key on those two entries only (text splice)
- script.js: renderBlog() draws .blog-thumb only when the entry carries an image
- style.css: .blog-thumb rule
- sitemap.xml: lastmod 2026-10-24 for the invoice-migration post
Built by profile webmaster (run 20261024_094206_5c4f28, verified PROBLEMS: 0 /
node --check OK); committed by olchi after that run died on an OpenRouter 402.