Architecture¶
This page explains the structural decisions behind the Naluma WordPress project --- why the codebase is organized the way it is, how the pieces fit together, and what constraints shaped those choices.
Bedrock: Why Not a Standard WordPress Install?¶
A vanilla WordPress installation mixes application code, configuration, third-party plugins, and WordPress core into a single directory tree. That makes it difficult to version-control cleanly, manage dependencies reproducibly, or deploy safely.
Bedrock restructures WordPress to treat it as a proper application:
tinnitus-wordpress/
config/
application.php # Central config (replaces wp-config.php)
environments/
development.php # Dev-only overrides
staging.php # Staging overrides
web/ # Document root (Nginx points here)
app/ # Application content (replaces wp-content/)
mu-plugins/ # Custom must-use plugins (committed to git)
plugins/ # Composer-managed plugins (gitignored)
themes/naluma-theme/ # FSE block theme
wp/ # WordPress core (Composer-managed, gitignored)
composer.json # All PHP dependencies
.env # Environment variables (never committed)
The key separations are:
Configuration vs. code. All environment-specific values (database credentials, URLs, API keys) live in .env files and flow through config/application.php. Per-environment overrides sit in config/environments/. This means the same codebase deploys identically to local Docker, Kinsta staging, and Kinsta production --- only the .env changes.
WordPress core vs. application code. WordPress itself lives in web/wp/ and is managed as a Composer dependency (roots/wordpress). It is gitignored. This means WordPress core updates are a Composer version bump, not a manual file replacement, and developers never accidentally modify core files.
Committed code vs. vendor code. Third-party plugins in web/app/plugins/ are Composer-managed and gitignored, just like vendor/ in any PHP project. Only must-use plugins and the theme are committed to the repository.
Why this matters
This separation means every dependency --- WordPress core, ACF Pro, Polylang, Rank Math, and all Performance Lab modules --- has a pinned version in composer.json. A fresh clone followed by composer install produces an identical environment every time.
Must-Use Plugins: The Custom Code Layer¶
WordPress offers two plugin types: regular plugins (can be deactivated from WP Admin) and must-use plugins (always loaded, no deactivation toggle). Naluma uses mu-plugins for all custom business logic:
| Mu-Plugin | Responsibility |
|---|---|
content-types.php |
CPT and taxonomy registration |
acf-field-groups.php |
ACF field definitions (code, not GUI exports) |
tinnitus-rest-api/ |
Custom REST API for the Flutter app |
cors-headers.php |
CORS configuration for cross-origin API access |
security-hardening.php |
XML-RPC, user enumeration, registration locks |
polylang-customizations.php |
Language configuration, hreflang filters |
tinnitus-blocks/ |
Server-rendered Gutenberg blocks |
author-profiles.php |
E-E-A-T author credential fields |
cornerstone-redirect.php |
301 redirects for cornerstone taxonomy archives |
performance-hints.php |
Preconnect and dns-prefetch resource hints |
rankmath-polylang-compat.php |
Rank Math + Polylang integration bridge |
relevanssi-fse-compat.php |
Relevanssi search for FSE theme |
tinnitus-ai-translate/ |
AI-powered translation (OpenAI/Anthropic) |
The reasoning behind mu-plugins for custom code rather than a regular plugin:
- Guaranteed loading. Content types, field groups, and API routes must always be active. A regular plugin can be accidentally deactivated, breaking the entire content model.
- No database dependency. Regular plugin activation state is stored in the
active_pluginsoption. Mu-plugins load from the filesystem, making them reproducible across environments without database synchronization. - Version control clarity. Everything in
web/app/mu-plugins/is committed and reviewed. Everything inweb/app/plugins/is Composer-managed. The distinction is visible at the directory level.
The bedrock-autoloader mu-plugin enables subdirectory mu-plugins (like tinnitus-rest-api/ and tinnitus-blocks/) to be loaded automatically --- standard WordPress only auto-loads single-file mu-plugins.
FSE Block Theme: No Classic PHP Templates¶
The naluma-theme is a standalone Full Site Editing (FSE) block theme, originally forked from Twenty Twenty-Five but now independent (no parent theme dependency). This means:
- No
index.php,single.php,archive.phpor any classic PHP template hierarchy files. All page layouts are defined as block templates intemplates/*.html. - No
header.php/footer.php. The header and footer are block template parts, editable in the Site Editor. - Block patterns in
patterns/*.phpprovide reusable layout sections. WordPress auto-registers them from file header comments. theme.jsoncontrols design tokens (colors, typography, spacing) and block-level style defaults.
The theme consumes design tokens from the Naluma Design System (naluma-design-system repository). Token values flow from build/wordpress/theme-tokens.json into theme.json, ensuring visual consistency across the website, email templates, and mobile app.
Translatable text in FSE
FSE .html template files cannot use PHP translation functions. User-visible text must go through PHP patterns (static text), shortcodes (dynamic text), or custom blocks (complex rendering). This is a core architectural constraint --- see the Multilingual how-to guide for the decision rule.
System Architecture¶
The WordPress site does not exist in isolation. It serves as both a public marketing website and a headless CMS consumed by the Flutter mobile app and the automated content pipeline.
graph TD
subgraph "tinnitus-wordpress"
Config["config/<br/>Environment config"] --> WP["WordPress Core<br/>(web/wp/)"]
MuPlugins["mu-plugins/<br/>Custom business logic"] --> WP
Theme["naluma-theme/<br/>FSE block theme"] --> WP
Plugins["plugins/<br/>(Composer-managed)"] --> WP
end
Pipeline["content-automation<br/>(Python pipeline)"] -->|"REST API<br/>/wp-json/tinnitus/v1/"| WP
Flutter["Flutter App"] -->|"REST API<br/>/wp-json/tinnitus/v1/"| WP
CDN["Kinsta CDN<br/>(Cloudflare)"] --> WP
DesignSystem["naluma-design-system<br/>(tokens)"] -.->|"theme.json sync"| Theme
EmailAutomation["naluma-marketing-automation"] -->|"REST API<br/>(read-only)"| WP
REST API: The Integration Surface¶
The custom REST API under /tinnitus/v1/ is the primary integration point. It combines CPT data, ACF fields, and taxonomy terms into single responses optimized for consumers:
- Flutter app reads articles and research summaries for display in the mobile experience.
- Content automation pipeline (a separate Python project using Prefect and Anthropic APIs) writes content to WordPress via the standard WP REST API with authentication.
- Marketing automation reads article summaries and permalinks to populate email campaigns.
The custom API uses an abstract base controller (Tinnitus_REST_Content_Controller) with concrete implementations for articles and research, keeping the serialization logic DRY while allowing post-type-specific customization.
Read vs. write API split
Custom endpoints under /tinnitus/v1/ are read-only and publicly accessible (for the Flutter app). Content creation from the Python pipeline uses the standard WordPress REST API (/wp/v2/) with application password authentication. This separation keeps the public-facing API surface minimal.
Deployment Architecture¶
The project deploys to Kinsta's managed WordPress hosting:
| Branch | Environment | Trigger |
|---|---|---|
main |
Production | Merge to main triggers cd-production.yml |
staging |
Staging | Merge to staging triggers cd-staging.yml |
| Feature branches | Local only | CI runs on PRs to main |
Deployment is handled by a reusable GitHub Actions workflow (deploy.yml) that lints, builds frontend assets, and rsyncs files to Kinsta via SSH. The bedrock-disallow-indexing mu-plugin prevents search engines from indexing non-production environments.
Why These Choices?¶
The architecture reflects three priorities:
-
Reproducibility. Every dependency is Composer-managed, every configuration value is environment-driven, and every piece of custom code is version-controlled. A new developer can clone the repository, run
./scripts/setup.sh, and have an identical local environment. -
Separation of concerns. WordPress core, vendor plugins, custom code, configuration, and content are each isolated. Changes to one layer do not require changes to another --- upgrading WordPress is a Composer version bump, not a theme rewrite.
-
Multi-consumer readiness. The site was designed from the start to serve both a web frontend and a mobile app. The headless CMS architecture (FSE for web, REST API for mobile) avoids the need to maintain two separate content management systems.