# PHASE 12: SEO, SCHEMA AND ANALYTICS

Read decisions D-12, D-13, D-14 and D-16.

## Scope

URLs, structured data, meta patterns, indexation control, GA4, Meta Pixel and the Conversions API purchase event.

**Do this on staging, not locally.** Staging exists from phase 00 (decision D-24). Rewrite rules, canonicals and crawls behave differently on LiteSpeed and cannot be verified against a Docker container.

## Build

### URLs and indexation

1. Category base stripped to bare, product base set to `/shop/`. Implement on staging first.
2. **Crawl every URL with Screaming Frog before declaring this done.** A stripped category base is the most common source of silent 404s in WooCommerce. Keep the ability to revert to the default bases as a one-setting rollback and document it.
3. `robots.txt`: disallow `/cart/`, `/checkout/`, `/my-account/` and `/*?add-to-cart=`. Allow `/wp-content/uploads/`.
   **Do not disallow `/*?filter_` or `/*?orderby=`** (decision D-25). Those URLs carry `noindex, follow`, and a disallowed URL is never fetched, so the crawler would never see the noindex and anything already indexed would stay indexed as an untitled entry. It has to be one mechanism or the other, and for de-indexing it must stay crawlable.
4. Filter and sort URLs already carry `noindex, follow` from phase 6. Verify with `curl -I` on staging.

### Meta and schema

5. Install The SEO Framework for canonicals, the sitemap and a client-editable meta box.
   **TSF has no `{token}` placeholder system in its settings UI.** Typing these patterns into a settings field renders them literally. Generate them in code instead, through `the_seo_framework_title_from_generation` and the description generation filters in `BC_Schema`, with TSF's own field left as a per-product override the client can fill in when they want something bespoke. The patterns to generate:
   - Product title: `{Product Name} in Pakistan | Price Rs. {price} | {Brand}`
   - Product description: `Buy {Product Name} online in Pakistan. {Fabric} {Size}, {Pieces} pieces. Cash on Delivery all over Pakistan. Rs. {price}.`
   - Category title: `{Category} Online in Pakistan | Buy {Category} at Best Price | {Brand}`
   - Category description: `Shop {count}+ {Category} online in Pakistan. From Rs. {min_price}. Cash on Delivery, fast Pakistan-wide delivery.`
   - Homepage: `Bed Sheets, Blankets & Bedding Sets Online in Pakistan | {Brand}`
6. Open Graph and Twitter card tags: let TSF emit them, and override `og:image` per product to the `bc-gallery` size, `og:type` to `product`, and add `product:price:amount` and `product:price:currency`. Brief section 32 requires this and a Meta ad linking to an unstyled preview costs clicks.
7. **Disable The SEO Framework's schema output.** Hand-roll JSON-LD in `BC_Schema` (decision D-13): `Product` with `offers` including `priceCurrency` PKR, `availability`, `priceValidUntil`, `shippingDetails` and `hasMerchantReturnPolicy` (the last two are what Merchant Center free listings need and plugin output does not reliably provide); `AggregateRating` only when a real review exists; `BreadcrumbList` on every archive and product page; `Organization` and `WebSite` with `SearchAction` on the homepage; `ItemList` on category pages.
8. Image alt pattern from the design system doc, applied at render. Heading structure audited: exactly one `h1` per page (product name, category name, homepage brand line), `h2` for sections, no level skipped.
9. `BC_Feed`: a cron-generated XML feed at `/feeds/google.xml` for Merchant Center, reused for the Meta catalogue. Four things that are easy to get wrong and each one silently breaks the feed:
   - **One row per variation, not per parent.** Variable products are the majority here. Each variation is its own `id` (the variation SKU), and every variation of a product shares one `item_group_id` (the parent SKU). Feeding parents only means Merchant Center prices nothing correctly.
   - **`identifier_exists: no`** on everything. Local-brand bedding has no GTIN or MPN, and without this the items are disapproved.
   - **Map `google_product_category` per category**, not one hard-coded value. Bed sheets, blankets, pillows and razai sets sit in different nodes of Google's taxonomy. A lookup table keyed on our category slug.
   - **The same ID space as the Pixel `content_ids`.** If the pixel sends a product ID and the feed carries a SKU, catalogue matching fails silently and dynamic remarketing never works.

### Analytics

9. `BC_Analytics`: `gtag.js` and the Meta base pixel loaded directly from the plugin. **No GTM container** (decision D-16). Push a `dataLayer` anyway for future portability, but call `gtag()` and `fbq()` directly.
10. Load both after first user interaction or a 2 second timeout on content pages, to protect LCP. Fire immediately on checkout pages.
11. Events: `view_item` / ViewContent, `view_item_list`, `select_item`, `add_to_cart` / AddToCart, `view_cart`, `begin_checkout` / InitiateCheckout, `add_payment_info`, `purchase` / Purchase. Always send `currency: 'PKR'`, `value`, `transaction_id` as the order number, and `content_ids` as the SKU.
12. `purchase` fires on `woocommerce_thankyou`, guarded by `_bc_ga_tracked` so a page refresh cannot double count.
13. `BC_Capi`: Meta Conversions API, **purchase only**. `wp_remote_post` from `woocommerce_thankyou` with an `event_id` matching the browser event for deduplication.
    **Phone hashing will not match unless it is normalised Meta's way first.** Our canonical stored form is `03001234567`. Meta wants country code, digits only, no leading zero, no plus: `923001234567`, lowercased, then SHA-256. Hashing the `03` form produces zero matches and silently destroys the exact attribution this exists to recover. Email is lowercased and trimmed before hashing. Guarded by `_bc_capi_sent`. Roughly 80 lines. Full CAPI coverage is a future feature.
14. `gtag('consent', 'default', ...)` scaffolding in place but granted, so a future regulatory change is a one-line switch. Add a plain privacy policy page.

## Acceptance criteria

- [ ] `robots.txt` does not disallow filter or sort URLs
- [ ] Screaming Frog crawl: zero 404s, zero redirect chains, canonicals self-reference except on filters
- [ ] Rich Results Test passes for Product, BreadcrumbList and Organization
- [ ] Product schema includes `shippingDetails` and `hasMerchantReturnPolicy`
- [ ] `AggregateRating` is absent on a product with no reviews, present with them
- [ ] The feed is publicly fetchable and validates in Merchant Center
- [ ] The feed contains one row per variation with a shared `item_group_id`, and `identifier_exists: no`
- [ ] `google_product_category` differs correctly between a bedsheet, a pillow and a blanket
- [ ] Feed IDs match Pixel `content_ids` exactly
- [ ] The CAPI phone hash is built from `923001234567`, verified against Meta's own test tool
- [ ] Open Graph tags render with the right image, type and price on a product page
- [ ] Exactly one `h1` per page across homepage, category and product
- [ ] GA4 DebugView shows every event once with correct PKR values
- [ ] Meta Pixel Helper shows every event once
- [ ] Meta Events Manager shows one Purchase, browser and server deduplicated by `event_id`
- [ ] Refreshing the thank-you page three times produces one purchase event, not four
- [ ] Analytics scripts do not appear in the LCP critical path on a content page
- [ ] Sitemap contains products, categories and pages, and excludes filtered and paginated URLs

## Stop

No performance tuning pass yet, no security pass. Next phases.
