Setting up WordPress is famously easy! It takes only 5 minutes but it’s worth taking some time to look at the best way to structure your next WordPress project.
I think I should start by saying I’m not a fan of the way WordPress manages and lays out files, because by default it assumes the entire system should be simply dumped in public_html and things added to its sub folders. This makes automating deployments a bit of a pain and it also doesn’t really fit the way my brain likes to structure projects. Luckily it’s fairly easy to customise and move certain components.
This article is not a step by step “How to set up WordPress”, though it certainly has step by step components. Instead, its more a “How I like to structure projects”, and as such it’s opinionated both in terms of naming conventions and locations. So take what I do and borrow the bits you like and get rid of the bits you don’t.
Separating the application from custom code
WordPress consists of a “Core” application and then themes, plugins, and dropins.
By default themes and plugins are placed in a folder within the core application, so by separating out our “custom” code (so our plugins, themes etc) we can separate the common application from our custom code.
In effect we create something like this:
public_html |-> core |->custom |-->plugins |-->themes
To get this setup we do need to tell WordPress where we have hidden our plugins and themes so we need to add a wp-config file. However as WordPress looks in both the folder it’s in and the folder above we will place this file in the public_html folder. For now this wp-config.php will be pretty much the same as normal but with addition of a couple of lines in this case
define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/custom' ); define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/custom' );
This means all the folders that would have been stored under wp-content should now be found in /custom. This doesn’t just mean plugins and themes but also things like mu-plugins. It’s also worth noting that dropins should be placed directly in the /custom folder.
We also need to add an index.php to the public_html to bootstrap it all together, in this case simply copying the index.php in the core folder and putting it in public_html and adjusting the require to be:
// WordPress view bootstrapper define( 'WP_USE_THEMES', true ); require( './core/wp-blog-header.php' );
So now we have:
public_html |->index.php |->wp-config.php |-> core |->custom |-->plugins |-->themes
When you install WordPress you will want to put the site address as your public_html root while the WordPress URL would be the /core. So for example:
You would get to WP-Admin by going to
Getting intimate with wp-config
We have already made an edit to wp-config.php but it’s worth taking a few minutes to look over this humble file.
As we have already seen, WordPress looks for the file in the root of the core folder first and then looks one directory up.
The file itself contains a bunch of config options in the form of defines, and it’s worth remembering that wp-config is a php file so you can include additional logic in there, for example you can include additional files, or add logic depending on circumstances. The basic file also contains only required defines and just within WordPress core there are dozens of “options” that can be defined in the file. Plugins may also suggest you add defines in there too.
Separating Routing and Passwords
This is a bit controversial but I split my wp-config.php into at least two parts, taking the passwords, salts and debug information and putting them in a separate file, one level down, which puts them outside of my public_html folder, leaving just routing information and an include to the password file in the wp-config.php.
There are a couple of reasons for doing this, the biggest single reason is it means local deployment still has the same routing, but can maintain an independent user and salt information. When managing version control these local config files are not versioned and are maintained separately. Also it means debug info can be on by default on your dev version but off on production.
The reason for dropping the password config file below the public_html is in the very rare chance of the server spitting out unprocessed php code, the sites passwords can’t directly be accessed. That said if something like that is occurring, those passwords should be changed as a matter of course and you got some pretty serious problems. Ultimately you need to weigh up the overhead of the extra include vs slightly cleaner and potentially more secure setup.
So now our application looks more like:
example.com/ |->local-config.php |->public_html |-->index.php |-->wp-config.php |--> core |-->custom |--->plugins |--->themes
Separating theme assets from code
example.com/ |->local-config.php |->public_html |-->index.php |-->wp-config.php |--> core |-->custom |--->plugins |--->themes |-->assets |--->images |--->css |--->js
The contents of this folder is normally auto-generated using grunt or similar, as it’s normally combination of minified and compiled css and js. This folder and its subfolders can then be pulled by your CDN if you are using one with ease.
User generated uploads are one of those things that as an admin I would happily ban, but unfortunately clients tend to want to include image in posts. As such I tend to use custom code which modifies the media manager to direct it to drop items in tmp and sends it to a queue using Gearman. A separate BASH script then does some basic checks before moving it to a subfolder in assets folder. This means the entire of the assets folder is set to make sure its not writable by the web server user.
However you can simply move uploads folder by using:
define( 'UPLOADS', 'assets/uploads' );
Remember, if you don’t set up a specific upload location, the media manager will obey the WP-CONTENT defines we included earlier. So in our setup the default location for uploads would be custom/uploads/
Scripts, logs and other bits
Most sites have one or more script that is going to be running, be it queue processing, batching, backups etc. These are nearly all run headless and separate to WordPress or through WP-CLI such scripts. To keep this together I tend to put them in a shell folder below the public_html folder, and to keep everything tidy I use a generic var folder for all these additional folders. In a similar vein I tend to keep my log files with my application so create a var/log while most exceptions will be going into Airbrake (well in my instance a CodebaseHQ Exceptions) I will normally have a range of log files. Non WordPress config files, where appropriate, get popped in var/conf.
Really Abstracting Config Files
WordPress wp-config.php is more then a config file, as a PHP file it can contain almost any code. For example I tend to put API Keys in as defines where possible to avoid them ending up in wp-options table. One idea that I have experimented with was creating dedicated config files using both YAML and .ini files.
The idea was that individual config sections would get their own files (so database.yml or database.ini keys.yml etc), these would be placed in the /var/config folder, and then called by the wp-config file.
I haven’t yet put this style setup on a live server, mainly because of the large overhead of introducing a YAML parser for yml files, though wp-cli makes use of yml files. While the overhead is less for using .ini files as there is a built in function within php. However, while I use .ini files day in day out, for some reason I don’t feel they are suitable though this is just a personal feeling.
The second reason I haven’t gone down this route is that WordPress is currently not accessing /var/ folder, so while PHP or HHVM (in some cases) might be running via supervisorD or cronjob this is normally the CLI version. So the var/ folder is not readable or writable normally by the http user.
That can easily be overcome by moving the conf folder or creating a second one at the root level. Ultimately I’m not sure if the abstraction is worth doing, maybe something for another blog post!
The final folder inside var is normally backup. As most of the files barring user uploaded material are stored in version control (see below) I tend to just backup by doing a mySQL dump of the database tables into a folder in var imaginatively called var/backup. This then rsyncs with my remote backup system. User uploads are handled separately.
So the final structure looks something like
example.com/ |->local-config.php |->public_html |-->index.php |-->wp-config.php |--> core |-->custom |--->plugins |--->themes |-->assets |--->images |--->css |--->js |--->uploads |->var |-->log |-->conf |-->shell |-->backup
Putting it in version control
There are several ways you can maintain this sort of structure in GIT or your version control of choice. The first is to put the whole lot in a single repo and just git exclude the local-config.php, var folder and assets/upload folder. However I prefer to do a bit more separation to make everything a bit more reusable and create a single repo for WordPress core which just has the WordPress core code in it. This is then added as a GIT sub module. When you are ready to deploy a new version of WordPress it can be deployed to all your sites simultaneously.
Pro Tip, make sure your deployment tool copes with sub modules, or this will fail spectacularly
This means you are no longer maintaining multiple versions of WordPress, and makes updating when new releases come out a doddle. It also allows you to have different “streams” so your dev sites can work on a bleeding edge release of WordPress while older clients with more complex compliance issues can use an older version.
The above has lots of opinionated choices about naming conventions and locations but I think its a good starting place for most projects. A slightly simpler skeleton was done by Mark Jaquith (and can be grabbed from GitHub). Indeed, his skeleton was the basis for a lot of the above.
The result of the above is a robust(ish) application setup which is easy to deploy and backup but also simple to manage.
Photo of folders: https://www.flickr.com/photos/odoublesnap/5040478919/
If you liked this post why not subscribe to my Newsletter (the signup is in the left hand sidebar). I promise not to spam you, I just send out an occasional email with exclusive content as well as a round-up of posts from TimNash.co.uk.