Repository navigation
feat(standard-site): publish posts as site.standard.document records - #47
Merged
Merged
Conversation
Adds minimum-viable standard.site support: every shared post can now also produce a site.standard.document record on the user's PDS, with a back-reference to the bsky post we already write. The site itself gets a site.standard.publication record (kept in sync with WP settings) so other AT Protocol readers can discover and follow it. Backend - Utils: is_root_install() + is_standard_site_enabled() guard helpers. - Admin: register autoblue_publish_documents_enabled (boolean) and autoblue_publication_overrides (object) settings. - Meta: register autoblue_publish_document per-post meta on every enabled post type, defaulting from the global toggle. - Bluesky/API: add put_record (for in-place publication updates). - Standard\Publication: build, create, sync, and cache the site.standard.publication record on the connected PDS. Reactive hooks update on blogname/blogdescription/site_icon/override changes; fingerprint short-circuits no-op updates. - Standard\Document: build and write site.standard.document records per shared post, carrying title/path/description/textContent/tags/ coverImage/bskyPostRef. Reuses the blob ref from the Bluesky share for coverImage to avoid a duplicate upload. - Standard\Verification: serve /.well-known/site.standard.publication as plain-text AT-URI, inject <link rel="site.standard.document"> into singular post heads. - Bluesky::share_to_bluesky: after a successful bsky post, conditionally hand off to Standard\Document::publish. Document failures are logged but never roll back the bsky post. - Subdir installs (home_url() or site_url() not at root) fully disable the feature. Frontend - Settings: new 'standard.site' section with global toggle, per-field override inputs (name, description) showing live WP defaults as placeholders, read-only publication URL + AT-URI once published. Info notice on subdir installs. - Editor sidebar: 'Also publish as a standard.site document' toggle below 'Share to Bluesky', disabled with a contextual hint when the feature isn't available (subdir, global off, broken connection). - use-settings: useEntityProp for both new settings, with the same loading-spinner pattern we landed for post types. Tests - New Standard\PublicationTest, Standard\DocumentTest, Standard\VerificationTest (build_record shapes, override fallback, uri caching, head-link rendering). - UtilsTest extended with is_root_install + is_standard_site_enabled. - 63 tests / 136 assertions, all green; phpcs clean. Non-goals (deferred to future PRs) - OAuth + DPoP migration. - Subscriptions, recommends, threading, reaction sync, comment publishing. - Edit syncing of existing documents on post edits. - Cleanup of records on toggle-off.
Per-keystroke insta-save was jarring while typing. Local draft state in the Settings component holds the input values; an 'Update publication' button enables when the drafts differ from the saved overrides and saves once on click. After save, drafts re-sync from the entity prop so the button auto-disables. In the hook, replace setPublicationOverride(key, value) with savePublicationOverrides(overrides) which replaces the whole object in one save and shows a snackbar success.
When standard.site publishing is on for a post, write the document
FIRST and then embed it as the bsky post's record (instead of the
usual app.bsky.embed.external link card). Lets standard.site-aware
clients render the document inline; lets us see how Bluesky itself
renders a non-app.bsky.* record embed.
- Bluesky::share_to_bluesky: when should_publish_document_for is
true, publish the document up front (with bskyPostRef=null so we
don't reference a post that doesn't exist yet), then swap the bsky
post's embed for { $type: app.bsky.embed.record, record: { uri,
cid } } pointing at the freshly-written document. Falls back to
the existing link-card path if the document write returns a
WP_Error.
- Standard\Document::publish + build_record: accept null for
bsky_post_ref. The bskyPostRef field is omitted when null so the
record passes lexicon validation.
The link-card path remains for posts where standard.site is off.
Note: bskyPostRef on the document is intentionally not populated in
this experiment (would require a second putRecord after the bsky
post lands). If we keep this approach, we can add the back-fill.
Bluesky clients render non-app.bsky.* embedded records as 'Deleted' because they have no way to display the embedded record's content. The result is a worse UX than the plain link card. Restore the original publish order: bsky post first with the external link card, then Document::publish with the bsky strongRef. The Document::publish API keeps its optional bsky_post_ref parameter for future use (won't change anyone's existing callers).
Restore the document-first / record-embed path so we can keep poking at it. Revert this if we settle on the link-card design.
Match Atmosphere's design: bsky post stays a plain link card, document lives independently on PDS, and standard.site readers discover the document via the HTML <link> tag + the document's bskyPostRef field. Tried app.bsky.embed.record pointing to the document; Bluesky clients render non-app.bsky.* embedded records as "Deleted" so the UX is worse than the link card. The Document::publish API keeps its optional bsky_post_ref parameter in case it's useful later.
Four hex color overrides (background/foreground/accent/accentForeground)
in the publication settings card. Each gets parsed to {r,g,b} ints and
slotted into a site.standard.theme.basic structure on the publication
record. Empty values are omitted, so users can ship only the colors
they care about (or none). Atmosphere doesn't have this; Wireservice
does it the same way.
When a fresh Bluesky connection is added, search the user's PDS for a
site.standard.publication record whose 'url' field matches our
home_url(). If one's there, store its { uri, cid, rkey } so the next
publish reuses it instead of writing a duplicate.
- Bluesky/API: list_records helper (com.atproto.repo.listRecords).
- ConnectionsManager: fires autoblue/connection_added action after a
successful add or upsert. Carries the connection + is_reconnect flag.
- Standard/Publication: hooks the action; no-ops when we're already
linked to this DID. Match by exact untrailingslashit URL.
New 'Records' tab on the Autoblue settings page (shown only when
standard.site publishing is enabled) that fetches the publication
record + paginated document records from the user's PDS via
com.atproto.repo.listRecords.
- includes/Endpoints/StandardRecordsController.php: new REST endpoint
/autoblue/v1/standard/records returning { publication, documents,
cursor }. Cursor-paginated.
- includes/Setup.php: register the new route.
- src/js/components/records/: React component that renders the
publication card + a list of document cards, with Load more for
pagination.
- src/js/components/admin-page: conditional third tab driven by the
global toggle from useSettings.
Restructure the Records tab to use BaseControl with the section label outside the Card (consistent with the Settings tab). Render the publication icon and document coverImage thumbnails using Bluesky's public blob CDN (https://cdn.bsky.app/img/feed_thumbnail/plain/ {did}/{cid}@jpeg) so users can visually scan their PDS records. Featured images were already being written into the document record as coverImage; the browser just wasn't displaying them.
Backend already accepted icon_id in the overrides and Publication already resolved a blob from the override-or-site-icon attachment; the Settings UI just wasn't exposing a picker. - Assets: enqueue WP media library on the settings page; ship siteIconUrl (fallback) and iconOverrideUrl (current override) in initial state. - Settings: render the current icon (override > site icon > placeholder), MediaUpload button to pick a custom one, Remove button to fall back to the site icon. Wires through draftIconId + draftIconUrl alongside the other draft fields so the existing 'Update publication' button picks it up. - Adds wp-media-utils as an auto-discovered admin bundle dep.
@wordpress/media-utils' MediaUpload / MediaUploadCheck were undefined at runtime on this WP version, crashing the Settings tab. Replace with a small inline openIconPicker() that calls window.wp.media() directly (wp_enqueue_media() is already being called server-side). Drops the wp-media-utils dep from the admin bundle.
- Each record now has a 'View raw' link to pdsls.dev/{at-uri} so users
can inspect the underlying record.
- Documents with a bskyPostRef get a 'View on Bluesky' link that opens
the matching post in the Bluesky app.
- Refresh button at the top re-fetches publication + first page from
the PDS (useful right after publishing).
- Dates render relative via humanTimeDiff() from @wordpress/date with
the absolute timestamp in the title attribute on hover.
- Records tab gets max-width: 600px to match the Settings tab. - Each document renders in its own Card instead of all sharing one card with VStack spacing — easier to scan, more consistent with the rest of the admin UI. - Replace the title attribute on the relative date with Gutenberg's Tooltip component for a styled hover; <time> stays focusable for keyboard users.
Compact icon-only button using RefreshIcon from the local icons module, with the same .refreshing spin animation the Logs page uses.
Replaces the four hex TextControls with Gutenberg's __experimentalPaletteEdit component (with canOnlyChangeValues so the four named slots stay fixed — users can change colors but not add or remove slots). Each slot opens the standard WP color picker popover on click. Drafts still flow through the existing isPublicationDirty + Update publication button machinery.
Empty theme slots render as transparent in PaletteEdit (clean visual 'unset' state). On change, map transparent back to '' so the existing empty-string semantics in the backend keep working — transparent never gets persisted as an override.
New 'Customize publication colors' toggle replaces the always-visible palette. When the toggle is off, the four theme_* override keys are omitted from the saved overrides so the publication record has no basicTheme. When the toggle flips on, sensible defaults seed any empty slot (#ffffff / #111111 / #0073aa / #ffffff) and the PaletteEdit appears below. Saved drafts persist across toggle flips so a user can quickly toggle off, save, and toggle back on without losing their color picks until the next save. isDirty tracks the toggle state too so saving 'off' actually clears the theme on the next request.
… toggle Records: render publication.basicTheme as a row of ColorIndicators (background / foreground / accent / accent foreground) with a Tooltip on each showing the slot name and hex code. RGB → hex helper handles the byte clamping. Settings: wrap the Customize publication colors toggle in a BaseControl labelled 'Publication colors' so the section reads with the same hierarchy as the rest of the publication card.
Redundant with the 'Publication colors' BaseControl label above it.
- Move Publication URL (and AT-URI if present) up to sit right under the description, since URL is a derived identity field rather than an editable detail. - Wrap Publication icon and Publication colors in a .split flex container with flex: 1 1 240px on each child. Side-by-side on a 600px settings page; wraps to stacked when the container gets narrower than ~500px.
- Wrap PaletteEdit in a small div + scoped CSS that hides its built-in header HStack (an empty <h2> plus a 3-dot menu that's inert with canOnlyChangeValues). Cleaner visual. - Remove the read-only Publication AT-URI field from the Settings page. It's debug info that belongs to the Records tab, not the editing form.
… post
Match the pattern used by other standard.site publishers (e.g. Jake
Lazaroff): the bsky post stays a normal app.bsky.embed.external link
card so Bluesky users see the usual UX, but external.associatedRefs
carries com.atproto.repo.strongRef entries pointing at the
site.standard.document and site.standard.publication records on the
PDS. Standard.site-aware clients can follow the refs to discover the
long-form content; Bluesky-only users see no difference.
- Publication: new get_strongref() returns { uri, cid } from the
cached publication record without a network call.
- Bluesky::share_to_bluesky: when standard.site is enabled for the
post, publish the document up front (with bskyPostRef=null since we
don't have the bsky URI yet) and use it + the publication strongref
to populate associatedRefs on the embed. Falls back gracefully to a
plain link card if either record write fails.
Bluesky's standard.site renderer validates the publication's basicTheme strictly. We were: 1. Putting $type 'site.standard.theme.basic' at the top of basicTheme — wrong: that level shouldn't have a $type since the parent record declares the field type via ref. 2. Omitting $type on each individual color — wrong: each color needs '$type: site.standard.theme.color#rgb' as the union discriminator, because the color lexicon is an open union (could also be #hsl, etc.). With the bad shape, the publication record failed validation in Bluesky's appview, so posts that referenced it via associatedRefs rendered as a plain link card without the publication footer. Matches the actual shape used by other standard.site publishers (e.g. jakelazaroff.com).
…t lands After the bsky post is written we now follow up with a putRecord on the document, rebuilding the same record with bskyPostRef pointing at the freshly-created bsky strongRef. Lets standard.site-aware readers navigate from the document back to the bsky thread (comments, likes, reposts). - Standard\Document::update_bsky_ref: rebuilds the record (same publication_uri + cover blob + everything else) plus bskyPostRef and calls put_record at the existing rkey. Updates the cached CID on the post meta so future reads stay in sync. - Bluesky::share_to_bluesky: after a successful bsky post, when associated_refs were attached (i.e. document was published up front), call update_bsky_ref. Failures only log; the bsky share is already in the bag.
The back-fill writes a new document revision (new CID) AFTER the bsky post is already on PDS with the OLD doc CID in its associatedRefs[0]. Bluesky's appview is strict about the document strongRef CID matching the current record and refuses to render the publication footer when it's stale. Jake Lazaroff's working setup skips bskyPostRef entirely — it's optional per the lexicon. The Document::update_bsky_ref method stays in place (unused) in case we ever find a way to update both records atomically without invalidating the CID match.
…bskyPostRef back-fill The publication failed validation in Bluesky's renderer because we dropped the top-level '$type: site.standard.theme.basic' on the basicTheme object earlier. The spec shows both discriminators are required: $type on basicTheme itself AND $type on each color value. With the corrected basicTheme, the publication record validates, the publication footer renders, and the bskyPostRef back-fill on the document is safe to re-enable — Bluesky was rejecting the WHOLE publication, not the stale doc CID, so the back-fill wasn't actually the cause of the failure. Now matches the official site.standard.publication example exactly.
danielpost
force-pushed
the
feat/standard-site
branch
from
May 31, 2026 18:29
bde5e20 to
c15364a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
standard.site is a set of AT Protocol lexicons (
site.standard.*) for long-form publishing — your full article lives as a typed record on your PDS where any AT-aware reader/aggregator can discover it. Bluesky stays the social engagement layer; standard.site is the content layer.This PR adds the publishing half. When Autoblue shares a post to Bluesky, it now also writes a
site.standard.documentfor the full article (withbskyPostRefback to the Bluesky post) and ensures asite.standard.publicationexists for the site itself. Verification endpoints are served so AT clients can confirm the records belong to this domain.The work intentionally stays narrow — no OAuth (app-password sessions already authorize writes to any collection), no subscriptions/recommends/threading/edit syncing/reaction sync. The plan file under
/Users/danielpost/.claude/plans/documents the design decisions and explicit non-goals.Backend
Utils::is_root_install()/is_standard_site_enabled()— guard helpers.Admin— registersautoblue_publish_documents_enabled(boolean) andautoblue_publication_overrides(object) settings.Meta— registersautoblue_publish_documentper-post meta, looped over enabled post types (the pattern from feat(post-types): support sharing from additional public post types #46).Bluesky\API::put_record— for in-place publication updates viacom.atproto.repo.putRecord.Standard\Publication— build, create, sync, and cache the publication record on the PDS. Reactive hooks update onblogname/blogdescription/site_icon/ override changes; fingerprint short-circuits no-op updates.Standard\Document— build and write document records per shared post (title,path,description,textContent,tags,coverImage,bskyPostRef). Reuses the blob ref from the Bluesky share forcoverImageto avoid a duplicate upload.Standard\Verification— serves/.well-known/site.standard.publicationas plain-text AT-URI, injects<link rel="site.standard.document">into singular post heads.Bluesky::share_to_bluesky— after a successful bsky post, hands off toDocument::publishwhen the feature is on for that post. Document failures are logged but do not roll back the bsky post./.well-known/at the domain root).Frontend
useSettings—useEntityPropfor both new settings, with the same loading-spinner pattern landed in feat(post-types): support sharing from additional public post types #46.Test plan
vendor/bin/codecept run wpunit— 63 tests / 136 assertions, all green (includes newStandard\PublicationTest,Standard\DocumentTest,Standard\VerificationTest, plus extendedUtilsTest).npm run build— clean.vendor/bin/phpcs— clean.Scheduling share for post …Processing share for post …[Success] Shared post … to Bluesky: https://bsky.app/…[Success] Created standard.site publication record at at://…/site.standard.publication/3mn5fekab4e2f(one-time on first publish)[Success] Published standard.site document for post … at at://…/site.standard.document/3mn5feky6ws24GET /.well-known/site.standard.publicationreturns the plain-text publication AT-URI.<link rel="site.standard.document" href="at://…">.