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.
| Component | What it does |
|---|
| S3UploadsBootstrap | Configures humanmade/s3-uploads from FP_S3_* env vars. Refuses uploads when S3 isn’t fully configured (no silent local-disk fallback). |
| SouinInvalidator | DELs Souin’s Redis cache entries directly on save_post, clean_post_cache, switch_theme, etc. — bypasses cache-handler’s broken HTTP invalidation. |
| SiteHealth | Suppresses 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. |
| SMTPMailer | Wires 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
| Var | Default | Required |
|---|
FP_S3_BUCKET | — | ✓ |
FP_S3_KEY | — | ✓ |
FP_S3_SECRET | — | ✓ |
FP_S3_REGION | us-east-1 | |
FP_S3_BUCKET_URL | — | optional CDN URL — auto-sets WP_CONTENT_URL if undefined |
FP_S3_ENDPOINT | — | optional, 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_DISABLED | false | truthy → 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.
| Category | Hooks | Action |
|---|
| Post lifecycle | save_post, wp_trash_post, before_delete_post, deleted_post, clean_post_cache | DEL post URL + tag + home |
| Global-impact post types | save_post for wp_navigation, wp_block, wp_template, wp_template_part, wp_global_styles | invalidate_all |
| Comments | comment_post, transition_comment_status | DEL parent post |
| Term lifecycle | created_term, edited_term, delete_term | invalidate_all |
| User lifecycle | profile_update, user_register, deleted_user | DEL author archive + home |
| Site-wide | switch_theme, permalink_structure_changed, wp_update_nav_menu, customize_save_after, activated_plugin, deactivated_plugin | invalidate_all |
| Options | updated_option for blogname, home, siteurl, permalink_structure, sidebars_widgets, widget_* | invalidate_all |
Env vars
| Var | Default | Purpose |
|---|
FP_SOUIN_REDIS_HOST | redis | Redis hostname |
FP_SOUIN_REDIS_PORT | 6379 | Redis port |
FP_SOUIN_REDIS_PASSWORD | (empty) | Redis AUTH password |
FP_SOUIN_REDIS_DB | 0 | Logical database |
FP_SOUIN_REDIS_TIMEOUT | 1.0 | Connect timeout (seconds) |
FP_SOUIN_DISABLED | false | Truthy → 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
| Var | Default | Purpose |
|---|
FP_SMTP_HOST | (unset) | SMTP hostname (e.g. smtp.postmarkapp.com). Component is a no-op when unset. |
FP_SMTP_PORT | 587 | TCP port |
FP_SMTP_ENCRYPTION | tls | tls (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_DISABLED | false | Truthy → 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)