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 passNot doneNo 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.