{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "x:cards:1.0",
  "title": "X (Twitter) Cards markup",
  "description": "The `twitter:*` meta tags that turn a shared link into a card on X. The companion to `ogp:article:1.0`: same `<meta>` channel, and the pair is normally shipped together, which is why a site with Open Graph tags and no card tags is the usual half-done state. Only `twitter:card` is required. `twitter:title`, `twitter:description` and `twitter:image` fall back to their `og:` equivalents, so requiring them here would fail pages that are correct — but `twitter:card` has no fallback, and without it X picks no card type at all. Like Open Graph, this checks something no build tool checks: nothing fails when the tags are wrong, the link just renders badly everywhere it is shared. In HTML these sit on `<meta name=\"…\">`; in front matter they are ordinary quoted keys. See https://developer.x.com/en/docs/x-for-websites/cards/overview/markup",
  "type": "object",
  "required": ["twitter:card"],
  "additionalProperties": true,
  "$defs": {
    "handle": {
      "type": "string",
      "pattern": "^@?[A-Za-z0-9_]{1,15}$",
      "description": "An X handle, with or without the leading `@`. Fifteen characters at most, letters, digits and underscores only — the pattern rejects an email address or a profile URL, which is how this field is usually written wrong."
    },
    "accountId": {
      "type": ["string", "integer"],
      "pattern": "^[0-9]+$",
      "description": "A numeric account id. Typed for both channels: from HTML every attribute is a string, while front matter can carry a real number."
    },
    "httpsUrl": {
      "type": "string",
      "format": "uri",
      "pattern": "^https://",
      "description": "An absolute HTTPS URL. X does not load card media over plain HTTP."
    },
    "pixels": {
      "type": ["string", "integer"],
      "pattern": "^(?!0+$)[0-9]+$",
      "minimum": 1,
      "description": "A pixel count, at least 1. Typed for both channels: an HTML `content=` attribute is always a string, while front matter can carry a real number. Both branches enforce the same floor — `pattern` is ignored on a number and `minimum` on a string, so each needs its own spelling of \"not zero\", or the string form silently accepts a `0` the number form rejects."
    }
  },
  "properties": {
    "twitter:card": {
      "type": "string",
      "enum": ["summary", "summary_large_image", "app", "player"],
      "description": "The card type. Required, and the one tag with no `og:` fallback — omit it and no card renders at all. Enumerated because X defines exactly these four and ignores anything else."
    },
    "twitter:site": {
      "$ref": "#/$defs/handle",
      "description": "The handle of the site the card belongs to."
    },
    "twitter:site:id": {
      "$ref": "#/$defs/accountId",
      "description": "The numeric id of the site's account, an alternative to `twitter:site`."
    },
    "twitter:creator": {
      "$ref": "#/$defs/handle",
      "description": "The handle of the content's author."
    },
    "twitter:creator:id": {
      "$ref": "#/$defs/accountId",
      "description": "The numeric id of the author's account."
    },
    "twitter:title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 70,
      "description": "The card's title. Truncated past 70 characters, so the length is a real limit rather than a style rule. Falls back to `og:title`."
    },
    "twitter:description": {
      "type": "string",
      "maxLength": 200,
      "description": "The card's description. Truncated past 200 characters. Falls back to `og:description`."
    },
    "twitter:image": {
      "$ref": "#/$defs/httpsUrl",
      "description": "The card's image. Must be HTTPS — an HTTP image is dropped and the card renders without it. Falls back to `og:image`."
    },
    "twitter:image:alt": {
      "type": "string",
      "maxLength": 420,
      "description": "Alternative text for the image, for people using a screen reader. 420 characters at most."
    },
    "twitter:player": {
      "$ref": "#/$defs/httpsUrl",
      "description": "The HTTPS URL of the iframe player. Player cards only."
    },
    "twitter:player:width": {
      "$ref": "#/$defs/pixels",
      "description": "The player's width in pixels."
    },
    "twitter:player:height": {
      "$ref": "#/$defs/pixels",
      "description": "The player's height in pixels."
    },
    "twitter:player:stream": {
      "$ref": "#/$defs/httpsUrl",
      "description": "The URL of a raw video stream, as an alternative to an iframe player."
    },
    "twitter:app:country": {
      "type": "string",
      "pattern": "^[A-Z]{2}$",
      "description": "The two-letter ISO 3166-1 alpha-2 country code for the app store to use. Uppercase."
    },
    "twitter:app:name:iphone": { "type": "string", "minLength": 1 },
    "twitter:app:id:iphone": { "$ref": "#/$defs/accountId" },
    "twitter:app:url:iphone": { "type": "string", "format": "uri" },
    "twitter:app:name:ipad": { "type": "string", "minLength": 1 },
    "twitter:app:id:ipad": { "$ref": "#/$defs/accountId" },
    "twitter:app:url:ipad": { "type": "string", "format": "uri" },
    "twitter:app:name:googleplay": { "type": "string", "minLength": 1 },
    "twitter:app:id:googleplay": { "type": "string", "minLength": 1 },
    "twitter:app:url:googleplay": { "type": "string", "format": "uri" }
  },
  "if": {
    "properties": { "twitter:card": { "const": "player" } },
    "required": ["twitter:card"]
  },
  "then": {
    "required": [
      "twitter:title",
      "twitter:image",
      "twitter:player",
      "twitter:player:width",
      "twitter:player:height"
    ],
    "description": "A player card is the one type with its own required set: without the player URL and its dimensions there is nothing to embed, and X falls back to no card rather than to a summary."
  }
}
