OGKit

Put Open Graph in CI: a regression gate that actually fails

  • open-graph
  • CI
  • lint
  • regression

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:

  1. An installable CLI or script
  2. Deterministic exit codes (pass / findings / tool failure)
  3. 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-url makes 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: 0 pass, 1 findings, 2 tool 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:

  1. 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.

  2. Baseline known debt

    npx --yes @og-kit/cli check ./out \
      --base-url https://example.com \
      --baseline .ogkit-baseline.json \
      --update-baseline
    

    Commit the file. Matching target + ruleId (optional message pin) no longer fails exit 1; new regressions still fail.

  3. Disable rules, not the whole job
    --disable title-truncation-risk,og-title-matches-title
    or set rules / failOn / baseline in ogkit.config.* (merged with CLI flags).

  4. Do not use permanent continue-on-error as 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 check on build output or critical public URLs
  • --fail-on error (or config equivalent) fails on missing og:image and 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

  1. What Open Graph actually solves — full workflow map (CI is step five)
  2. Why your Open Graph card is blank — five-minute triage
  3. Required meta, image engineering, cache / rescrape — neighboring posts in the series
  4. 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.