Skip to content

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:

  1. 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.
  2. No database dependency. Regular plugin activation state is stored in the active_plugins option. Mu-plugins load from the filesystem, making them reproducible across environments without database synchronization.
  3. Version control clarity. Everything in web/app/mu-plugins/ is committed and reviewed. Everything in web/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.php or any classic PHP template hierarchy files. All page layouts are defined as block templates in templates/*.html.
  • No header.php / footer.php. The header and footer are block template parts, editable in the Site Editor.
  • Block patterns in patterns/*.php provide reusable layout sections. WordPress auto-registers them from file header comments.
  • theme.json controls 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:

  1. 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.

  2. 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.

  3. 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.