Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .claude/agents/wp-environment-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,85 @@ docker compose exec wordpress wp option update show_on_front page --allow-root
docker compose exec wordpress wp option update page_on_front [page_id] --allow-root
```

### 8. Multisite Network Operations

The repo ships a one-shot installer that converts a working single-site install into a subdirectory network. **Subdomain mode is intentionally not wired up** — it needs wildcard DNS that's awkward in generic Docker. Document the manual flip and stop, don't try to hand-roll it.

**First-time setup** (idempotent — re-runs harmlessly):

```bash
# Compose profile (preferred)
docker compose --profile multisite up multisite-installer

# Or run the script directly on the host
./scripts/wordpress-install/setup-multisite.sh \
--network-title "Flavian Network" \
--second-site-slug site2 \
--second-site-title "Site Two"
```

What it does:
1. Adds `WP_ALLOW_MULTISITE=true` to wp-config.php.
2. Runs `wp core multisite-convert` (subdirectory mode, base `/`).
3. Writes `MULTISITE`, `SUBDOMAIN_INSTALL=false`, `DOMAIN_CURRENT_SITE`, `PATH_CURRENT_SITE='/'`, `SITE_ID_CURRENT_SITE=1`, `BLOG_ID_CURRENT_SITE=1` via `wp config set`.
4. Replaces `.htaccess` with the multisite rewrite block.
5. Creates a sample second site (skip with `--no-second-site`).
6. Promotes the WP admin (user 1) to super admin.

**Site management:**

```bash
# List sites
docker compose exec wordpress wp site list --allow-root

# Create a new site (subdirectory)
docker compose exec wordpress wp site create \
--slug=marketing --title="Marketing" --email=admin@localhost --allow-root

# Archive / deactivate / delete
docker compose exec wordpress wp site archive 2 --allow-root
docker compose exec wordpress wp site deactivate 2 --allow-root
docker compose exec wordpress wp site delete 2 --yes --allow-root

# Inspect a single site (every wp command supports --url=)
docker compose exec wordpress wp post list --url=http://localhost:8080/site2/ --allow-root
```

**Super admin management:**

```bash
docker compose exec wordpress wp super-admin list --allow-root
docker compose exec wordpress wp super-admin add jdoe --allow-root
docker compose exec wordpress wp super-admin remove jdoe --allow-root
```

**Network-wide plugin/theme activation** — required for code to be available on every sub-site:

```bash
docker compose exec wordpress wp plugin activate woocommerce --network --allow-root
docker compose exec wordpress wp theme enable flavian-shop --network --allow-root
```

`--network` activates a plugin for every site; `theme enable --network` only makes a theme available — sub-sites still have to switch to it individually.

**Multisite-aware code patterns** to recommend when reviewing themes or plugins:

- Guard cross-site logic with `if ( is_multisite() ) { ... }`.
- When iterating sites, use `get_sites()` (not the deprecated `wp_get_sites()`) and always pair `switch_to_blog( $id )` with `restore_current_blog()`.
- Cache cross-site queries with `set_site_transient()` — `mu-plugins/flavian-multisite.php` already does this for the network site list under the `flavian_multisite_sites` key.
- For URLs that should resolve on the right site, prefer `network_home_url()` / `get_admin_url( $blog_id )` over hand-built strings.

**Mu-plugin helpers shipped in this repo** (`mu-plugins/flavian-multisite.php`):

| Helper | Purpose |
|---|---|
| `Flavian\\Multisite\\get_network_sites_cached()` | `get_sites()` with a 5-minute transient cache. Auto-invalidated on `wp_initialize_site`, `wp_delete_site`, `wp_update_site`. |
| Network admin dashboard widget | Lists every site with links into each one's admin. |
| Per-site dashboard notice | Surfaces the network admin link to super admins viewing a single site. |
| `[flavian_network_sites]` shortcode | Lists sister sites; `exclude_current="false"` to include the current site. |

The mu-plugin is gated on `is_multisite()` and is a no-op on single-site deployments — safe to leave in place.

## Workflow: Full Environment Setup

```
Expand All @@ -179,6 +258,20 @@ docker compose exec wordpress wp option update page_on_front [page_id] --allow-r
12. Report environment status
```

## Workflow: First-Time Multisite Setup

```
1. Verify WordPress is installed (single-site) and reachable on localhost:8080.
2. docker compose --profile multisite up multisite-installer
3. Visit http://localhost:8080/wp-admin/network/ and confirm the network
dashboard renders.
4. Confirm sub-site loads: http://localhost:8080/site2/
5. wp site list — should return at least 2 rows.
6. wp super-admin list — should include the WP admin.
7. If a theme or plugin needs to be available everywhere, activate it with
--network.
```

## Workflow: Theme Testing Setup

```
Expand Down Expand Up @@ -225,3 +318,8 @@ docker compose exec wordpress wp option update page_on_front [page_id] --allow-r
- Theme activation fails → Check style.css header, check for PHP errors
- Database error → Check DB container logs, verify credentials in wp-config.php
- Permission errors → Check volume mount permissions, use `--allow-root`
- Multisite: `wp_initialize_site` errors during site create → DB tables didn't exist; re-run `wp core multisite-convert` (idempotent) or run the installer
- Multisite: sub-site returns 404 → `.htaccess` missing the multisite rewrite block; re-run the installer or use `--skip-htaccess` and write it by hand
- Multisite: "Sorry, you are not allowed to access this page" on `/wp-admin/network/` → user 1 isn't a super admin; `wp super-admin add <login>`
- Multisite: plugin works on main site but not on sub-sites → wasn't network-activated; `wp plugin activate <slug> --network`
- Multisite: subdomain mode requested → DO NOT silently switch; explain that subdomain mode needs wildcard DNS (`*.localhost`, dnsmasq, or hosts entries) and that this repo ships subdirectory mode only
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ WC_DEFAULT_COUNTRY=US:CA
HEADLESS_FRONTEND_URL=http://localhost:3000
HEADLESS_INSTALL_JWT=true
HEADLESS_INSTALL_BLOCKS=true

# Multisite installer (compose profile: multisite)
# Run: docker compose --profile multisite up multisite-installer
# Subdirectory mode only. See docs/multisite/README.md for subdomain notes.
MS_NETWORK_TITLE=Flavian Network
MS_CREATE_SECOND_SITE=true
MS_SECOND_SITE_SLUG=site2
MS_SECOND_SITE_TITLE=Site Two
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,18 @@ gh auth status # Check authentication
```
Configs live in `.claude/config/deployment/<env>.yml` (gitignored). Templates: `*.example.yml`.

**Multisite network (subdirectory mode):**
```bash
# Convert the single-site install into a subdirectory network + sample sub-site
docker compose --profile multisite up multisite-installer

# Or run the script directly
./scripts/wordpress-install/setup-multisite.sh --network-title "My Network"
```
Mu-plugin: `mu-plugins/flavian-multisite.php` (no-op on single-site installs).
Agent: `wp-environment-manager` § 8 covers site CRUD, super admins, and network activation.
Full guide: `docs/multisite/README.md`.

**Headless WordPress (decoupled frontends):**
```bash
# One-shot install: WPGraphQL + CORS + preview secret
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,39 @@ services:
[ "$$HEADLESS_INSTALL_BLOCKS" = "false" ] && ARGS="$$ARGS --no-blocks"
bash /scripts/setup-headless.sh $$ARGS

# One-shot multisite installer.
#
# Converts the install into a subdirectory multisite network and creates
# a sample sub-site so the network admin has something to look at:
#
# docker compose --profile multisite up multisite-installer
#
# Re-running is safe — every step is idempotent.
multisite-installer:
image: docker:cli
container_name: flavian-multisite-installer
profiles: ["multisite"]
depends_on:
wordpress:
condition: service_healthy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./scripts/wordpress-install:/scripts:ro
working_dir: /scripts
environment:
MS_NETWORK_TITLE: ${MS_NETWORK_TITLE:-Flavian Network}
MS_SECOND_SITE_SLUG: ${MS_SECOND_SITE_SLUG:-site2}
MS_SECOND_SITE_TITLE: ${MS_SECOND_SITE_TITLE:-Site Two}
MS_CREATE_SECOND_SITE: ${MS_CREATE_SECOND_SITE:-true}
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
apk add --no-cache bash docker-cli-compose >/dev/null
ARGS="--network-title \"$$MS_NETWORK_TITLE\" --second-site-slug $$MS_SECOND_SITE_SLUG --second-site-title \"$$MS_SECOND_SITE_TITLE\""
[ "$$MS_CREATE_SECOND_SITE" = "false" ] && ARGS="--network-title \"$$MS_NETWORK_TITLE\" --no-second-site"
eval bash /scripts/setup-multisite.sh $$ARGS

# Optional: phpMyAdmin for database management
phpmyadmin:
image: phpmyadmin:latest
Expand Down
104 changes: 104 additions & 0 deletions docs/multisite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# WordPress Multisite

The repo ships an opt-in multisite installer that converts the local WordPress install into a **subdirectory** network in one command. Subdomain mode is intentionally out of scope for the MVP — see [Why subdomain mode isn't shipped](#why-subdomain-mode-isnt-shipped).

## What's in the box

| Piece | Path | Purpose |
|---|---|---|
| Installer script | `scripts/wordpress-install/setup-multisite.sh` | Runs `wp core multisite-convert`, writes the network constants via `wp config set`, replaces `.htaccess`, optionally creates a sample second site, and promotes user 1 to super admin. |
| Compose profile | `docker-compose.yml` → `multisite-installer` | Wraps the installer in a one-shot container. Run with `docker compose --profile multisite up multisite-installer`. |
| Mu-plugin | `mu-plugins/flavian-multisite.php` | Cached site list, network admin dashboard widget, per-site notice for super admins, `[flavian_network_sites]` shortcode. Gated on `is_multisite()`. |
| Agent | `.claude/agents/wp-environment-manager.md` § 8 | Multisite operations: install, site CRUD, super admin mgmt, network-wide activation, code patterns. |

## Five-minute setup

```bash
# 1. Bring up WordPress (if not already running)
docker compose up -d

# 2. Convert to multisite (subdirectory mode + sample second site)
docker compose --profile multisite up multisite-installer

# 3. Visit the network admin
open http://localhost:8080/wp-admin/network/
```

That's it. The installer is idempotent — re-running detects an existing network and bails out cleanly.

## Customising the installer

Environment variables read by the compose profile:

| Variable | Default | Effect |
|---|---|---|
| `MS_NETWORK_TITLE` | `Flavian Network` | Title shown on the network admin. |
| `MS_CREATE_SECOND_SITE` | `true` | Create the sample sub-site. |
| `MS_SECOND_SITE_SLUG` | `site2` | Slug for the sample sub-site → `/site2/`. |
| `MS_SECOND_SITE_TITLE` | `Site Two` | Title for the sample sub-site. |

Or call the script directly with flags:

```bash
./scripts/wordpress-install/setup-multisite.sh \
--network-title "Acme Holdings" \
--second-site-slug press \
--second-site-title "Acme Press"
```

## Day-to-day site operations

```bash
# List sites
docker compose exec wordpress wp site list --allow-root

# Create another site
docker compose exec wordpress wp site create \
--slug=marketing --title=Marketing --email=admin@localhost --allow-root

# Run wp-cli against a specific site
docker compose exec wordpress wp post list --url=http://localhost:8080/site2/ --allow-root

# Network-wide plugin activation (otherwise sub-sites can't see the plugin)
docker compose exec wordpress wp plugin activate woocommerce --network --allow-root

# Make a theme available to every sub-site (each site still has to switch to it)
docker compose exec wordpress wp theme enable flavian-shop --network --allow-root

# Super admins
docker compose exec wordpress wp super-admin add <login> --allow-root
```

## The mu-plugin

`mu-plugins/flavian-multisite.php` is a no-op on single-site installs (early `return` on `is_multisite()`). When multisite is active, it adds:

- `Flavian\Multisite\get_network_sites_cached()` — `get_sites()` wrapped in a 5-minute transient. Invalidated on `wp_initialize_site`, `wp_update_site`, `wp_delete_site`.
- A **Network sites** dashboard widget on the network admin.
- A dashboard notice on per-site admin screens linking back to the network admin (super admins only).
- A `[flavian_network_sites]` shortcode for embedding a cross-site nav. `exclude_current="false"` to include the current site.

## Why subdomain mode isn't shipped

Subdomain mode (e.g. `site2.localhost`) needs the request to actually arrive at WordPress with the right `Host` header. Locally that means one of:

- `*.localhost` resolution. Modern Chrome handles `*.localhost` natively, but Safari and curl don't by default.
- A `/etc/hosts` entry per sub-site (`127.0.0.1 site2.localhost`). Easy but unmaintainable as you create more sites.
- `dnsmasq` configured to wildcard-resolve `.localhost`.

The script bails out on `--subdomain` rather than silently producing a broken setup. If you need subdomain mode, set up wildcard DNS first, then in `wp-config.php`:

```php
define( 'SUBDOMAIN_INSTALL', true ); // was false
define( 'DOMAIN_CURRENT_SITE', 'localhost' );
```

…and run `wp rewrite flush --hard`. The repo's mu-plugin doesn't care which mode you're in.

## Troubleshooting

See the **Error Recovery** section of `.claude/agents/wp-environment-manager.md` for the canonical list. The three issues that cover ~90% of failures:

1. **`/wp-admin/network/` returns "not allowed"** → user 1 isn't a super admin yet. `wp super-admin add <login>`.
2. **Sub-site URLs return 404** → the multisite `.htaccess` rewrite block is missing. Re-run the installer (it's idempotent) or run it with `--skip-htaccess` and edit the file by hand.
3. **Plugin works on main site, missing on sub-sites** → wasn't network-activated. Re-run `wp plugin activate <slug> --network`.
Loading
Loading