# CBW Website — SEO Checklist (ground-up pass, 2026-10-22)

Site: Ceylon Build Works one-pager · `website/index.html` (+robots.txt, sitemap.xml)
Canonical base: **https://ceylonbuildworks.lk/** — ⚠️ **DOMAIN PLACEHOLDER** (not yet
registered). Every absolute URL intentionally uses it; marked in source comments
("DOMAIN PLACEHOLDER") in `index.html`, `robots.txt`, `sitemap.xml`.

## 1. Head metadata (all in `<head>`, verified by QA `checks.seo.meta`)

| Item | Value / rule |
|---|---|
| `<title>` | `Ceylon Build Works — Home Construction & Renovation, Colombo` (59 chars, ≤60, unique keywords: brand + "home construction" + "renovation" + geo) |
| meta description | ≤155-char final string: "CBW builds and renovates Colombo homes — structural, waterproofing, MEP, painting & demolition. Milestone payments, daily photo logs, quote in 48 hours." (QA gates `description_chars <= 155` in `checks.seo.meta.values`) |
| meta keywords | intentionally omitted (ignored by all ranking engines; avoids dilution) |
| canonical `<link rel="canonical">` | `https://ceylonbuildworks.lk/` (placeholder domain) |
| meta robots | `index, follow, max-image-preview:large` |
| theme-color | `#1A2C44` (navy, brand token — present since first build) |
| color-scheme | `light` (fixed light theme is a brand decision) |
| geo.region / geo.placename | `LK-1` / `Kolonnawa, Colombo, Sri Lanka` — **[GEO PLACEHOLDER: lat/long omitted until a verified coordinate exists]** |

## 2. Open Graph + Twitter cards

- `og:type website`, `og:site_name`, `og:title` = title, `og:description` = tagline + 48h hook
- `og:url` = canonical placeholder URL
- `og:image` = **absolute** `https://ceylonbuildworks.lk/assets/img/og-cover.jpg`
  (1200×630, cropped + resampled from the hero photo with `sips`: center-crop 1920×1071 → 1008p aspect band → resample 1200×630, JPEG q72, ≈?KB — see `og:image:width/height/alt` inline)
- `og:image:alt` descriptive (crew plastering a Colombo home, golden hour)
- `og:locale` `en_LK`
- `twitter:card summary_large_image` + twitter title/description/image/image:alt

## 3. JSON-LD (2 blocks, both parse-checked by QA `checks.seo.jsonld`)

1. **GeneralContractor** (`@id #organization`): name, alternateName, url, logo
   (`assets/logos/cbw-logo-horizontal-dark.svg` absolute), image (og-cover + hero),
   slogan, email, `+94 72 637 3133`, `priceRange: "LKR"`, PostalAddress
   (No. 37A Jahena Lane, Nagamulla Road, Kolonnawa — Colombo 05, Western Province, LK),
   areaServed (Colombo District / Western Province / Sri Lanka),
   openingHours `Mo-Sa 07:00-18:00`, sameAs ×5 (IG/TikTok/YT/X/Facebook
   `ceylonbuildworks` handles). **Graph note:** geo coordinates intentionally omitted —
   marked `[GEO PLACEHOLDER]` comment directly above the script.
2. **ItemList** of the 6 services (itemListElement → `Service` each with its page
   anchor URL, areaServed, provider `@id` ref, and an `Offer` with
   `priceCurrency: "LKR"` + price from the WhatsApp-catalogue rate card:
   Structural 3,500,000 · Renovations 25,000 · Waterproofing 12,500 · MEP 7,500 ·
   Painting 22,000 · Demolition 18,000).
3. BreadcrumbList deliberately not used (single page, no hierarchy to encode).

## 4. Semantic discipline (QA `checks.seo.headings / images / links / sections`)

- Exactly **one `<h1>`** ("CEYLON BUILD WORKS", hero).
- `<h2>` per section: What we build · Built the right way, by default · How a CBW job runs · Request a quote (+ footer column heads). `<h3>` per service card & process step. Heading level walk asserted **jump-free**.
- All content sections carry `aria-labelledby` → real heading ids (services/whyus/process/contact) or `aria-label` (trust strip); footer lists carry their own `aria-label`s.
- In-page anchors match nav vocabulary exactly (Services · Why CBW · Process · Contact), visible in both nav and footer ("Explore" column) — footer links use the nav's vocabulary, not novel labels.
- `scroll-margin-top` keeps anchor landings below the sticky header.
- Descriptive link text law: zero "click here"-class links; the six repeated "Get a quote" links each carry a **unique aria-label** ("Get a structural and house-building quote on WhatsApp" etc.); icon-only social links are text pills with aria-labels.
- Every `<img>` has an alt; content images use ≥3-word keyword-descriptive alts (e.g. "Gloved hands levelling a brick on a wet mortar course"); the demolition watermark is `alt=""` + `aria-hidden` (decorative, QA asserts this pairing).
- Landmarks: `<header> <nav aria-label="Primary"> <main id="main"> <section> ×4 <aside> (trust) <footer>` — no orphan `<div>` responsible for headings.

## 5. Files

- `robots.txt` — `User-agent: * / Allow: /` + `Sitemap: https://ceylonbuildworks.lk/sitemap.xml` (placeholder-marked).
- `sitemap.xml` — single `<url>` (canonical placeholder) + `lastmod 2026-10-22`, `<changefreq>monthly</changefreq>`, `<priority>1.0</priority>`.
- 404 fallback: **optional / not shipped** — pure `python3 -m http.server` static serving shows its default listing on 404; a `404.html` should be added at deploy if the host allows custom error pages. (Decision: won't ship a fake-looking 404 page into an asset dir used by unknown hosting.)
- favicon set: svg + 16/32 png + ico + apple-touch 180 + android-512, all referenced from head and all 200.

## 6. Performance SEO

- Preloads: hero photo (`fetchpriority="high"`) + 2 critical fonts (Montserrat-800 display, Inter-400 body); everything else lazy (8 `loading="lazy"` imgs below fold).
- JS: single `js/main.js` with `defer` — no blocking scripts.
- CSS: one local stylesheet, zero CDN, zero external requests (robots/sitemap only add same-origin GETs).
- CLS: all images carry explicit width+height (QA asserts 0 missing → zero layout shift), hero is a positioned `<picture>` (absolute fill, no doc-flow reflow on decode).
- OG image has explicit dims (1200×630) so sharers never re-fetchguess.

## 7. ✅ AT DEPLOY checklist (execute before going live)

1. **Domain**: replace every `https://ceylonbuildworks.lk` occurrence — `index.html` (canonical, og:url, og:image, twitter:image, JSON-LD url/logo/image), `robots.txt` (Sitemap line), `sitemap.xml` (`<loc>`) — with the purchased domain. Then delete the "DOMAIN PLACEHOLDER" banner comments.
2. **Geo**: add `"geo": {"@type":"GeoCoordinates","latitude":…,"longitude":…}` to the GeneralContractor block once a verified coordinate exists (remove the `[GEO PLACEHOLDER]` comment), and optionally `og:latitude/longitude`.
3. **lastmod**: bump `sitemap.xml` `<lastmod>` to the deploy date.
4. **Search engines**: submit the sitemap in Google Search Console + Bing Webmaster (verify domain first).
5. **Hosting enables custom error pages** → add `404.html` and wire it.
6. Re-run `QA`: `.venv/bin/python website/qa/capture-verify.py` must stay exit 0 (it asserts the canonical string, robots Sitemap line, sitemap loc — all of which you just changed; update them there temporarily or the gate will flag the mismatch by design).
7. Verify social handles still resolve; the 5 `sameAs`/footer links must not 404.

## 8. QA wiring

`website/qa/capture-verify.py` asserts all of the above (`checks.seo` sub-block in
`qa-report.json`): title/desc lengths, canonical identity, robots meta, OG/Twitter
completeness, OG-image absolute+dims+alt, hreflang en-LK + x-default, both JSON-LD
blocks parsed with field gates, one h1 + jump-free heading walk, alt discipline
(≥3 words or decorative+aria-hidden), generic-link-text scan, unique "Get a quote"
aria-labels, section aria-labelledby resolution, and 200s for robots.txt /
sitemap.xml / og-cover.jpg. Gate failures fail the script (exit 1) by design.

## 9. Footer Standardization (2026-10-22)

The footer IA from `../research/footer-standard-brief.md` §6 is now on disk, plus 4 new
indexable subpages. SEO-relevant facts for future passes:

- **New indexable URLs** — `/careers/`, `/payment-options/`, `/terms/`, `/privacy/`, each in
  `sitemap.xml` (`lastmod 2026-10-22`, 5 loc entries total) with the DOMAIN PLACEHOLDER comment intact.
- **Per-subpage metadata** — unique title (<=60 chars) + unique meta description (50–155 chars),
  canonical `https://ceylonbuildworks.lk/<slug>/`, OG + `summary_large_image` Twitter cards with the
  shared 1200×630 cover, hreflang `en-LK` + `x-default`, exactly one `<h1>`, `BreadcrumbList`
  JSON-LD, `theme-color`/viewport set, all assets local, single deferred script.
- **Nav-link rewrite on subpages** — homepage anchors use `/`-absolute paths (`/#services`,
  About -> `/#why-us` — no `#about` anchor exists, do not invent one). This keeps deep-linked
  fragments resolvable from any depth and keeps `js/main.js`'s `href^="#"` active-nav logic idle.
- **Honesty-constrained SEO** — no registration number anywhere (BRN unissued -> gold
  `[OWNER TODO: registration number]` chip); no "CIDA registered" claim; Koko/Payzy described as
  arriving-with-checkout, never as live merchants; Careers publishes how-to-apply only, no
  fabricated postings. Watch for regression on the job-posting scan inside
  `research/footer-audit.py` (it pattern-matches `VACANCY`/`Position:` on `/careers/` — avoid
  those tokens even in HTML comments).
- **Footer social links** — 5 icon-only links (IG/TikTok/YT/X/FB, Tabler v3.34.1 outline glyphs in
  the inline sprite), distinct `aria-label` per platform, tap targets via `var(--tap-min)`, and the
  anchors carry the `icon-svg` marker class that `research/footer-audit.py` greps for.
- **QA coverage** — `checks.footer_system` block added to `capture-verify.py` (5x200 routes, 0
  console errors, 0 h-scroll @1440/768/390 per route, 5 distinct social labels, pay band + legal
  row parity, per-subpage meta gates, sitemap 4-URL check). Both gates: `capture-verify.py` exit 0
  and `research/footer-audit.py` stays seed-green (138 checks, 0 fails).
