app.zeens.zeen
{
"$type": "com.atproto.lexicon.schema",
"defs": {
"link": {
"description": "DEPRECATED. Was one entry in the back cover's link row, before the colophon dropped links. Retained so older records remain valid.",
"properties": {
"label": {
"description": "Short text beside the mark. Falls back to the URL's host when unset.",
"maxLength": 64,
"type": "string"
},
"platform": {
"description": "Which platform's glyph to draw beside the label (matches the ids in socialLinks.ts: 'flickr', 'vsco', 'bluesky' and so on). A free-form string rather than a closed set, so the catalogue can gain a platform without making existing records invalid; a name the renderer has no mark for falls back to a globe. Absent means a plain website.",
"maxLength": 32,
"type": "string"
},
"url": {
"format": "uri",
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
},
"main": {
"description": "A zeen: a sequence of 1:1.414 pages (ISO A-series proportion) mixing photographs and writing, published as a readable object. Pages may draw photos from a collection, from individual posts, or from the author's own uploads — a zeen is a standalone document, not a view of a collection. The AUTHORED PAGES are the artifact: layout is recomputed from them at read time, so a layout improvement reaches every existing zeen. Replaces an earlier record of this NSID that described a print-ready PDF; that shape is gone and its records are inert.",
"key": "tid",
"record": {
"properties": {
"allowDownload": {
"description": "Whether readers other than the author may export this zeen as a PDF. Absent means false: an author who has not said otherwise has not agreed to hand out a file. The author can always export their own work, so this governs OTHER people only. It is a permission on the export endpoint, not a protection on the pages themselves, which are readable by anyone once the zeen is published.",
"type": "boolean"
},
"app.zeens.v": {
"description": "Zeens marker so the firehose and lua filters can identify our records.",
"type": "integer"
},
"cover": {
"description": "The photo to show as this zeen's card in listings and as its og:image. Normally the front cover's photo, stored here so a listing needs neither to parse `pages` nor to lay anything out.",
"ref": "#photoRef",
"type": "ref"
},
"createdAt": {
"format": "datetime",
"type": "string"
},
"description": {
"description": "Short summary for listings and share previews. Used as the meta description when the zeen is published.",
"maxLength": 2000,
"type": "string"
},
"pages": {
"description": "The zeen in reading order, first page first. Stored as authored specs rather than computed geometry: the layout engine turns these into frames at read time, so pages stay editable and re-layout when the engine improves. Capped at 60 — well past the length anyone reads in one sitting, and it bounds the record size.",
"items": {
"ref": "#page",
"type": "ref"
},
"maxLength": 60,
"minLength": 1,
"type": "array"
},
"publishedAt": {
"description": "When the zeen was first published. Set once and not updated by later edits, so a re-edited zeen keeps its place in reverse-chronological listings.",
"format": "datetime",
"type": "string"
},
"sourceCollection": {
"description": "Optional provenance: the app.zeens.collection photos were seeded from. A zeen is a SNAPSHOT — later changes to that collection do not alter it.",
"format": "at-uri",
"type": "string"
},
"status": {
"description": "'draft' = still being authored; visible only to its author, carries no engagement, and is never indexed. 'published' = readable by anyone, likeable and commentable. Absent means 'draft': a record that never said it was published should not be treated as public.",
"knownValues": [
"draft",
"published"
],
"type": "string"
},
"theme": {
"description": "Type and colour choices. Absent means the defaults (modern / neutral).",
"ref": "#theme",
"type": "ref"
},
"title": {
"description": "Display title. Also the front cover's title unless that page overrides it.",
"maxLength": 200,
"type": "string"
}
},
"required": [
"title",
"pages",
"createdAt"
],
"type": "object"
},
"type": "record"
},
"page": {
"description": "One authored page. `layout` names the arrangement; `photos` fills its slots in order and `text` fills its text roles. A layout that wants more photos than are supplied simply renders fewer — a missing blob must never break the page around it.",
"properties": {
"align": {
"description": "Horizontal alignment for this page's title and byline. Absent means left, which is what the Figma frames show. Chiefly for covers: a portrait photograph leaves the title floating against one edge, and centring settles it.",
"knownValues": [
"left",
"center",
"right"
],
"type": "string"
},
"avatarUrl": {
"description": "The author's profile photo on the back cover, as an image-proxy path. A URL rather than a blob ref because an avatar lives on the profile record rather than being one of the author's posts: it has no post URI, no EXIF and no aspect ratio, so threading it through `photos` would mean inventing all three. Snapshotted with the rest of the colophon, so a zeen keeps the face its author had when they made it. Ignored on every other layout.",
"maxLength": 500,
"type": "string"
},
"body": {
"description": "Running text for story and back-cover pages. Blank lines separate paragraphs. Plain text, not markdown: a zeen page is laid out to a fixed frame, and inline formatting would fight the measured geometry.",
"maxLength": 5000,
"type": "string"
},
"bodyScale": {
"description": "Size of this page's RUNNING TEXT — the story body, the photo captions, or the back cover's about text — as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). One field for all three because a page only ever draws one of them, and an author sizing the caption under a photograph is making the same decision as one sizing a story paragraph. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale` and `bylineScale`. Deliberately does NOT cover the location and camera-settings lines: those are joined on from the photograph's own record rather than authored on the page, and are already the smallest type on the sheet.",
"maximum": 200,
"minimum": 50,
"type": "integer"
},
"byline": {
"description": "The line under a cover title. Usually the author's display name, but authored rather than derived so a zeen can credit differently.",
"maxLength": 200,
"type": "string"
},
"bylineScale": {
"description": "Size of this page's byline, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale`.",
"maximum": 200,
"minimum": 50,
"type": "integer"
},
"captions": {
"description": "Caption per photo slot, parallel to `photos` and keyed by SLOT rather than by photo: swapping the photo in a slot keeps the writing, and one photo can carry different words on different pages. A non-empty caption IS the switch to show one — an empty box and a hidden box mean the same thing on a page.",
"items": {
"maxLength": 500,
"type": "string"
},
"maxLength": 6,
"type": "array"
},
"layout": {
"description": "Which arrangement to lay out. Named for what the page IS rather than numbered, so a reader of the record can tell what a page looks like. An unknown value falls back to 'story', which renders whatever text the page carries and drops nothing silently — which is also what happens to the four square-page layouts this set replaced (photo-portrait, photo-pair, photo-wide-over-pair, photo-grid): pages using them keep their words, and their photos stay on the record.",
"knownValues": [
"front-cover",
"front-cover-full",
"story",
"story-photo",
"photo-wide",
"photo-tall",
"photo-wide-plain",
"photo-portrait-plain",
"photo-stack",
"photo-quad-split",
"photo-quad-rows",
"photo-one-over-two",
"photo-six-grid",
"story-top",
"story-bottom",
"back-cover"
],
"type": "string"
},
"links": {
"description": "DEPRECATED and no longer written. The back cover was reduced to a photo, a name and a line about the author — a colophon is a signature, not a link tree. Kept in the lexicon so records written before that change stay valid; the renderer ignores it.",
"items": {
"ref": "#link",
"type": "ref"
},
"maxLength": 6,
"type": "array"
},
"photos": {
"description": "Photos in slot order. Longer than the layout's slot count is ignored; shorter leaves slots empty. maxLength here and on every array parallel to it (slotFraming, captions, showMeta, showLocation) is the largest slot count any layout draws. It was 4 while the six-square grid drew 6, which silently dropped that page's last two photographs on save, and left its last two slots unable to carry a caption or an EXIF switch.",
"items": {
"ref": "#photoRef",
"type": "ref"
},
"maxLength": 6,
"type": "array"
},
"scheme": {
"description": "This page's ground and ink, as a PAIR. 'light' is a light ground with dark text, 'dark' the reverse; 'default' (or absent) follows the zeen's theme. Set per page so a dark text page can sit next to a light photo page — which is the usual way a printed zeen paces itself. A pair rather than two fields because the halves are only ever meaningful together: a light ground with light text is not a design, it is an unreadable page.",
"knownValues": [
"default",
"light",
"dark"
],
"type": "string"
},
"showLocation": {
"description": "Per-slot switch for the LOCATION line, parallel to `photos`. Separate from `showMeta` so the place and the camera settings can be shown independently. The value shown is the free-form location the author already published on the post, never anything extracted from the file's GPS. Absent or false prints nothing.",
"items": {
"type": "boolean"
},
"maxLength": 6,
"type": "array"
},
"showMeta": {
"description": "Per-slot switch for the CAMERA SETTINGS line, parallel to `photos`. Per photo rather than per page because on a multi-photo page one frame may be worth annotating and the others not. Absent or false prints nothing. Location is a separate switch (`showLocation`): the two are different disclosures, and an author who wants to name the place has not thereby agreed to publish the lens they used, or the reverse.",
"items": {
"type": "boolean"
},
"maxLength": 6,
"type": "array"
},
"slotFraming": {
"description": "Per-slot presentation, in slot order. Parallel to `photos`: entry i describes how photo i sits in its slot. Absent, or an absent entry, means the layout's own defaults, so every zeen written before this field stays correct.",
"items": {
"ref": "#slotFrame",
"type": "ref"
},
"maxLength": 6,
"type": "array"
},
"title": {
"description": "Heading, on the layouts that have one (covers and story pages).",
"maxLength": 200,
"type": "string"
},
"titleScale": {
"description": "Size of this page's title, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `bylineScale`.",
"maximum": 200,
"minimum": 50,
"type": "integer"
}
},
"required": [
"layout"
],
"type": "object"
},
"photoRef": {
"description": "A photograph, addressed so it resolves without the reader needing the post it came from. `did` is the photo's AUTHOR — which owns the blob — and is NOT necessarily the zeen's owner, because photos may be drawn from a collection holding other people's work. Readers must ignore a ref missing either `cid` or `did`.",
"properties": {
"alt": {
"description": "Alt text, carried on the ref so the zeen stays accessible without re-fetching the source post.",
"maxLength": 2000,
"type": "string"
},
"aspectRatio": {
"description": "Natural dimensions, so a page can reserve the right space before the blob loads and the layout engine can crop to fill a slot without measuring.",
"ref": "app.bsky.embed.defs#aspectRatio",
"type": "ref"
},
"cid": {
"description": "CID of the image blob itself, NOT of the post record.",
"format": "cid",
"type": "string"
},
"did": {
"description": "DID of the photo's author, whose repo holds the blob.",
"format": "did",
"type": "string"
},
"uri": {
"description": "Optional AT-URI of the post this photo came from, kept as provenance and so a page can link back to it.",
"format": "at-uri",
"type": "string"
}
},
"required": [
"cid",
"did"
],
"type": "object"
},
"slotFrame": {
"description": "How one photo sits in its slot. Every field is optional; an empty object means the layout's defaults.",
"properties": {
"fit": {
"description": "'fill' crops the photo to cover the slot; 'contain' shows the whole photograph with space around it. Absent means the layout's own choice, which differs per layout (a framed cover contains, a full-bleed one fills).",
"knownValues": [
"fill",
"contain"
],
"type": "string"
},
"focalX": {
"description": "Horizontal focal point when `fit` is 'fill', as thousandths so the value stays an integer (ATProto rejects floats). 0 = left edge, 500 = centred, 1000 = right. Absent means 500. Ignored under 'contain', where nothing is cropped.",
"maximum": 1000,
"minimum": 0,
"type": "integer"
},
"focalY": {
"description": "Vertical focal point when `fit` is 'fill'. 0 = top, 500 = centred, 1000 = bottom. Absent means 500.",
"maximum": 1000,
"minimum": 0,
"type": "integer"
},
"ratio": {
"description": "Aspect ratio for the slot's box. The reshaped box always fits INSIDE the space the layout gave the slot and is centred in it, so a ratio can never push a photo over text or off the page. Absent means the layout's own box.",
"knownValues": [
"1:1",
"4:3",
"3:4",
"3:2",
"2:3",
"5:6",
"6:5",
"2:1",
"1:2",
"16:9",
"9:16"
],
"type": "string"
}
},
"type": "object"
},
"theme": {
"description": "Presentation, as two INDEPENDENT axes rather than bundled styles, so any palette is available with any voice and a new face does not multiply the list. Both resolve to concrete faces and colours at layout time, so re-theming is a one-field edit that never rewrites pages. These are the ZEEN's own colours and do not follow the reader's app theme — a zeen looks the same to everyone, the way a printed object does, which is also what keeps the screen and the PDF in agreement.",
"properties": {
"color": {
"description": "Palette: page ground, primary ink, muted ink for metadata, and the photo placeholder fill. Only 'neutral' (white ground, near-black ink) is implemented. Unknown or absent = 'neutral'.",
"knownValues": [
"neutral",
"cool",
"warm",
"dark"
],
"type": "string"
},
"type": {
"description": "Type pairing. 'modern' (Inter throughout, F5.6 for metadata) is the only one implemented; it matches what collection captions already use, so a zeen page and a collection read as one system. 'editorial' (DM Serif Display + Cascadia Mono) and 'classic' (fully serif) are reserved and need font files in BOTH woff2 and TrueType before they can render. Unknown or absent = 'modern'.",
"knownValues": [
"modern",
"editorial",
"classic"
],
"type": "string"
}
},
"type": "object"
}
},
"id": "app.zeens.zeen",
"lexicon": 1
}
Metadata
- DID
-
did:plc:pwpuqwge6g7xrjmfbw73uycw - CID
-
bafyreieyspvb645t7lnquz34c4d3rht4ysg2pwpsjazk55t3zm3qgpi7we - Indexed At
- 2026-10-22 16:31 UTC
- AT-URI
-
at://did:plc:pwpuqwge6g7xrjmfbw73uycw/com.atproto.lexicon.schema/app.zeens.zeen
Referenced Schemas (1)
Similar Lexicons
Lexicons whose schemas describe something close to this one. Check them before publishing a new one that overlaps.
-
app.zeens.collection record
A user-curated collection (folder) of Zeens posts and articles. Visibility controls whether anyone other than the owner can see it via the AppView. The cover is the owner's chosen `coverPhoto` when set, otherwise the mo…
-
app.offprint.page record
-
pub.leaflet.document record
A lexicon for long form rich media documents
-
app.zeens.portfolio record
Configuration for a user's public Portfolio site (e.g. alice.zeens.app). One per user, keyed 'self'. A portfolio is a curated list of the user's COLLECTIONS, displayed beautifully: it holds the subdomain, a theme + layo…