Put Open Graph in CI: a regression gate that actually fails
Three weeks after a redesign merges, marketing shares the new product URL on LinkedIn. The card has no image; the title falls back to whatever <title> the layout still emits. Git blame shows og:image died in a "dedupe head tags" cleanup. The PR passed Lighthouse and visual regression. Nobody inspected the raw HTML meta.
That is not a problem you solve with one more share-preview screenshot. Open Graph is a classic break it and nobody notices surface: humans see a fine page; crawlers see the card contract.
Incident write-up template (paste into the next postmortem)
Fill these five boxes instead of writing only "OG broke":
| Box | What to capture | Example |
|---|---|---|
| Symptom | Platform + card shape | LinkedIn no image; Slack title from the wrong page |
| First seen | Who, when, channel | Ops on ship day; customer screenshot |
| Introducing commit | Deleted tags / template / render path | Head refactor, CMS defaults, SPA shell replacing SSR |
| Why the gate missed it | No check / browser-only / preview-only | No OG step in CI; extension only on localhost |
| Fix + prevention | Restore meta + which automation | Restore tags; og-kit check on out/ in CI |
The "why the gate missed it" box forces process debt into the open: the rules usually already exist — stable, fail-able automation does not.
Why the browser extension cannot replace CI
Extensions shine in development and private networks: localhost, auth walls, staging, rescrape helpers. They run in your session and can see the real DOM.
GitHub Actions and other CI runners do not have your extension. A check that runs headless must be:
- An installable CLI or script
- Deterministic exit codes (pass / findings / tool failure)
- Machine-readable output (JSON / SARIF / JUnit) so PR annotations and Code Scanning can consume it
Treat OG like ESLint: editor plugins improve the day-to-day feel; the repo gate stops someone else from merging a regression. Same story as Lighthouse — a local score feels great until the PR never fails.
Minimum gate (ship these three layers first)
You do not need all 32 rules on day one. For regression prevention, these pay rent immediately:
1. Required fields still present
| Rule id | Severity | Meaning |
|---|---|---|
og-title-missing |
error | No og:title |
og-type-missing |
error | No og:type |
og-url-missing |
error | No og:url |
og-image-missing |
error | No og:image |
In practice also watch og-description-missing and twitter-card-missing (both warn). With default --fail-on error, warns do not fail the PR but still show up in the report.
2. The image is actually fetchable by crawlers
| Rule id | Severity | Common failure |
|---|---|---|
og-image-relative-url |
error | Relative path; crawlers often drop it |
og-image-not-https |
error | Non-HTTPS |
og-image-forbidden |
error | Auth-walled CDN / 403 |
og-image-content-type-mismatch |
error | Content-Type disagrees with bytes |
When you scan static HTML only, some network image rules need reachable public assets. Against a pure build-output tree, at least fail hard on missing tags and relative paths.
3. Size / bytes that blow up IM clients (warn first)
| Rule id | Severity | Note |
|---|---|---|
og-image-bytes-exceeds-platform-budget |
warn | Over a platform byte budget |
og-image-dimensions-too-small |
warn | Below the usual 1200×630 recommendation |
Byte numbers differ by platform; many are community measurements rather than a single official promise — rule docs mark provenance. In CI, errors for required fields, warns for budgets is usually the sane default.
Stable rule ids are public API: docs URLs, --disable / --only, and baseline entries all hang off the id. Renaming breaks gates — same discipline as ESLint rule ids.
Drop-in example: conceptual GitHub Actions
The published entry point is npx --yes @og-kit/cli (binary name og-kit). It uses the same rules and report schema as the site scanner.
# .github/workflows/og-check.yml — conceptual; change paths to your build output
name: Open Graph gate
on:
pull_request:
push:
branches: [main]
jobs:
og:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
# Your site build: Next / Astro / any static export
- name: Build site
run: npm ci && npm run build
- name: Check Open Graph on build output
run: |
npx --yes @og-kit/cli check ./out \
--fail-on error \
--base-url https://example.com \
--json > og-report.json
# Adjust directory: out/ dist/ build/ .next/ …
# Exit codes: 0 pass · 1 findings at fail-on · 2 tool/config failure
- name: Upload report (optional)
if: always()
uses: actions/upload-artifact@v4
with:
name: og-report
path: og-report.json
Notes that matter:
- Build first, then scan the output (or scan public URLs you care about). Scanning TSX templates alone misses the HTML the framework actually emits.
--base-urlmakes relative resolution and page-identity rules meaningful; without it, rules that need a public identity are skipped and listed — not silently passed.- Human-readable output is not a stable machine API; automation should use
--json,--sarif, or--junit. - Exit-code contract:
0pass,1findings,2tool failure — so CI can separate "OG is broken" from "install blew up".
Local equivalents:
npx --yes @og-kit/cli check https://example.com
npx --yes @og-kit/cli check ./dist --base-url https://example.com --fail-on error
npx --yes @og-kit/cli --version
# Four lines: og-kit / engine / ruleset / report-schema (version contract in repo docs)
GitHub Action wrapper status (honest boundary)
The monorepo has an @og-kit/action package intended as a thin CLI wrapper. As of the current repository state it is still a stub / not a published marketplace Action — do not document it as a one-click official Action. The production path today is npx @og-kit/cli check … above. When a real Action ships with action.yml and version tags, you can switch the YAML to uses: without changing gate semantics (rule ids, exit codes, JSON).
Baselines and noise control
Turning every error on for a legacy site on day one often paints the PR red with historical debt. Adoption path:
Narrow the target
--only og-title-missing,og-type-missing,og-url-missing,og-image-missing,og-image-relative-url
or only critical templates:check ./out/pricing.html ./out/index.html.Baseline known debt
npx --yes @og-kit/cli check ./out \ --base-url https://example.com \ --baseline .ogkit-baseline.json \ --update-baselineCommit the file. Matching target + ruleId (optional message pin) no longer fails exit 1; new regressions still fail.
Disable rules, not the whole job
--disable title-truncation-risk,og-title-matches-title
or setrules/failOn/baselineinogkit.config.*(merged with CLI flags).Do not use permanent
continue-on-erroras a baseline
That silences regressions. A baseline keeps old debt visible and new debt red.
Monthly hygiene: peel entries out of the baseline as you fix tags, instead of only appending exemptions. Stable ids make it easy to write tickets like "disable og-image-bytes-exceeds-platform-budget until the image pipeline lands."
How this splits work with visual QA and preview tools
| Tool | Good at | Bad at |
|---|---|---|
| Design review / human eye | Brand, safe zone, contrast | Meta deleted after merge |
| Third-party / in-product preview | Approximate layout and truncation | Authoritative cache; headless CI |
| Browser extension | Localhost, auth, rescrape guidance | Actions runners |
| CLI gate | Stable rule ids, exit codes, failing PRs | Visual taste |
Third-party previews are approximate; authoritative cache invalidation still belongs to each platform's official debugger (covered earlier in the series). CI answers "are the tags and fetchability still true?", not "is LinkedIn still showing last week's image?" — that needs rescrape.
Checklist (wiki-ready)
- PR runs
og-kit checkon build output or critical public URLs -
--fail-on error(or config equivalent) fails on missingog:imageand friends - Use
--json/ SARIF / JUnit for artifacts or Code Scanning - Legacy debt uses baseline or
--only, not a disabled job - Docs and tickets cite stable ids (e.g.
og-image-missing) - Do not depend on an unpublished GitHub Action; path of record is
npx @og-kit/cli - Redesign / head-component PRs must hit that job
Run it once yourself
For a public URL, paste into the home scanner to see report shape; for CI use the CLI with the same rules:
npx --yes @og-kit/cli check https://your-domain/critical-page --fail-on error
Full catalog: rules reference. Platform differences and rescrape are out of scope here — a gate guarantees the contract fields and image reachability, not that every platform cache has refreshed.
What to read next
- What Open Graph actually solves — full workflow map (CI is step five)
- Why your Open Graph card is blank — five-minute triage
- Required meta, image engineering, cache / rescrape — neighboring posts in the series
- Localhost / staging blind spots — extension path when CI cannot reach the page
Three takeaways: visual QA is not a meta regression gate; the minimum gate is required fields + fetchable images + optional budgets with stable ids; ship npx @og-kit/cli check in Actions today and treat any Action wrapper as not-marketplace until it actually is.