Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt

Use this file to discover all available pages before exploring further.

mu-plugin is the WordPress-side glue for the FrankenPress stack. Four components, all platform-housekeeping — anything else (URL fixers, object cache, metrics, WC log handlers) is optional and is the consumer site’s responsibility.
ComponentWhat it does
S3UploadsBootstrapConfigures humanmade/s3-uploads from FP_S3_* env vars. Refuses uploads when S3 isn’t fully configured (no silent local-disk fallback).
SouinInvalidatorDELs Souin’s Redis cache entries directly on save_post, clean_post_cache, switch_theme, etc. — bypasses cache-handler’s broken HTTP invalidation.
SiteHealthSuppresses the Site Health tests whose failure is intentional under the immutable-image lockdown (background_updates, FS-write probes, plugin_theme_auto_updates) and adds a passing FrankenPress-branded test that explains why. Also adds an SMTP-reachability test when SMTPMailer is configured.
SMTPMailerWires the global PHPMailer to send via SMTP from FP_SMTP_* env vars. The runtime image ships no MTA, so without this every wp_mail() call fails silently. Transport-agnostic (Postmark, SendGrid, Mailgun, AWS SES, in-cluster relay). Opt-in: no-op when FP_SMTP_HOST is unset.
Composer name: frankenpress/mu-plugin. Latest release: v0.5.0.

S3UploadsBootstrap

Maps FP_S3_* env vars to humanmade/s3-uploads’ S3_UPLOADS_* constants and auto-loads the s3-uploads plugin from composer’s vendor / plugins dir. Refuses media uploads via wp_handle_upload_prefilter when required env vars are missing.

Why refuse rather than fall back?

In a containerized deploy, local disk is ephemeral (gone on pod restart) and inconsistent across replicas (write to pod A, read from pod B → 404). Silently writing uploads to local disk would produce broken behaviour that surfaces minutes-to-hours later. A hard upload failure surfaces the misconfiguration immediately.

Env vars

VarDefaultRequired
FP_S3_BUCKET
FP_S3_KEY
FP_S3_SECRET
FP_S3_REGIONus-east-1
FP_S3_BUCKET_URLoptional CDN URL — auto-sets WP_CONTENT_URL if undefined
FP_S3_ENDPOINToptional, for non-AWS S3-compatible (MinIO, R2, GCS XML)
FP_S3_OBJECT_ACL(empty — no ACL)optional S3 object ACL. Leave unset for buckets with Object Ownership = “Bucket owner enforced” (the AWS default since April 2023, ACLs disabled). Set to public-read / private / authenticated-read only for ACL-enabled buckets. See troubleshooting → 0-byte uploads.
FP_S3_DISABLEDfalsetruthy → skip the bootstrap entirely (local dev only — never in production)

Endpoint filter (v0.1.1)

humanmade/s3-uploads documents S3_UPLOADS_ENDPOINT but doesn’t apply it to the AWS SDK by itself — you have to register a filter. v0.1.1 of this plugin registers s3_uploads_s3_client_params automatically when the endpoint constant is defined, with use_path_style_endpoint: true (MinIO and most S3-compatibles don’t support virtual-host bucket addressing).

SouinInvalidator

Connects directly to Redis and DELs Souin’s cache entries on WordPress lifecycle events.

Hooks

The invalidator covers every WP write event whose output is templated into cached pages. Bounded changes invalidate the affected URL; changes with site-wide blast radius flush the entire HTTP cache. TTL expiry is the safety net for anything missed.
CategoryHooksAction
Post lifecyclesave_post, wp_trash_post, before_delete_post, deleted_post, clean_post_cacheDEL post URL + tag + home
Global-impact post typessave_post for wp_navigation, wp_block, wp_template, wp_template_part, wp_global_stylesinvalidate_all
Commentscomment_post, transition_comment_statusDEL parent post
Term lifecyclecreated_term, edited_term, delete_terminvalidate_all
User lifecycleprofile_update, user_register, deleted_userDEL author archive + home
Site-wideswitch_theme, permalink_structure_changed, wp_update_nav_menu, customize_save_after, activated_plugin, deactivated_plugininvalidate_all
Optionsupdated_option for blogname, home, siteurl, permalink_structure, sidebars_widgets, widget_*invalidate_all

Env vars

VarDefaultPurpose
FP_SOUIN_REDIS_HOSTredisRedis hostname
FP_SOUIN_REDIS_PORT6379Redis port
FP_SOUIN_REDIS_PASSWORD(empty)Redis AUTH password
FP_SOUIN_REDIS_DB0Logical database
FP_SOUIN_REDIS_TIMEOUT1.0Connect timeout (seconds)
FP_SOUIN_DISABLEDfalseTruthy → no-op the invalidator (cache then expires only by TTL)

Why direct Redis DEL?

Souin’s documented HTTP invalidation APIs (PURGE, POST-CRUD, the /api.souin/* admin endpoint) are broken in cache-handler v0.16.0:
  • PURGE returns 200 but caches the PURGE response itself instead of invalidating the GET.
  • POST likewise — caches the POST response.
  • The admin endpoint at /api.souin/... returns 404 even when the API basepath is configured.
Direct Redis manipulation is sub-millisecond, has no parser to break, and works against any Redis-compatible service (including DragonflyDB). Full investigation log: runtime/PHASE-0.md.

Failure mode

If ext-redis isn’t loaded, or the connection fails, or FP_SOUIN_DISABLED is set, the invalidator is a silent no-op. Errors are logged via error_log but never raised — a broken cache layer must not break WordPress itself.

SiteHealth

Tweaks WordPress’s Site Health screen so it reports signal, not noise, on a FrankenPress site. Removes the four lockdown-related tests (background_updates, update_temp_backup_writable, available_updates_disk_space, plugin_theme_auto_updates) — their failure is correct behaviour under the immutable-image lockdown — and adds a passing FrankenPress-branded test that explains the lockdown to anyone reading the dashboard. When FP_SMTP_HOST is set (i.e. SMTPMailer has been opted into), this component also registers a frankenpress_smtp_reachability Site Health test that does a 2-second TCP connect to the configured SMTP server. Failure produces a critical red badge on the wp-admin Site Health screen — the canonical “is my email actually working” check.

SMTPMailer

Reads FP_SMTP_* environment variables and hooks phpmailer_init to override the global PHPMailer instance with SMTP. Transport-agnostic — the same env contract works for Postmark, SendGrid, Mailgun, AWS SES, or any in-cluster relay (mailpit, mailhog, postfix-relay). For end-to-end recipes, see Operations → Email.

Env vars

VarDefaultPurpose
FP_SMTP_HOST(unset)SMTP hostname (e.g. smtp.postmarkapp.com). Component is a no-op when unset.
FP_SMTP_PORT587TCP port
FP_SMTP_ENCRYPTIONtlstls (STARTTLS), ssl (implicit TLS), none (local dev only)
FP_SMTP_USERNAME(unset)SMTP auth username
FP_SMTP_PASSWORD(unset)SMTP auth password
FP_SMTP_FROM_EMAIL(WP admin_email)wp_mail_from filter target
FP_SMTP_FROM_NAME(WP blogname)wp_mail_from_name filter target
FP_SMTP_DISABLEDfalseTruthy → bootstrap is a no-op. Local-dev escape hatch only. The Helm chart never sets this; chart-side smtp.enabled: false covers the same need by simply not injecting the env.

Failure mode

If FP_SMTP_HOST is unset, the component is a silent no-op and wp_mail() falls through to PHP’s mail() — which fails on the runtime image because no MTA is shipped. That’s the intentional default for sites that haven’t opted into SMTP yet. When the host is set but unreachable / auth fails, wp_mail() returns false and the failure is logged via error_log. The component never retries, queues, or falls back to mail() — surfacing the failure beats hiding it. The SiteHealth frankenpress_smtp_reachability test surfaces the same failure on the Site Health dashboard.

Plugin coexistence

A site that composer-installs a competing SMTP plugin (WP Mail SMTP, FluentSMTP, the official Postmark plugin) overrides SMTPMailer’s config — those plugins run as regular plugins after must-use plugins and re-hook phpmailer_init at the same priority. Last-writer wins. Correct behaviour: the user opted into the plugin explicitly; we don’t fight them.

Install

composer require frankenpress/mu-plugin
site-template ships this dep preconfigured. The plugin lands at web/app/mu-plugins/mu-plugin/ and is auto-loaded by roots/bedrock-autoloader via the 00-stack.php loader.

Local testing

The package ships PHPUnit unit tests using Brain Monkey (WP function stubs) and Mockery (Redis client mocks). No real Redis or WP install needed:
composer install
composer test     # phpunit
composer lint     # phpcs (WordPress-Core ruleset)
composer stan     # phpstan level 6
composer ci       # all four checks (lint + stan + test + audit)