Skip to main content

opengraph-linter

A pluggable CLI meta-tag inspector for Open Graph and X card tags — a terminal take on opengraph.xyz.

Install

uv tool install opengraph-linter             # core: reports without previews
uv tool install "opengraph-linter[preview]"  # + inline og:image previews

The inline preview needs the optional preview extra (textual-image), and is off by default. Turn it on with --preview or with preview = true in the config. Without the extra, the report still runs every rule and prints a one-line hint instead of the picture.

Usage

# straight from a checkout, no install
uv run src/opengraph_linter https://example.com/

# the installed console script; several URLs work in one run
uv run opengraph-linter https://example.com/ https://example.org/

# equivalents
uv run python -m opengraph_linter https://example.com/
uv run scripts/og_preview.py https://example.com/

Options:

Flag Effect
--preview / --no-preview Force the inline og:image preview on or off; overrides the preview config (off by default, needs the preview extra)
--width Image width in terminal cells (default: half the panel width)
--skip-image-checks Do not download og:image
--list-rules List every registered rule and exit
--only X / --disable X Run or skip a rule by id or lint code, repeatable
--no-plugins Ignore third-party plugin entry points
--json Emit findings as JSON
--strict Exit non-zero on warnings as well as criticals
--timeout Request timeout in seconds (default 15)

Exit codes: 0 clean, 1 critical findings (warnings too with --strict), 2 a page could not be fetched or an option was invalid. A run with several URLs prints one panel per URL and a final overview table.

--json emits a list with one {url, findings} object per URL.

Configuration

Every threshold lives on the Settings model. Override any field from a [tool.opengraph-linter] table in the pyproject.toml of the directory you run from:

[tool.opengraph-linter]
og_title_max = 70
max_image_bytes = 8388608
# render og:image inline (needs the `preview` extra, off by default);
# --preview/--no-preview wins
preview = true
# rules to skip on every run, by lint code or rule id
ignore = ["OG004", "image-conversion-text"]

When pyproject.toml has no [tool.opengraph-linter] table, the linter falls back on an opengraph-linter.toml file in the same directory. Its keys sit at the top level:

# opengraph-linter.toml
og_title_max = 70
ignore = ["OG004", "image-conversion-text"]

The ignore list is merged with any --disable flags. Unknown settings, out-of-range values, and unknown rule codes or ids exit with code 2.

Rules

Every data point is a Rule — a lint code, an id, a tag, and a check(context) -> Finding. Twenty-three ship built in. Each reports critical, warning, or success; the "Fails as" column shows the severity when the check does not pass. Ignore any of them by code or id with --disable or the ignore list.

Open Graph (OG)

Code Rule id Tag Fails as Checks
OG001 og-title-present og:title critical The tag is set; platforms fall back to the page title or the URL without it
OG002 og-title-length og:title warning The title fits the 60-character target (og_title_max)
OG003 og-description-length og:description critical / warning The tag is set (critical) and fits a preview card (og_description_max, warning)
OG004 og-site-name og:site_name warning The tag is set; Discord and others show the bare domain without it
OG005 og-url og:url warning The canonical URL is set and absolute, so shares consolidate on one URL
OG006 og-type og:type warning The object type is declared; platforms assume website without it
OG007 og-image-alt og:image:alt warning Alt text is set for the image; the spec asks for it and screen readers announce it
OG008 og-image-dimensions og:image:width warning Width and height are declared (and match the file), so the first share renders before download
OG009 og-image-secure-url og:image:secure_url warning The image has an HTTPS URL, directly or via og:image:secure_url
OG010 og-type-valid og:type warning The value is a global type from the spec, or a namespaced custom type with a colon
OG011 og-locale-format og:locale warning The locale, when set, matches language_TERRITORY (e.g. en_US)
OG012 og-determiner og:determiner warning The determiner, when set, is one of a, an, the, "", auto
OG013 og-vertical-properties og:type warning An article, book, profile, music.*, or video.* page carries its vertical's recommended properties

X / Twitter cards (TW)

Code Rule id Tag Fails as Checks
TW001 twitter-card-type twitter:card warning The card type is summary_large_image (preferred_card) for a full-width preview
TW002 twitter-title twitter:title warning A title is available (falls back to og:title) and fits twitter_title_max
TW003 twitter-description-length twitter:description warning A description is available (falls back to og:description) and fits twitter_description_max
TW004 twitter-image twitter:image critical An image is available for the card (falls back to og:image)

The og:image asset (IMG)

Code Rule id Tag Fails as Checks
IMG001 image-loads og:image critical The tag is set and the file downloads cleanly
IMG002 image-aspect-ratio og:image critical The image is close to target_ratio (1.91:1, 1200×630) within ratio_tolerance
IMG003 image-file-size og:image warning The file is under max_image_bytes (5 MB); platforms reject larger files
IMG004 image-conversion-text og:image warning The image reads as a designed card with a headline or CTA (see below)

Plain SEO tags (SEO)

Code Rule id Tag Fails as Checks
SEO001 page-title-present title critical The document has a <title>
SEO002 meta-description-length description warning A meta description is set and fits a Google snippet (meta_description_max)

The IMG rules that need the downloaded file (IMG002–IMG004) report a warning when the image was not fetched, for example with --skip-image-checks. Run opengraph-linter --list-rules for the same table in the terminal.

The conversion-text rule is a heuristic, not OCR: it measures how much of the image is covered by its dominant flat colors, which separates designed cards from photographs and auto-generated video thumbnails. Every threshold lives on the Settings model in src/opengraph_linter/models.py.

Plugins

Rules are registered through pluggy, so a separate package can add or replace them:

from opengraph_linter import Rule, hookimpl


class FacebookAppId(Rule):
    id = "fb-app-id"
    code = "FB001"  # pick an unused prefix for your plugin
    tag = "fb:app_id"
    description = "fb:app_id is set for Facebook insights"
    order = 35

    def check(self, context):
        value = context.page.get("fb:app_id")
        if not value:
            return self.warn("Facebook app id is missing", "Set fb:app_id.")
        return self.ok("Facebook app id is set", value)


@hookimpl
def opengraph_rules():
    return [FacebookAppId()]

Expose it as an opengraph_linter entry point and it loads automatically:

[project.entry-points.opengraph_linter]
my-rules = "my_package.rules"

Two hooks are available:

  • opengraph_rules() — return rules to add. Reusing a built-in id replaces it.
  • opengraph_finding(finding, context) — return a Finding to override one, or None to leave it alone.

Rules are plain classes; everything they consume and emit — Page, ImageInfo, Settings, Context, Finding — is a pydantic model.

Metadata

Release files for opengraph-linter 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for opengraph-linter 0.1.1
File Size Uploaded
opengraph_linter-0.1.1.tar.gz 20.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for opengraph-linter 0.1.1
File Interpreter ABI Platform
opengraph_linter-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 48.6 kB

Release files / opengraph_linter-0.1.1.tar.gz

Download URL opengraph_linter-0.1.1.tar.gz
Size 20.8 kB
Tags Source
SHA-256 checksum
How to use checksums
7884a516e86a1cae3c11f080622ccddea6a4089f53201f333331c53a987a084e
BLAKE2b-256 checksum
How to use checksums
7a8f30aed34087d25ac8889967aabd1914b7dec1621002222bc200465180990f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release files / opengraph_linter-0.1.1-py3-none-any.whl

Download URL opengraph_linter-0.1.1-py3-none-any.whl
Size 27.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c92a2e3563b399f76193e7ac935dad330c09d1c9f6dde81fdbfd45e4765f96de
BLAKE2b-256 checksum
How to use checksums
940c38e112b86a4735c981b55769fa042f1937b939c2b4eb2794c4020c4ed132
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page