Local WordPress stack (Docker Compose) with wp-cli auto-install and GCA Intranet themes.
This repo also includes an AWS-friendly WordPress container build for deployment to EC2 / ECS-style environments.
- WordPress runs from the official WordPress Docker image
- WordPress core is not committed to Git
Custom themes and plugins live under:
wp-content/themes/gca-intranet-foundation(parent theme)wp-content/themes/gca-intranet(child theme)wp-content/plugins/gca-custom
Note: The entire local ./wp-content folder is bind-mounted to the container locally. If you install a new plugin via the WordPress Admin GUI, it will sync to your local machine so it can be committed to Git.
- MySQL 8.0 for local development (via Docker Compose)
A wp-cli container handles the one-time WordPress setup:
- Creates
wp-config.php - Installs WordPress
- Activates the GCA theme
- Sets permalinks
A Dockerfile is included for AWS container platforms.
In AWS you would typically use RDS MySQL.
DB credentials should be provided via environment variables or secrets.
- Docker Desktop (or Docker Engine) installed and running
- Git
- No local PHP, Node.js, or npm required (Docker handles all dependencies).
Copy the example file and adjust values if needed:
cp .env.example .envCommon values you may want to change:
WP_URLWP_PORT- Admin username / password
- Important: Ensure
WP_HOMEandWP_SITEURLexplicitly match your URL and port (e.g.,http://localhost:8090) to prevent WordPress redirect loops.
If you're on local you need to pass in the compose file to use: docker-compose.local.yml.
LOCAL build:
docker compose -f docker-compose.local.yml up -d --buildSERVER build:
docker compose --env-file .env up -d --build(Optional) Confirm services are running:
docker compose psNote: the first run can take a short while while MySQL initialises.
Run the wp-cli bootstrap container. This is safe to re-run — it will skip steps if WordPress is already installed.
docker compose --env-file .env run --rm wpcliThis will:
- Ensure WordPress core exists
- Create
wp-config.php(if missing) - Install WordPress
- Activate the GCA theme
- Set permalink structure
- Site:
http://localhost:8080(or whateverWP_PORTis set to) - Admin:
http://localhost:8080/wp-admin
Admin credentials are defined in .env:
WP_ADMIN_USERWP_ADMIN_PASSWORD
If things get into a bad state, do a full reset:
docker compose down -v
docker compose --env-file .env up -d --build
docker compose --env-file .env run --rm wpcliThis removes all local data and re-initialises WordPress.
The gca-feature-flags plugin provides a lightweight feature-flag system for toggling functionality without code deploys.
- Flags are registered in code (in the theme) and toggled via the WordPress Admin UI.
- The Admin UI lives at WP Admin → Settings → Feature Flags.
- Each flag has a label, optional description, optional tags, and a default on/off state.
- Flags that have never been saved in the admin fall back to their registered
default.
wp-content/plugins/gca-feature-flags/gca-feature-flags.php
This plugin is committed to Git and activated alongside the theme.
Each feature lives in its own file under wp-content/themes/gca-intranet/inc/features/. All files in that directory are automatically loaded by inc/features.php.
Create a new file, e.g. inc/features/my-feature.php:
gca_register_feature_flag('my-feature', [
'label' => 'My Feature',
'description' => 'Short description of what this controls.',
'default' => false, // on or off by default
'tags' => ['ui', 'beta'], // optional — used to filter flags in the admin UI
]);The flag slug (my-feature) must be a unique, URL-safe string. It is used as the key for checking and storing the flag's state.
Call gca_flag_enabled() anywhere in template files, theme PHP, or plugins:
if (gca_flag_enabled('my-feature')) {
// render the new feature
}- Go to WP Admin → Settings → Feature Flags.
- Use the toggle switch next to the flag you want to change.
- Click Save Changes — changes take effect immediately.
The admin page supports search and tag filtering to quickly find flags in large lists.
The built-in environment-banner flag (inc/features/environment-banner.php) is a working example:
gca_register_feature_flag('environment-banner', [
'label' => 'Environment Banner',
'description' => 'Shows a yellow banner at the top of every page indicating the current environment. Never shown in production.',
'default' => false,
'tags' => ['ui', 'environment'],
]);
add_action('wp_body_open', function (): void {
if (!gca_flag_enabled('environment-banner')) {
return;
}
// ... render the banner
});| Function | Description |
|---|---|
gca_register_feature_flag($id, $args) |
Register a flag (call before any is_enabled check) |
gca_flag_enabled($id) |
Returns true if the flag is enabled, false otherwise |
Each feature file lives in wp-content/themes/gca-intranet-foundation/inc/features/.
| Flag ID | Label | Description | Cron job |
|---|---|---|---|
environment-banner |
Environment Banner | Shows a yellow banner at the top of every page indicating the current environment. Never shown in production. | — |
cron-manager |
Cron Manager | Admin interface (Tools → Cron Jobs) to view, create, edit, delete, and manually run WordPress cron jobs. | — |
workday-user-sync |
Workday User Sync | Syncs WordPress users with the Workday staff list API. Schedule via Tools → Cron Jobs or run manually with wp gca sync-users. |
gca_sync_workday_users |
google-profile-picture |
Google Profile Picture Sync | Downloads the user's Google profile picture on each SSO login and uses it as their avatar across the site. Generated letter-avatars (Google's default when no real photo is set) are automatically discarded using GD colour analysis — only genuine photographs are stored. Run wp gca sync-profile-pictures to retroactively clean up any letter-avatars stored before this check was added. Requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env. |
— |
purge-events |
Purge Events | Archives event posts more than one month after their end date (or start date if no end date is set) by moving them to the gca_archived post status. Archived events are hidden from the frontend but remain recoverable in the admin. Schedule via Tools → Cron Jobs or run manually with wp gca purge-events. |
gca_purge_events |
author-selector |
Author Selector | Replaces the default WordPress author meta box on blog and work_update posts with a searchable Select2 dropdown. Each option shows the user's profile image (Google SSO photo → Gravatar → WP default). All users are pre-loaded as inline JSON — no AJAX required. |
— |
directorate-contact |
Directorate Contact Popup | Shows a contact popup in the footer for pages tagged with a responsible_team taxonomy term. The popup title, description, and contact items are configured on the term edit screen (Taxonomy → Edit Term → Directorate Contact Popup). The responsible-team pill always renders; the popup is only attached when this flag is enabled. |
— |
staff-profiles |
Staff Profiles | Enables public staff profile pages at /profile/<username>. Data is pulled from WordPress user meta synced via the Workday User Sync feature. Enabled by default. |
— |
page-feedback |
Page Feedback | Shows an "Is this page useful?" feedback widget above the footer on all singular pages. Yes/No panels collect checkbox reasons; Report a Problem collects free-text. Three Gravity Forms are created programmatically on first use and are editable from WP Admin → Forms. Requires Gravity Forms. | — |
| — (always on) | Likes & Comments | REST API endpoints for post and comment interactions. Provides routes to get interactions, toggle post/comment likes, add/delete comments, and search users for @mentions. All routes require authentication. See inline route documentation in inc/features/likes-and-comments.php. |
— |
The purge-events feature archives rather than hard-deletes old event posts. Key details:
- Custom post status —
register_archived_status()registersgca_archivedon the WordPressinithook withpublic: falseandexclude_from_search: true, so archived events are invisible on the frontend but visible in the admin under a separate Archived status filter. - Archive instead of delete —
wp_delete_post( $post_id, true )has been replaced withwp_update_post( ['ID' => $post_id, 'post_status' => 'gca_archived'] ), making the operation fully reversible. - No double-processing — the query that selects events to evaluate is scoped to
['publish', 'private', 'draft'], so events already ingca_archivedare naturally excluded from subsequent cron runs. - Stats & logs — all log messages and the stats array use
archived(wasdeleted) to reflect the new behaviour.
The gca-custom plugin (wp-content/plugins/gca-custom) contains PHP classes and a PHPUnit test suite. These run outside Docker using a local PHP and Composer install.
- PHP 7.4+
- Composer
cd wp-content/plugins/gca-custom
composer installvendor/ is git-ignored — you must run this after cloning or pulling changes to composer.json.
cd wp-content/plugins/gca-custom
./vendor/bin/phpunitAll tests should pass with output ending in OK (N tests, N assertions).
Note: The current test suite only covers the Workday user sync (
GCA_Sync_Users). Other plugin functionality is not yet tested.
Two constants in library/class-gca-sync-users.php control offboarding behaviour:
| Constant | Default | Description |
|---|---|---|
ENABLE_DELETE |
true |
Set to false to pause hard-deletes without affecting soft-deletes |
SOFT_DELETE_GRACE_DAYS |
5 |
Number of days between soft-delete and hard-delete |
Soft-delete (first sync the user is absent from Workday): sets a deleted_at usermeta timestamp. The user retains full login access and remains visible in the staff directory and profiles during the grace period.
Hard-delete (after the grace period, when ENABLE_DELETE = true): reassigns posts and comments to the former-employee system user, clears Workday usermeta, then permanently deletes the WP user. The former-employee system user (login: former-employee, email: former-employee@gca.co.uk) is created automatically on first use — do not delete it.
- Themes live directly in this repo under
wp-content/themes/ - No symlinks are used
- Changes to themes should be committed to Git
- WordPress core is provided entirely by Docker
If your theme uses an npm build step for CSS/JS, use the built-in Docker container to compile assets (no local Node installation required).
To install dependencies and build once:
docker compose -f docker-compose.local.yml run --rm theme-builderTo watch for file changes during development:
docker compose -f docker-compose.local.yml run --rm theme-builder npm run watchWhether compiled assets should be committed depends on repo convention. Follow existing patterns in this repo.
Use these checks when validating header/footer changes.
-
Desktop (≥992px)
- Logo sits in the left gutter (outside the boxed container alignment)
- Boxed area: row 1 = utility links + search, row 2 = primary nav
- Dropdown chevrons appear to the right of nav items (outlined “V”)
-
Mobile (<992px)
- Row 1: logo left, utility links top-right (stacked)
- Row 2: search left, menu toggle right
- Keyboard: Tab through skip link → utility → search → nav; focus visible throughout
- Mobile menu toggle updates
aria-expanded - Dropdowns open via click and keyboard (Enter/Space)
- Set
WP_URLto the EC2 public IP or DNS name
e.g.http://<public-ip>orhttps://intranet.example.gov.uk - Set
WP_PORT=80if exposing WordPress directly
In production:
- Use RDS MySQL
- Do not use the local MySQL container
.envis local only — never commit.env- Use
.env.exampleas the template
Start Docker Desktop and retry.
- Change
WP_PORTin.env - Restart containers
WordPress relies on absolute URLs. Ensure WP_HOME and WP_SITEURL in your .env file explicitly include your custom port (e.g., http://localhost:8090). Restart the container and test in an Incognito Window to clear cached browser redirects.
Run the wp-cli bootstrap again:
docker compose --env-file .env run --rm wpcliIf still broken, do a full reset (see above).
Confirm MySQL container is running:
docker compose psCheck DB logs:
docker compose logs db