From 32ae691468a6fe0a15203a39c19647584695b7c5 Mon Sep 17 00:00:00 2001 From: Jim Birch Date: Thu, 30 Oct 2025 11:06:15 -0400 Subject: [PATCH 1/2] feat(pantheon): add support for root-level WordPress installations Adds configurable DOCROOT variable for Pantheon projects to support legacy sites with WordPress at application root (no subdirectory). Modern sites with web/ subdirectory remain fully supported. Changes include: - Interactive docroot prompt in project-configure - Path handling in all commands (project-init, project-wp, theme-activate) - Nginx template with {{DOCROOT}} placeholder replacement - Updated install.yaml for wp-config and mu-plugin path detection - Comprehensive documentation with troubleshooting guidance - New integration test for root-level WordPress validation Backward compatible - existing projects continue working without changes. --- CLAUDE.md | 4 +- README.md | 10 ++--- commands/host/project-configure | 22 ++++++++++- commands/host/project-init | 13 ++++++- commands/host/project-wp | 10 ++++- commands/web/theme-activate | 7 +++- docs/providers/pantheon.md | 66 +++++++++++++++++++++++++++++++++ install.yaml | 47 +++++++++++++++++++---- nginx_full/nginx-site.conf | 2 +- scripts/load-config.sh | 1 + scripts/refresh-pantheon.sh | 7 +++- tests/test.bats | 42 +++++++++++++++++++++ 12 files changed, 209 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 26ae008..6e08651 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,10 +68,11 @@ The add-on uses a **modular command approach** where `project-init` orchestrates ## Hosting Provider Support ### Pantheon -- **Recommended Docroot**: `web` (set during `ddev config`) +- **Recommended Docroot**: `web` (recommended for modern sites) or root/empty (legacy sites) - **Environments**: dev, test, live, multidev - **Authentication**: Terminus machine token - **Database**: Automated backup management with age detection +- **Note**: Root-level WordPress (no webroot subdirectory) is fully supported for older Pantheon sites ### WPEngine - **Recommended Docroot**: `public` (set during `ddev config`) @@ -110,6 +111,7 @@ Variables are stored in multiple locations: #### Pantheon Configuration - `HOSTING_SITE`: Pantheon site machine name - `HOSTING_ENV`: Default environment for database pulls (dev/test/live) +- `DOCROOT`: Document root directory (`web` for modern sites, empty string for root-level WordPress on legacy sites) - `MIGRATE_DB_SOURCE`: Source site for migrations (optional) - `MIGRATE_DB_ENV`: Source environment for migrations (optional) diff --git a/README.md b/README.md index 1babe1f..dc33100 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,11 @@ ddev project-init ## 🌐 Hosting Provider Support -| Provider | Authentication | Features | -|----------|---------------|----------| -| **[Pantheon](https://kanopi.github.io/ddev-kanopi-wp/providers/pantheon/)** | Machine Token | Terminus integration, multidev support | -| **[WPEngine](https://kanopi.github.io/ddev-kanopi-wp/providers/wpengine/)** | SSH Key (local config) | Nightly backup utilization | -| **[Kinsta](https://kanopi.github.io/ddev-kanopi-wp/providers/kinsta/)** | SSH Key | Direct server access | +| Provider | Authentication | Features | Docroot | +|----------|---------------|----------|---------| +| **[Pantheon](https://kanopi.github.io/ddev-kanopi-wp/providers/pantheon/)** | Machine Token | Terminus integration, multidev support | `web` (recommended) or root (legacy) | +| **[WPEngine](https://kanopi.github.io/ddev-kanopi-wp/providers/wpengine/)** | SSH Key (local config) | Nightly backup utilization | Configurable | +| **[Kinsta](https://kanopi.github.io/ddev-kanopi-wp/providers/kinsta/)** | SSH Key | Direct server access | `public` | ## 📋 Installation Options diff --git a/commands/host/project-configure b/commands/host/project-configure index df6df28..27d60c6 100755 --- a/commands/host/project-configure +++ b/commands/host/project-configure @@ -134,6 +134,14 @@ case "$HOST_PROVIDER" in echo "--------------------------" prompt_input "Site machine name (e.g., my-site)" "${HOSTING_SITE}" HOSTING_SITE prompt_input "Default environment (dev/test/live)" "${HOSTING_ENV:-dev}" HOSTING_ENV + prompt_input "Document root directory (web for modern sites, or leave empty for root/legacy sites)" "${DOCROOT:-web}" DOCROOT + + # Normalize empty/dot to empty string for root-level WordPress + if [ "$DOCROOT" = "." ]; then + DOCROOT="" + fi + + echo "💡 Document root configured: ${DOCROOT:-[root/application root]}" ;; "wpengine") echo "⚡ WPEngine Configuration:" @@ -313,6 +321,7 @@ case "$HOST_PROVIDER" in "pantheon") write_config_var "HOSTING_SITE" "$HOSTING_SITE" write_config_var "HOSTING_ENV" "$HOSTING_ENV" + write_config_var "DOCROOT" "$DOCROOT" ;; "wpengine") write_config_var "HOSTING_SITE" "$HOSTING_SITE" @@ -463,13 +472,22 @@ if [ -n "$PROXY_URL" ]; then esac # Update the nginx configuration with actual values + # Replace {{DOCROOT}} placeholder with actual docroot value + # Handle empty docroot by removing trailing slash + if [ -z "$DOCROOT" ]; then + NGINX_ROOT="/var/www/html" + else + NGINX_ROOT="/var/www/html/$DOCROOT" + fi + sed -i.bak \ -e "s|HOSTING_ENV-HOSTING_SITE.HOSTING_DOMAIN|$PROXY_HOST|g" \ - -e "s|/var/www/html/web|/var/www/html/$DOCROOT|g" \ + -e "s|/var/www/html/{{DOCROOT}}|$NGINX_ROOT|g" \ + -e "s|/var/www/html/web|$NGINX_ROOT|g" \ ".ddev/nginx_full/nginx-site.conf" rm -f ".ddev/nginx_full/nginx-site.conf.bak" - echo " ✅ Nginx proxy configuration updated (docroot: $DOCROOT, proxy: $PROXY_HOST)" + echo " ✅ Nginx proxy configuration updated (docroot: ${DOCROOT:-[root]}, proxy: $PROXY_HOST)" else echo " ⚠️ .ddev/nginx_full/nginx-site.conf not found - proxy configuration not updated" fi diff --git a/commands/host/project-init b/commands/host/project-init index 37f1721..2873ae6 100755 --- a/commands/host/project-init +++ b/commands/host/project-init @@ -33,12 +33,21 @@ refresh_command='ddev db-refresh' # Handle Pantheon mu-plugin conflicts early (before any wp-cli commands) echo -e "\n${construction} ${yellow} Checking for Pantheon mu-plugin conflicts...${NC} ${construction}\n" -PANTHEON_LOADER="web/wp-content/mu-plugins/pantheon-mu-loader.php" + +# Determine the correct path based on whether docroot is configured +# Check DOCROOT from config.yaml (empty string means root) +if [ -z "${DOCROOT}" ] || [ "${DOCROOT}" = "." ]; then + MU_PLUGINS_PATH="wp-content/mu-plugins" +else + MU_PLUGINS_PATH="${DOCROOT}/wp-content/mu-plugins" +fi + +PANTHEON_LOADER="${MU_PLUGINS_PATH}/pantheon-mu-loader.php" if [ -f "$PANTHEON_LOADER" ]; then # Check if the loader tries to load pantheon-mu-plugin if grep -q "pantheon-mu-plugin/pantheon.php" "$PANTHEON_LOADER"; then # Check if the actual plugin directory exists - if [ ! -d "web/wp-content/mu-plugins/pantheon-mu-plugin" ]; then + if [ ! -d "${MU_PLUGINS_PATH}/pantheon-mu-plugin" ]; then echo "📝 Found Pantheon mu-plugin loader but plugin directory missing" echo " Disabling to prevent PHP fatal errors in DDEV environment..." mv "$PANTHEON_LOADER" "${PANTHEON_LOADER}.disabled" diff --git a/commands/host/project-wp b/commands/host/project-wp index 8692bea..bfc887f 100755 --- a/commands/host/project-wp +++ b/commands/host/project-wp @@ -21,7 +21,15 @@ load_kanopi_config # Determine docroot based on hosting provider case "${HOSTING_PROVIDER}" in "pantheon") - DOCROOT="web" + # Handle application root case (. or empty) + if [ "$DOCROOT" = "." ] || [ -z "$DOCROOT" ]; then + DOCROOT="" + DOCROOT_PATH="/var/www/html" + echo "Using application root (no subfolder) for Pantheon" + else + DOCROOT_PATH="/var/www/html/${DOCROOT}" + echo "Using docroot: $DOCROOT for hosting provider: $HOSTING_PROVIDER" + fi ;; "wpengine") # Use configured DOCROOT from environment, fallback to 'web' diff --git a/commands/web/theme-activate b/commands/web/theme-activate index b438595..42c4f9c 100755 --- a/commands/web/theme-activate +++ b/commands/web/theme-activate @@ -15,7 +15,12 @@ load_kanopi_config # Determine docroot based on hosting provider case "${HOSTING_PROVIDER}" in "pantheon") - DOCROOT_PATH="/var/www/html/web" + # Handle application root case (. or empty) + if [ "$DOCROOT" = "." ] || [ -z "$DOCROOT" ]; then + DOCROOT_PATH="/var/www/html" + else + DOCROOT_PATH="/var/www/html/${DOCROOT}" + fi ;; "wpengine") # Use configured DOCROOT from environment, fallback to 'web' diff --git a/docs/providers/pantheon.md b/docs/providers/pantheon.md index d9fbc16..2846de8 100644 --- a/docs/providers/pantheon.md +++ b/docs/providers/pantheon.md @@ -7,6 +7,7 @@ Full integration with Pantheon hosting including Terminus CLI, automated backup ### Required Variables - `HOSTING_SITE` - Pantheon site machine name - `HOSTING_ENV` - Default environment for database pulls (dev/test/live) +- `DOCROOT` - Document root directory (`web` recommended for modern sites, empty for root-level WordPress on legacy sites) - `MIGRATE_DB_SOURCE` - Source project for migrations (optional) - `MIGRATE_DB_ENV` - Source environment for migrations (optional) @@ -27,8 +28,73 @@ Full integration with Pantheon hosting including Terminus CLI, automated backup # Select Pantheon as provider # Enter site machine name # Choose default environment + # Configure document root (web or leave empty for root) ``` +## WordPress Installation Location + +Pantheon sites can have WordPress installed in different locations: + +### Modern Sites (Recommended) +- **Document root**: `web/` +- **WordPress core**: `/web/` directory +- **Configuration during setup**: Enter "web" when prompted for document root +- **Best for**: New projects, Composer-managed WordPress, clean directory structure + +### Legacy Sites (Alternative) +- **Document root**: Root/application root +- **WordPress core**: Root directory (no subdirectory) +- **Configuration during setup**: Leave document root empty or press Enter when prompted +- **Best for**: Older Pantheon sites, sites migrated from other hosts + +### Configuration Examples + +**Modern site with web/ subdirectory:** +```bash +ddev project-configure +# When prompted: "Document root directory (web for modern sites, or leave empty for root/legacy sites) [web]:" +# Enter: web (or press Enter to accept default) +``` + +**Legacy site with root-level WordPress:** +```bash +ddev project-configure +# When prompted: "Document root directory (web for modern sites, or leave empty for root/legacy sites) [web]:" +# Enter: (press Enter without typing anything, or explicitly type empty string) +``` + +### Troubleshooting 404 Errors + +If you experience 404 errors after setup, verify your document root configuration: + +1. **Check your Pantheon site structure**: + - Does `wp-config.php` exist in the root directory or in a `web/` subdirectory? + - Where are `wp-content/`, `wp-includes/`, and `wp-admin/` located? + +2. **Verify DDEV configuration**: + ```bash + grep '^docroot:' .ddev/config.yaml + # Should show: docroot: web (for modern sites) + # Or: docroot: "" (for root-level WordPress) + ``` + +3. **Re-run configuration if needed**: + ```bash + ddev project-configure + # Provide the correct document root + ddev restart + ``` + +### Migrating Between Configurations + +**Existing projects** with `web/` subdirectory continue working without changes. To explicitly update configuration: + +```bash +ddev project-configure +# Re-enter your settings with correct document root +ddev restart +``` + ## Features ### Smart Database Refresh diff --git a/install.yaml b/install.yaml index fce7f68..7090d29 100644 --- a/install.yaml +++ b/install.yaml @@ -75,12 +75,21 @@ post_install_actions: done if [ -n "$CONFIG_FILE" ]; then - DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''' || echo "web") + DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''') + # If DOCROOT is empty or not found, default to empty string (root-level WordPress) + if [ -z "$DOCROOT" ]; then + DOCROOT="" + fi else DOCROOT="web" fi - WP_CONFIG_PATH="../../${DOCROOT}/wp-config.php" + # Handle empty docroot (root-level WordPress) + if [ -z "$DOCROOT" ]; then + WP_CONFIG_PATH="../../wp-config.php" + else + WP_CONFIG_PATH="../../${DOCROOT}/wp-config.php" + fi # Add ddev-managed settings to wp-config.php if [ -f "$WP_CONFIG_PATH" ]; then @@ -125,18 +134,29 @@ post_install_actions: done if [ -n "$CONFIG_FILE" ]; then - DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''' || echo "web") + DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''') + # If DOCROOT is empty or not found, default to empty string (root-level WordPress) + if [ -z "$DOCROOT" ]; then + DOCROOT="" + fi else DOCROOT="web" fi - PANTHEON_LOADER="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-loader.php" + # Handle empty docroot (root-level WordPress) + if [ -z "$DOCROOT" ]; then + PANTHEON_LOADER="../wp-content/mu-plugins/pantheon-mu-loader.php" + PANTHEON_PLUGIN_DIR="../wp-content/mu-plugins/pantheon-mu-plugin" + else + PANTHEON_LOADER="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-loader.php" + PANTHEON_PLUGIN_DIR="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-plugin" + fi if [ -f "$PANTHEON_LOADER" ]; then # Check if the loader tries to load pantheon-mu-plugin if grep -q "pantheon-mu-plugin/pantheon.php" "$PANTHEON_LOADER"; then # Check if the actual plugin directory exists - if [ ! -d "../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-plugin" ]; then + if [ ! -d "$PANTHEON_PLUGIN_DIR" ]; then echo "📝 Found Pantheon mu-plugin loader but plugin directory missing" echo " Disabling to prevent PHP fatal errors in DDEV environment..." mv "$PANTHEON_LOADER" "${PANTHEON_LOADER}.disabled" @@ -260,14 +280,25 @@ removal_actions: done if [ -n "$CONFIG_FILE" ]; then - DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''' || echo "web") + DOCROOT=$(grep '^docroot:' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2 | tr -d ' "'\''') + # If DOCROOT is empty or not found, default to empty string (root-level WordPress) + if [ -z "$DOCROOT" ]; then + DOCROOT="" + fi else DOCROOT="web" fi - PANTHEON_LOADER_DISABLED="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-loader.php.disabled" - if [ -f "$PANTHEON_LOADER_DISABLED" ]; then + # Handle empty docroot (root-level WordPress) + if [ -z "$DOCROOT" ]; then + PANTHEON_LOADER_DISABLED="../wp-content/mu-plugins/pantheon-mu-loader.php.disabled" + PANTHEON_LOADER="../wp-content/mu-plugins/pantheon-mu-loader.php" + else + PANTHEON_LOADER_DISABLED="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-loader.php.disabled" PANTHEON_LOADER="../${DOCROOT}/wp-content/mu-plugins/pantheon-mu-loader.php" + fi + + if [ -f "$PANTHEON_LOADER_DISABLED" ]; then mv "$PANTHEON_LOADER_DISABLED" "$PANTHEON_LOADER" echo "✅ Restored Pantheon mu-plugin loader (was disabled during add-on installation)" fi diff --git a/nginx_full/nginx-site.conf b/nginx_full/nginx-site.conf index 5e4cd1e..13f7771 100644 --- a/nginx_full/nginx-site.conf +++ b/nginx_full/nginx-site.conf @@ -12,7 +12,7 @@ server { listen 80 default_server; listen 443 ssl default_server; - root /var/www/html/web; + root /var/www/html/{{DOCROOT}}; ssl_certificate /etc/ssl/certs/master.crt; ssl_certificate_key /etc/ssl/certs/master.key; diff --git a/scripts/load-config.sh b/scripts/load-config.sh index e0faf59..543c194 100644 --- a/scripts/load-config.sh +++ b/scripts/load-config.sh @@ -34,6 +34,7 @@ load_kanopi_config() { if [[ "${HOSTING_PROVIDER}" == "pantheon" ]]; then export HOSTING_SITE=${HOSTING_SITE:-''} export HOSTING_ENV=${HOSTING_ENV:-'dev'} + export DOCROOT=${DOCROOT:-''} # Pantheon default is root, but can be configured to 'web' # Migration Configuration for Pantheon export MIGRATE_DB_SOURCE=${MIGRATE_DB_SOURCE:-''} export MIGRATE_DB_ENV=${MIGRATE_DB_ENV:-''} diff --git a/scripts/refresh-pantheon.sh b/scripts/refresh-pantheon.sh index bf1c458..b3d08d5 100755 --- a/scripts/refresh-pantheon.sh +++ b/scripts/refresh-pantheon.sh @@ -121,7 +121,12 @@ DB_DUMP="/tmp/pantheon_backup.${SITE_ENV}.sql.gz" terminus backup:get ${SITE_ENV} --element=database --to=${DB_DUMP} echo -e "\nReset DB" -cd ${DDEV_DOCROOT} +# Stay in DDEV_APPROOT if DDEV_DOCROOT is empty (root-level WordPress) +if [ -n "${DDEV_DOCROOT}" ]; then + cd ${DDEV_APPROOT}/${DDEV_DOCROOT} +else + cd ${DDEV_APPROOT} +fi wp db reset --yes --skip-plugins --skip-themes echo -e "\nImport db" diff --git a/tests/test.bats b/tests/test.bats index 8869ac5..11b9d7b 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -296,6 +296,48 @@ EOF ddev exec wp core version 2>/dev/null || echo "WP-CLI not available or WordPress not fully configured" } +@test "pantheon root-level wordpress configuration" { + set -eu -o pipefail + cd $TESTDIR + # Configure with empty docroot for root-level WordPress (legacy Pantheon sites) + ddev config --project-name=$PROJNAME --project-type=wordpress --docroot="" --create-docroot + + # Verify empty docroot in config + grep -q '^docroot: ""' .ddev/config.yaml || grep -q '^docroot:$' .ddev/config.yaml + + # Install add-on with root-level WordPress + ddev add-on get $DIR + + # Configure for Pantheon with root-level WordPress + export DDEV_NONINTERACTIVE=true + ddev config --web-environment-add="HOSTING_PROVIDER=pantheon" + ddev config --web-environment-add="HOSTING_SITE=test-site" + ddev config --web-environment-add="HOSTING_ENV=dev" + ddev config --web-environment-add="DOCROOT=" + + ddev start + + # Verify nginx configuration uses correct root path + ddev exec "grep -q 'root /var/www/html;' /etc/nginx/sites-enabled/nginx-site.conf" || echo "Nginx root path should be /var/www/html for empty docroot" + + # Verify commands handle empty DOCROOT correctly + ddev project-wp --help >/dev/null 2>&1 + ddev theme-activate --help >/dev/null 2>&1 + + # Test mu-plugin handling with empty docroot + mkdir -p wp-content/mu-plugins + cat > wp-content/mu-plugins/pantheon-mu-loader.php << 'EOF' +/dev/null 2>&1 || echo "project-init command exists" +} + @test "db-refresh error handling and exit status" { set -eu -o pipefail cd $TESTDIR From 4766758fa9339824db0734db9e1a44318c799ede Mon Sep 17 00:00:00 2001 From: Jim Birch Date: Thu, 30 Oct 2025 11:56:48 -0400 Subject: [PATCH 2/2] fix(nginx): use default docroot path instead of placeholder The nginx template was using {{DOCROOT}} placeholder which caused web container health checks to fail during before project-configure runs. Changed to use default /var/www/html/web path that works out of the box. The project-configure command already has logic to replace this default path when users configure their docroot, so this maintains full functionality while fixing the initial installation issue. Fixes CI test failures where web container was unhealthy. --- nginx_full/nginx-site.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx_full/nginx-site.conf b/nginx_full/nginx-site.conf index 13f7771..5e4cd1e 100644 --- a/nginx_full/nginx-site.conf +++ b/nginx_full/nginx-site.conf @@ -12,7 +12,7 @@ server { listen 80 default_server; listen 443 ssl default_server; - root /var/www/html/{{DOCROOT}}; + root /var/www/html/web; ssl_certificate /etc/ssl/certs/master.crt; ssl_certificate_key /etc/ssl/certs/master.key;