This post is over a year old, its content may be outdated.

Matt Wilcox

Web Development

Articles Feb 04th 2015

2015 rebuild: A technical overview

How I used nginx and Craft to rebuild this website.

Note: This post is a follow up to The 2015 edition of mattwilcox.net

I've been using nginx as my preferred web server on personal projects for about a year and a half, and have found it an excellent replacement for Apache. It's faster, more intuitive, and just feels more well put together and actively developed.

Craft is a CMS I've used for around six months, and I adore it. With both bits of software, I know I still have a lot to learn - but what I've got for this site at the moment seems to work well, and I thought it might be interesting to have a look under the hood of this rebuild to see what I've done, how, and why.

If you've got any improvements I might make - please hit me up on Twitter!

nginx

My nginx is not the stock version from Debian's apt resources.

I've got the latest nginx set up on my server and configured to serve SPDY rather than HTTP, as documented in an earlier blog post. That said, I'm not using any fancy new features apart from OCSP Stapling which I believe is a relatively minor benefit, so the stock apt based version would likely perform nearly as well as my actual set-up.

Configuring nginx for Craft

Craft doesn't have 'official' support for nginx, but it will work just fine anyway. I think it's more of a Support related business decision than a technical one, and indeed you can get hold of a Craft config for nginx from Pixel & Tonic if you ask.

Here's a bare minimum config file for getting Craft to run. This file would be at /etc/nginx/sites-available/your-website-name.conf

Bare minimum config for running Craft.

server { # This block is the server settings for the domain
    listen 80;

    server_name DOMAIN.EXT WWW.DOMAIN.EXT;
    root        /websites/DOMAIN/public;
    index       index.html index.htm index.php;

    access_log /websites/DOMAIN/logs/access.log;
    error_log   /websites/DOMAIN/logs/error.log;

    error_page 404 /404.html;

    # Various security related configurations
        location ~ /(data|conf|bin|inc)/ {
            deny all;
        }

        location ~ /\.ht {
            deny  all;
        }

    # Avoid flooding logs with pointless stuff
        location = /favicon.ico {
            log_not_found off;
            access_log    off;
        }

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log    off;
        }

    #Pass php files over to PHP itself
        location ~ [^/]\.php(/|$) {
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;

            if (!-f $document_root$fastcgi_script_name) {
                return 404;
            }

            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass  unix:/var/run/php5-fpm.sock;
            fastcgi_index index.php;
            include       fastcgi_params;
        }

    # Try loading files, if thats a 404, pass the request to Craft
        location ~ ^(.*)$ {
            try_files $uri $uri/ /index.php?p=$uri&$args;
        }
}

You also need the fastcgi_params file which is included in the above code. This file would be located at /etc/nginx/fastcgi_params

fastcgi_param   QUERY_STRING            $query_string;
fastcgi_param   REQUEST_METHOD          $request_method;
fastcgi_param   CONTENT_TYPE            $content_type;
fastcgi_param   CONTENT_LENGTH          $content_length;

fastcgi_param   SCRIPT_FILENAME         $document_root$fastcgi_script_name;
fastcgi_param   SCRIPT_NAME             $fastcgi_script_name;
fastcgi_param   PATH_INFO               $fastcgi_path_info;
fastcgi_param   REQUEST_URI             $request_uri;
fastcgi_param   DOCUMENT_URI            $document_uri;
fastcgi_param   DOCUMENT_ROOT           $document_root;
fastcgi_param   SERVER_PROTOCOL         $server_protocol;

fastcgi_param   GATEWAY_INTERFACE       CGI/1.1;
fastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;

fastcgi_param   REMOTE_ADDR             $remote_addr;
fastcgi_param   REMOTE_PORT             $remote_port;
fastcgi_param   SERVER_ADDR             $server_addr;
fastcgi_param   SERVER_PORT             $server_port;
fastcgi_param   SERVER_NAME             $server_name;

fastcgi_param   HTTPS                   $https if_not_empty;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param   REDIRECT_STATUS         200;

And with those two files set up you would then need to symlink the configuration file from sites-available into sites-enabled. The sites available folder is just a library of available configurations... they're not loaded into nginx. To do so you add them into the sites-enabled directory via a sym-link. You'd do that by:

ln -s /etc/nginx/sites-available/your-website-name.conf /etc/nginx/sites-enabled/your-website-name.conf

However that website config is not what I'm using because there's more to be done than the bare minimum!

Going further than the basics

  • I'm using HTTPS because I want to take advantage of the SPDY protocol for faster site delivery, and I'm forcing http to become https because it's good practice to not have the same site on two different URLs/protocols.
  • I'm enabling Strict Transport Security to stop 'ssl downgrade' attacks and help against cookie hijacking.
  • I'm using OCSP Stapling which makes things faster for users because then the browser doesn't have to make a separate third-party request to verify the authenticity of my SSL certificate.
  • I'm caching the static assets of the site with a long expiry so that browsers won't keep re-requesting files that won't ever change (such as images and CSS).
  • Finally, I've got a couple of simple re-write rules to help avoid link-rot and forward old URLs that are no longer correct to new URLs that work.

Complete config for running Craft with forced HTTPS, OCSP Stapling, static caching of assets, and some re-writes of legacy URLs to new ones.

server { # This block forces non https traffic to redirect to https
    server_name www.DOMAIN.EXT DOMAIN.EXT; # Match with and without the www prefix

    # Enable this if your want HSTS (recommended, but be careful)
    add_header Strict-Transport-Security max-age=15768000;

    return 301 https://DOMAIN.EXT$request_uri;
}

server { # This block is the server settings for the domain
    listen 443 ssl spdy;
    ssl on;

    server_name DOMAIN.EXT WWW.DOMAIN.EXT; # Match with and without the www prefix
    root        /websites/DOMAIN/public;
    index       index.html index.htm index.php;

    access_log  /websites/DOMAIN/logs/access.log;
    error_log   /websites/DOMAIN/logs/error.log;

    error_page 404 /404.html;

    # HTTPS/SPDY related parameters
        # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
            ssl_certificate     /etc/nginx/ssl_stuff/YOURDOMAIN.pem;
            ssl_certificate_key /etc/nginx/ssl_stuff/YOURDOMAIN.key;

        # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
            ssl_dhparam /etc/nginx/ssl_stuff/dhparam.pem;

        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:50m;

        # Enable this if your want HSTS (recommended, but be careful)
            add_header Strict-Transport-Security max-age=15768000;

        # OCSP Stapling: fetch OCSP records from URL in ssl_certificate and cache them
            ssl_stapling on;
            ssl_stapling_verify on;

        # verify chain of trust of OCSP response using Root CA and Intermediate certs
            ssl_trusted_certificate /etc/nginx/ssl_stuff/ca-bundle.pem;
            resolver 8.8.8.8;

    # Various security related configurations
        location ~ /(data|conf|bin|inc)/ {
            deny all;
        }

        location ~ /\.ht {
            deny  all;
        }

    # Avoid flooding logs with pointless stuff
        location = /favicon.ico {
            log_not_found off;
            access_log    off;
        }

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log    off;
        }

    # 301 redirects for my old 2002-2012 website to the archived sub-domain
        rewrite ^(/archive/(entry|media)/id/(\d+))/?$ https://2002-2012.mattwilcox.net$1 permanent;

    # 301 redirects for old WordPress URLs to new site URLs
        rewrite ^/archives/(setting-up-a-recent-version-of-nginx-with-https-and-spdy-support-on-a-raspberry-pi)/?$ /web-development/setting-up-a-secure-website-with-https-and-spdy-support-under-nginx-on-a-raspberry-pi permanent;
        rewrite ^/archives/(.*)$ /web-development/$1 permanent;

    # static asset caching
        location ~*  \.(jpg|jpeg|png|gif|ico|css|js|map)$ {
            expires 365d;
            add_header Pragma public;
            add_header Cache-Control "public, must-revalidate, proxy-revalidate";
            try_files $uri /index.php?$query_string;
        }

    # Pass php files over to PHP itself
        location ~ [^/]\.php(/|$) {
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;

            if (!-f $document_root$fastcgi_script_name) {
                return 404;
            }

            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass  unix:/var/run/php5-fpm.sock;
            fastcgi_index index.php;
            include       fastcgi_params;
            fastcgi_param HTTPS on;
        }

    # Try loading files, if that's a 404, pass the request to Craft
        location ~ ^(.*)$ {
            try_files $uri $uri/ /index.php?p=$uri&$args;
        }
}

nginx's static asset caching and Craft

The static asset caching was tricky to get working properly with Craft - make sure you have the try_files line in your configuration block. If you don't the front-end of your website will work fine and its assets will be cached... but the back-end of Craft will be broken. All the Craft specific assets will resolve to 404's - this is because Craft's admin area uses 'cache busting' URLs; one's with parameters after them, such as /whatever/styles.css?12390128 - these do not work when you're using static asset caching unless you include the try_files line.

Craft

I've not done anything particularly special with this build, but it might prove useful to see what it is I have done, in case its new to you...

Environment specific configurations

It's easy to set Craft up so that its aware of the server environment its running in and tailors its settings accordingly. This way you can have Craft run in Dev mode when its running on your local machine, but automatically switch to a staging server's configuration when it's running there, and change again when it runs live. This means you never have to remember to manually adjust the configuration when you move it around.

The /craft/config/db.php file

return array(
	// Settings that apply to all cases
	'*' => array(
		'tablePrefix' => 'craft',
	),

	// Local development
	'.local'     => array(
		'server'   => 'localhost',
		'user'     => 'USERNAME',
		'password' => 'PASSWORD',
		'database' => 'DATABASE',
	),

	// Staging server
	'.staging.ext'     => array(
		'server'   => 'database.server',
		'user'     => 'USERNAME',
		'password' => 'PASSWORD',
		'database' => 'DATABASE',
	),

	// Live server
	'mattwilcox.net' => array(
		'server'   => 'localhost',
		'user'     => 'USERNAME',
		'password' => 'PASSWORD',
		'database' => 'DATABASE',
	),
);

The /craft/config/general.php file

return array(
	// Settings that apply to all cases
	'*' => array(
		'cpTrigger'            => 'SLUG-FOR-ADMIN-AREA',
		'omitScriptNameInUrls' => TRUE,
		'maxUploadFileSize'    => '16777216', // 16Mb
		'phpMaxMemoryLimit'    => '128M', // The maximum amount of memory Craft will try to reserve during memory intensive operations such as zipping, unzipping and updating
		'searchIgnoreWords'    => array('a','the','and'), // Words that should be ignored when indexing search keywords and preparing search terms to be matched against the keyword index
		'generateTransformsBeforePageLoad' => true, // generate the transforms before they're requested on the front end
	),

	// Local development
	'.local' => array(
		'devMode'            => true, // See http://buildwithcraft.com/help/dev-mode
		'testToEmailAddress' => 'DEVELOPMENT-EMAIL-ADDRESS', // Configures Craft to send all system emails to a single email address, for testing purposes
		'siteUrl'            => array(
			''   => 'http://my-website.local',
		),
		'environmentVariables' => array(
			'fileSystemPath' => '/Users/USER/Sites/my-website/public'
		),
	),

	// Staging server
	'.staging.ext' => array(
		'devMode'            => false, // See http://buildwithcraft.com/help/dev-mode
		'siteUrl'            => array(
			''   => 'http://my-website.staging.ext/',
		),
		'environmentVariables' => array(
			'fileSystemPath' => '/var/www/my-website/public',
		),
	),

	// Live server
	'mattwilcox.net' => array(
		'devMode'            => false, // See http://buildwithcraft.com/help/dev-mode
		'siteUrl'            => array(
			''   => 'https://mattwilcox.net/',
		),
		'environmentVariables' => array(
			'fileSystemPath' => '/live/filesystem/path',
		),
	),
);

Handling SEO

As much as 'SEO' leaves a bad taste in my mouth, and I firmly believe good content does by far the most for discoverability... SEO is a necessary thing to tackle. I've found the simplest way to deal with this in Craft is to create an SEO fieldgroup in any applicable Entry Type, and drag three fields into it:

  • Google Description (this is output as a meta description tag in the head).
  • A Facebook/OpenGraph Matrix
  • A Twitter Matrix

The first one is obvious, the second two are mostly the same thing; a Matrix field type which allows setting some of the various properties that Facebook or Twitter will look for in any linked page. I can set a title, description, and thumbnail to appear as I want them to appear for any post of mine that's linked to from either of these service. I can only imagine that there's some form of minor SEO boost that might also come with that too.

How my fields are laid out for a 'Generic' entry type in my Web Development section.

How the SEO field group looks while editing an entry.

Keeping things flexible with Matrix

The Matrix fieldtype is one of the shining jewels of Craft. By wisely planning out how to use a Matrix you can create complex pages with ease. For example, this page you're reading has only two fields which are responsible for all the content you see here. The first field is a summary of the entry, which can be pulled on listing pages. The second field is a Matrix, and it hold all of the rest of this page's content. Here's how that Matrix field is set up in the back-end...

The Matrix field responsible for this pages content.

If you're not familiar with how a Matrix works; it allows me to have a post that consists of any number of these 'block types' in any order. It means I can have a simple text only entry, or a two thousand word long detailed tutorial full of code examples, images, video's, and text. That's awesome.

Keeping code compact and reusable with Macro's

Macros are a brilliant part of Craft that I've only just started using (well, Twig, the template language Craft uses). It's clear macros offer a very powerful method of coding but I'm only using them for fairly basic things at the moment.

A Macro is a lot like defining your own PHP function. You give it a name, pass through some arguments, and then define what it does with those arguments. I'm using it to output the Matrix field shown previously. That matrix is re-used on a number of different Entry Types over a number of Sections, so it's a perfect place to use a Macro.

Why not use an Include? Well it'd be possible but then I'd need to make sure all my variables are identical so it wouldn't break. Using a Macro instead encapsulates the logic for the Martix's display and means that I can use more sensible variable names on other pages.

Calling a Macro

{{ macros.mixedContent(entry.mixedContent) }}

In the context of a Craft template, that would look something like this:

<h1>{{ entry.title }}</h1>
<div class="excerpt">
	{{ entry.synopsis }}
</div>
<div class="page-content" id="page_content">
	{{ macros.displayMixedContent(entry.mixedContent) }}
</div>

So; I just pass the entry's mixedContent field to the Macro, and it renders the entire content of the page as HTML.

Because the Macro is essentially a function you define, you could pass it multiple arguments and have it do complex logic. For this macro, it's just looping through the Matrix field passed to it and outputting the appropriate HTML for each block type accordingly.

Conclusion

Although this isn't a thorough post, hopefully it's enough of an overview to be interesting and/or useful for you. I've thoroughly enjoyed building this site, and hopefully this post will either help you out or convince you to go try Craft and nginx on your next project.

I've got a number of new sections, entry types, and designs still to come, and I'm looking forward to implementing them soon!