app.zeens.portfolio

zeens.app

Documentation

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 + layout/columns/gap applied to every collection, the ordered list of collection AT-URIs to show, and an optional home collection. Collections own their own title/subtitle/description/photos/slug, which the portfolio reuses; the site header comes from the user's profile. Stored on the user's own PDS so the site is portable and user-owned; PocketBase only mirrors {subdomain -> did} for host routing + uniqueness. Rendered publicly only when `enabled` is true.

main 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 + layout/columns/gap applied to every collection, the ordered list of collection AT-URIs to show, and an optional home collection. Collections own their own title/subtitle/description/photos/slug, which the portfolio reuses; the site header comes from the user's profile. Stored on the user's own PDS so the site is portable and user-owned; PocketBase only mirrors {subdomain -> did} for host routing + uniqueness. Rendered publicly only when `enabled` is true.

Record Key literal:self Fixed literal value

Properties

about ref #aboutPage Optional

Optional About page (alice.zeens.app/about). Absent, or present with enabled=false, means the portfolio has no About page and no About entry in its rail.

app.zeens.v integer Optional

Zeens marker so the firehose / lua filters can identify our records.

appearance string Optional

Light/dark appearance. 'auto' (default) follows the visitor's system preference; 'light'/'dark' force the theme's respective palette.

Known values: auto, light, dark
collections array of ref #collectionRef Optional

Ordered list of the collections shown in the portfolio menu. Each entry references an app.zeens.collection record on the same repo. Array order is the menu order.

maxLength: 50 items
columns integer Optional

Column count for grid/masonry layouts. Allowed 2-4. Default 3.

minimum: 2maximum: 4
createdAt string datetime Required

An RFC 3339 formatted timestamp.

enabled boolean Required

Whether the public site is live. When false, the subdomain 404s even if reserved.

featured ref #featuredBlock Optional

Hand-picked photos under a heading the owner chooses, shown on the home page below the hero. Independent of any collection. Absent means no section.

gap integer Optional

Gap in pixels between photos. Allowed 0, 4, 8, 16, 24, 32. Default 8.

minimum: 0maximum: 32
hero ref #heroBlock Optional

The home page's hero — the owner's own photos across the top. ABSENT DOES NOT MEAN NO HERO: the render path falls back to the owner's 6 most-liked photos as a square grid, so a portfolio has a hero without its owner picking one. Present means they chose the photos themselves.

home ref #homePage Optional

Optional plain-text heading + paragraph shown ABOVE the home page's existing content (the collection card grid, or homeCollection's photos when one is set). Absent means the home page renders exactly as it did before this field existed, so no portfolio changes without its owner acting.

homeCollection string at-uri Optional

DEPRECATED. AT-URI of a collection rendered inline as the portfolio's home page. Superseded by the portfolio's own home page (see `home`): the editor no longer offers this, and new records should not set it. Still honoured on read so existing sites don't change appearance, and the editor lets an owner who has it set turn it off.

layout string Optional

Photo layout applied to every collection shown in the portfolio. 'single' = one full-width column; 'grid' = uniform columns; 'masonry' = variable-height columns.

Known values: single, grid, masonry
menu ref #menuSettings Optional

How the navigation menu renders. Absent means the default: the home page shows the owner's collections.

palette string Optional

Id of the chosen color palette (each has a light+dark variant, defined app-side, e.g. 'mono'). Independent of typography. Unknown/absent falls back to the default at render time. (Legacy records may carry a bundled 'theme' field instead; read paths migrate it.)

maxLength: 64 bytes
socials ref #socialsVisibility Optional

Which of the owner's profile social links the public site shows. ABSENT MEANS SHOW ALL — every portfolio created before this field keeps showing every link with no migration.

subdomain string Optional

The chosen subdomain slug (the 'alice' in alice.zeens.app). Lowercase DNS-safe label; uniqueness + reserved-word validation happen at write time and are mirrored into the PB portfolio_slugs table. Absent until the user picks one.

maxLength: 63 bytes
tagline string Optional

One-line tagline for the site. Mirrored onto site.standard.publication.description, since the portfolio IS the publication — this is the line other standard.site readers show under the name.

maxLength: 2000 bytesmaxGraphemes: 200 graphemes
title string Optional

Custom site title shown in the portfolio rail (free text, e.g. 'Ro', 'Alice Photography'). Falls back to the user's profile display name when absent.

maxLength: 120 bytes
typography string Optional

Id of the chosen typography pairing (font choice, defined app-side, e.g. 'space-grotesk'). Independent of palette. Unknown/absent falls back to the default at render time.

maxLength: 64 bytes
View raw schema
{
  "description": "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 + layout/columns/gap applied to every collection, the ordered list of collection AT-URIs to show, and an optional home collection. Collections own their own title/subtitle/description/photos/slug, which the portfolio reuses; the site header comes from the user's profile. Stored on the user's own PDS so the site is portable and user-owned; PocketBase only mirrors {subdomain -> did} for host routing + uniqueness. Rendered publicly only when `enabled` is true.",
  "key": "literal:self",
  "record": {
    "properties": {
      "about": {
        "description": "Optional About page (alice.zeens.app/about). Absent, or present with enabled=false, means the portfolio has no About page and no About entry in its rail.",
        "ref": "#aboutPage",
        "type": "ref"
      },
      "app.zeens.v": {
        "description": "Zeens marker so the firehose / lua filters can identify our records.",
        "type": "integer"
      },
      "appearance": {
        "description": "Light/dark appearance. 'auto' (default) follows the visitor's system preference; 'light'/'dark' force the theme's respective palette.",
        "knownValues": [
          "auto",
          "light",
          "dark"
        ],
        "type": "string"
      },
      "collections": {
        "description": "Ordered list of the collections shown in the portfolio menu. Each entry references an app.zeens.collection record on the same repo. Array order is the menu order.",
        "items": {
          "ref": "#collectionRef",
          "type": "ref"
        },
        "maxLength": 50,
        "type": "array"
      },
      "columns": {
        "description": "Column count for grid/masonry layouts. Allowed 2-4. Default 3.",
        "maximum": 4,
        "minimum": 2,
        "type": "integer"
      },
      "createdAt": {
        "format": "datetime",
        "type": "string"
      },
      "enabled": {
        "description": "Whether the public site is live. When false, the subdomain 404s even if reserved.",
        "type": "boolean"
      },
      "featured": {
        "description": "Hand-picked photos under a heading the owner chooses, shown on the home page below the hero. Independent of any collection. Absent means no section.",
        "ref": "#featuredBlock",
        "type": "ref"
      },
      "gap": {
        "description": "Gap in pixels between photos. Allowed 0, 4, 8, 16, 24, 32. Default 8.",
        "maximum": 32,
        "minimum": 0,
        "type": "integer"
      },
      "hero": {
        "description": "The home page's hero — the owner's own photos across the top. ABSENT DOES NOT MEAN NO HERO: the render path falls back to the owner's 6 most-liked photos as a square grid, so a portfolio has a hero without its owner picking one. Present means they chose the photos themselves.",
        "ref": "#heroBlock",
        "type": "ref"
      },
      "home": {
        "description": "Optional plain-text heading + paragraph shown ABOVE the home page's existing content (the collection card grid, or homeCollection's photos when one is set). Absent means the home page renders exactly as it did before this field existed, so no portfolio changes without its owner acting.",
        "ref": "#homePage",
        "type": "ref"
      },
      "homeCollection": {
        "description": "DEPRECATED. AT-URI of a collection rendered inline as the portfolio's home page. Superseded by the portfolio's own home page (see `home`): the editor no longer offers this, and new records should not set it. Still honoured on read so existing sites don't change appearance, and the editor lets an owner who has it set turn it off.",
        "format": "at-uri",
        "type": "string"
      },
      "layout": {
        "description": "Photo layout applied to every collection shown in the portfolio. 'single' = one full-width column; 'grid' = uniform columns; 'masonry' = variable-height columns.",
        "knownValues": [
          "single",
          "grid",
          "masonry"
        ],
        "type": "string"
      },
      "menu": {
        "description": "How the navigation menu renders. Absent means the default: the home page shows the owner's collections.",
        "ref": "#menuSettings",
        "type": "ref"
      },
      "palette": {
        "description": "Id of the chosen color palette (each has a light+dark variant, defined app-side, e.g. 'mono'). Independent of typography. Unknown/absent falls back to the default at render time. (Legacy records may carry a bundled 'theme' field instead; read paths migrate it.)",
        "maxLength": 64,
        "type": "string"
      },
      "socials": {
        "description": "Which of the owner's profile social links the public site shows. ABSENT MEANS SHOW ALL — every portfolio created before this field keeps showing every link with no migration.",
        "ref": "#socialsVisibility",
        "type": "ref"
      },
      "subdomain": {
        "description": "The chosen subdomain slug (the 'alice' in alice.zeens.app). Lowercase DNS-safe label; uniqueness + reserved-word validation happen at write time and are mirrored into the PB portfolio_slugs table. Absent until the user picks one.",
        "maxLength": 63,
        "type": "string"
      },
      "tagline": {
        "description": "One-line tagline for the site. Mirrored onto site.standard.publication.description, since the portfolio IS the publication — this is the line other standard.site readers show under the name.",
        "maxGraphemes": 200,
        "maxLength": 2000,
        "type": "string"
      },
      "title": {
        "description": "Custom site title shown in the portfolio rail (free text, e.g. 'Ro', 'Alice Photography'). Falls back to the user's profile display name when absent.",
        "maxLength": 120,
        "type": "string"
      },
      "typography": {
        "description": "Id of the chosen typography pairing (font choice, defined app-side, e.g. 'space-grotesk'). Independent of palette. Unknown/absent falls back to the default at render time.",
        "maxLength": 64,
        "type": "string"
      }
    },
    "required": [
      "enabled",
      "createdAt"
    ],
    "type": "object"
  },
  "type": "record"
}
aboutLink object

One titled external link on the About page. Readers MUST re-validate `url` and accept only https: the record lives on the owner's PDS and any client can write it, so the value reaches an href untrusted. Writers upgrade a typed http:// to https:// rather than storing insecure links.

Properties

title string Required

Link label, e.g. 'Buy prints'.

maxLength: 60 bytes
url string uri Required

Destination URL. https only.

View raw schema
{
  "description": "One titled external link on the About page. Readers MUST re-validate `url` and accept only https: the record lives on the owner's PDS and any client can write it, so the value reaches an href untrusted. Writers upgrade a typed http:// to https:// rather than storing insecure links.",
  "properties": {
    "title": {
      "description": "Link label, e.g. 'Buy prints'.",
      "maxLength": 60,
      "type": "string"
    },
    "url": {
      "description": "Destination URL. https only.",
      "format": "uri",
      "type": "string"
    }
  },
  "required": [
    "title",
    "url"
  ],
  "type": "object"
}
aboutPage object

The portfolio's About page. Plain text, like #homePage. The owner's portrait is NOT stored here: `useProfileAvatar` is a flag and the avatar is resolved from the profile at render time, so it tracks their Zeens avatar with no copy to go stale.

Properties

body string Optional

About text, rendered with preserved line breaks.

maxLength: 4000 bytes
enabled boolean Required

Whether the About page exists and appears in the rail.

heading string Optional

Page heading, also used as the rail's label for it. Defaults to 'About' when absent.

maxLength: 120 bytes
photos array of ref#aboutPhoto Optional

Photos chosen from the owner's own posts, in display order. Rendered as a grid below the About text: one photo full width, two side by side, three or more in three columns (six fills a 3x2 grid). Only the first image of a multi-image post is shown.

maxLength: 6 items
useProfileAvatar boolean Optional

Show the owner's live profile avatar as the portrait. Opt-in: absent means false, so a portrait never appears on the public page unless the owner asked for it.

View raw schema
{
  "description": "The portfolio's About page. Plain text, like #homePage. The owner's portrait is NOT stored here: `useProfileAvatar` is a flag and the avatar is resolved from the profile at render time, so it tracks their Zeens avatar with no copy to go stale.",
  "properties": {
    "body": {
      "description": "About text, rendered with preserved line breaks.",
      "maxLength": 4000,
      "type": "string"
    },
    "enabled": {
      "description": "Whether the About page exists and appears in the rail.",
      "type": "boolean"
    },
    "heading": {
      "description": "Page heading, also used as the rail's label for it. Defaults to 'About' when absent.",
      "maxLength": 120,
      "type": "string"
    },
    "links": {
      "description": "Titled external links shown as one horizontal row above the About photo grid (e.g. 'Buy prints', 'My Instagram'). Distinct from the profile socials in the rail: those are platform accounts, these are anything the owner wants to point at.",
      "items": {
        "ref": "#aboutLink",
        "type": "ref"
      },
      "maxLength": 8,
      "type": "array"
    },
    "photos": {
      "description": "Photos chosen from the owner's own posts, in display order. Rendered as a grid below the About text: one photo full width, two side by side, three or more in three columns (six fills a 3x2 grid). Only the first image of a multi-image post is shown.",
      "items": {
        "ref": "#aboutPhoto",
        "type": "ref"
      },
      "maxLength": 6,
      "type": "array"
    },
    "useProfileAvatar": {
      "description": "Show the owner's live profile avatar as the portrait. Opt-in: absent means false, so a portrait never appears on the public page unless the owner asked for it.",
      "type": "boolean"
    }
  },
  "required": [
    "enabled"
  ],
  "type": "object"
}
aboutPhoto object

A reference to one of the owner's own posts, used wherever a block stores picked photos — the About page, page heroes, and Featured Work. Named for its first use; kept under that name because renaming a def would orphan the records already pointing at it.

Properties

cid string Required

CID of the referenced post at pick time.

uri string at-uri Required

AT-URI of an app.zeens.post / app.bsky.feed.post record on the owner's repo.

View raw schema
{
  "description": "A reference to one of the owner's own posts, used wherever a block stores picked photos — the About page, page heroes, and Featured Work. Named for its first use; kept under that name because renaming a def would orphan the records already pointing at it.",
  "properties": {
    "cid": {
      "description": "CID of the referenced post at pick time.",
      "type": "string"
    },
    "uri": {
      "description": "AT-URI of an app.zeens.post / app.bsky.feed.post record on the owner's repo.",
      "format": "at-uri",
      "type": "string"
    }
  },
  "required": [
    "uri",
    "cid"
  ],
  "type": "object"
}
collectionRef object

A reference to one of the user's collections, in menu order.

Properties

collection string at-uri Required

AT-URI of an app.zeens.collection record on the owner's repo.

View raw schema
{
  "description": "A reference to one of the user's collections, in menu order.",
  "properties": {
    "collection": {
      "description": "AT-URI of an app.zeens.collection record on the owner's repo.",
      "format": "at-uri",
      "type": "string"
    }
  },
  "required": [
    "collection"
  ],
  "type": "object"
}
featuredBlock object

Featured work: photos picked directly rather than through a collection. The heading is the owner's own — 'Featured Work' is one gallery's word for it and 'Selected' or 'Latest' is another's.

Properties

photos array of ref#aboutPhoto Required

Photos chosen from the owner's own posts, in display order.

maxLength: 12 items
title string Optional

Section heading. ABSENT MEANS 'Featured Work' — the default lives in the render path, so it is not frozen into every record.

maxLength: 60 bytes
View raw schema
{
  "description": "Featured work: photos picked directly rather than through a collection. The heading is the owner's own — 'Featured Work' is one gallery's word for it and 'Selected' or 'Latest' is another's.",
  "properties": {
    "photos": {
      "description": "Photos chosen from the owner's own posts, in display order.",
      "items": {
        "ref": "#aboutPhoto",
        "type": "ref"
      },
      "maxLength": 12,
      "type": "array"
    },
    "title": {
      "description": "Section heading. ABSENT MEANS 'Featured Work' — the default lives in the render path, so it is not frozen into every record.",
      "maxLength": 60,
      "type": "string"
    }
  },
  "required": [
    "photos"
  ],
  "type": "object"
}
heroBlock object

A page hero: the owner's photos across the top of a page. One def serves all three arrangements because they are the same data seen differently. A hero with no photos is dropped on write rather than stored empty — an absent hero falls back to the owner's most-liked photos, which is a better empty state than a blank band.

Properties

mode string Required

'single' = one full-width photo; 'carousel' = a swipeable run of them; 'grid' = all of them as a grid. Unknown/absent reads as 'single'.

Known values: single, carousel, grid
photos array of ref#aboutPhoto Required

Photos chosen from the owner's own posts, in display order. Only the first image of a multi-image post is used.

maxLength: 6 items
View raw schema
{
  "description": "A page hero: the owner's photos across the top of a page. One def serves all three arrangements because they are the same data seen differently. A hero with no photos is dropped on write rather than stored empty — an absent hero falls back to the owner's most-liked photos, which is a better empty state than a blank band.",
  "properties": {
    "mode": {
      "description": "'single' = one full-width photo; 'carousel' = a swipeable run of them; 'grid' = all of them as a grid. Unknown/absent reads as 'single'.",
      "knownValues": [
        "single",
        "carousel",
        "grid"
      ],
      "type": "string"
    },
    "photos": {
      "description": "Photos chosen from the owner's own posts, in display order. Only the first image of a multi-image post is used.",
      "items": {
        "ref": "#aboutPhoto",
        "type": "ref"
      },
      "maxLength": 6,
      "type": "array"
    }
  },
  "required": [
    "mode",
    "photos"
  ],
  "type": "object"
}
homePage object

Plain-text intro shown at the top of the portfolio home page. Deliberately NOT rich text: it renders into a public page, so a markup format would need sanitising and carry XSS surface. Mirrors how collections store their description.

Properties

body string Optional

Home page paragraph, rendered with preserved line breaks.

maxLength: 2000 bytes
heading string Optional

Home page heading. When present it supplies the page's <h1>, and a home collection's own title renders as <h2> so the page keeps exactly one top-level heading.

maxLength: 120 bytes
View raw schema
{
  "description": "Plain-text intro shown at the top of the portfolio home page. Deliberately NOT rich text: it renders into a public page, so a markup format would need sanitising and carry XSS surface. Mirrors how collections store their description.",
  "properties": {
    "body": {
      "description": "Home page paragraph, rendered with preserved line breaks.",
      "maxLength": 2000,
      "type": "string"
    },
    "heading": {
      "description": "Home page heading. When present it supplies the page's <h1>, and a home collection's own title renders as <h2> so the page keeps exactly one top-level heading.",
      "maxLength": 120,
      "type": "string"
    }
  },
  "type": "object"
}
menuSettings object

Navigation menu appearance. Its one property defaults to the behaviour that predates this def, so an absent `menu` never changes how a portfolio renders. Records written before the template took over navigation may still carry `icons` and `collections`; both axes were retired and readers ignore them.

Properties

homeCollections boolean Optional

Whether the home page shows the owner's collections. ABSENT MEANS TRUE. When on, the home page shows the 3 most recently updated as a row linking through to the Galleries page that lists the rest. Lives here rather than on #homePage because it concerns the collections, not the home text, and an all-empty homePage is dropped from the record.

View raw schema
{
  "description": "Navigation menu appearance. Its one property defaults to the behaviour that predates this def, so an absent `menu` never changes how a portfolio renders. Records written before the template took over navigation may still carry `icons` and `collections`; both axes were retired and readers ignore them.",
  "properties": {
    "homeCollections": {
      "description": "Whether the home page shows the owner's collections. ABSENT MEANS TRUE. When on, the home page shows the 3 most recently updated as a row linking through to the Galleries page that lists the rest. Lives here rather than on #homePage because it concerns the collections, not the home text, and an all-empty homePage is dropped from the record.",
      "type": "boolean"
    }
  },
  "type": "object"
}
socialsVisibility object

Controls which profile social links the public portfolio shows. Filtering is applied server-side AFTER the Bluesky link is synthesised from the owner's handle, so hiding Bluesky works even though it is not stored on the profile record.

Properties

mode string Required

'all' (also the meaning of an absent socials field) shows every link; 'selected' shows only the platforms in `shown`; 'none' hides the block entirely, website included.

Known values: all, selected, none
shown array of string Optional

Platform ids to show when mode is 'selected'. Ignored for other modes.

maxLength: 32 items
website boolean Optional

Whether the profile website link is shown. Absent means true.

View raw schema
{
  "description": "Controls which profile social links the public portfolio shows. Filtering is applied server-side AFTER the Bluesky link is synthesised from the owner's handle, so hiding Bluesky works even though it is not stored on the profile record.",
  "properties": {
    "mode": {
      "description": "'all' (also the meaning of an absent socials field) shows every link; 'selected' shows only the platforms in `shown`; 'none' hides the block entirely, website included.",
      "knownValues": [
        "all",
        "selected",
        "none"
      ],
      "type": "string"
    },
    "shown": {
      "description": "Platform ids to show when mode is 'selected'. Ignored for other modes.",
      "items": {
        "maxLength": 32,
        "type": "string"
      },
      "maxLength": 32,
      "type": "array"
    },
    "website": {
      "description": "Whether the profile website link is shown. Absent means true.",
      "type": "boolean"
    }
  },
  "required": [
    "mode"
  ],
  "type": "object"
}

Lexicon Garden

@