Commit Graph
25 Commits
Author SHA1 Message Date
webmaster 53e9ea999d feat(footer): add Odoo Freelancer Directory to Useful Links; drop the directory page's bottom Start CTA
- 13 tracked pages + the directory page's footer carry a new <li>
  <a href="/odoo-community-developers/">Odoo Freelancer Directory</a>,
  placed after "ODOO Hosting in 2027" (our service links together, before Support/Legal).
- The directory page itself is excluded (a page does not link to itself in its own footer).
- Removed the final Start / "Browse the freelancers" CTA section from the directory page;
  the hero copy keeps its one "Browse the freelancers" link. .cmp-cta CSS untouched (11 pages).
2026-11-03 17:05:18 +00:00
webmaster 092f1e44a2 feat(freelancer-directory): Best Practices — 12 items in two independent columns
Adds the twelfth item ("Look at a similar job they have already done") and
splits the accordion into two independent columns of six.

- Two independent columns (.bp-list > .bp-col, each its own grid, parent
  align-items:start) so opening an item grows only its own column and cannot
  shove the other. A single grid would re-size rows and make both columns
  jump on every click.
- DOM / reading order is 1–6 (left column) then 7–12 (right column) — a
  decision, not an accident.
- Exclusivity still one-open-at-a-time via the shared <details name> plus the
  inline JS fallback; re-proved, including the strip-the-name test.
- <=720 px collapses to a single column, no horizontal overflow.
2026-11-03 15:51:18 +00:00
webmaster 5535f10e7d feat(freelancer-directory): anonymous-contact modal live, description cap, copy removals, Get-listed modals, Best Practices accordion
Publishes the accumulated freelancer-directory work (six held passes) plus the
contact-modal success copy the user supplied today.

1. Anonymous-question button + modal — LIVE. ODOO_EXPERTS_QUOTE_WEBHOOK is now
   set, so the per-card "Ask question anonymously" button and its modal render.
   The button is added by script only on cards marked data-relay (a record we can
   relay to); the modal posts {freelancers:[{id,name}], brief, email} to the
   odoo_experts_quote route. With the constant empty the surface stays dark and
   no dead button is rendered.
2. Description cap 230 — the "About you" textarea is capped at 230 characters
   with a live counter (DESC_MAX_CHARS); the same cap truncates a longer legacy
   value on the render path. The matching change to the owner's edit form lives
   in odoo-community-developers/admin/index.html (included here).
3. Removal of "Ask us to find one" — the dead call to action is gone.
4. Copy removals — the invitation swap, four lede sentences, the whole invite
   section, and the Start line are removed.
5. Two "Get listed" buttons + the application form as a modal — the ONE
   #applyForm stays inline in the served HTML (works with JS off) and is moved
   into #applyModal once when the script runs; the data-apply-open anchors then
   open the dialog instead of jumping, and are working links with JS off.
6. Best Practices accordion — 11 <details name="best-practices">, all CLOSED on
   load; at most one open via the shared name attribute plus a JS fallback.

Contact-modal success copy (user, 2026-11-03): the receipt clause is kept and the
user's two promises are used in their words —

  "Thank you — we have your question. We will contact the freelancer, and as soon
   as we have their answer we will come back to you."

The sending and error states are unchanged. NOTE: both promises (the relay and
the reply) depend on the n8n workflow that does not exist yet — the endpoint is a
stub. The owner took that decision knowingly; the copy ships as asked.

QC: verify-html.py 0 · check-inline-js OK · competitor QC exit 3, 74 site-wide
pre-existing, 0 in our files · impeccable detect --no-advisory dir 14 (0 new).
No form submitted.
2026-11-03 15:19:15 +00:00
webmaster f3e2812694 fix(freelancer-directory): remove the Clear filter button; give the way back inside the empty state
The Clear button, its .dir-clear-wrap container, the .dir-clear CSS and the JS
that showed, hid and wired it are gone from the filter row. The search box keeps
its native type=search clear. The empty state no longer names a missing control:
it now reads "Nothing matches. Widen your search, or set the filters back to All."
— with that phrase an inline text action (#dirEmptyReset) that does exactly what
Clear did, so the one-click way back survives where a visitor needs it.
2026-11-03 14:37:05 +00:00
webmaster 80bf4bc019 fix(freelancer-directory): the clients filter is a yes/all facet, the dropdowns are 30% narrower, the badge is opaque and inset
The "Clients on ODOO4projects" filter was a numeric minimum (data-match="min",
"4+ clients"). Both live records carry the same count, so no threshold could
ever narrow it — it was dead on live data. Replaced with one two-option control,
All "" / Yes "yes", and the test is now "has a count above zero". The threshold
machinery (data-match="min" and its numeric branch) is dropped from both
renderers and from build-directory.py.

A facet is still printed only when the data supports it: the yes/all control
renders only when a record with a count and a record without one (0 or absent)
both exist — otherwise "Yes" and "All" return the same list. With today's two
live records (counts 2 and 4) it therefore does not print; it appears by itself
the moment a zero lands.

The four dropdown boxes are 30% narrower: .dir-filter:has(> select) min-width
180px -> 126px, so only the <select> facets change; the search box (240px) and
the Clear wrapper are untouched.

The OCA Member Profile badge no longer hangs past the card's right border and is
no longer translucent: right -0.55rem -> 0.5rem (an 8px gap to the border, the
top border still straddled), background rgba(201,162,39,0.2) -> var(--brass)
(solid, the palette token the tint was built from), hover var(--brass-bright).
Label contrast on the solid fill 7.15:1 idle / 9.09:1 hover.

Rebuilt the card HTML from the live webhook (build-directory.py) so the built
list and the runtime renderer agree. No form submitted, nothing else touched.
2026-11-03 14:12:01 +00:00
webmaster a8c5c1837d fix(freelancer-directory): the OCA Member Profile badge straddles the card's top-right corner
The badge sat inside the frame (top/right: 0.85rem). It now straddles the
border — top: -0.7rem, right: -0.55rem — so the card's top and right border
lines run through it, like a tag on the corner.

The :has() header reserve (padding-top: 3.15rem) is no longer needed and is
removed: the badge now intrudes only ~12px into the card (was ~36px), so the
name clears it at the card's normal 20px padding — proven with a long-name
variant. At 375px the page keeps zero horizontal overflow (scrollWidth ==
clientWidth); the badge's 8px right protrusion has 21px of slack. Geometry is
identical in both renderers (built HTML and the JS list): badge offset
dt=-10px, dr=+8px. DOM order, the OCA predicate, the label and the link
attributes are unchanged.
2026-11-03 14:04:29 +00:00
webmaster 04199bd606 fix(freelancer-directory): delete the dark anonymous-contact modal; retire hello@ -> support@ site-wide
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).
2026-11-03 13:52:44 +00:00
webmaster 570085cb04 fix(freelancer-directory): the OCA Member Profile badge moves onto the card frame, top right
The badge was an inline-block inside .dev-head, below the name. It is now
absolutely positioned in the card's top-right corner, inset 0.85rem from both
edges (inside the 1rem radius, nothing clipped), with .dev-card as its
positioning context.

The badge keeps its DOM home with the name (h3 then a), so a screen reader
still reads the freelancer's name first; only the paint position moved. No
markup change in either renderer — the build-time HTML and the runtime
liveCard() path both already emit the badge inside .dev-id, so the CSS change
covers them identically (verified: byte-identical geometry at 375 and 1280).

No overlap at any width: .dev-card:has(.dev-oca-badge) reserves the badge's
band by padding the header down, so the name keeps the card's full width and
wraps freely instead of running under the badge. A horizontal reserve was
measured and rejected — at 375 px it left the name column 42 px. Longest live
name (Oliver - The OC Guy) and a deliberately long name both clear by 0 px of
overlap; .dev-name gains overflow-wrap: anywhere so a name with no space in it
cannot push the card open.

Untouched: the OCA_link predicate, the data-oca attribute and the OCA filter
(1 of 2 when set to OCA member), the label, href, target and rel.
2026-11-03 13:49:50 +00:00
webmaster 2677881a88 fix(freelancer-directory): apply-form focus sliders default to 3 (15 of 15 on load)
The application form's five focus sliders now open at 3 each — exactly the 15-point
budget — so the form starts from a legal profile and the applicant adjusts, rather
than building up from zero. The scale copy now states the rule the default teaches:
raising one axis forces lowering another. The noscript fallback number inputs
default to 3 as well.

Gate unchanged: submit is still blocked unless the five sum to exactly 15; the
default satisfies it on load, it does not skip it.

The admin form is deliberately NOT changed: it keeps loading the record's real
values (populate()), and 0 for a brand-new record — an operator editing only a
description must not silently write a 3/3/3/3/3 profile. Its "leave all five at 0
to leave the profile untouched" escape is intact.
2026-11-03 13:41:42 +00:00
webmaster 68dd48e506 fix(freelancer-directory): card + forms — drop the count hedge, the 15-point sentence and the axis digits; kill every ", as declared"; rename Focus→Skill profile; languages on the shared chart; OCA becomes an "OCA Member Profile" badge; OCA filter re-added off OCA_link
- card count reads "4 clients on ODOO4projects" (no "— a number we recorded"); the
  lede says "our own count" for the same reason. Admin form hint left alone (it is
  an instruction to the operator, not a claim to the visitor).
- removed "15 points over five areas — nobody can be maximum in everything." from
  the card (build + runtime); the forms keep their own explanation.
- removed the visible axis digit (dev-axis-val) from both renderers; the five
  squares stay and each row carries its value as role="img" + aria-label="N of 5".
- removed every ", as declared" (10 on the served page) and rewrote the trust block
  so it no longer promises a label that no longer exists.
- "Focus profile" -> "Skill profile" on the card (build + runtime) and in both form
  legends. The separate `Focus` payload field is untouched.
- languages render through the same dev-axes chart as the skill profile (one
  component, two labels; no digit either).
- OCA becomes a clickable "OCA Member Profile" badge in the card header, once; the
  links row no longer carries it. Same predicate drives the re-added OCA filter
  (OCA_link under https://apps.odoo-community.org/community/), which is a facet in
  the existing filter row, renders only when the data supports it, and filters
  rather than ranks.
2026-11-03 13:38:12 +00:00
webmaster 5aa4782de7 fix(freelancer-directory): remove the Post-a-request section and every relay promise; state the service is free both sides; count = clients, one label; render every confirmed record
- 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).
2026-11-03 13:33:53 +00:00
webmaster 99245764a9 feat(freelancer-directory): 5-axis focus profile, 3 numeric languages, OCA from the link; drop odoo_versions
The user's spec (2026-11-03): "remove the ODOO versions. people are lying anyways";
"15 points to distribute between functional system modules integration ai"; "the
languages should be self assessed with 0 can not speak and 5 native. we have the
languages english german and spanish"; "lets limit the focus on the form to 70 chars";
"lets remove the member OCA question … use the OCA link to determine if someone is a
member … show the oca link on top of all links and make it apart with a color, also
dont show the link itself just a button or a chip"; "the only quality signal we can give
and verify is how many projects the freelancer brought to our platform"; "no sorting we
keep the no rank policy"; "get the sort on the page out."

REMOVED (both forms + the filter + the hints):
- `odoo_versions` — the webhook never carried it, so every value was silently dropped
  (re-verified: the payload has no `odoo_versions` field).
- `specialities` — replaced by the focus profile.
- the OCA-member question — membership is now DERIVED from `OCA_link`.

ADDED:
- Five focus sliders (functional, system, modules, integration, ai), each 0-5, from ONE
  constant (`FOCUS_AXES`), on both forms. Live running total ("12 of 15 points"); the
  submit is BLOCKED with an inline warning unless the five total exactly 15. The 15-point
  budget of a possible 25 is what stops anyone claiming maximum everywhere, and the form
  says so. No-JS: a `<noscript>` block swaps the sliders for five number inputs.
- Three language sliders (English, German, Spanish), each 0-5 (0 = cannot speak,
  5 = native), as SEPARATE numeric wire fields — no CSV, no 114-name ISO dataset, no
  free text. Preset to 0 (the user's decision, stated): an untouched slider records
  "cannot speak" rather than a blank.
- `Focus` capped at 70 characters on both forms with a live counter; the render path
  truncates a longer legacy value to 70 with a visible ellipsis.
- The card, in the user's order: photo+name · "N clients on ODOO4projects" (once, with
  weight) · Focus · the freelancer's own words · the focus-profile chart (one row per
  axis the payload carries, five squares) · the language line ("English 4 · German 5") ·
  the links, with the OCA profile chip FIRST and APART (a label, never the raw URL;
  "(as declared)") · the anonymous-contact button (JS-only — dark).
- The anonymous-contact modal: sends `{freelancers:[{id,name}], brief, email}` for the
  one card, opens only when `ODOO_EXPERTS_QUOTE_WEBHOOK` is set (still empty — ships
  dark), and says plainly that the email goes to us, never to the freelancer.
- OCA membership from `OCA_link`, matched on host + PATH PREFIX
  (`https://apps.odoo-community.org/community/`), never the bare domain.

ORDER: the client-side `Name` sort is gone from BOTH renderers — the endpoint's order is
rendered as received, so cards reshuffle between loads (the server randomises on purpose).
Nothing is ever ordered by the client count.

Also: the count is rendered ONCE (the payload has one field for it — "clients" and
"projects" are the same number here), `0` renders (a fact) while absent/null renders
nothing, and the legacy `languages`/`specialities` fields are left in the payload but no
longer read.

QC: verify-html.py 0 · check-inline-js OK · test-directory-render.py PASS · competitor QC
exit 3, 74 site-wide pre-existing, 0 in our files · impeccable detect --no-advisory
dir 14 (0 added) / admin 7 (0 added) · 375 px 0 overflow · no form submitted by hand.
2026-11-03 13:26:26 +00:00
webmaster 5ce14ea666 fix(freelancer-directory): render an uploaded photo (data:image/), not only a linked URL
Both card renderers emitted an <img> only for a URL, so a record whose Image is an
inline data:image/... URI (what the upload step stores today) fell through to the
monogram. The avatar check now allowlists exactly two schemes — https:// and
data:image/ — in build-directory.py and in the page script; anything else (empty,
junk like "gineoirn", another scheme) still falls back to the monogram, and the
value never reaches src unchecked. The lede copy is corrected to match: a photo
shows where the freelancer gave a link OR uploaded an image.
2026-11-03 13:04:06 +00:00
webmaster e067fec168 feat(freelancer-directory): full-text search, OCA/projects/language filters, standardised languages
- Search box: substring, case- and accent-insensitive, over Name, description,
  Focus, specialities and language names; combines with every filter (AND).
- Filters: OCA membership (any / member / not a member), Language (canonical
  names only), plus Country/Speciality; a project MINIMUM ('N+ projects'),
  rendered only when a threshold can narrow the list.
- Languages standardised: a closed ISO 639-1 checklist on both forms, with the
  proficiency as a SEPARATE axis (language_levels, 'Spanish:beginner'). A stored
  'Spanish - beginners' now maps to the language 'Spanish' and never becomes a
  filter option.
- Fix: .dev-card/.dir-filters set display, which beat the UA [hidden] rule, so
  filtered-out cards and the JS-off filter bar stayed visible.
- Deterministic order by Name (the endpoint's order alternates between reads).
- Card states plainly that a photo shows only when it is a link (the stored
  value is base64 and is not rendered); admin Image field says the same.
- specialities stays free text.
2026-11-03 12:57:51 +00:00
webmaster ff42a242db fix(freelancer-directory): render the list from the webhook at runtime
A record saved after the last build never reached the page: the list was
build-time HTML only, so the new freelancer (id=2) stayed invisible until
someone re-ran the build. That is the cause, not the symptom.

- The page now GETs the odoo_experts webhook on load and re-renders the cards
  and the filter bar from what it returns.
- The build-time HTML stays in the page and is the fallback: it is replaced
  only when the fetch succeeds, so JavaScript-off and webhook-down still show
  the last built list. An empty response never overwrites a populated list.
- The renderer mirrors build-directory.py field for field (casing aliases
  included); every freelancer-written value goes in as textContent.
- initFilters() is split so one delegated listener re-reads the selects on
  every change: the same code filters the built list and the rendered one.
- Confirmation is NOT gated: the endpoint carries no status/email_verified
  marker, so nothing was guessed. Recorded in the page comment as a data gap.
- rebuild: build-directory.py, so the served HTML starts from the real set.
2026-11-03 12:45:26 +00:00
webmaster ada9605c90 feat(freelancer-directory): Country is an ISO 3166-1 dropdown on both forms
Country was free text, so one country could be entered as Germany / DE /
Deutschland / germany and split the directory filter four ways. Both the
application and the admin form now submit from a native <select> of the
249 ISO 3166-1 alpha-2 codes: the option shows the English name, the value
is the code. No free-text fallback and no "other" escape hatch. Required,
with an empty "— choose —" first option. The directory filter shows the
English name for the stored code, so one country is one option.

Non-trivial, non-UI: the submitted value format lives in one constant per
page, COUNTRY_VALUE_FORMAT ("code" | "name") — switching to full country
names is a one-line change if the n8n workflow expects names, not codes.
The webhook field name stays exactly "Country".
2026-11-03 12:41:02 +00:00
webmaster babc4642ce feat(freelancer-directory): add the trust block (what we do / don't)
The keyword work found no trust keyword with demand and none we could answer
without a review/vetting claim, so the trust content ships as a block on the
directory page itself. It folds the old "How this works" section and states
our limits plainly: you deal with the freelancer, we rank nobody, we verify
nothing (declarations tagged '(as declared)'), invitations carry a one-click
opt-out. The contract point links to the note under the listings instead of
copying it. No trust adjective is a claim; no review/rating/star.
2026-11-03 12:32:25 +00:00
oliver d69a3da26e feat(freelancer-directory): go live — indexable, sitemap + llms, og:image, Plausible, homepage inbound link
- /odoo-community-developers/ no longer noindex,nofollow; title/description/H1 target 'hire odoo developer'
- added og:image + og:image:alt (the page's own hero), twitter summary_large_image
- Plausible snippet copied byte-for-byte as the last thing in <head>
- sitemap.xml + llms.txt entries
- one inbound link: a homepage FAQ card (client-facing, the established FAQ->page pattern)
- relay forms untouched: ODOO_EXPERTS_QUOTE_WEBHOOK / ODOO_EXPERTS_INVITE_WEBHOOK stay empty
- admin page (/admin/) stays noindex and unlinked
2026-11-03 12:26:23 +00:00
webmaster 1c3a2a4b0e fix(freelancer-directory): success copy is a receipt, not a delivery claim
The two relay success strings still described a delivery the stub endpoints
cannot perform ("on its way", "we will email them one invitation"). They were
unreachable only while ODOO_EXPERTS_QUOTE_WEBHOOK / ODOO_EXPERTS_INVITE_WEBHOOK
are empty — setting either re-armed the F1 false confirmation.

- request form success -> "we have your request … come back to you by email"
- invite form success -> "we have recorded your invitation … one-click opt-out"
- comment at each constant: setting it switches the form on; the success copy
  must already be a receipt.

Constants stay empty; both forms stay dark.
2026-11-03 12:11:03 +00:00
webmaster 4d5d59f3bd fix(freelancer-directory): apply the ICP re-review — claims sweep, F1 gate
- F2: 'We publish exactly what you write' -> 'We publish what you write —
  after we check it: your photo, your email address and for spam' (the page's
  own consent line + photo check + spam gate made the absolute false)
- claims sweep: drop the 'no claim we cannot back' meta-claim and the
  'not through an agency' absolute; state the individual rule as the
  declaration we ask for, and make the card's individual tag read
  '(as declared)' to match the OCA tag
- F3: directory lede now says the individual rule is a declaration, not a fact
- F4: 'How this works' 110 -> 44 words (three short lines, no relay promise)
- F5: count framed as 'a number we recorded' (owner-typed, not derived);
  photo hint agrees with its '(optional)' label; kicker 'ODOO COMMUNITY
  FREELANCERS' -> 'ODOO FREELANCERS' and the meta/og drop 'Odoo Community'
  so an Enterprise client is not invited to self-exclude
- F1: the request and invite forms SHIP DARK — their endpoint constants are
  empty and the forms stay hidden behind a plain 'not open yet' line
  (no submit, so no false confirmation); the invite copy carries the
  one-click opt-out line the ICP asked for before the form returns
2026-11-03 12:04:04 +00:00
webmaster c55c707694 fix(freelancer-directory): keep the final-CTA link legible on the dark section
The new '#request' link in the closing CTA was inheriting the site-wide brass-deep
link colour on the midnight background (3.6:1). It uses brass-bright now, so the
page holds its zero-added impeccable profile (14, same as before the change).
2026-11-03 11:56:43 +00:00
webmaster 63e43ce7a1 feat(freelancer-directory): CSV wire shape for languages/specialities, wire the quote route, add invite-a-friend
- languages/specialities now POST as comma-separated STRINGS ("English,German"),
  not arrays; the build script splits them into separate filter options and tags.
- the client request form is no longer inert: it posts to odoo_experts_quote with
  an explicit freelancer pick-list (never an implicit all) and a client address.
- new invite-a-friend form posts to odoo_experts_invite_friend (sender name +
  friend email); we send the invitation, not the sender's address.
- both new forms show success only on a returned 200 (n8n answers 200 before the
  workflow runs) and promise no timing.
- build-directory.py: render the pick-list, fix the country option casing.
- pages stay drafts (noindex, not in sitemap/llms, no inbound links).
2026-11-03 11:54:38 +00:00
webmaster 64540e208f chore(freelancer-directory): drop the admin page URL from the directory's script comment 2026-11-03 11:47:43 +00:00
webmaster c74315e950 fix(freelancer-directory): apply the ICP's five ship-blockers
- 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
2026-11-03 11:47:05 +00:00
webmaster e8c93d89ad feat(freelancer-directory): add /odoo-community-developers/ as a draft
Odoo freelancer directory for our clients, built as a draft (noindex, out of
sitemap.xml and llms.txt, no inbound links, no announcement surface, no tracking).

- The list is rendered at BUILD TIME from the single freelancer webhook
  (GET/POST .../odoo_experts) so it works with JavaScript disabled; the page
  script only filters that HTML.
- Filters (country, odoo_versions, languages, specialities) are printed only when
  the live data actually supports them, so a filter that returns nothing never
  ships. No record has them yet, so none render today.
- Three forms: freelancer application (POST, no id -> create), admin add/edit
  (POST with id -> update), and a client request form whose endpoint is a single
  inert constant until the workflow exists.
- No reviews, no ratings, no claims beyond what a freelancer wrote and the count
  from our own records. No competitor named.
2026-11-03 11:37:17 +00:00