app.zeens.postMeta

zeens.app

Documentation

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.

main record

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.

Record Key tid Timestamp-based ID

Properties

aiDeclaration string Optional

Creator's declaration about AI involvement in this image.

Known values: no-ai, ai-assisted, contains-ai
categories array of string Optional

Photography categories selected by the creator: up to 3 per photo in the post, so 12 for a four-photo post.

maxLength: 12 items
colorHexes array of string Optional

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.

maxLength: 3 items
colors array of string Optional

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.

maxLength: 5 items
createdAt string datetime Required

Timestamp when this metadata record was created.

editCount integer Optional

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
exif ref #exifData Optional

Camera EXIF metadata extracted from the original image.

film ref #filmData Optional

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.

hasBorder boolean Optional

Deprecated — use images[].borderCrop instead.

images array of ref #imageMeta Optional

Per-image extended metadata, indexed by position in the post's app.bsky.embed.images.images array.

maxLength: 4 items
license string Optional

Creative Commons or rights license for the photo.

Known values: all-rights-reserved, cc-by, cc-by-sa, cc-by-nc, cc-by-nc-sa, cc-by-nd, cc-by-nc-nd, cc0
location string Optional

Optional free-form location name where the photo was taken.

maxLength: 256 bytes
locationLat integer Optional

Latitude in WGS84, scaled by 1e7 (AT Protocol does not support floats). Divide by 1e7 to get decimal degrees.

minimum: -900000000maximum: 900000000
locationLng integer Optional

Longitude in WGS84, scaled by 1e7 (AT Protocol does not support floats). Divide by 1e7 to get decimal degrees.

minimum: -1800000000maximum: 1800000000
monochrome boolean Optional

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.

noAiTraining boolean Optional

Deprecated — use aiDeclaration instead. True if creator chose 'no-ai'.

palette array of string Optional

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.

maxLength: 8 items
paletteShares array of integer Optional

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.

maxLength: 8 items
post string at-uri Required

AT-URI of the app.bsky.feed.post record this metadata belongs to.

View raw schema
{
  "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"
}
borderCrop object

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 integer Optional

No description available.

minimum: 0maximum: 5000000
left integer Optional

No description available.

minimum: 0maximum: 5000000
right integer Optional

No description available.

minimum: 0maximum: 5000000
top integer Optional

No description available.

minimum: 0maximum: 5000000
View raw schema
{
  "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 object

Subset of EXIF metadata relevant to photography.

Properties

aperture string Optional

Aperture as a human-readable string (e.g. f/1.8).

maxLength: 32 bytes
focalLength string Optional

Focal length as a human-readable string (e.g. 85mm).

maxLength: 32 bytes
iso integer Optional

ISO sensitivity value.

minimum: 1maximum: 1638400
lens string Optional

Lens model (e.g. FE 85mm F1.4 GM).

maxLength: 256 bytes
lensMake string Optional

Lens manufacturer from EXIF LensMake (e.g. Tamron, Sigma). Often absent — many bodies omit it for third-party lenses.

maxLength: 128 bytes
make string Optional

Camera manufacturer (e.g. Sony, Canon, Nikon).

maxLength: 128 bytes
model string Optional

Camera model (e.g. A7R V, EOS R5).

maxLength: 128 bytes
shutter string Optional

Shutter speed as a human-readable string (e.g. 1/250s).

maxLength: 32 bytes
software string Optional

Software used to process the image (e.g. Lightroom, Capture One).

maxLength: 128 bytes
takenAt string datetime Optional

Date and time the photo was taken, from EXIF DateTimeOriginal.

View raw schema
{
  "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 object

An external URL with an optional display label.

Properties

label string Optional

No description available.

maxLength: 64 bytes
url string uri Required

A valid URI.

maxLength: 2048 bytes
View raw schema
{
  "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 object

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 string Optional

Aperture as the creator wrote it (e.g. f/8, 8, f/1.4). Stored verbatim; renderers normalise the display.

maxLength: 32 bytes
camera string Optional

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 bytes
lens string Optional

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 bytes
shutter string Optional

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 bytes
stockName string Optional

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 bytes
stockSlug string Optional

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 bytes
View raw schema
{
  "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 object

Per-image extended metadata. All fields optional.

Properties

borderCrop ref #borderCrop Optional

Confirmed-by-user border crop, applied in grid views only.

exif ref #exifData Optional

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

location string Optional

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 bytes
locationLat integer Optional

Latitude of THIS image in WGS84, scaled by 1e7, as the record-level `locationLat`.

minimum: -900000000maximum: 900000000
locationLng integer Optional

Longitude of THIS image in WGS84, scaled by 1e7, as the record-level `locationLng`.

minimum: -1800000000maximum: 1800000000
monochrome boolean Optional

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.

palette array of string Optional

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

maxLength: 8 items
paletteShares array of integer Optional

Coverage of each colour in THIS image's `palette`, in basis points, positionally aligned with it. Same shape as the record-level `paletteShares`.

maxLength: 8 items
View raw schema
{
  "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"
}

Lexicon Garden

@