In this article, I propose a compilation of techniques to completely modify the tree structure of a site that works with WordPress. The objective is to make it more compliant with web application standards 1.

Preliminary comment

Before you attempt to modify your production site, pratice on a test site similar to the production site. If, like me, your web host provider is o2switch, it’s easy, you have as many sites as you want #promo 🙂 )

Expected tree structure

To illustrate the following, we will use the following root tree structure – close to the one recommended by my o2switch host (it may be different for you, just adapt accordingly):

  • /home/myuser/sites contains all sites, including the WordPress site. This folder must not be visible on Internet, therefore is not published by the web server.
  • /home/myuser/sites/myblog is the installation directory of the site using WordPress (probably a blog: -) ). This folder is also not visible. Only the WordPress installation should, for example by a subdomain that we will call heremyblog.mydomain. So this subdomain publishes /home/myuser/sites/myblog/wordpress.

Here is an example of the classic WordPress tree structure that we will modify:

|_myblog          : site root containing wp-config.php and index.php
  |_/wordpress    : content visible from Internet
    |_/wp-admin   : wordpress admin
    |_/wp-includes: wordpress engine
    |_/wp-content : themes, plugins, mu-plugins, upgrade, cache, uploads, languages folders

And the one we expect:

|_media/myblog: uploads folder content
|_myblog      : site root containing private data
  |_/web      : contenu visible from Internet
  |_/web/app  : wordpress engine (root files, wp-admin and wp-includes folders
  |_/web/ext  : plugins, mu-plugins and themes folders
  |_/web/data : working data such as caches, upgrade, image optimization, etc.
  |_/web/lang : translation files

Later in this articles, we will make copies of files and folders to be relocated in order to avoid making our WordPress installation unavailable during the operation. In the end, the old installation will have to be deleted to save room. Of course, we should not make any changes in WordPress during the operation, otherwise we could lose changes.

Let’s start creating the new tree structure: web/, web/app/, web/ext/, web/data/ et web/lang/ in /home/myuser/sites/myblog/.

Then we copy files located at the root of wordpress/, wp-admin/ and wp-includes/ folders to web/app/.

That’s all for the moment, we’ll go on copying later in each following steps.

Move wp-content outside WordPress tree structure

Here, we will move the wp-content folder out of the WordPress installation folder. The target location will be the web/data/ folder that we created earlier. We will copy the content of wp-content into web/data/, except for the plugins/, mu-plugins/, themes/, languages/ and uploads/ folders that will be processed later. As mentioned earlier, we will only clean up at the end, so that our site will keep operate during the changes.

Add the two following lines in wp-config.php 2, they will set the web/data/ folder as wp-content:

define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/data' );
define( 'WP_CONTENT_URL', $host . '/data' );

Move plugins and mu-plugins folders

Now, we are going to move wp-content/plugins/ and wp-content/mu-plugins/ folders to web/ext/plugins/ and web/ext/mu-plugins/. First, let’s copy the two folders to their target location.

Then we add two lines in wp-config.php to tell WordPress the new location of the plugins folder 2:

define( 'WP_PLUGIN_DIR', dirname(<strong>FILE</strong>) . '/ext/plugins' );<br>
define( 'WP_PLUGIN_URL', $host . '/ext/plugins' );

It’s the same for the mu-plugin folder. Add those two lines in wp-config.php 2:

define( 'WPMU_PLUGIN_DIR', dirname(__FILE__) . '/ext/mu-plugins' );
define( 'WPMU_PLUGIN_URL', $host . '/ext/mu-plugins' );

Move translations folder

Then move the translations folder from wp-content/languages/ to app/web/lang/. We add again one line in wp-config.php:

define( 'WP_LANG_DIR', dirname(__FILE__) . '/lang' );

Just like before, we copy wp-content/languages/ content to app/web/lang/.

Move themes folder

Unfortunatly, there is no WP_THEMES_DIR that we could use, so it will be a little more complex to achieve. We are going to create a mu-plugin following two bug reports from 3, 4.

The first report give us a good code snippet for the mu-plugin and the second report helps us make it work by adding the $wp_theme_directories global variable.

Let’s start creating a new wp-themes-dir.php file in web/ext/mu-plugins/ and copy the following code which will define web/ext/themes/ as themes location:

 * Plugin Name: Change Themes folder
 * Description: A WordPress Plugin For Change themes folder
 * Author: Mehrshad Darzi
 * Version:     1.0.0

// themes folder is located in public/ext folder.
global $wp_theme_directories;
$wp_theme_directories[] = $_SERVER['DOCUMENT_ROOT'].'/ext/themes';

register_theme_directory( ABSPATH . 'ext/themes' );

// ABSPATH points to app.
add_filter( 'theme_root', function () { return ABSPATH . '../ext/themes';  });
add_filter( 'theme_root_uri', function () { return home_url( '/ext/themes' ); }, 10, 1 );

As usual, we copy wp-content/themes/ content to its new location web/ext/themes/.

Move media files to a dedicated tree structure

The most sensible modification is to move the media files to a separate folder from the site, ideally in a dedicated sub-domain. Indeed, this implies direct update of the database. After that, it will be possible to move this subdomain to a CDN if needed in order to speed up the site load 5.

Updating the WordPress database consists of modifying fields containing URLs and paths to images. For this you can use phpMyAdmin. The detailed operations are described in the very good article quoted in reference 5.

First, we copy wp-content/uploads/ content to /home/myuser/sites/media/myblog/. This is the target location for images in the new tree structure.

Before attempting the updates described below, it is of course critical to export the WordPress database to save it in case of handling errors. We can use the export function of phpMyAdmin for this purpose.

We suppose in the following that the WordPress database prefix is wp_and that we move the media files to /home/myuser/sites/medias/myblog/ and the URL to media.mydomain/myblog. So we must create the folder and subdomain accordingly.

The first database update request will modify the URLs of the images in the content of the articles (you will notice that access is in https, which is a good practice):

UPDATE wp_posts SET post_content = REPLACE(post_content,'https://myblog.mydomain/wordpress/wp-content/uploads','https://media.mydomain/myblog')
UPDATE wpnt_posts SET guid = REPLACE(guid,'https://myblog.mydomain/wordpress/wp-content/uploads','https://media.mydomain/myblog')

Then we open the “search” menu of the wp_options table and search for ‘option_name‘ ‘like %...%‘ ‘upload‘. Three lines are found and we modify two of them as follow:

  • In ‘upload_path‘, remplace ‘wp-content/uploads‘ with ‘/home/myuser/sites/media.mydomain/myblog
  • In ‘upload_url_path‘ (which should be empty), add ‘https://media.mydomain/myblog

Protect sensitive data

The last change is to separate what really needs to be exposed on the Internet (the WordPress engine for example) from the rest (sensitive data such as database access codes). To do this, we use the web/ folder in which we will put everything that must be visible on the Internet.

Sensitive WordPress data are stored in the wp-config.php file which is often located one level up to the WordPress installation folder, as it can find it there. To make it clearer, we are going to extract sensitive data from wp-config.php (such as database credentialsles) and only let in that file generic WordPress code. So we create a file called config.php in /home/myuser/sites/myblog. It will be included by /home/myuser/sites/myblog/web/wp-config.php 6.

Let’s start creating the new file wp-config.php which no longer contains any sensitive data, in the web/ folder. Note that the lines added previously to move wp-content/, plugins/, mu-plugins/ and languages/ folders are also added:

 * This file automates all WordPress configuration intialisation,
 * based on config files stored outside the web folder.
 * There is nothing to change here, check <root_dir>/xxx-config.php files.
 * @package WordPress

ini_set( 'display_errors', 0 );

// ** Load parameters ** //
include( dirname( __FILE__ ) . '/../config.php' );

// ** Dynamically find host url depending on HTTPs usage. ** //
if ( isset( $_SERVER[ 'HTTP_HOST' ] ) ) {
	if ( !isset( $_SERVER[ 'HTTPS' ] ) ) {
		$_SERVER[ 'HTTPS' ] = '';

	// Check for https or http.
	$protocol = ( !empty( $_SERVER[ 'HTTPS' ] ) && $_SERVER[ 'HTTPS' ] !== 'off' || $_SERVER[ 'SERVER_PORT' ] == 443 ) ? 'https' : 'http';
	$host = $protocol . '://' . $_SERVER[ 'HTTP_HOST' ];
} else {
	$host = 'http://' . SUB_DOMAIN;

// ** WordPress URL ** //
define( 'WP_SITEURL', $host . '/app' );
define( 'WP_HOME',    $host . '/' );

// ** Custom folders locations ** //
define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/data' );
define( 'WP_CONTENT_URL', $host . '/data' );

define( 'WP_PLUGIN_DIR', dirname( __FILE__ ) . '/ext/plugins' );
define( 'WP_PLUGIN_URL', $host . '/ext/plugins' );

define( 'WPMU_PLUGIN_DIR', dirname( __FILE__ ) . '/ext/mu-plugins' );
define( 'WPMU_PLUGIN_URL', $host . '/ext/mu-plugins' );

define( 'WP_LANG_DIR', dirname( __FILE__ ) . '/lang' );

// ** themes folder customization is handled by wp-themes-dir mu-plugins. ** //

/** Absolute path to WordPress folder. */
if ( !defined( 'ABSPATH' ) )
	define( 'ABSPATH', dirname( __FILE__ ) . '/' );

/** WordPress variables settings and included files. */
require_once( ABSPATH . 'wp-settings.php' );

And here is the new config.php at the root of /home/myuser/sites/myblog, so outside of web/ :

 * WordPress private configuration.
 * @package WordPress

// ** WordPress site sub-domain
define( 'SUB_DOMAIN', 'myblog.mydomain' );

// ** MySQL settings ** //
define( 'DB_NAME', 'mydb' );
define( 'DB_USER', 'mydb' );
define( 'DB_PASSWORD', 'mydb' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8' ); // You would probably not want to change this.
define( 'DB_COLLATE', '' );     // You should not change this except if you know what you are doing.

// ** Database table prefix ** //
$table_prefix  = 'wp_';

// ** Debug level ** //
define( 'WP_DEBUG', false );
if( WP_DEBUG ) {
	define( 'WP_DEBUG_LOG', true );
	define( 'WP_DEBUG_DISPLAY', true );
	@ini_set( 'display_errors', E_ALL );

// ** Authentification and salt keys ** //
define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY',  'put your unique phrase here');
define('LOGGED_IN_KEY',    'put your unique phrase here');
define('NONCE_KEY',        'put your unique phrase here');
define('AUTH_SALT',        'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT',   'put your unique phrase here');
define('NONCE_SALT',       'put your unique phrase here');

A last thing to do

There wad already an index.php file at the same level than wp-config.php, at the site root. We must copy it to web/ and update it to make it require the wp-blog-header.php file in app/ folder instead of wordpress/ folder, for instance like this:

 * Front to the WordPress application. This file doesn't do anything, but loads
 * wp-blog-header.php which does and tells WordPress to load the theme.
 * @package WordPress

 * Tells WordPress to load the WordPress theme and output it.
 * @var bool
define('WP_USE_THEMES', true);

/** Loads the WordPress Environment and Template */
require( dirname( __FILE__ ) . '/app/wp-blog-header.php' );


So it is, our WordPress tree structure is completely reworked! We just have to delete the old installation contained in wordpress/folder and the old wp-config.php and index.php … after we wait for some days to make sure the new tree structure is working.


  1. Bedrocks – Better WordPress project structure 
  2. How to change location of the plugins & WordPress themes folders – Charles Clarkson – WordPress StackExchange – october 25th, 2013   
  3. Create wp_theme_directory_constants() function and dynamic WordPress Themes folder – Mehrshad Darzi – – avril 25, 2019 
  4. Register theme directory() not working for themes outside WP_CONTENT_DIR – T. Liebig – – may 28th, 2011 
  5. How to Move WordPress Uploads Path to Subdomain – Richie KS – Dezzain – may 6th, 2013  
  6. Managing Your WordPress Site with Git and Composer Part 4 – Gilbert Pellegrom – Delicious Brains – october 6th, 2015