├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── conf ├── wp-constants.php └── wp-env-config.php ├── vendor └── index.php └── web ├── app ├── index.php ├── mu-plugins │ ├── disallow-indexing.php │ ├── index.php │ └── register-theme-directory.php └── uploads │ └── index.php ├── index.php └── wp-config.php /.env.example: -------------------------------------------------------------------------------- 1 | # Environment, e.g. 'development', 'staging', 'production' (optional). 2 | WP_ENV= 3 | 4 | # Home URL. 5 | WP_HOME=https://www.example.com 6 | 7 | # Database credentials. 8 | DB_NAME=database_name_here 9 | DB_USER=username_here 10 | DB_PASSWORD=password_here 11 | 12 | # DB_HOST and DB_COLLATE can be left blank in most cases. 13 | DB_HOST= 14 | DB_COLLATE= 15 | DB_CHARSET=utf8 16 | 17 | # Can be changed (default is 'wp_'). 18 | $table_prefix=wp_ 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Application (plugins ignored since Composer manages them). 2 | web/app/plugins/* 3 | !web/app/plugins/index.php 4 | web/app/mu-plugins/*/ 5 | !web/app/mu-plugins/index.php 6 | 7 | # Add plugins not managed by Composer. 8 | #!web/app/plugins/plugin-name 9 | 10 | web/app/upgrade 11 | web/app/uploads/* 12 | !web/app/uploads/index.php 13 | 14 | # WordPress 15 | web/wp 16 | 17 | # PHP dotenv 18 | .env 19 | .env.* 20 | !.env.example 21 | 22 | # Composer 23 | vendor/* 24 | !vendor/index.php 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Purbeck Pixels - https://purbeckpixels.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Multitenancy Boilerplate 2 | 3 | Use Composer to configure and manage a WordPress instance (including themes and plugins) that's shared with multiple sites. 4 | - [WordPress Multitenancy Boilerplate](#wordpress-multitenancy-boilerplate) 5 | - [Features](#features) 6 | - [Requirements](#requirements) 7 | - [Prerequisites](#prerequisites) 8 | - [Installation](#installation) 9 | - [Configuration](#configuration) 10 | - [Themes](#themes) 11 | - [Plugins](#plugins) 12 | - [Constants](#constants) 13 | - [Directory Structure](#directory-structure) 14 | - [Adding Sites](#adding-sites) 15 | - [Configuration](#configuration-1) 16 | - [See Also](#see-also) 17 | - [Credit](#credit) 18 | 19 | ## Features 20 | 21 | - Improved directory structure 22 | - Dependency management with [Composer](https://getcomposer.org) 23 | - Easy WordPress configuration with environment and constants files 24 | - Environment variables with [PHP dotenv](https://github.com/vlucas/phpdotenv) 25 | - Enhanced security (separated web root and secure passwords with [roots/wp-password-bcrypt](https://github.com/roots/wp-password-bcrypt)) 26 | - WordPress [multitenancy](https://en.wikipedia.org/wiki/Multitenancy) (a single instance of WordPress core, themes and plugins serving multiple sites) 27 | 28 | ## Requirements 29 | 30 | - PHP 8.1+ 31 | - Composer 32 | 33 | ## Prerequisites 34 | 35 | [Install Composer](https://getcomposer.org/doc/00-intro.md): 36 | 37 | ```bash 38 | $ curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer 39 | ``` 40 | 41 | ## Installation 42 | 43 | ```bash 44 | $ composer create-project handpressed/wp-multitenancy-boilerplate:dev-main {directory} 45 | 46 | $ cd {directory} 47 | ``` 48 | 49 | Replace `{directory}` with the name of your new WordPress project, e.g. its domain name. 50 | 51 | Composer will download WordPress, move it to `/var/opt/wp` and then symlink `/var/opt/wp` to `web/wp` (see [Directory Structure](#directory-structure)). 52 | 53 | Composer will also symlink `/var/opt/wp/wp-content/themes` to `web/app/themes`, `/var/opt/wp/wp-content/plugins` to `web/app/plugins` and `/var/opt/wp/wp-content/mu-plugins` to `web/app/mu-plugins`. 54 | 55 | Sites can now share this single instance of WordPress. 56 | 57 | ## Configuration 58 | 59 | Open the `conf/.env` file and add your new site's home URL (`WP_HOME`) and database credentials (`DB_NAME`, `DB_USER`, `DB_PASSWORD`). You can also define the database `$table_prefix` (default is `wp_`) if required. 60 | 61 | Set your site's vhost document root to `/path/to/{directory}/web`. 62 | 63 | ### Themes 64 | 65 | Add themes in `web/app/themes` as you would for a normal WordPress install. 66 | 67 | ### Plugins 68 | 69 | [WordPress Packagist](https://wpackagist.org) is already registered in the `composer.json` file so any plugins from the [WordPress Plugin Directory](https://wordpress.org/plugins/) can easily be required. 70 | 71 | To add a plugin, use `composer require /` from the command-line. If it's from WordPress Packagist then the namespace is always `wpackagist-plugin`, e.g.: 72 | 73 | ```bash 74 | $ composer require wpackagist-plugin/wp-optimize 75 | ``` 76 | 77 | Whenever you add a new plugin or update WordPress core, run `composer update` to install your new packages. 78 | 79 | Themes and plugins are installed in the symlinked `themes` and `plugins` directories in `/var/opt/wp/wp-content` and will be available to all multitenancy sites. 80 | 81 | Note: Some plugins may make modifications to the core `wp-config.php` file. Any modifications to `wp-config.php` that are needed by an individual site should be moved to the site's `conf/wp-constants.php` file. 82 | 83 | ### Constants 84 | 85 | Put custom core, theme and plugin constants in `conf/wp-constants.php`. 86 | 87 | ## Directory Structure 88 | 89 | ├── composer.json → Manage versions of WordPress, plugins and dependencies 90 | ├── conf → WordPress configuration files 91 | │ ├── .env → WordPress environment variables (WP_HOME, DB_NAME, DB_USER, DB_PASSWORD required) 92 | │ ├── wp-constants.php → Custom core, theme and plugin constants 93 | │ ├── wp-env-config.php → Primary WordPress config file (wp-config.php equivalent) 94 | │ └── wp-salts.php → Authentication unique keys and salts (auto generated) 95 | ├── vendor → Composer packages (never edit) 96 | └── web → Web root (vhost document root) 97 | ├── app → wp-content equivalent 98 | │ ├── mu-plugins ↔ Must-use plugins symlinked to /var/opt/wp/wp-content/mu-plugins 99 | │ ├── plugins ↔ Plugins symlinked to /var/opt/wp/wp-content/plugins 100 | │ ├── themes ↔ Themes symlinked to /var/opt/wp/wp-content/themes 101 | │ └── uploads → Uploads 102 | ├── index.php → Loads the WordPress environment and template (never edit) 103 | └── wp ↔ WordPress core symlinked to /var/opt/wp (never edit) 104 | └── wp-config.php → Required by WordPress - loads conf/wp-env-config.php (never edit) 105 | 106 | `↔` denotes a symlink. 107 | 108 | ## Adding Sites 109 | 110 | Use [WP Multitenancy Add Site](https://github.com/handpressed/wp-multitenancy-add-site). 111 | 112 | ```bash 113 | $ composer create-project handpressed/wp-multitenancy-add-site {new_directory} 114 | 115 | $ cd {new_directory} 116 | ``` 117 | 118 | Replace `{new_directory}` with the name of your new project, e.g. its domain name. 119 | 120 | ### Configuration 121 | 122 | Open the `conf/.env` file and add the new site's home URL (`WP_HOME`) and database credentials (`DB_NAME`, `DB_USER`, `DB_PASSWORD`). You can also define the database `$table_prefix` (default is `wp_`) if required. 123 | 124 | Set the new site's vhost document root to `/path/to/{new_directory}/web`. 125 | 126 | Added sites will use the existing WordPress instance (including themes and plugins) in `var/opt/wp`. 127 | 128 | ## See Also 129 | 130 | [WordPress Substratum](https://github.com/handpressed/substratum) 131 | 132 | ## Credit 133 | 134 | Based on [handpressed/substratum](https://github.com/handpressed/substratum). Inspired by [roots/bedrock](https://github.com/roots/bedrock) and [wpscholar/wp-skeleton](https://github.com/wpscholar/wp-skeleton). 135 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handpressed/wp-multitenancy-boilerplate", 3 | "type": "project", 4 | "description": "WordPress multitenancy boilerplate configured and managed with Composer and PHP dotenv.", 5 | "keywords": [ 6 | "WordPress", 7 | "multitenancy" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Nicholas at HandPressed", 13 | "email": "nicholas@handpressed.net", 14 | "homepage": "https://handpressed.net", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.1", 20 | "composer/installers": "@stable", 21 | "johnpbloch/wordpress": "@stable", 22 | "oscarotero/env": "^1.1.0", 23 | "roots/wp-password-bcrypt": "1.0.0", 24 | "vlucas/phpdotenv": "^2.4" 25 | }, 26 | "suggest": { 27 | "wp-cli/wp-cli": "@stable" 28 | }, 29 | "extra": { 30 | "installer-paths": { 31 | "web/app/mu-plugins/{$name}": [ 32 | "type:wordpress-muplugin" 33 | ], 34 | "web/app/plugins/{$name}": [ 35 | "type:wordpress-plugin" 36 | ], 37 | "web/app/themes/{$name}": [ 38 | "type:wordpress-theme" 39 | ] 40 | }, 41 | "wordpress-install-dir": "web/wp" 42 | }, 43 | "repositories": { 44 | "wpackagist": { 45 | "type": "composer", 46 | "url": "https://wpackagist.org/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-create-project-cmd": [ 51 | "composer run generate-salts", 52 | "php -r \"rename('.env.example', '.env');\"", 53 | "php -r \"rename('web/wp-config.php', 'web/wp/wp-config.php');\"", 54 | "php -r \"rename('web/wp', '/var/opt/wp');\"", 55 | "php -r \"symlink('/var/opt/wp', 'web/wp');\"", 56 | "php -r \"symlink('/var/opt/wp/wp-content/themes', 'web/app/themes');\"", 57 | "php -r \"symlink('/var/opt/wp/wp-content/plugins', 'web/app/plugins');\"", 58 | "php -r \"rename('web/app/mu-plugins', '/var/opt/wp/wp-content/mu-plugins');\"", 59 | "php -r \"symlink('/var/opt/wp/wp-content/mu-plugins', 'web/app/mu-plugins');\"" 60 | ], 61 | "generate-salts": [ 62 | "echo ' conf/wp-salts.php && curl -L https://api.wordpress.org/secret-key/1.1/salt/ >> conf/wp-salts.php" 63 | ] 64 | }, 65 | "support": { 66 | "issues": "https://github.com/handpressed/wp-multitenancy-boilerplate/issues", 67 | "source": "https://github.com/handpressed/wp-multitenancy-boilerplate" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /conf/wp-constants.php: -------------------------------------------------------------------------------- 1 | load(); 30 | $dotenv->required( [ 'WP_HOME', 'DB_NAME', 'DB_USER', 'DB_PASSWORD' ] ); 31 | } 32 | 33 | /** 34 | * Database. 35 | */ 36 | define( 'DB_NAME', env( 'DB_NAME' ) ); 37 | define( 'DB_USER', env( 'DB_USER' ) ); 38 | define( 'DB_PASSWORD', env( 'DB_PASSWORD' ) ); 39 | define( 'DB_HOST', env( 'DB_HOST' ) ? env( 'DATABASE_SERVER' ) : 'localhost' ); 40 | define( 'DB_CHARSET', env( 'DB_CHARSET' ) ); 41 | define( 'DB_COLLATE', env( 'DB_COLLATE' ) ); 42 | 43 | $table_prefix = env( '$table_prefix' ) ?: 'wp_'; 44 | 45 | /** 46 | * URLs. 47 | */ 48 | define( 'WP_HOME', rtrim( env( 'WP_HOME' ), '/' ) ); 49 | define( 'WP_SITEURL', WP_HOME . '/wp' ); 50 | 51 | /** 52 | * Custom content folder. 53 | */ 54 | define( 'CONTENT_DIR', '/app' ); 55 | define( 'WP_CONTENT_URL', WP_HOME . CONTENT_DIR ); 56 | define( 'WP_CONTENT_DIR', $webroot_dir . CONTENT_DIR ); 57 | 58 | /** 59 | * Check for https. 60 | */ 61 | $is_ssl = (boolean) env( 'HTTPS' ) || 443 === env( 'SERVER_PORT' ) || 'https' === env( 'HTTP_X_FORWARDED_PROTO' ); 62 | $protocol = $is_ssl ? 'https' : 'http'; 63 | 64 | /** 65 | * Constants. 66 | */ 67 | if ( 'https' === $protocol ) { 68 | define( 'FORCE_SSL_LOGIN', true ); 69 | define( 'FORCE_SSL_ADMIN', true ); 70 | } 71 | 72 | define( 'WP_CACHE_KEY_SALT', WP_HOME . '_' ); 73 | define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) ); 74 | define( 'FS_CHMOD_FILE', ( 0644 & ~ umask() ) ); 75 | define( 'WP_AUTO_UPDATE_CORE', 'minor' ); 76 | 77 | /** 78 | * Disable all file modifications including updates and update notifications. 79 | */ 80 | define( 'DISALLOW_FILE_MODS', true ); 81 | 82 | if ( file_exists( $root_dir . '/conf/wp-constants.php' ) ) { 83 | require_once $root_dir . '/conf/wp-constants.php'; 84 | } 85 | 86 | /** 87 | * Authentication unique keys and salts. 88 | */ 89 | if ( file_exists( $root_dir . '/conf/wp-salts.php' ) ) { 90 | require_once $root_dir . '/conf/wp-salts.php'; 91 | } 92 | 93 | /** 94 | * Bootstrap WordPress. 95 | */ 96 | if ( ! defined( 'ABSPATH' ) ) { 97 | define( 'ABSPATH', $webroot_dir . '/wp' ); 98 | } 99 | -------------------------------------------------------------------------------- /vendor/index.php: -------------------------------------------------------------------------------- 1 |