app.zeens.postMeta

zeens.app

{
  "$type": "com.atproto.lexicon.schema",
  "defs": {
    "borderCrop": {
      "description": "Per-edge crop fractions (0–1) to clip a decorative border or letterbox in grid views. Stored as integers scaled by 1e7 because AT Protocol does not support floats. Divide by 1e7 to get the fraction.",
      "properties": {
        "bottom": {
          "maximum": 5000000,
          "minimum": 0,
          "type": "integer"
        },
        "left": {
          "maximum": 5000000,
          "minimum": 0,
          "type": "integer"
        },
        "right": {
          "maximum": 5000000,
          "minimum": 0,
          "type": "integer"
        },
        "top": {
          "maximum": 5000000,
          "minimum": 0,
          "type": "integer"
        }
      },
      "type": "object"
    },
    "exifData": {
      "description": "Subset of EXIF metadata relevant to photography.",
      "properties": {
        "aperture": {
          "description": "Aperture as a human-readable string (e.g. f/1.8).",
          "maxLength": 32,
          "type": "string"
        },
        "focalLength": {
          "description": "Focal length as a human-readable string (e.g. 85mm).",
          "maxLength": 32,
          "type": "string"
        },
        "iso": {
          "description": "ISO sensitivity value.",
          "maximum": 1638400,
          "minimum": 1,
          "type": "integer"
        },
        "lens": {
          "description": "Lens model (e.g. FE 85mm F1.4 GM).",
          "maxLength": 256,
          "type": "string"
        },
        "lensMake": {
          "description": "Lens manufacturer from EXIF LensMake (e.g. Tamron, Sigma). Often absent — many bodies omit it for third-party lenses.",
          "maxLength": 128,
          "type": "string"
        },
        "make": {
          "description": "Camera manufacturer (e.g. Sony, Canon, Nikon).",
          "maxLength": 128,
          "type": "string"
        },
        "model": {
          "description": "Camera model (e.g. A7R V, EOS R5).",
          "maxLength": 128,
          "type": "string"
        },
        "shutter": {
          "description": "Shutter speed as a human-readable string (e.g. 1/250s).",
          "maxLength": 32,
          "type": "string"
        },
        "software": {
          "description": "Software used to process the image (e.g. Lightroom, Capture One).",
          "maxLength": 128,
          "type": "string"
        },
        "takenAt": {
          "description": "Date and time the photo was taken, from EXIF DateTimeOriginal.",
          "format": "datetime",
          "type": "string"
        }
      },
      "type": "object"
    },
    "externalLink": {
      "description": "An external URL with an optional display label.",
      "properties": {
        "label": {
          "maxLength": 64,
          "type": "string"
        },
        "url": {
          "format": "uri",
          "maxLength": 2048,
          "type": "string"
        }
      },
      "required": [
        "url"
      ],
      "type": "object"
    },
    "filmData": {
      "description": "Film/analog photography metadata supplied by the creator. EXIF cannot capture this for film photos, and scanner EXIF is unreliable, so this is captured via the upload UI when the post is marked as film.",
      "properties": {
        "aperture": {
          "description": "Aperture as the creator wrote it (e.g. f/8, 8, f/1.4). Stored verbatim; renderers normalise the display.",
          "maxLength": 32,
          "type": "string"
        },
        "camera": {
          "description": "Film body the frame was shot on, typed by the creator (e.g. Nikon FM2, Pentax 67). Deliberately NOT exif.model: a film scan's EXIF describes the scanner, so this is the only trustworthy record of the camera. Free text, since no catalog of film bodies is complete.",
          "maxLength": 128,
          "type": "string"
        },
        "lens": {
          "description": "Lens the frame was shot on, typed by the creator (e.g. 50mm f/1.8 AI-S). Free text, for the same reason as `camera`.",
          "maxLength": 256,
          "type": "string"
        },
        "shutter": {
          "description": "Shutter speed as the creator wrote it (e.g. 1/250, 1/250s, 2s, B). Kept as a string rather than a number because film shutters are named in fractions and bulb, and because the value is transcribed from a dial, not measured.",
          "maxLength": 32,
          "type": "string"
        },
        "stockName": {
          "description": "Display name of the film stock at the time the post was created. Snapshotted so renames in the catalog don't change rendered post pages.",
          "maxLength": 128,
          "type": "string"
        },
        "stockSlug": {
          "description": "Canonical slug of the film stock from the Zeens catalog (e.g. portra-400). Used for joining to film_stocks for icon + filtering.",
          "maxLength": 64,
          "type": "string"
        }
      },
      "type": "object"
    },
    "imageMeta": {
      "description": "Per-image extended metadata. All fields optional.",
      "properties": {
        "borderCrop": {
          "description": "Confirmed-by-user border crop, applied in grid views only.",
          "ref": "#borderCrop",
          "type": "ref"
        },
        "exif": {
          "description": "EXIF for THIS image, by embed position. Multi-image posts can mix cameras/lenses; the post page shows the entry matching the visible carousel image. The record-level exif field holds the first image's data for backward compatibility (search indexing, older clients).",
          "ref": "#exifData",
          "type": "ref"
        },
        "location": {
          "description": "Free-form location name for THIS image, by embed position, same shape as the record-level `location`. Present only when the creator placed the photos of a post separately. The record-level location fields then hold the first located image's place, so feeds, search and the map keep working for readers that know nothing of per-image locations.",
          "maxLength": 256,
          "type": "string"
        },
        "locationLat": {
          "description": "Latitude of THIS image in WGS84, scaled by 1e7, as the record-level `locationLat`.",
          "maximum": 900000000,
          "minimum": -900000000,
          "type": "integer"
        },
        "locationLng": {
          "description": "Longitude of THIS image in WGS84, scaled by 1e7, as the record-level `locationLng`.",
          "maximum": 1800000000,
          "minimum": -1800000000,
          "type": "integer"
        },
        "monochrome": {
          "description": "True if THIS image is black-and-white. Carried per image because a post can mix a colour frame with a monochrome one, and a monochrome image's palette is deliberately ABSENT rather than a ramp of greys: on a B&W frame every histogram cell is grey and the swatches would describe sensor noise.",
          "type": "boolean"
        },
        "palette": {
          "description": "Palette for THIS image, by embed position, same shape as the record-level `palette`. Photos in one post rarely share a palette, so the post page shows the entry matching the visible carousel image. The record-level field holds the COVER image's colours for backward compatibility (feed cards, search, older clients).",
          "items": {
            "maxLength": 7,
            "type": "string"
          },
          "maxLength": 8,
          "type": "array"
        },
        "paletteShares": {
          "description": "Coverage of each colour in THIS image's `palette`, in basis points, positionally aligned with it. Same shape as the record-level `paletteShares`.",
          "items": {
            "maximum": 10000,
            "minimum": 0,
            "type": "integer"
          },
          "maxLength": 8,
          "type": "array"
        }
      },
      "type": "object"
    },
    "main": {
      "description": "Extended metadata for a Zeens photo post — EXIF, categories, license, and optional external link. The rkey matches the rkey of the associated app.bsky.feed.post record.",
      "key": "tid",
      "record": {
        "properties": {
          "aiDeclaration": {
            "description": "Creator's declaration about AI involvement in this image.",
            "knownValues": [
              "no-ai",
              "ai-assisted",
              "contains-ai"
            ],
            "type": "string"
          },
          "categories": {
            "description": "Photography categories selected by the creator: up to 3 per photo in the post, so 12 for a four-photo post.",
            "items": {
              "maxLength": 64,
              "type": "string"
            },
            "maxLength": 12,
            "type": "array"
          },
          "colorHexes": {
            "description": "Deprecated — use `palette` instead. Was intended to hold the colours behind the hue tags in `colors`, positionally aligned, but was never written by any client: every record in the wild lacks it. `palette` supersedes it and is not tag-aligned.",
            "items": {
              "maxLength": 7,
              "type": "string"
            },
            "maxLength": 3,
            "type": "array"
          },
          "colors": {
            "description": "Color tags detected on the cover image at upload: up to 3 prominent hues (red/orange/yellow/green/teal/blue/purple/pink), ranked most prominent first and de-duplicated so adjacent hues do not repeat, followed by up to 2 tone tags (warm/cool/earth/pastel). Empty when monochrome. A dark or muted photo still carries its hues, and its temperature. Used by user-built feeds and search to filter posts by color.",
            "items": {
              "maxLength": 32,
              "type": "string"
            },
            "maxLength": 5,
            "type": "array"
          },
          "createdAt": {
            "description": "Timestamp when this metadata record was created.",
            "format": "datetime",
            "type": "string"
          },
          "crossLink": {
            "description": "True if the creator enabled 'Cross-link to Bluesky' at upload. When set, a link to the canonical Zeens post URL was appended to the app.bsky.feed.post text, and the Zeens post page shows a 'View on Bluesky' pill (and hides the appended self-link in the rendered caption).",
            "type": "boolean"
          },
          "editCount": {
            "description": "Number of times the creator has edited this post via Zeens. Editing is capped (currently 3 edits) to discourage rewriting published photos; the edit button disables once this reaches the cap.",
            "minimum": 0,
            "type": "integer"
          },
          "exif": {
            "description": "Camera EXIF metadata extracted from the original image.",
            "ref": "#exifData",
            "type": "ref"
          },
          "film": {
            "description": "Film/analog metadata. Present when the post is marked with app.zeens.film on the underlying app.bsky.feed.post record. EXIF and film blocks can coexist.",
            "ref": "#filmData",
            "type": "ref"
          },
          "hasBorder": {
            "description": "Deprecated — use images[].borderCrop instead.",
            "type": "boolean"
          },
          "images": {
            "description": "Per-image extended metadata, indexed by position in the post's app.bsky.embed.images.images array.",
            "items": {
              "ref": "#imageMeta",
              "type": "ref"
            },
            "maxLength": 4,
            "type": "array"
          },
          "license": {
            "description": "Creative Commons or rights license for the photo.",
            "knownValues": [
              "all-rights-reserved",
              "cc-by",
              "cc-by-sa",
              "cc-by-nc",
              "cc-by-nc-sa",
              "cc-by-nd",
              "cc-by-nc-nd",
              "cc0"
            ],
            "type": "string"
          },
          "link": {
            "description": "Optional external link attached by the creator (e.g. portfolio, print shop).",
            "ref": "#externalLink",
            "type": "ref"
          },
          "location": {
            "description": "Optional free-form location name where the photo was taken.",
            "maxLength": 256,
            "type": "string"
          },
          "locationLat": {
            "description": "Latitude in WGS84, scaled by 1e7 (AT Protocol does not support floats). Divide by 1e7 to get decimal degrees.",
            "maximum": 900000000,
            "minimum": -900000000,
            "type": "integer"
          },
          "locationLng": {
            "description": "Longitude in WGS84, scaled by 1e7 (AT Protocol does not support floats). Divide by 1e7 to get decimal degrees.",
            "maximum": 1800000000,
            "minimum": -1800000000,
            "type": "integer"
          },
          "monochrome": {
            "description": "True if the cover image is black-and-white / grayscale. Detected client-side at upload by averaging HSV saturation across a downscaled bitmap (threshold 0.05). Powers the Black & White feed.",
            "type": "boolean"
          },
          "noAiTraining": {
            "description": "Deprecated — use aiDeclaration instead. True if creator chose 'no-ai'.",
            "type": "boolean"
          },
          "palette": {
            "description": "The cover image's actual colours, as #rrggbb, most prominent first. Up to eight: the largest distinct colours, plus accents — small but vivid regions a viewer notices first, which selection by area alone never surfaces. Displayed as unnamed swatches; `colors` remains what search filters on. Empty when monochrome. Absent on records written before palette extraction, which fall back to the hue buckets' stand-in colours.",
            "items": {
              "maxLength": 7,
              "type": "string"
            },
            "maxLength": 8,
            "type": "array"
          },
          "paletteShares": {
            "description": "How much of the cover image each `palette` colour covers, in basis points (1/100 of a percent), positionally aligned with `palette`. Integers because AT Protocol rejects floats; basis points rather than percent so a small accent does not round to 0. Lets a renderer size swatches by prominence. Absent on records written before it existed, which render every swatch at equal weight.",
            "items": {
              "maximum": 10000,
              "minimum": 0,
              "type": "integer"
            },
            "maxLength": 8,
            "type": "array"
          },
          "post": {
            "description": "AT-URI of the app.bsky.feed.post record this metadata belongs to.",
            "format": "at-uri",
            "type": "string"
          }
        },
        "required": [
          "post",
          "createdAt"
        ],
        "type": "object"
      },
      "type": "record"
    }
  },
  "id": "app.zeens.postMeta",
  "lexicon": 1
}

Validate Record

Validate a record against app.zeens.postMeta

Validation Options
Treat any remaining unresolved references as valid

Metadata

DID
did:plc:pwpuqwge6g7xrjmfbw73uycw
CID
bafyreiaqqneh4unnefelpmhxsslmkthjcajsovutv3wakytdovpuybz2cm
Indexed At
2026-10-22 16:31 UTC
AT-URI
at://did:plc:pwpuqwge6g7xrjmfbw73uycw/com.atproto.lexicon.schema/app.zeens.postMeta

Similar Lexicons

Lexicons whose schemas describe something close to this one. Check them before publishing a new one that overlaps.

  • EXIF metadata for a photo. Stored as a sidecar record using the post rkey with an image index suffix (e.g. '3lqbc5xyz-0'). Float values are stored as strings to avoid precision issues.

  • A Zeens-only photo post. Structurally a subset of app.bsky.feed.post, but written to a Zeens-owned collection so it is NOT indexed by the Bluesky AppView and never appears on Bluesky. Used when the creator disables 'Pos…

  • A documented photo of a graffiti piece.

  • ATPix photo record. Metadata mapped to Dublin Core and Schema.org ImageObject terms (see docs/lexicon/net.atpix.gallery.md).

  • A static image post. Supports single or multiple images (up to 9).

Lexicon Garden

@