WordPress plugin that publishes posts to AT Protocol in both app.bsky.feed.post (Bluesky) and site.standard.document / site.standard.publication (standard.site) formats via native OAuth.
Tech stack: PHP 8.2+, WordPress 6.5+, wp-env for local dev, PHPUnit for tests, Jetpack Changelogger for changelog management.
Do NOT:
- Edit WordPress core files
- Hardcode new version numbers in changelog messages
- Create a PR without running
composer lintandnpm run env-testfirst - Create a PR without a changelog entry in
.github/changelog/or a "Skip Changelog" label
atmosphere.php # Main plugin file.
includes/
├── class-*.php # Core classes (Atmosphere, API, Publisher, Backfill, Handle, Post_Types, Reaction_Sync, Connectors).
├── functions.php # Helper functions.
├── content-parser/ # Pluggable content formats for site.standard.document (interface only by default).
├── oauth/ # OAuth flow (Client, Client_Authentication, DPoP, Encryption, Resolver, Nonce).
├── rest/ # REST controllers (public + admin-only under rest/admin/).
├── transformer/ # AT Protocol record transformers (Post, Document, Publication, Comment, Facet, TID).
└── wp-admin/ # Admin UI (settings page, sidebar panel, posts-list column).
integrations/ # Plugin-specific content-parser integrations (stubs).
templates/ # PHP template files.
assets/ # CSS, JS, and images.
src/ # Source for the wp-scripts build (editor panels, reactions block, connectors card, settings typeahead).
├── editor-plugin/ # Document sidebar share-to-Bluesky panel.
├── pre-publish-panel/ # Pre-publish share status panel.
├── reactions/ # Bluesky likes/reposts block.
├── connectors-card/ # Connectors screen connect/disconnect card.
├── settings-connect/ # Settings page connect-field typeahead enhancement.
└── shared/ # Shared JS reused across the above (incl. the handle typeahead component).
tests/
└── phpunit/ # PHPUnit tests.
# Environment
npm run env-start # Start WordPress at http://localhost:8884.
npm run env-stop # Stop WordPress environment.
# PHP tests (require wp-env running)
npm run env-test # All PHP tests.
npm run env-test -- --filter=pattern # Tests matching pattern.
# Local tests (require MySQL)
composer test # Full test suite.
vendor/bin/phpunit --filter=pattern # Matching tests.
# Code quality
composer lint # Check PHP coding standards (PHPCS).
composer lint:fix # Auto-fix PHP issues.
# Changelog
composer changelog:add # Add a changelog entry.
composer changelog:write # Write entries to CHANGELOG.md.
# Release
npm run release # Interactive release: bumps version, regenerates CHANGELOG/readme.txt, pushes a release/X.Y.Z PR.- Custom autoloader, NOT Composer. Runtime classes are loaded by
includes/class-autoloader.php(PSR-4-ish with WordPressclass-{name}.phpfilenames);composer.json'sautoloadis empty.composer install/updateonly refreshes dev dependencies (PHPUnit, PHPCS, Changelogger) and cannot break the running plugin. New classes work as soon as the file is in place — nocomposer dump-autoloadstep. - Changelog entries MUST be end-user friendly and end with punctuation. Users see these in the WordPress update screen. Describe what changed from their perspective — no jargon, class names, or method names.
Files: class-{name}.php
Namespaces: Atmosphere, Atmosphere\{OAuth,Transformer,WP_Admin}
Text domain: always 'atmosphere'.
MUST backslash-prefix all WordPress/PHP global functions in namespaced code: \get_option(), \add_action(), \apply_filters(), \strlen(), \time(), etc.
MUST use use imports for cross-namespace references — no inline \Namespace\Class.
MUST build any front-end-displayed Bluesky/appview web link (profile, post, hashtag, mention) through Atmosphere\appview_url( $path, $context ) in includes/functions.php — never hardcode https://bsky.app/.... The helper centralizes the host and makes it filterable (atmosphere_appview_host / atmosphere_appview_url). Keep escaping at the call site (\esc_url() for HTML, \esc_url_raw() for storage); the helper returns an unescaped URL on purpose. This applies to display links only, not to AT Protocol records being published.
Uses the custom Atmosphere\Autoloader in includes/class-autoloader.php, which respects WordPress filename conventions (class-foo.php, lowercase, hyphenated). composer.json declares an empty autoload block — Composer is only used for dev tooling (PHPUnit, PHPCS, Changelogger). Helper functions in includes/functions.php are loaded via a direct require_once from atmosphere.php.
Tests use namespace Atmosphere\Tests and extend WP_UnitTestCase. Use @group annotations to categorize tests (@group atmosphere, @group transformer).
Test files live in tests/phpunit/tests/ mirroring includes/ structure. Files are prefixed class-test-.
Transformers — Convert WordPress content into AT Protocol records. Extend Atmosphere\Transformer\Base. Each transformer defines transform(), get_collection(), and get_rkey(). See includes/transformer/.
OAuth — Full PKCE + DPoP + PAR native OAuth flow. Handle → DID → PDS → Auth Server resolution chain. See includes/oauth/.
API Client — DPoP-authenticated PDS requests with automatic nonce retry. See includes/class-api.php.
Publisher — Atomic batch applyWrites for both bsky post + standard.site document. See includes/class-publisher.php.
Well-known endpoints — Rewrite rules + template_redirect handlers in Atmosphere class serve /.well-known/atproto-did (domain handle verification) and /.well-known/site.standard.publication (publication AT-URI). All share the atmosphere_wellknown query var.
Connectors API — Progressive-enhancement registration on the WordPress 7.0 Settings → Connectors screen. Atmosphere\Connectors (includes/class-connectors.php), guarded throughout by class_exists( 'WP_Connector_Registry' ).
- Registration & card. Registers an
atmosphereconnector onwp_connectors_initwithauthentication.method => 'none'(core only auto-renders a UI forapi_keyconnectors) and ships a script module (src/connectors-card/) that renders the connect/disconnect card. The card drives the flow throughAtmosphere\Rest\Admin\Connection_Controller(authorize/disconnect), reusing the OAuthClient. - Two screen URLs. Core's top-level
options-connectors.php(Connectors::SCREEN) vs. the Gutenberg plugin's Settings submenu (options-general.php?page=options-connectors-wp-admin). Both matched via the sharedConnectors::SCREEN_SLUGmarker:Connectors::is_connectors_screen()gates the card enqueue;Connectors::screen_url()resolves the OAuth return destination. - Return destination is server-side only. The card sets a boolean
atmosphere_oauth_from_connectorstransient — no URL crosses the wire.Admin::handle_oauth_callback()derives the destination fromscreen_url()(which reads the registered admin$submenu), so nothing external can steer thewp_safe_redirect(). - Reconnect destination. Every reconnect prompt (the admin notice, both editor surfaces, Site Health) resolves through
Atmosphere\reconnect_url(): the settings page while visible, else the Connectors screen when one exists, else nothing. Theatmosphere_reconnect_urlfilter runs last, so a host plugin with its own connect UI can redirect all four at once; returning''drops the link and leaves plain text. - Handle typeahead. The card's handle field uses the
atmosphere_handle_typeahead_urlfilter (defaultpublic.api.bsky.app;''disables). Shared with the plugin's own Settings connect field viaAtmosphere\handle_typeahead_url()andsrc/shared/handle-typeahead.js.
Icon library — Progressive-enhancement registration with the WordPress 7.1 icon library. Atmosphere\Icons (includes/class-icons.php), gated on function_exists( 'wp_register_icon_collection' ) (same shape as Connectors; the plugin's floor is 6.5). Registers the atmosphere collection with the ATmosphere mark and the Bluesky butterfly, monochrome, from assets/svg/. Those SVGs MUST stay within the library sanitizer's allow-list (svg/path/polygon elements only, no stroke); a test pins that property, so redraw assets accordingly.
Embedding as a connection layer — one filter lets a host plugin reuse just the connection:
atmosphere_connection_only_mode(viaAtmosphere\is_connection_only_mode(), defaultfalse) forces cross-posting, reaction sync, reply sync, comment publishing, andsite.standard.publicationupkeep off, unschedules the reaction-sync crons, and hides the plugin's own settings page. The settings page stays registered (add_options_page) because the OAuth callback lands on its URL, but drops from the menu. The reauth notice still renders in this mode, pointing its reconnect link at the Connectors screen when one exists.- Layered gating. Each behavior resolves through a dedicated helper (
is_auto_publish_enabled(),is_reaction_sync_enabled(),is_reply_sync_enabled(),is_comment_publishing_enabled(),is_publication_sync_enabled()): read the stored option (via the sharedfeature_option_enabled()resolver, except publication which has no user option) → force off in connection-only mode → apply a per-feature filter last (atmosphere_should_auto_publish,atmosphere_should_sync_publication, etc.), so a host can re-enable a single lane. Settings-page visibility (Admin::is_settings_page_visible()) has no separate override — it simply follows connection-only mode. All behavioral call sites route through these helpers; only the settings form fields read the raw option.
README.md — public-facing repo entry point (lean: intro + docs links).
readme.txt — WordPress.org plugin readme (end-user friendly description, FAQ, changelog).
docs/developer-docs.md — developer entry doc; index over the rest of docs/.
docs/development-environment.md — wp-env setup, prerequisites, troubleshooting, coverage.
docs/php-coding-standards.md — naming, escaping, error handling, performance, cron rules.
docs/php-class-structure.md — directory layout, namespaces, architectural patterns.
docs/code-linting.md — PHPCS rules and common fixes.
docs/pull-request.md — branching, pre-PR checklist, commit format, special situations.
docs/release-process.md — `npm run release`, patch releases, GitHub Release UI.
docs/translations.md — text domain, GlotPress, translator-friendly strings.
docs/content-formats.md — survey of AT Protocol `content` types for site.standard.document.
docs/org.wordpress.html.md — Lexicon for the org.wordpress.html content type.
integrations/README.md — registering custom Content_Parser implementations from third-party plugins.
.github/PULL_REQUEST_TEMPLATE.md — PR template (changelog block + testing instructions).
The skills under .agents/skills/ are quick-references that link into these docs. Update the docs when conventions change; the skills inherit the change.
npm run releaseThe release script (bin/release.js) does all of the version bookkeeping in one step:
- Runs
composer changelog:writeto roll up.github/changelog/entries intoCHANGELOG.md(the next semver version is inferred from the entries' significance). - Creates a
release/X.Y.Zbranch. - Updates the version in
atmosphere.php(header +ATMOSPHERE_VERSION),readme.txt(Stable tag), andpackage.json. - Mirrors the new changelog section into
readme.txt(same major-version history, with a link to the full GitHubCHANGELOG.md). - Replaces
@since unreleased/@deprecated unreleasedand the equivalent_deprecated_*/_doing_it_wrongliterals in PHP files with the new version. - Optionally prompts for an Upgrade Notice.
- Pushes the branch and opens a PR titled
Release X.Y.Zagainsttrunk.
The .gitattributes file controls what's excluded from git archive exports and GitHub release tarballs.
Skills are complex procedures loaded on demand. Canonical files live in .agents/skills/; .claude/skills/*/SKILL.md are 1-line stubs that point at them so both Claude Code and other agents pick up the same instructions.
| Skill | Use when… |
|---|---|
| code-style | Writing PHP code, creating classes, implementing hooks. |
| dev | Setting up environment, running tests, linting. |
| test | Writing tests, debugging failures, test patterns. |
| pr | Creating or reviewing pull requests. MUST invoke before any PR creation. |
| release | Creating releases, bumping versions, managing changelogs. |
| Agent | Trigger |
|---|---|
| code-review | Auto-invoked before PR creation to review changes. |
| spec-check | Audit against AT Protocol and standard.site Lexicon specs. |
| security-audit | Audit for OAuth bypass, SSRF, token leakage, XSS, and DPoP issues. |