A Statamic 6 add-on that syncs entries to standard.site lexicons on the AT Protocol.
What this does
When you save an entry in a configured collection, this add-on creates or updates a site.standard.document record on your AT Protocol PDS. It also serves the /.well-known/site.standard.publication verification endpoint and provides a Statamic tag for document verification link tags.
Built on top of publish-php/atproto-standard-site, a focused PHP XRPC client for standard.site lexicons.
Requirements
- Statamic 6+
- PHP 8.3+
- A Bluesky/AT Protocol account with an app password
Limitations
- Multi-site / localization: Only the origin entry is synced to the AT Protocol. Localized entries are not synced independently, and deleting the origin entry does not cascade to delete localized AT Protocol records. Multi-site support is planned for a future release.
- Drafts: Only published entries are synced. The AT Protocol is currently 100% public — drafts are never published to the PDS.
- Content source: The content field is determined by convention (the field with handle
content). Per-entry content override is not supported at this time.
Installation
composer require publish-php/statamic-standard-site
After installation, go to CP → Settings → Standard Site to configure your credentials and publication record.
Quick Start
- Generate a Bluesky app password at Settings → App Passwords
- Open CP → Settings → Standard Site
- Enter your handle and app password
- Click "Check for existing publications" to discover any existing records
- Select an existing publication or create a new one
- Go to Settings → Standard Site → Collections tab and enable syncing for the collections you want
- Add the verification tag to your template
<head>(see below)
Verification
Publication
The add-on automatically serves /.well-known/site.standard.publication on your domain, returning your publication's AT-URI.
Document
Add this tag to your template's <head>:
{{ standard_site:document_link }}
This outputs the <link rel="site.standard.document"> tag for the current entry.
It reads the synced AT-URI from the current template context, so it works in an
entry/show template, in a shared layout <head>, or inside a collection loop.
It renders nothing when the entry hasn't been synced (no URI), so a single
placement in your layout <head> is safe — it simply no-ops on the homepage,
listing pages, and any unsynced entry.
Content conversion
The content field is converted to at.markpub.markdown
for the document's content union, plus a plaintext textContent. Bard fields
(including Bard sets) are supported.
Assets resolve to absolute URLs
Any asset referenced in your content — inline Bard images, or assets fields
inside a Bard set — is resolved to a fully-qualified absolute URL via
Statamic's own asset container configuration. This works for every driver
(local disk, S3, Scaleway, custom CDN) with no configuration. External readers
(e.g. Standard Reader) can therefore load your media directly.
Media that Markdown can't represent renders as pure Markdown
Markdown has no native syntax for video or audio, and raw HTML embeds
(<video>/<audio>) don't survive: the most popular Standard.Site reader
(Standard Reader) doesn't render Markdown as generic HTML — it parses each block
into AT Protocol's facet-based richtext model, where a block survives only if it
has non-empty text (like a link) or is a standalone image. An image wrapped
in a link contributes no text, so a "clickable poster"
([](media)) collapses to an empty block and is dropped entirely.
So media falls back to constructs that the reader's model actually keeps:
-
Images →
 -
Video / audio with a poster → the poster as a standalone image, followed by a separate labelled link to the media:
[label](media_url) -
Video / audio without a poster → a plain link
[label](media_url) -
Other files →
[label](url)link
This is a general rule keyed off the asset's media type, not a special case for any particular set.
The link label is the media asset's alt text when present; when it's empty
(common for talk media) it defaults to Watch the video / Listen to the audio
— a link with no label is itself empty text and would be dropped by the reader.
The poster convention (video/audio poster frames)
⚠️ Convention alert — this changes output based on a field handle.
Within a single Bard set, if an image asset's field handle contains
poster (e.g. poster, video_poster), it is treated as the poster frame
for a video/audio asset in the same set. It is rendered as that media's
standalone poster image (immediately above the media's link) rather than as a
separate image elsewhere in the set:
 [A talk slide](https://cdn.example.com/slide.mp4)
To opt out of this behavior, rename the field handle so it does not
contain poster (e.g. thumbnail, still). The image will then render as its
own standalone  image alongside the video's link.
If a poster-handled image has no video or audio in the same set to attach to,
it falls back to rendering as a normal image (it is never silently dropped).
Cover / feature images (auto-detected)
⚠️ Convention alert — this reads a separate field by handle and injects it into your content body.
Cover / feature images almost always live in their own blueprint field
(e.g. cover, feature_image), outside the content field. That field is
not part of the content body, so on its own it never reaches the document — and
that causes a subtle bug in how covers are chosen.
Why this needs a little magic. The site.standard.document lexicon does
have a dedicated coverImage field, but the dominant reader (Standard Reader)
does not use it first. It derives the hero/cover image from the first
image block of the content body, and only falls back to coverImage when the
body has no leading image. So without help, an early in-body image gets promoted
to the cover while your real cover image is missing entirely.
To fix this, the addon auto-detects your cover field and prepends it as the
first block of the Markdown content (and leads textContent with its alt
text):
 …your content body…
The real cover then becomes the hero everywhere — including image-first content like talk recordings — and any early body image is demoted to normal body flow instead of being mistaken for the cover.
Detection. The override field standard_site_cover_image wins if present;
otherwise the first of these handles that exists on the blueprint and has a
value is used:
cover, cover_image, feature_image, featured_image, hero, hero_image,
banner, og_image, thumbnail
(The bare image handle is deliberately not matched — too ambiguous. Use
the standard_site_cover_image override to point at a non-standard handle.) The
field is read through Statamic augmentation, so an assets field resolves to a
fully-qualified URL on any driver. To opt out, rename the field so it doesn't
match one of the handles above.
Why prepend to the body instead of the coverImage blob field? Two reasons:
- Standard Reader prioritizes a leading body image over
coverImage, so setting the blob would not fix image-first content anyway. - Populating both a leading body image and the
coverImageblob risks other consumers rendering the cover twice.
Prepending keeps a single source of truth that matches how the reader actually picks the hero. This is a deliberately conservative choice for a young lexicon: it does not alter the original form of your content, and if the ecosystem converges on different behavior, the approach can change and documents can simply be re-synced.
Set field handling
Each field inside a Bard set is resolved through its real Statamic fieldtype, so
this works for any set blueprint — nothing is hardwired to specific field
names (aside from the documented poster convention above):
assetsfields → resolved asset URL(s) with media-type dispatch (above)- Nested
bardfields (e.g. a slide description) → rendered as Markdown at full fidelity - Text/textarea/markdown fields → included as text
License
MIT