Skip to content

Latest commit

 

History

History
288 lines (219 loc) · 14.7 KB

File metadata and controls

288 lines (219 loc) · 14.7 KB

ATmosphere Plugin Developer Documentation

Table of Contents

Introduction

This documentation is for developers who want to extend, integrate with, or build on the ATmosphere plugin — whether you're writing a companion plugin, adding a content parser for the site.standard.document content union, or hooking into the publish / reaction pipeline.

If you're contributing to ATmosphere itself, start with AGENTS.md for repository conventions.

Where to Start

Public Hooks

ATmosphere exposes a small set of filters and actions for plugins to extend behaviour. The full catalog with signatures lives in docs/php-coding-standards.md → Hook Patterns. The most commonly used:

Hook Type Use
atmosphere_content_parser filter Deprecated parser hook; use Content_Parser\Registry::register() instead.
atmosphere_document_content filter Last-chance modification of the parsed content object.
atmosphere_document_links filter Add a typed links union to site.standard.document records.
atmosphere_document_labels filter Add standard self-labels to site.standard.document records.
atmosphere_document_contributors filter Add contributor metadata to site.standard.document records.
atmosphere_publication_labels filter Add standard self-labels to site.standard.publication records.
atmosphere_publication_show_in_discover filter Override preferences.showInDiscover (defaults to the site's blog_public option) for site.standard.publication records.
atmosphere_syncable_post_types filter Add or remove post types eligible for cross-posting.
atmosphere_should_publish_comment filter Customise which approved comments are mirrored as Bluesky replies.
atmosphere_should_sync_reply filter Customise which inbound Bluesky replies become WordPress comments.
atmosphere_transform_bsky_post filter Mutate the Bluesky post record before write.
atmosphere_transform_document filter Mutate the document record before write.
atmosphere_transform_publication filter Mutate the publication record before write.
atmosphere_atproto_preview_transformers filter Add a transformer to the ?atproto={$type} preview for posts and the front page.
atmosphere_appview_host filter Point Bluesky web links at an alternative AT Protocol appview (host or subpath).
atmosphere_appview_url filter Rewrite the whole assembled appview link, including its route.
atmosphere_publish_post_result action React to a post-publish outcome (success or WP_Error).
atmosphere_publish_comment_result action React to a comment-publish outcome.
atmosphere_reaction_synced action React when a Bluesky reaction is stored as a WordPress comment.

When adding a new public hook, mark its @since tag as unreleased — the release script rewrites it (see Release Process → Marking Unreleased Code).

Pointing Bluesky links at another appview

Rendered links to Bluesky (profiles, hashtags, mentions, posts) default to the bsky.app web appview. Two filters let you redirect them, depending on how much you need to change.

Both filters pass up to three arguments. As with any WordPress filter, register with $accepted_args = 3 if your callback needs $path and $context:

  • $path — the path being built, e.g. profile/<did> or hashtag/<tag>.
  • $context — array with the available parts: type (one of profile, post, mention, hashtag), did, handle, rkey, tag.

atmosphere_appview_host — swap the host (or subpath)

Use this when the alternative appview mirrors bsky.app's routes (/profile/..., /hashtag/...) and you only need to change where they live. The first argument is the default host, 'bsky.app'.

The returned value can be a bare host, a host on a subdomain, or a host with a path prefix, with or without a scheme or trailing slash — it's normalized before use, so an appview hosted on a subpath works cleanly:

// Bare host.
add_filter( 'atmosphere_appview_host', fn() => 'deer.social' );

// Appview living on a subpath: yields https://something.social/atblue/profile/<did>.
add_filter( 'atmosphere_appview_host', fn() => 'something.social/atblue' );

// Route by context: send profiles elsewhere, keep hashtags on bsky.app.
add_filter(
	'atmosphere_appview_host',
	function ( $host, $path, $context ) {
		return 'hashtag' === ( $context['type'] ?? '' ) ? $host : 'deer.social';
	},
	10,
	3
);

atmosphere_appview_url — rewrite the whole link

Use this when the appview's routes differ from bsky.app's — for example /account/<did> instead of /profile/<did>, or a custom hashtag route. The first argument is the fully assembled URL (after the host filter has run); rebuild it from $context and return a complete URL:

// Custom profile route: /account/<did> instead of /profile/<did>.
add_filter(
	'atmosphere_appview_url',
	function ( $url, $path, $context ) {
		if ( 'mention' === ( $context['type'] ?? '' ) || 'profile' === ( $context['type'] ?? '' ) ) {
			return 'https://my.appview/account/' . ( $context['did'] ?? $context['handle'] ?? '' );
		}
		return $url;
	},
	10,
	3
);

Of note: links rendered on the fly (facet mentions, hashtags, and the "View on Bluesky" link) pick up the filters on every render, so changing them updates immediately. The author and source links stored on synced reaction comments are resolved once at sync time, so they keep whichever host was in effect when the comment was synced.

Extending Standard.site metadata

ATmosphere emits the core site.standard.publication and site.standard.document fields from WordPress data. Optional Standard.site fields that do not have a native WordPress source are extension points.

Document metadata filters:

add_filter(
	'atmosphere_document_links',
	static fn( $links, \WP_Post $post ) => array(
		'$type' => 'example.document.links',
		'items' => array(
			array( 'uri' => 'https://example.com/source' ),
		),
	),
	10,
	2
);

add_filter(
	'atmosphere_document_labels',
	static fn() => array(
		'$type'  => 'com.atproto.label.defs#selfLabels',
		'values' => array(
			array( 'val' => 'adult' ),
		),
	)
);

add_filter(
	'atmosphere_document_contributors',
	static fn( $contributors, \WP_Post $post ) => array(
		array(
			'did'         => 'did:plc:editor123',
			'role'        => 'editor',
			'displayName' => 'Jane Editor',
		),
	),
	10,
	2
);

Publication metadata filters:

add_filter(
	'atmosphere_publication_labels',
	static fn() => array(
		'$type'  => 'com.atproto.label.defs#selfLabels',
		'values' => array(
			array( 'val' => 'adult' ),
		),
	)
);

// `showInDiscover` defaults to the site's `blog_public` option; force it
// off (or return null to omit the preference entirely) regardless.
add_filter( 'atmosphere_publication_show_in_discover', '__return_false' );

The field-specific filters run before atmosphere_transform_document and atmosphere_transform_publication, so a final record-level filter can still inspect or override the complete record.

ATmosphere models one root publication per WordPress site. It verifies that publication at /.well-known/site.standard.publication and does not currently implement Standard.site's non-root publication verification path (/.well-known/site.standard.publication/path/to/publication). Social Standard.site lexicons such as site.standard.graph.subscription and site.standard.graph.recommend are also out of scope for the plugin's publishing flow; ATmosphere requests explicit repo: scopes only for app.bsky.feed.post, site.standard.document, and site.standard.publication, and intentionally keeps the documented include:site.standard.authFull permission set for Standard.site compatibility even though it does not publish or manage social records itself.

Previewing AT Protocol Records

Append ?atproto to a URL while logged in to see the JSON records ATmosphere would publish, without writing anything. Post previews require the edit_post capability for that specific post (the same gate as the block-editor panel); the front-page publication preview requires edit_posts:

URL Returns
?atproto on a post The site.standard.document record (default).
?atproto=app.bsky.feed.post on a post The Bluesky record(s) — a single post or a thread.
?atproto / ?atproto=site.standard.publication on the front page The site-level site.standard.publication record.
?atproto=all Every record family for that view, keyed by its lexicon $type.
?atproto={unknown} A 400 JSON error listing the supported selectors.

Each selector is the lexicon NSID of a transformer (Atmosphere\Transformer\Base). The preview reuses the same transformers as the publish path, so what you see is what would be written. That includes the document strongRef in a long-form Bluesky record's associatedRefs — its CID is computed from the previewed document record, exactly like the publish path computes it. One caveat: on a post that has never been published to the PDS the ref is omitted, because its rkey is only reserved when the post is first published; it appears once the post has been published.

Adding your own lexicon to the preview

The atmosphere_atproto_preview_transformers filter receives the transformers offered for the current view and the queried post (null on the front page). Append any Base subclass; it becomes available under ?atproto={its-collection-nsid} and in ?atproto=all automatically — its get_collection() NSID is the selector, and get_preview_records() (which defaults to a single transform(), overridden when a post fans out into multiple records) supplies the JSON. get_preview_records() must be read-only — it runs on a GET request, so it must not upload blobs, write meta, or reserve rkeys.

add_filter(
	'atmosphere_atproto_preview_transformers',
	static function ( array $transformers, ?\WP_Post $post ): array {
		// Only offer this preview on singular posts.
		if ( $post instanceof \WP_Post ) {
			$transformers[] = new My_Plugin\Example_Transformer( $post );
		}

		return $transformers;
	},
	10,
	2
);
class Example_Transformer extends \Atmosphere\Transformer\Base {

	public function transform(): array {
		return array(
			'$type'  => 'com.example.document',
			'postId' => $this->object->ID,
		);
	}

	public function get_collection(): string {
		return 'com.example.document'; // The ?atproto selector.
	}

	public function get_rkey(): string {
		return (string) $this->object->ID;
	}
}

Entries that are not Base instances are ignored, and a filter that returns a non-array falls back to the built-in transformers — so a malformed filter return cannot break the endpoint. A transformer whose get_collection() matches a built-in NSID supersedes that built-in for the request, mirroring how Content_Parser\Registry::register() lets a registration override a default.

Extending Content Formats

The site.standard.document record's content field is a singular open union of typed content objects (see docs/content-formats.md). ATmosphere ships built-in parsers for HTML, Markpub, Leaflet, and pckt formats, and integrations can register additional parsers.

To provide a parser:

  1. Implement Atmosphere\Content_Parser\Content_Parser (defined in includes/content-parser/interface-content-parser.php), or extend Atmosphere\Content_Parser\Parser_Base for WordPress/block helpers.
  2. Register the parser with Atmosphere\Content_Parser\Registry::register( $parser, $priority ).
  3. Optionally expose applies_to( \WP_Post $post ): bool so the registry can skip posts the parser cannot represent.

The deprecated atmosphere_content_parser filter remains for existing integrations. A returned parser still wins over the registry, and null still suppresses the content field, but using the filter emits a deprecation notice.

The Content format setting is a preference, not an absolute guarantee for every post. When the selected parser does not apply or cannot safely represent a post, the registry falls back to the next applicable parser, normally rendered HTML.

A complete worked example (with class-load.php registration) is in integrations/README.md.

Custom Post Type Support

ATmosphere only cross-posts post types that opt in. Two ways to add one:

Per-site option

\update_option( 'atmosphere_support_post_types', array( 'post', 'product' ) );

Native theme/plugin support

\add_post_type_support( 'product', 'atmosphere' );

Filter override

\add_filter(
    'atmosphere_syncable_post_types',
    static function ( array $types ): array {
        $types[] = 'event';
        return $types;
    }
);

The plugin merges all three sources, dedupes, and sanitises.

Templates and Admin UI

ATmosphere's admin screens render from templates/. The settings page is rendered from a single template; the editor sidebar panel is a React surface registered through class-admin.php. There is currently no public template-override mechanism — file an issue if you have a use case that requires one.

Reporting Issues

Bugs and feature requests: GitHub Issues.

Security issues: see the project's security disclosure policy in README.md.