# Operational Runbook — HVAC site build Companion to https://fc-build-spec-a5aaf279.vercel.app/ (architecture) and /annex.md (build spec). This file holds the procedures that are deployment and maintenance concerns, not architecture. Fortitude Creative. Last updated 2026-09-25. Everything here has a named owner. A runbook item with no owner is a wish. --- ## 1. Non-production mirrors must never be indexable Any demo, staging, client-review, or legacy mirror of a site ships with `X-Robots-Tag: noindex` at the edge **and** `` in the head. HTTP auth in front of it is better still, and is required for anything carrying real client data. Two headers rather than one because they fail differently: the meta tag is lost the moment a page is served as a non-HTML response or fetched by something that does not parse HTML, and the header survives that. The header can be dropped by a proxy or a platform migration, and the meta tag survives that. **Owner:** whoever stands the mirror up, at the moment they stand it up. Not a later cleanup pass. **Known live example:** the v2 wireframe mirror on surge.sh is a public, indexable copy of superseded content. Decide per mirror: noindex it, or take it down. Leaving a stale version reachable is how a client's "old site" outranks their new one. --- ## 2. Quarterly schema drift review Google deprecates structured data types on its own schedule, and the deprecation is announced in developer documentation, never in your build output. Markup that quietly stopped earning anything keeps validating, so nothing fails and nobody notices. **Cadence:** quarterly. **Owner:** SEO Lead. **Scope:** every type emitted by the entity graph in annex section C, plus `FAQPage`, `HowTo`, `Speakable`, and `Article`. **Escalation:** a deprecation that affects an emitted type opens a ticket against the schema generator the same week. Removing dead markup is cheap; leaving it is a slow-growing mismatch between what the site claims and what the guidelines say. This is the review that catches the next `HowTo`, which is exactly the trap already corrected once in annex section C. --- ## 3. llms.txt regeneration triggers Regenerate `/llms.txt` and `/llms-full.txt` on: - Publish of any new ServicePillar. - Publish of any new LocationHub. - Any change to the tier structure or slug conventions. On a CMS-driven production site this belongs in the publish workflow for those two content types, not in a human checklist. The build lints for staleness and warns; it does not fail (annex section F). **Owner:** SEO Lead owns the content of the files; the publish hook owns the regeneration. --- ## 4. AuthorityClaims audit trail CMS version history is the audit trail. No spec-level change is needed, but two habits are: - A compliance audit pulls version history to show what was published when. Confirm the CMS retains version history for the singleton indefinitely, not on a 90-day window, before relying on this. - License renewals update `expires` on renewal day, not when the reminder fires. The nightly job flags anything inside 60 days; publishing with an expired license fails the build (annex K.1). **Owner:** Compliance Lead. --- ## 5. Per-state license regex map The format validation map lives in code, deliberately (annex K.1). Adding a state means a DevOps change. - **Compliance Lead** owns the contents of the map and files the request. - **DevOps** ships it. - An unmapped state does **not** block a launch. The license saves flagged *unverified format* and goes to manual review, because a market waiting on a regex ticket is a worse failure than a typo. --- ## 6. Approved models list `approvedModels[]` gates every equipment page (annex M.2). Each entry records `brand`, `model`, `monthlySearchVolume`, `dataSource`, and `lastUpdated`. - **Cadence:** quarterly review. Entries older than two quarters are re-verified or removed. - **Owner:** SEO Lead. - Caps are 20 model pages total and 5 per brand. The caps are the point: an uncapped model tier is a thin-content generator with a spec sheet on it. - A model page whose Project reference has not arrived within its 90-day exemption unpublishes itself. That is by design; re-publish it when a truck has actually installed one. --- ## 7. Cache rules for time-sensitive content Pages carrying an active `OfferBadgeSet`, and any page carrying a `FinancingBlock`, emit `Cache-Control: max-age=3600`. The reason is not performance, it is liability: an expired promo or a stale financing line sitting in an edge cache is a offer the business is no longer making, still being served. One hour is the longest that should outlive its expiry. --- ## 8. Conversion debt escalation Conversion modules that ship empty and stay empty are the predictable failure here. The chain: ```yaml conversionDebtEscalation: scope: - OfferBadgeSet empty > 90 days - FinancingBlock missing where serviceType includes installation or replacement - LocalManagerEndorsement missing on a LocationHub - wordCountBelowFloor > 30 days tier1: slack_warn to #seo-alerts tier2: page marketing-ops lead tier3: recommend soft_unlink from global nav (90 days, named approval required) ``` **Monitoring alerts:** - Review aggregate: alert `#seo-alerts` when any location's GBP cache is more than 7 days stale. - Booking widget: alert when the failure rate passes 1% for a location, from GA4 error events. Tier 3 is a recommendation requiring approval, not an automatic action, for the reason given in annex M.5: withdrawing internal links makes a page harder to find, which makes the underlying problem harder to fix. --- ## 9. Service levels and on-call **Marketing-ops escalation SLA.** Acknowledge an alert within 2 business days. Resolve it, or document a deferral with a new target date, within 10 business days. A breach auto-escalates to the head of marketing operations. **Booking widget SLO.** 99.5% availability, measured monthly from GA4 `booking_iframe_error` events. | Severity | Trigger | Response | |---|---|---| | P2 | error rate above 0.5% in a rolling hour | page engineering on-call | | P1 | error rate above 2%, or total outage | major incident response | **PagerDuty routing:** `marketing_ops_30day` escalations to the marketing-ops schedule; `booking_widget` alerts to engineering-primary. **Cache invalidation.** An update to a FinancingBlock or an OfferBadgeSet triggers an edge purge for the affected URLs. Name the mechanism per client: the directive says `fastly-purge`, which is right only if that client is on Fastly. Most of ours are not, so the runbook entry for each site records its actual purge call. --- ## 10. Pre-launch verification, per city Run before activating any city: - 3 or more Featured Local Projects exist for that market (annex section G). - The local manager endorsement is in place on the LocationHub (annex N.3). - `contentTier` is set for the market; tier3 is the default, not an oversight. - No license expires within 60 days. - The nightly orphan report is clean for the new pages. - Every new intersection page clears the no-JS render gate (annex K.6). **Owner:** SEO Lead, with the content coordinator confirming project count.