Skip to content

GreenGale Lexicons

Developer Documentation

GreenGale
Jan 23, 202610 min read

GreenGale uses AT Protocol Lexicons to define its data schemas. The GreenGale namespace has two main record types: document and publication. These map closely to the standard.site document and publication collections, which are also available for dual-publishing but are optional for GreenGale use.

Collections

CollectionKeyDescription
app.greengale.documenttidBlog posts
app.greengale.publicationliteral:selfPublication configuration (one per user)
site.standard.documenttidCross-platform blog posts (dual-published)
site.standard.publicationtidCross-platform publication metadata

app.greengale.document

A markdown document with theme, image, SVG, and LaTeX support. This is the primary record type for blog posts, and the only record required to publish to GreenGale.

Required fields: content, url, path, title, publishedAt

{
  "lexicon": 1,
  "id": "app.greengale.document",
  "defs": {
    "contentRef": {
      "type": "object",
      "description": "Reference to external content via AT-URI. Used in site.standard.document content union.",
      "required": ["uri"],
      "properties": {
        "uri": {
          "type": "string",
          "format": "at-uri",
          "description": "AT-URI pointing to the full document content"
        }
      }
    },
    "main": {
      "type": "record",
      "description": "A markdown document with extended theme and LaTeX support.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["content", "url", "path", "title", "publishedAt"],
        "properties": {
          "content": {
            "type": "string",
            "maxLength": 100000,
            "description": "Markdown content of the document"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Base publication URL (e.g., https://greengale.app)"
          },
          "path": {
            "type": "string",
            "maxLength": 500,
            "description": "Document path relative to the publication URL (e.g., /handle/rkey)"
          },
          "title": {
            "type": "string",
            "maxLength": 1000,
            "description": "Document title"
          },
          "subtitle": {
            "type": "string",
            "maxLength": 1000
          },
          "publishedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Publication timestamp"
          },
          "theme": {
            "type": "ref",
            "ref": "app.greengale.blog.defs#theme"
          },
          "visibility": {
            "type": "string",
            "enum": ["public", "url", "author"],
            "default": "public",
            "description": "Controls who can view this document"
          },
          "ogp": {
            "type": "ref",
            "ref": "app.greengale.blog.defs#ogp"
          },
          "blobs": {
            "type": "array",
            "items": {
              "type": "ref",
              "ref": "app.greengale.blog.defs#blobMetadata"
            }
          },
          "tags": {
            "type": "array",
            "maxLength": 100,
            "items": {
              "type": "string",
              "maxLength": 100,
              "maxGraphemes": 50
            },
            "description": "Tags to categorize the document. Avoid prepending with hashtags."
          }
        }
      }
    }
  }
}

Field Details

FieldTypeRequiredDescription
contentstringYesMarkdown body (max 100,000 chars)
urluriYesBase publication URL
pathstringYesPath relative to publication URL
titlestringYesDocument title (max 1,000 chars)
subtitlestringNoSubtitle (max 1,000 chars)
publishedAtdatetimeYesPublication timestamp
themerefNoTheme preset or custom colors
visibilityenumNopublic (default), url, or author
ogprefNoOpen Graph image metadata
blobsarrayNoEmbedded image references
latexbooleanNoLegacy field (always enabled)
tagsarrayNoCategorization tags

contentRef

Used in site.standard.document's content union to reference the full GreenGale document by AT-URI, avoiding content duplication across collections.

Special Formatting

Bluesky Posts

Links to Bluesky posts are extracted from post content and converted to embeds on the frontend by default.

@-Mentions

The standard internet handle, an @ symbol followed by a domain name, when found in post content will be automatically converted on the frontend into a clickable link to that user's profile.

In addition to Markdown, GreenGale includes rendering for KaTeX and SVG.

Mathematical Expressions

GreenGale posts support KaTeX math rendering in the post content via remark-math and rehype-katex.

Inline math — wrap expressions with single dollar signs:

The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$ for any quadratic equation.

Display math — wrap expressions with double dollar signs on their own lines:

$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$

To render a literal dollar sign without triggering math mode, escape it with a backslash: \$.

See the KaTeX supported functions for a full reference of available commands.

SVG

Posts support SVG diagrams via fenced code blocks in the post content. Use the svg language tag for the code block:

```svg
<svg viewBox="0 0 200 100" width="200" height="100">
  <rect width="200" height="100" fill="#1e293b" rx="8"/>
  <text x="100" y="55" text-anchor="middle" fill="white" font-size="16">Hello, SVG!</text>
</svg>
```

app.greengale.publication

Optional publication-level configuration. Each user has at most one record (key: literal:self). When defined, the theme in the publication record will be applied to that user's profile page, and any of their blog posts that do not have a different theme applied.

Required fields: url, name

{
  "lexicon": 1,
  "id": "app.greengale.publication",
  "defs": {
    "main": {
      "type": "record",
      "description": "A publication configuration with title, description, and default theme.",
      "key": "literal:self",
      "record": {
        "type": "object",
        "required": ["url", "name"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publication base URL (e.g., https://greengale.app)"
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Publication/blog title"
          },
          "description": {
            "type": "string",
            "maxLength": 1000,
            "description": "Publication description"
          },
          "theme": {
            "type": "ref",
            "ref": "app.greengale.blog.defs#theme",
            "description": "Default theme for posts in this publication"
          },
          "enableSiteStandard": {
            "type": "boolean",
            "default": false,
            "description": "When enabled, also publishes to site.standard collections for cross-platform compatibility"
          },
          "voiceTheme": {
            "type": "ref",
            "ref": "app.greengale.blog.defs#voiceTheme",
            "description": "Default voice settings for TTS playback on posts from this user"
          }
        }
      }
    }
  }
}
FieldTypeRequiredDescription
urluriYesPublication base URL
namestringYesPublication/blog title (max 200 chars)
descriptionstringNoPublication description (max 1,000 chars)
themerefNoDefault theme for all posts
enableSiteStandardbooleanNoDual-publish to site.standard.* collections
voiceThemerefNoDefault TTS voice configuration

theme

A post or publication can use a preset name, custom colors, or both (custom overrides preset defaults).

PresetDescription
github-lightLight theme inspired by GitHub
github-darkDark theme inspired by GitHub
draculaDark grey-purple theme
nordArctic, north-bluish palette
solarized-lightWarm light theme
solarized-darkDark blue theme
monokaiBrown theme with vivid accents

customColors

Custom colors are defined using hex color codes in the following fields:

FieldDescription
backgroundPage background color
textPrimary text color
accentLink and accent color
codeBackgroundCode block background color

blobMetadata

Attached to documents via the blobs array. Each entry references an image uploaded to the user's PDS.

selfLabels

Content warning labels for images. Common values:

  • nudity - Non-sexual nudity
  • sexual - Sexually suggestive
  • porn - Explicit content (18+)
  • graphic-media - Violence or disturbing imagery

voiceTheme

Controls default TTS voice for a publication. Voices are provided by the Kokoro TTS model.

Voice ID format: {accent}{gender}_{name} where accent is a (American) or b (British), and gender is f (female) or m (male).

CategoryVoice IDs
American Femaleaf_heart (default), af_alloy, af_aoede, af_bella, af_jessica, af_kore, af_nicole, af_nova, af_river, af_sarah, af_sky
American Maleam_adam, am_echo, am_eric, am_fenrir, am_liam, am_michael, am_onyx, am_puck, am_santa
British Femalebf_alice, bf_emma, bf_isabella, bf_lily
British Malebm_daniel, bm_fable, bm_george, bm_lewis

Pitch and speed are integers representing the multiplier times 100 (e.g., 100 = 1.0x normal).

FieldAvailable steps (x100)Description
pitch50, 75, 90, 100, 110, 125, 150Pitch multiplier (100 = normal)
speed50, 75, 90, 100, 110, 125, 150, 200Playback speed multiplier (100 = normal)

site.standard.document

Cross-platform document record from the standard.site specification. GreenGale dual-publishes to this collection when enableSiteStandard is enabled at the publication level, and/or the option is enabled in the document editor, and the document has public visibility. This uses the same rkey as the app.greengale.document record.

How GreenGale Uses site.standard.document

When dual-publishing, GreenGale populates the content field with a contentRef pointing to the corresponding app.greengale.document AT-URI. The textContent field is populated with a plaintext extraction of the markdown for search indexing.

Note: with standard.site publishing enabled on the GreenGale frontend, both app.greengale.document and site.standard.document records will be created, with the site.standard record that points to the app.greengale record. If you publish externally using custom tools, and you want standard.site support, you will need to write both of these records and ensure that the rkeys match and the content of the site.standard contains the contentRef to the greengale post.

Required fields: site, title, publishedAt

{
  "lexicon": 1,
  "id": "site.standard.document",
  "defs": {
    "main": {
      "type": "record",
      "description": "A document record representing a published article, blog post, or other content. Documents can belong to a publication or exist independently.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["site", "title", "publishedAt"],
        "properties": {
          "site": {
            "type": "string",
            "format": "uri",
            "description": "Points to a publication record (at://) or a publication url (https://) for loose documents. Avoid trailing slashes."
          },
          "path": {
            "type": "string",
            "description": "Combine with site or publication url to construct a canonical URL to the document. Prepend with a leading slash."
          },
          "title": {
            "type": "string",
            "maxLength": 5000,
            "maxGraphemes": 500
          },
          "description": {
            "type": "string",
            "maxLength": 30000,
            "maxGraphemes": 3000,
            "description": "A brief description or excerpt from the document."
          },
          "coverImage": {
            "type": "blob",
            "accept": ["image/*"],
            "maxSize": 1000000,
            "description": "Image for thumbnail or cover image. Less than 1MB in size."
          },
          "content": {
            "type": "union",
            "closed": false,
            "refs": [],
            "description": "Open union for content. GreenGale uses app.greengale.document#contentRef here."
          },
          "textContent": {
            "type": "string",
            "description": "Plaintext representation of document contents. Should not contain markdown or other formatting."
          },
          "bskyPostRef": {
            "type": "ref",
            "ref": "com.atproto.repo.strongRef",
            "description": "Strong reference to a Bluesky post. Useful to keep track of comments off-platform."
          },
          "tags": {
            "type": "array",
            "items": { "type": "string", "maxLength": 1280, "maxGraphemes": 128 },
            "description": "Tags to categorize the document. Avoid prepending with hashtags."
          },
          "publishedAt": {
            "type": "string",
            "format": "datetime"
          },
          "updatedAt": {
            "type": "string",
            "format": "datetime"
          }
        }
      }
    }
  }
}

site.standard.publication

Cross-platform publication metadata from the standard.site specification.

Required fields: url, name

{
  "lexicon": 1,
  "id": "site.standard.publication",
  "defs": {
    "main": {
      "type": "record",
      "description": "A publication record representing a blog, website, or content platform. Publications serve as containers for documents and define the overall branding and settings.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["url", "name"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path."
          },
          "name": {
            "type": "string",
            "maxLength": 5000,
            "maxGraphemes": 500,
            "description": "Name of the publication."
          },
          "description": {
            "type": "string",
            "maxLength": 30000,
            "maxGraphemes": 3000,
            "description": "Brief description of the publication."
          },
          "icon": {
            "type": "blob",
            "accept": ["image/*"],
            "maxSize": 1000000,
            "description": "Square image to identify the publication. Should be at least 256x256."
          },
          "basicTheme": {
            "type": "ref",
            "ref": "site.standard.theme.basic",
            "description": "Simplified publication theme for tools and apps to utilize when displaying content."
          },
          "preferences": {
            "type": "ref",
            "ref": "#preferences",
            "description": "Object containing platform specific preferences (with a few shared properties)."
          }
        }
      }
    },
    "preferences": {
      "type": "object",
      "description": "Platform-specific preferences for the publication, including discovery and visibility settings.",
      "properties": {
        "showInDiscover": {
          "type": "boolean",
          "default": true,
          "description": "Boolean which decides whether the publication should appear in discovery feeds."
        }
      }
    }
  }
}

Theme Conversion

GreenGale converts its theme format to site.standard.theme.basic (which uses primaryColor, backgroundColor, and accentColor). The full GreenGale theme is preserved in preferences.greengale for round-tripping.

Visibility Levels

Both app.greengale.document and the legacy app.greengale.blog.entry support three visibility levels:

ValueDescription
publicVisible to everyone, indexed in feeds
urlOnly accessible via direct link
authorOnly visible to the author (draft)

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions