TCB Shopz — Developer & SEO Documentation

Internal reference for junior/senior developers and SEO/digital marketers. Not linked publicly, not indexed.

1. Project Overview

TCB Shopz (The Coins Bureau Shopz, tcbshopz.com) is a PHP/HTML5/CSS3/JavaScript website with no framework dependency, running on standard Apache/Linux hosting. It connects the USA and Cameroon through personal shopping (Shop-For-Me), two-way shipping, gift delivery, and gift card exchange. See Project Transmission Document_ TCB Shopz Website Development.md for the full historical build log (V1–V8), and all_forms_to_crm.md for the original Zoho CRM integration brief this build follows.

Forms POST via AJAX to process_*.php handlers, which send email through Brevo, log leads into Zoho CRM, and redirect the browser (client-side, via JSON redirect field) to thank-you.php.

2. File Structure

PathPurpose
includes/header.php / includes/footer.phpGlobal layout, nav, meta tags, JSON-LD, analytics, WhatsApp/Tawk.to widgets — the ones actually used by every page.
header.php / footer.php / main.js (root)Legacy/unused — not included by any page. Safe to delete once confirmed; kept for now to avoid unrelated churn.
assets/css/style.css + style.min.cssSite-wide styling. No build/minify tool in this repo — the two files must be kept identical (copy one over the other) after any CSS change, since includes/header.php loads style.min.css.
assets/js/main.js + main.min.jsMobile nav toggle, hero slider, AJAX form handling. Same "keep both in sync" rule as CSS.
assets/js/whatsapp-button.js + assets/css/whatsapp-button.cssFloating WhatsApp button + pre-chat lead-capture modal.
includes/zoho-config.phpZoho CRM credentials. Never commit real secrets to a public repo. Blocked from direct web access by includes/.htaccess.
includes/zoho-client.phpZohoCRM class: token refresh/cache, createLead(), testConnection().
includes/brevo-api.phpBrevoEmailService class for transactional email.
includes/geolocation.phpGeolocationService — IPgeolocation.io wrapper, used for lead context and fraud signals.
includes/b2-storage.phpBackblaze B2 client for career résumé uploads.
includes/recaptcha-config.phpreCAPTCHA v2 site key (public) + secret_key (currently empty — see §7). Blocked by includes/.htaccess.
includes/recaptcha.phpverifyRecaptcha(), isHoneypotTripped(), looksLikeSpamContent() — the bot-defense layer. See §7.
process_contact.php / process_order.php / process_career.php / process_whatsapp_lead.phpForm handlers. Pattern: honeypot/reCAPTCHA check → sanitize → validate → spam-content check → send Brevo email → push Zoho lead (non-blocking) → return JSON with redirect.
thank-you.phpShared success page for all form types (?type=contact|order|career). noindex. Has a hidden test panel at ?debug=1.
check_server.phpPHP/server environment compatibility check.
diagnostics.phpIntegration config/connectivity checker — never prints live secrets, only masked previews. Includes a live (side-effect-free) Zoho token test.
ZOHO_CRM_TEST_CHECKLIST.md (project root)Step-by-step pre-deploy checklist for finishing Zoho setup and verifying leads land correctly. Local reference only — do not upload to the web server.
robots.txt / llms.txt / sitemap.xmlCrawler and AI-agent access files. See §9.
.htaccess (root) / includes/.htaccessSecurity headers, gzip/caching, blocks .bak/.log/.md/.json downloads, denies all direct access to /includes/.

3. Quick Start by Role

Junior Developer

  1. Every page starts with include 'includes/header.php' and ends with include 'includes/footer.php' — set $pageTitle / $pageDesc (and optionally $pageNoIndex = true;) before the header include.
  2. To change global nav/footer/logo/phone number, edit only includes/header.php / includes/footer.php — never the root-level duplicates.
  3. After any CSS/JS edit, copy the file over its .min. counterpart (no build tool exists here).
  4. Test forms locally with php -S localhost:8000 from the project root.

Senior Developer

  1. Zoho/Brevo pushes are intentionally non-blocking and wrapped in try/catch — a CRM or email outage must never break the visitor-facing JSON response. Preserve this pattern in any new handler.
  2. ZohoCRM::createLead() requires "trigger":["workflow"] in the payload to fire CRM workflow emails — do not strip it.
  3. Token caching lives in includes/zoho_token.json (gitignore this), refreshed 5 minutes before expiry.
  4. Same non-blocking try/catch pattern applies to the honeypot/reCAPTCHA/spam checks in §7 — a bot-defense false positive must degrade to "submission rejected," never a PHP error.
  5. See §13 for what's still open (Zoho token, reCAPTCHA secret) before considering this deployment-ready.

SEO / Digital Marketer

  1. Every page's $pageTitle/$pageDesc feed the <title>, meta description, Open Graph, and Twitter Card tags automatically — edit those two variables, not the head markup.
  2. llms.txt is the AI-answer-engine equivalent of a meta description for the whole site — update it whenever pricing or service structure changes.
  3. New pages must be added to sitemap.xml and, if customer-facing, to includes/header.php nav + includes/footer.php links.
  4. See §8 for content-depth and heading-structure guidelines.

4. Integrations

ServiceUsed ForConfig Location
Brevo (SMTP API)Order/contact/WhatsApp-lead transactional emailincludes/brevo-api.php
Zoho CRM (API v8)Lead capture from all three sources (contact, order, WhatsApp)includes/zoho-config.php
IPgeolocation.ioApproximate location on lead submissionsincludes/geolocation.php
Backblaze B2 (S3-compatible)Career résumé storageincludes/b2-storage.php
Google reCAPTCHA v2Spam protection — client-side widget + server-side verification (see §7)Site key inline in each form page; secret key in includes/recaptcha-config.php
Google Analytics (gtag)Traffic analyticsincludes/header.php (loads on every page)
Tawk.toLive chat widget, pinned bottom-leftincludes/footer.php

Run diagnostics.php any time to see which of the above are configured, and check_server.php to verify the PHP/hosting environment itself.

5. Zoho CRM Setup

Full step-by-step is in all_forms_to_crm.md; a printable pre-deploy checklist with sign-off boxes is in ZOHO_CRM_TEST_CHECKLIST.md — use that one when actually doing the setup. Summary below.

  1. Log in to api-console.zoho.com with the account that owns Org 888320108 (The Coins Bureau Shopz).
  2. Open the Self Client → Generate Code tab.
  3. Scope: ZohoCRM.modules.leads.ALL,ZohoCRM.modules.deals.ALL,ZohoCRM.settings.READ
  4. Duration: 10 minutes. Copy the generated code (starts with 1000.) — it is single-use and expires fast.
  5. Immediately exchange it:
    curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
      -d "grant_type=authorization_code" \
      -d "client_id=1000.IR6C6WKA5T16NNMJXTNY9D4RP7SCZL" \
      -d "client_secret=<client secret>" \
      -d "code=PASTE_GRANT_CODE_HERE"
  6. Paste the returned refresh_token into includes/zoho-config.php. This is the only credential the site needs long-term — it does not expire unless revoked.
  7. Confirm on diagnostics.php with the "Test Zoho Connection" button.

TODO As of this build, includes/zoho-config.php ships with client_id/client_secret filled in but an empty refresh_token — the CRM push is currently a safe no-op (logged, never breaks the visitor flow) until someone with Zoho console access completes the steps above. Two different Client IDs were found across project docs during this build (...R7SCZL in all_forms_to_crm.md vs ...KPEG in self_client_CRM+ZOHO.json) — the guide's ID was used; confirm this is the intended one before generating the grant code.

6. WhatsApp Lead Capture

TCB Shopz now shows two WhatsApp numbers:

NumberRoleWhere it appears
+1 (555) 971-6317 (15559716317)Primary / main numberHeader utility bar, floating WhatsApp button + its script (WHATSAPP_NUMBER in assets/js/whatsapp-button.js), all CTA buttons, JSON-LD telephone
+237 670 931 202 (237670931202)Secondary / CameroonFooter, contact.php, thank-you.php — shown alongside the US number, never alone

The floating WhatsApp button (assets/js/whatsapp-button.js) is bottom-right, green, pulsing/bouncing to draw attention. Clicking it opens a modal asking for name, WhatsApp number, and email (optional) before handing the visitor off to wa.me. On submit:

  1. A fetch(..., {keepalive: true}) POST fires to process_whatsapp_lead.php — fire-and-forget, survives the page navigation.
  2. process_whatsapp_lead.php emails contact@tcbshopz.com + thecoinsbureau@icloud.com via Brevo, and pushes a Lead to Zoho CRM with Lead_Source = "Website - WhatsApp Button".
  3. window.open() immediately sends the visitor to wa.me/15559716317 with a pre-filled, personalized message.

The handler is designed to always respond success even if Brevo/Zoho fail — a backend hiccup must never block someone from reaching WhatsApp.

Tawk.to live chat is pinned bottom-left via Tawk_API.customStyle in includes/footer.php, specifically so it never overlaps the WhatsApp button.

Cache warning Every local CSS/JS include in includes/header.php/footer.php now carries a ?v=2.1 query string. The site's .htaccess caches JS/CSS for 1 month — bump this version number on every future CSS/JS edit, or returning visitors will keep running stale cached scripts (this is exactly what caused a "number won't update" bug after the phone number change).

7. Bot Protection & Spam Defense

Resolved An incident during this build is worth understanding before touching the forms again. Timeline:

  1. No server-side reCAPTCHA verification existed — only the client-side widget (grecaptcha.getResponse()) — so anything could POST straight to process_order.php/process_contact.php and skip the CAPTCHA entirely.
  2. A bot exploited this, relaying a crypto-scam payload through the order form (2026-07-21).
  3. Spam-bot traffic since then submitted garbage emails (mostly *.1win.xyz gambling-bot domains) that hard-bounced when the site tried to send them a "thank you" confirmation — 117 addresses landed on Brevo's suppression list.
  4. Those hard bounces degraded the sending reputation of notification@mail.tcbshopz.com enough that Brevo began silently returning event: "blocked" on all outgoing mail (confirmed via live Brevo Events API query) — even though the API call itself still returned 201 + a real messageId, making it look successful from the site's side. This is why "form submitted" stopped meaning "email arrived."

Fixed in code (includes/recaptcha.php):

Wired into process_contact.php, process_order.php, process_career.php (checked before any Brevo/B2/Zoho call), and process_whatsapp_lead.php (honeypot + content filter only, no CAPTCHA by design — a bot hit still gets a harmless 200 so a real visitor is never blocked from reaching WhatsApp, but the Brevo/Zoho pushes are silently skipped).

The Brevo reputation block itself is not something code can fix — it required action in the Brevo dashboard/support. Confirmed resolved: emails are arriving again as of this build.

8. Testing the Submission Pipeline

ToolWhat It Checks
check_server.phpPHP version, cURL/OpenSSL/JSON extensions, upload limits, required files present
diagnostics.phpWhich integrations are configured (masked, no secrets shown), a live Zoho token-refresh test, and reCAPTCHA/includes/-writability status
thank-you.php?debug=1Confirms which form type actually redirected here, plus a manual end-to-end checklist
ZOHO_CRM_TEST_CHECKLIST.mdFull pre-deploy sign-off checklist for the Zoho integration specifically

This build used a local PHP CLI (via Laragon's bundled php.exe) for php -l syntax linting on every changed file and a php -S localhost:8080 dev server for functional curl tests — honeypot rejection, spam-content rejection, and legitimate submissions were all verified end-to-end. The Brevo reputation incident in §7 was also diagnosed with live, read-only Brevo API calls (Events, Senders, Domains, Blocked Contacts) using the real API key already embedded in includes/brevo-api.php.

Still not done No connected browser automation (Playwright/chrome-devtools MCP) was available in this environment, so no actual click-through browser test has been run. Connect one of those MCP servers and re-request a UI test pass if you want that coverage.

9. SEO & GEO (AI Search) Setup

On-page

Every page sets $pageTitle and $pageDesc before including includes/header.php, which auto-generates: <title>, meta description, canonical URL, Open Graph tags, Twitter Card tags, and a sitewide LocalBusiness JSON-LD block. Content pages use a single <h1>, <h2> for major sections, <h3>/<h4> for sub-points, and comparison tables (.pricing-table / .table-responsive) where structured data helps both users and AI summarizers.

Crawler access

FilePurpose
robots.txtExplicitly allows major search bots and AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, etc.); disallows /includes/, form processors, and dev-only tools.
llms.txtMachine-readable site summary in the emerging llms.txt convention — key facts, page index, and citation notes for AI answer engines.
sitemap.xmlAll customer-facing pages, referenced from robots.txt.

Content depth guideline

Primary service/landing pages target ~800–1200 words of unique content with a full heading hierarchy and at least one data table where relevant (pricing, comparisons, process steps). Utility pages (privacy, terms) prioritize completeness and clarity over word count. thank-you.php, diagnostics.php, and check_server.php are intentionally noindex — they're conversion/dev tooling, not search-facing content.

Structured data (JSON-LD)

Every page inherits a sitewide LocalBusiness block from includes/header.php. how-it-works.php additionally ships HowTo (the 6-step process) and FAQPage (its FAQ section) blocks — both validated with json_decode() during this build. This is the pattern to copy for any future page with a clear process or FAQ: it's one of the highest-leverage things for AI answer engines (ChatGPT, Perplexity, Gemini) to cite the page correctly.

10. Diagrams & Illustrations

Kroki.io flow diagrams

how-it-works.php embeds a live-rendered flow diagram from kroki.io — no static image file, no build step. The diagram source (blockdiag, or actdiag for swimlanes) is compressed and encoded directly into the image URL:

$compressed = gzcompress($diagramSource, 9);           // PHP zlib = same format as pako.deflate()
$b64 = strtr(base64_encode($compressed), '+/', '-_');   // URL-safe base64
$url = "https://kroki.io/blockdiag/svg/$b64";

Brand colors are set directly in the diagram source via blockdiag's class syntax (color = "#0F4C81", textcolor = "#FFFFFF" — note: no style = filled, blockdiag rejects that attribute). Always curl-verify a new/edited diagram URL returns 200 + image/svg+xml before committing it — a malformed diagram source returns 400 with a plain-text error body, not a broken image, so it's easy to catch early. Keep new diagrams simple and linear for customer-facing pages — swimlane/actor diagrams read as "detailed/technical" and were deliberately replaced with a single-row flow chart after feedback.

Illustrations — undraw.co finding

Dead end, documented so it isn't re-attempted undraw.co illustrations are not hotlinkable. Their site renders illustrations as inline, recolorable SVG (needed for their in-page color picker) rather than serving separate static image files — confirmed by inspecting their pages directly. The one popular unofficial mirror (undraw.oswaldlabs.com) is dead (Cloudflare 530). There is no working undraw CDN as of this build.

Workaround in place: small hand-authored inline <svg> illustrations (package/order icon, WhatsApp chat icon) using only brand colors, embedded directly in the page markup — zero external dependency, guaranteed to render. See the <svg class="hiw-illustration"> blocks in how-it-works.php for the pattern; copy and adapt rather than re-attempting undraw hotlinks.

Photography — Unsplash

Photos use images.unsplash.com/photo-<id> direct CDN URLs (standard free license), never source.unsplash.com (deprecated, returns 503) and never plus.unsplash.com/premium_photo-* (Unsplash+ subscription required, don't hotlink without a license). Apply the .img-rounded class (soft corners + shadow + hover lift) to full-width content photos, or .img-card for smaller grid thumbnails — both defined in assets/css/style.css.

11. Brand Identity Reference

TokenValueSwatch
--primary-blue#0F4C81
--primary-gold#D4AF37
--secondary-blue#0A365C
WhatsApp green#25D366 (hover #1EBE5D)

Logo: https://tcb-media.s3.us-east-1.amazonaws.com/images/TCBSHOPZ/the_coin_bureau_shopz_logo.png (rectangular, transparent background). Used at ~54px height in the header and ~44px in the footer with a filter: brightness(0) invert(1) applied so the transparent-background mark reads clearly on the dark footer gradient. A local copy also lives in /logo/ for design reference — the live site pulls from the S3 URL directly, consistent with how every other site image is served.

12. Responsive / Mobile Notes

The async + DOMContentLoaded race condition (fixed)

Resolved The hero slider was silently invisible in production (images loaded fine, but every .hero-slide stayed at opacity: 0). Root cause: assets/js/main.js wrapped all of its init logic in document.addEventListener('DOMContentLoaded', ...), but its <script> tag loads with the async attribute at the very bottom of <body>. An async script can finish downloading and execute after DOMContentLoaded has already fired (especially once cached) — so the listener was registered too late and never ran. Fixed by adopting the same guard already used in whatsapp-button.js / geolocation-tracker.js:

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initMain);
} else {
    initMain();
}

Apply this pattern to any new script loaded with async or defer — never rely on a bare DOMContentLoaded listener alone.

13. Known TODOs & Open Items

ItemStatusNotes
Zoho refresh_tokenTODOStill empty. Work through ZOHO_CRM_TEST_CHECKLIST.md top to bottom — generate per §5 and paste into includes/zoho-config.php. CRM push remains a safe no-op until then; no lead has ever reached Zoho yet.
reCAPTCHA secret_keyTODOStill empty in includes/recaptcha-config.php. Server-side verification fails open until this is set — see §7. Get it from the Google reCAPTCHA admin console (same account as the existing site key).
US phone/WhatsApp numberResolvedConfirmed and now live as the primary number: +1 (555) 971-6317, in header/button/script. Cameroon number (+237 670 931 202) kept as secondary in footer/contact/thank-you. (Note: "555" is technically a fictional US exchange code per NANP conventions — flagged during this build, but the client explicitly confirmed and requested it as the primary number, so it's treated as final.)
Brevo sending reputation blockResolvedSee §7 for the full incident. Confirmed emails are arriving again as of this build. If "form submitted but no email" recurs, check Brevo's Events API for event: "blocked" before assuming it's a code bug.
ZIP codeResolvedConfirmed 20708 (site value) over the flyer's 20707.
wp-config.php.bakSecurityLeftover WordPress DB credentials file found in project root, unrelated to this site. Blocked via root .htaccess; recommend deleting it outright.
Legacy root header.php/footer.php/main.jsCleanupUnused duplicates of the real includes/ versions. Not referenced anywhere; safe to delete after confirming with the team.
services.phpFixedWas linked from nav/footer on every page but did not exist (404). Created during this build.
Hero slider invisible (async race condition)FixedSee §12 for the full explanation. Applies to any future async-loaded script.
Cache-busting on CSS/JSFixed?v=2.1 added to all local asset includes in includes/header.php/footer.php. Bump this on every future CSS/JS edit — 1-month browser cache means stale scripts otherwise persist for returning visitors.
undraw.co illustrationsNot possibleNo hotlinkable source exists (see §10) — do not re-attempt without a different illustration provider or a licensed local copy of the SVGs.
CSS/JS "minified" filesProcess gapNo build tool exists in this repo — style.min.css/main.min.js are plain copies of their source files, not actually minified. Fine functionally; consider adding a real build step later.
Browser/MCP UI test passDone (Oct 2026)No Playwright/chrome-devtools MCP was connected in this environment. All verification was PHP lint + curl-based. Connect a browser MCP for real click-through coverage.

14. Bilingual Site (EN/FR)

Every page exists in English and French. French pages are the English PHP pages translated from a DeepL dictionary (no API call when a visitor opens a page). Design, header, footer and brand are identical.

PieceFileRole
Page mapincludes/i18n/routes.jsonEN ↔ FR URL for every page (single source for links, hreflang, sitemaps, redirects)
Language logicincludes/lang.phpBrowser-language redirect on the first visit, ?hl=fr|en switch + cookie tcb_lang, hreflang, location bar for French-speaking countries
French rendererlang-router.php + includes/translate-runtime.phpRuns the English page in an output buffer, swaps strings from the dictionary, rewrites links and form actions
Dictionariesincludes/i18n/pages/*.fr.json, site.{en,fr}.json, forms.{en,fr}.jsonPage text, header/footer/widget strings, form messages and emails
Form handlersprocess_order.php, process_contact.php, process_career.php, includes/forms-i18n.phpFrench messages, French customer email, redirect to /fr/merci

Language rules: a French browser on its first visit is sent to the French page; an English browser in a French-speaking country sees a slim bar offering French; the EN/FR link in the top bar always wins; a French URL is never redirected away; crawlers are never redirected.

Maintain: edit the English page, run _build-tools/translate_pages.py (needs DEEPL_API_KEY in the environment), upload the changed includes/i18n/pages/<page>.fr.json. Fix a French sentence in _build-tools/translate_pages_overrides.json. Add a page: create it, add it to routes.json, translate, regenerate sitemaps.

15. Barrel & Sea Shipping Pages

Landing page, five product pages (two blue drums, extra-large suitcase, two Uline Mighty Totes), Shop & Load, inquiry page and thank-you page, in English and French (/barrel-shipping-to-cameroon, /fr/envoyer-un-baril-au-cameroun, /sea-shipping/<slug>, /fr/fret-maritime/<slug>). Code and copy live in sea-shipping/; prices in sea-shipping/inc/catalog.php (one source); text in inc/content/{en,fr}.json.

PageEnglish wordsFrench words
Overview2,9663,758
Product pages (each)1,200–1,2701,520–1,610
Shop & Load9391,182

French is naturally about 25% longer than English; both versions carry the same sections, FAQ (20 questions on the overview, 8 shared + 2–3 specific on each product) and calls to action.

Order flow: Buy Now (popup) or the inquiry page → sea-shipping/process_inquiry.php: reCAPTCHA → geolocation → local log (sea-shipping/storage/) → Brevo emails (team + customer, EN/FR) → Zoho lead → optional automatic WhatsApp alert (inc/notify-config.php) → thank-you page with a reference number. Content rules from the product handoff apply (prices are flyer references, shipping only; no unverified claims). Details: sea-shipping/TRANSMISSION.md.

16. Error Pages & .htaccess

StatusHandled byWhat the visitor sees
404lang-router.php → includes/redirects.php then includes/error-page.phpFirst a 301 if the URL is a known old/alternative address; otherwise the branded 404 (real status, header, menu, footer, logo, EN or FR, "Did you mean…" suggestions, WhatsApp link)
400 / 401 / 403error.php (includes/error-page.php)Same branded page with the right wording and status; folders without an index answer 403 (Options -Indexes)
500 / 503error-500.html (static)Bilingual branded page that works even if PHP is down

All error pages send X-Robots-Tag: noindex, are larger than 512 bytes (so browsers never swap in their own page) and keep the language switcher. .htaccess also: forces https://www.tcbshopz.com (delete the block "Canonical host + HTTPS" if a redirect loop ever appears), rewrites French and sea-shipping URLs, blocks .md/.json/.log/.bak downloads, sets security headers, gzip and caching.

17. Sitemaps, Redirects, llms & robots

FileContent
sitemap.xmlIndex of the two sitemaps below (submit this one)
sitemap-en.xml / sitemap-fr.xml23 URLs each (15 pages + 8 sea-shipping pages), every URL with hreflang en, fr, x-default; thank-you pages excluded (noindex)
llms.txt / llms-fr.txtAI-agent summaries with the rules for citing prices
robots.txtAll crawlers welcome, all three sitemaps listed, thank-you pages / handlers / router blocked
includes/redirects.php301 redirect map: add one line when a page moves
IndexNow key file + indexnow_ping.pyTells Bing and Yandex about new URLs; Google: Search Console → Sitemaps

Regenerate with python make_sitemap.py and python update_llms.py (they read routes.json and the sea-shipping catalogue). Submit https://www.tcbshopz.com/sitemap.xml in Google Search Console, Bing Webmaster Tools and Yandex Webmaster.

18. Analytics, WhatsApp & Chat

ToolWhereStatus
Google Analytics 4includes/analytics-config.phpActive (G-6RF1TFV1MN)
Microsoft Clarity / Yandex Metricasame fileNeeds IDs (empty = off)
Floating WhatsApp button + pre-chat formassets/js/whatsapp-button.js (v2.2) + process_whatsapp_lead.phpOn every page, EN/FR, number and page name on sea pages
Tawk.to live chatincludes/footer.phpOn every page except the static 500 page

window.tcbTrack(name, params) reports events (buy_now_click, whatsapp_click, whatsapp_lead, generate_lead) to every enabled tool. WhatsApp numbers: Cameroon +237 670 931 202 (sea-shipping CTAs and error pages) and the confirmed US number +1 (555) 971-6317 (site header/footer), see §13. Automatic WhatsApp alerts to the team need a provider (sea-shipping/inc/notify-config.php).

Speed: includes/optimize.php lazy-loads images after the first two, preloads the hero image on every page; it never alters links or text, so the French dictionary keeps matching.

19. Zoho CRM v2 (supersedes §5)

The client was rebuilt (includes/zoho-client.php): update-or-create on Email (no duplicates; earlier text kept as a Note), token refresh with one retry, any Zoho data centre, picklist fallback, outage queue, optional language field. Step-by-step guide in English and French: ZOHO_CRM_GUIDE.md / ZOHO_CRM_GUIDE.fr.md; helper _build-tools/zoho_setup.py turns a one-time grant code into includes/zoho-config.php. TODO the owner must generate the grant code.

20. Testing & QA Tools

Everything below ran on a local PHP 8.2 server (Brevo, Backblaze and geolocation stubbed; Zoho mocked) with headless-browser automation, not on the live server.

Tool (in _build-tools/)Checks
qa_site.pyEvery page in both languages: status, canonical, hreflang reciprocity, titles/descriptions, sitemap URLs, all internal links, language detection, 12+ redirect rules, error pages (400/401/403/404/500)
qa_pages.pySea-shipping pages: word counts, one H1, JSON-LD, links
qa_whatsapp.pyEvery WhatsApp link (215 checked) and the widget + Tawk scripts on every page
browser_pages.mjs54 pages at phone / tablet / desktop: overflow, broken images, JavaScript errors, header/footer present; screenshots
browser_forms.mjsFloating widget on 6 page types, sea-shipping popup + inquiry page (EN/FR), order, contact, careers with file upload (EN/FR); verifies team email, customer email, CRM lead, resume upload, one send per submission
browser_perf.mjsLCP, CLS, requests, lazy images
test_zoho.php18 Zoho client tests against a mock server

Run them again after every upload against the live site: python qa_site.py https://www.tcbshopz.com, python qa_whatsapp.py https://www.tcbshopz.com. A live order test is still needed once (Brevo is limited to the server IP, Zoho needs the grant code).

21. Deployment, Backup & Rollback

ZipUse
TCB-WEBSITE-FULL.zipThe whole website (all pages, assets, includes, sea-shipping, French site, error pages, sitemaps, docs) without secrets and personal data
TCB-CONFIG-FILES.zipOnly needed on an empty/new hosting: the five files that hold credentials (zoho-config.php, recaptcha-config.php, brevo-api.php, geolocation.php, b2-storage.php). Do not overwrite live values with it.
TCB-SEA-UPDATE-FULL.zipOnly the changed and new files (for a site that already runs)

Back up the replaced files first, extract the zip in the web root, set sea-shipping/storage to 775, run the checks in UPLOAD-README.md. Roll back by restoring the backed-up files. _build-tools/deploy.py can do the upload over FTPS with backups once an FTP account points at the web root.