Andrew Welch

Andrew Welch · Insights · #devops #performance #craftcms

Making the web better one site at a time, with a focus on performance, usability & SEO

· 5 min read ·

Static Page Caching with Craft CMS

Get the best of both worlds with static HTML generator performance on the frontend, and a content editor friendly db-based backend

Static Electricity

This website just got a whole lot faster, because pages are now statically cached via FastCGI Cache. If you want to take your website from annoyingly slow to fast & responsive, nothing will get you there quicker than static page caching.

And this article is going to explore how you can do it, too. From response times that make people smile, to being able to handle the “thundering herd” of massive traffic, nothing beats static page caching.

If you look at the response in Chrome dev tools, you’ll see the x-cache: HIT header:

X Cache Hit

This means that the page was served up by the FastCGI Cache, and PHP, Craft, and our templates were never even touched to make it happen.

Caching in general is just keeping data around that we need quick access to. We’ve discussed Craft’s built-in template caching in the The Craft {% cache %} Tag In-Depth article. Craft’s template cache is a fragment cache, where chunks of data are stored in a database for quick retrieval.

A full page static cache by contrast stores the entire rendered page as a flat file on disk. 

So why would you want to have a full page static cache? Performance, but more importantly, concurrency.​

Full page static caching is incredibly performant because when a webpage is requested, the HTML is just returned. There’s no computation involved in building the page.

When a thundering herd hits your website, it can handle the traffic easily, because the cost of each transaction is very low. For a discussion of why you should care about performance, check out the A Pretty Website Isn’t Enough article.

Static site generators are popular for this reason, but you often have to sacrifice the flexibility that database-powered CMS systems such as Craft CMS offer you. A static caching layer in front of a database-driven CMS offers you the best of both worlds.

Thundering Herd

One popular solution for full page static caching is Varnish; but while it offers a flexible solution, there are some downsides in terms of no native https support (you need to set up a proxy), and it involves installing and configuring another layer of software.

FastCGI Cache by contrast is available by default in any Nginx setup, is easy to configure, and actually performs better than Varnish. It’s not the right solution for every case, but it works remarkably well for most.

There are also some plugins available for Craft CMS that offer static page caching, but it’s a tricky thing to get right, and in general I prefer to use daemon-level services for this type of thing for dependability reasons.

Link Should you use a full page static cache?

It might sound like a static page cache is only for very high-end websites that need to handle massive amounts of traffic. While it’s true that this is a great application for a static page cache, it is also an excellent thing to use when resources are sparse.

A small, inexpensive VPS can handle a significant amount of traffic if a static page cache is used. This is because the cost in serving a webpage is very low, and thus the hardware powering it can be as well.

This makes static page caching a fantastic choice for websites with budgetary constraints as well.

Here’s a simplified view of what the flow looks like when a request comes in for a webpage and FastCGI Cache is active:

Fastcgi Cache Flow

As you can see, if there’s a cache hit (meaning the requested page is in the FastCGI Cache), the whole process gets short-circuited, and the page is immediately returned.

The whole cascade of PHP executing the compiled Twig template, issuing a number of MySQL queries, and so on just never happens. And that’s the lengthy and CPU/disk intensive part of the process.

So this is great, but there are some caveats. Here’s what you can’t have if you want a full page static cached site:

  • Server-rendered dynamic content that’s different on a per-user basis
  • Server-side cookies, such as logged in sessions, Craft Commerce carts, etc.

Remember, all that is delivered to every person who visits your site is the same exact raw HTML page. Your Twig templates (really, the compiled PHP version of them) are rendered out once and cached.

So if you want dynamic content that’s different on a per-user basis, you’ll need to use a frontend JavaScript framework to provide it, as per the Lazy Loading with the Element API & VueJS article.

Also, because server-side cookies are included in the actual HTTP Response as a header like:

set-cookie:abtest=blue;Path=/;Max-Age=86400;secure

It’d obviously be a disaster if something like the CraftSessionId or Craft Commerce Cart cookies were part of the cached HTML, because everyone visiting the site would get the same cookie. Check out the The Case of the Missing PHP Session for more than you ever wanted to know about session cookies.

You can, however, still user client-side cookies set via JavaScript when using full page static caching.

If you can live with these restrictions, you can get some absolutely fantastic performance out of full page static caching with FastCGI Cache. The static pages are cached on disk in a directory with a hashed name like this:

forge@nys-production /etc/nginx/cache/nystudio $ tree -L 3 .
.
├── 5
│   ├── 27
│   │   └── fa683a07f63d30ab4d89512c753a3275
│   └── d5
│       └── 7b7ccb3aa2eeebcd67640e93ebd45d55
├── 9
│   └── a9
│       └── 7468ea426f291d5f1573d1d113390a99
├── a
│   ├── 50
│   │   └── d39a8b6c62a4c8c0b584e279a2a6150a
│   └── c4
│       └── 4597d0b8f9eecb1b351742b9ef995c4a
└── e
    └── 0b
        └── eeedb43e78c8042f38b33f66fdc060be

The top level directory is the first letter of the hash, and the second-level directory is 2nd two letters of the hash, and the file name is the remainder of the hash. So if the full hash is e0beeedb43e78c8042f38b33f66fdc060be then it will be stored as e/0b/eeedb43e78c8042f38b33f66fdc060be on disk.

Let’s peek at the file contents:

forge@nys-production /etc/nginx/cache/nystudio $ head -30 e/0b/eeedb43e78c8042f38b33f66fdc060be
|�AY��������l�AY��L��
KEY: httpsGETnystudio107.com/blog/the-case-of-the-missing-php-sessiongreen
�Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
X-Powered-By: Craft CMS
Link: <https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/>; rel=dns-prefetch;,<https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/>; rel=preconnect;,<https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/>; rel=preconnect; crossorigin;
Content-Type: text/html; charset=utf-8
charset: utf-8

<!DOCTYPE html><!--[if lt IE 7 ]><html
class="ie ie6 fonts-loaded" lang="en"> <![endif]-->
<!--[if IE 7 ]><html
class="ie ie7 fonts-loaded" lang="en"> <![endif]-->
<!--[if IE 8 ]><html
class="ie ie8 fonts-loaded" lang="en"> <![endif]-->
<!--[if (gte IE 9)|!(IE)]><!--><html
class="fonts-loaded" lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#"> <!--<![endif]--><!--[if lt IE 7 ]><html
class="ie ie6" lang="en"> <![endif]-->
<!--[if IE 7 ]><html
class="ie ie7" lang="en"> <![endif]-->
<!--[if IE 8 ]><html
class="ie ie8" lang="en"> <![endif]-->
<!--[if (gte IE 9)|!(IE)]><!--><html
lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#"> <!--<![endif]--><head><link
rel="dns-prefetch" href="https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/"><link
rel="preconnect" href="https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/" crossorigin><meta
property="fb:pages" content="1635213886766237" /><link
rel="amphtml" href="https://nystudio107.com/blog/the-case-of-the-missing-php-session/amp"><link
rel="alternate" type="application/rss+xml" href="https://nystudio107.com/blog/feed" /><meta
name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /><title>nystudio107 | The Case of the Missing PHP Session</title><meta
http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta
name="referrer" content="always" /><meta
name="robots" content="all" /><meta
name="keywords" content="session garbage, session data, session, &quot; &quot;, ] &quot; select &quot; &quot;, ] &quot; get &quot; &quot;, ] &quot;, ubuntu has php session garbage, craft we wanted the session, redis, redis for redis, craft, garbage, sessions, sessions…" /><meta
name="description" content=" Two years ago or so I created a SaaS website using Craft CMS called TastyStakes.com. It was my first real Craft CMS project, and it aims to be like…" /><meta
name="generator" content="SEOmatic" /><link
rel="canonical" href="https://nystudio107.com/blog/the-case-of-the-missing-php-session" /><meta
name="geo.region" content="NY" /><meta

The gibberish characters are just some binary data, then we have the KEY for the cached file, and then the HTTP Response headers, and then the raw HTML of the webpage itself. When a request comes in, FastCGI Cache sees if it has the file in the cache, and if so, it just returns it. You can also set how long pages should be cached, and how long they should be kept around if they are inactive (not accessed).

That’s it, that’s all there is to it. It’s a very simple but effective page caching mechanism.

Link Some Timing Stats

This website was already quite performant before I implemented FastCGI Caching, but let’s have a look at the performance gains realized from moving it to using static page caching.

The first thing I did was I removed all of the {% cache %} tags from my templates. If we’re doing full page caching, there’s really no reason for us to bother with using Craft’s template caching as well.

Then I did some timings using curl, first with caching disabled:

andrew@haoyun-1887 ~ $ curl -w "@curl-format.txt" -o /dev/null -s "https://nystudio107.com/blog"
    time_namelookup:  0.005
       time_connect:  0.026
    time_appconnect:  0.095
   time_pretransfer:  0.095
      time_redirect:  0.000
 time_starttransfer:  0.872
                    ----------
         time_total:  1.087

Next up, I turned the FastCGI Cache on, and repeated the same timing test:

andrew@haoyun-1887 ~ $ curl -w "@curl-format.txt" -o /dev/null -s "https://nystudio107.com/blog"
    time_namelookup:  0.005
       time_connect:  0.024
    time_appconnect:  0.094
   time_pretransfer:  0.094
      time_redirect:  0.000
 time_starttransfer:  0.140
                    ----------
         time_total:  0.354

So that’s pretty good, we went from 1.087s to 0.354s to deliver the blog index page. That’s a bit over 3x faster, quite a gain!

The important stat to look at here is time_starttransfer. There’s a certain amount of overhead involved with any HTTPS connection, and as you can see the stats for everything else is about the same between the two samples. This amounts to the (mostly) fixed overhead; but time_starttransfer—which represents the time it takes for the web server to deliver the page—went from 0.872 uncached to 0.140.

Wow. That’s about 6x faster using a static page cache.

The fun thing to remember is that even if the page originally took about 4s to load, it’d still only take about 0.35s to load if it is statically cached. This is because no matter how long it takes to render the page without caching, once it’s cached, it’s just text that’s being sent back.

The important thing to keep in mind is that your webpages won’t just load faster—which is worth it alone—but also there is significantly less load on your server. This helps with concurrency, for when your website receives massive traffic.

And this is on a $40/m VPS. This can easily get down to .08s or lower on a beefier setup/connection

Here’s a timing for the homepage from WebPageTest.org before, when we used Craft’s template caching:

Template Cached Page

Timing: Craft Template Cached Page

And here’s the same page now that it’s a full page statically cached via FastCGI Cache:

Static Cached Page2

Timing: FastCGI Cache Cached Page

So our Time To First Byte (TTFB) went down 0.209s (almost 50% lower), and the load time went down 0.344s. Not too shabby, especially considering the site was already quite fast.

Again, if the site wasn’t performant to begin with, the gains would be even larger.

There’s a temptation to simply use a full page static cache as a way to, well, varnish over a poorly performing website. Don’t do this, folks.

Varnishing Over

Do your best to make the website reasonably performant without caching, and then implement caching to help with scalability & concurrency.

If you don’t, then non-cache hits will be unnecessarily slow, causing more strain on your server. It also is just sloppy to make site that performs like a dog, and mask it with a caching layer. It’s analogous to being too lazy to remove your wallpaper, and instead just painting over it with many coats of paint.

Yeah, it’ll work, but you’re setting yourself up for problems down the road.

Link Implementing FastCGI Cache

Alrighty, if you made it this far, I’ll assume you’re pumped to get FastCGI Cache implemented. Here’s how we do it.

You’re going to need to be running Nginx; my server is provisioned by Laravel Forge, so I’m running PHP 7.1 as well.

First in your virtualhost.conf file, add the following line at the top of the file, outside the server {} block:

fastcgi_cache_path /etc/nginx/cache/nystudio levels=1:2 keys_zone=NYSTUDIO:100m inactive=1d;

fastcgi_cache_path specifies that the static page cache files should be stored in /etc/nginx/cache/nystudio, how many levels deep the directory names should be, the keys_zone name & size, and inactive specifies how long since a cached item key has been accessed before it should be removed from the cache. You can name the zone whatever you want, it’ll be used again in the .conf file.

You can have more than one fastcgi_cache_path on your server. For example, if you’re hosting multiple websites on the same VPS, can you create a separate FastCGI Cache for each, by giving them their own fastcgi_cache_path. You may have to initially create the cache directory yourself, depending on the file permissions (it needs to be writeable by the web server).

Next, inside of the server {} block, add:

    #Cache everything by default
    fastcgi_cache_key "$scheme$request_method$host$request_uri$abtest";
    add_header X-Cache $upstream_cache_status;
    set $no_cache 0;
    if ($request_method = POST)
    {
        set $no_cache 1;
    }
    if ($request_uri ~* "/(admin/|cpresources/)")
    {
        set $no_cache 1;
    }

fastcgi_cache_key specifies the combination of Nginx variables that should be used to create a unique hash for each page in the cache. Most configurations will use just $scheme$request_method$host$request_uri that will look something like httpsGETnystudio107.com/. However, you can use anything you want; we added the variable $abtest to the mix as per the A/B Split Testing with Nginx & Craft CMS article.

add_header just sets a header that indicates the status of the X-Cache header in the HTTP Response (either HIT if it’s in the cache or MISS if it’s not), and then sets a $no_cache variable to 0 (false) by default, but sets it to 1 (true) if we don’t want to cache this response.

I have it set to not cache the response if the request is POST or if the URI matches any of the special Craft segments that are used for the AdminCP. You can add in other conditions, too, like if a particular cookie is set, or what have you.

Next in the server {} block, add:

    # Craft-specific location handlers to ensure AdminCP requests route through index.php
    # If you change your `cpTrigger`, change it here as well
    location ^~ /admin {
        try_files $uri $uri/ @phpfpm_nocache;
    }
    location ^~ /index.php/admin {
        try_files $uri $uri/ @phpfpm_nocache;
    }

What we’re doing here is ensuring that any accesses to the AdminCP goes through our special @phpfpm_nocache location. This is important, because for all statically cached pages, we’re going to strip all cookies from the HTTP Response (more on this later), but we don’t want to do this for AdminCP requests, or we can’t login in. Which is bad.

Note these these location blocks should be the first location blocks in your Nginx config, to ensure that they match first.

    # php-fpm configuration for non-cached content
    location @phpfpm_nocache {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_param PATH_INFO $query_string;

        # php-fpm parameters
        fastcgi_param HTTP_PROXY "";
        fastcgi_param CRAFTENV_CRAFT_ENVIRONMENT "live";
        fastcgi_param CRAFTENV_DB_HOST "localhost";
        fastcgi_param CRAFTENV_DB_NAME "nystudio";
        fastcgi_param CRAFTENV_DB_USER "nystudio";
        fastcgi_param CRAFTENV_DB_PASS "XXXX";
        fastcgi_param CRAFTENV_SITE_URL "https://nystudio107.com/";
        fastcgi_param CRAFTENV_BASE_URL "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/";
        fastcgi_param CRAFTENV_BASE_PATH "/home/forge/nystudio107.com/public/";

        # shared php-fpm configuration
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;

        # No FastCGI Cache
        fastcgi_cache_bypass 1;
        fastcgi_no_cache 1;
        }

Note that the fastcgi_param SCRIPT_FILENAME and fastcgi_param PATH_INFO are different here than for our normal PHP handler, and we’re specifying that requests handled at this location are never cached.

We also aren’t stripping cookies off of any requests at this location either, so if you have certain pages that need server-side cookies (such as, say, a Craft Commerce store), you can send them to the @phpfpm_nocache location as well.

If you’re wondering what all of the CRAFTENV_ settings are, check out the Multi-Environment Config for Craft CMS article for details.

Finally, here is our default location handler for .php files:

    # php-fpm configuration
    location ~ [^/]\.php(/|$) {
        try_files $uri $uri/ /index.php?$query_string;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;

        # php-fpm parameters
        fastcgi_param HTTP_PROXY "";
        fastcgi_param CRAFTENV_CRAFT_ENVIRONMENT "live";
        fastcgi_param CRAFTENV_DB_HOST "localhost";
        fastcgi_param CRAFTENV_DB_NAME "nystudio";
        fastcgi_param CRAFTENV_DB_USER "nystudio";
        fastcgi_param CRAFTENV_DB_PASS "XXXX";
        fastcgi_param CRAFTENV_SITE_URL "https://nystudio107.com/";
        fastcgi_param CRAFTENV_BASE_URL "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/";
        fastcgi_param CRAFTENV_BASE_PATH "/home/forge/nystudio107.com/public/";

        # shared php-fpm configuration
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;

        # FastCGI Cache settings
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
        fastcgi_cache NYSTUDIO;
        fastcgi_hide_header Set-Cookie;
        fastcgi_cache_valid 200 1d;
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_cache_bypass $no_cache;
        fastcgi_no_cache $no_cache;
        }

If you’re using a PHP version other than 7.1, change the fastcgi_pass setting as appropriate.

The fastcgi_ignore_headers setting tells FastCGI Cache to ignore the Cache-Control, Expires, and Set-Cookie headers that it would normally look at to decide whether or not it should cache the request.

The fastcgi_hide_header setting tells Nginx to strip out all cookies from the HTTP Response. We do this so that no server-side cookies are stored in the page cache, because we don’t want things like the CraftSessionId to be cached and thus delivered to and shared by every visitor to the site!

fastcgi_cache tells it which key_zone to use, as specified above.

The fastcgi_cache_valid setting tells FastCGI Cache what HTTP Response codes should be cached (200 = “OK”) and for how long the pages should be cached, in our case, 1 day.

And that’s it, save the settings, restart Nginx via sudo nginx -s reload and away you go! You can see all of the settings available to you on the Module ngx_http_fastcgi_module website.

Link Ch-ch-ch-changes

We’re very used to having a dynamic site generated by Craft, so we have to make some changes to our mindset as well as our code when working on a full page statically cached site.

Change For The Better

As mentioned above, I stripped out all of the {% cache %} tags from my templates. There’s just no point in having two layers of caching if we’re planning to have the FastCGI Cache last for a reasonably long time.

Tangent: An argument could be made for using the Craft template caching on top of full page FastCGI caching if you wanted to leverage Craft’s more intelligent cache-busting techniques to speed up cache misses. For example, we could globally {% cache %} the blog archives section on this site, and depend on Craft’s intelligent cache busting to handle when it needs to be regenerated. But in general, you can likely eschew multi-layer caching, especially if both caches are likely to be broken at the same time. Because then we end up doing more work than if we had a single layer of caching, as both caches need to be rebuilt and saved.

Next, anywhere I used conditionals based on cookies courtesy of my Cookies plugin, such as the Critical CSS described in the Implementing Critical CSS on your website article, I changed to use Nginx Server Side Includes (SSI):

{# -- CRITICAL CSS -- #}
<!--#if expr="$HTTP_COOKIE=/critical\-css\={{ craft.config.environmentVariables.staticAssetsVersion }}/" -->
    <link rel="stylesheet" href="{{ (baseUrl ~ 'css/site.combined.min.css') | append_suffix(staticAssetsVersion) }}">
<!--#else -->
    <script>
        Cookie.set('critical-css', {{ craft.config.environmentVariables.staticAssetsVersion }}, { expires: '7D' });
    </script>
        {% block _inline_css %}
        {% endblock %}
    <link rel="preload" href="{{ baseUrl }}css/site.combined.min{{staticAssetsVersion}}.css" as="style" onload="this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="{{ baseUrl }}css/site.combined.min{{staticAssetsVersion}}.css"></noscript>
    <script>
        {{ source('_inlinejs/loadCSS.min.js') }}
        {{ source('_inlinejs/cssrelpreload.min.js') }}
    </script>
<!--#endif -->

You have to make sure you set ssi on; inside of your server {} block, but once you have, you can leverage SSI to have conditionals in your statically cached pages.

I also wrapped my entire base _layout.twig template in {% minify %} tags courtesy of my Minify plugin, so that the cached pages are minified by stripping whitespace. Might as well, since they are going to be cached!

Just take care not to wrap our SSI directives in the {% minify %} tags, or they will be stripped out of the resulting HTML (as all comments are, by default). I just use multiple {% minify %} / {% endminify %} tags to exclude the SSI directives, while still minifying the entire page.

The big change, though, is mentally adjusting to the idea that the page people are receiving is static, and handling dynamic content via a frontend JavaScript framework or Nginx SSI conditionals.

Link Cache Busting

Finally, what do we do to bust the cache if we make changes to our website? Well, we could just wait until the page caches expire (after 1 day in our setup), but patience has never been my thing.

Cache Busting Morrison

First, if you use Craft-Scripts as described in the Database & Asset Syncing Between Environments in Craft CMS article, you can have the clear_caches.sh script automatically delete the FastCGI Cache on deploy by setting LOCAL_FASTCGI_CACHE_DIR in your .env.sh

This way, whenever you deploy code changes, you can ensure cache coherency by just cleaning the entire FastCGI Cache. This is really the only safe way to do it when we’re talking about a code change deploy (whether to Craft, or your templates, or your plugins).

For busting the cache due to changing things in the Craft CMS AdminCP (such as editing or adding an Entry), I created a simple FastCGI Cache Bust plugin. Install it, put your FastCGI Cache path (e.g.: /etc/nginx/cache/nystudio ) into the plugin Settings page, and any time you save anything in the AdminCP, it’ll bust the entire FastCGI Cache.

If you’re using Craft 3.x, there’s a version of the FastCGI Cache Bust plugin for Craft 3.x as well.

This is a bit of a brute force approach, and there are ways that it could cache bust only specific URLs, but it suits my needs for now. In the future, I’ll likely make it a little less ham-handed.

So that’s it, folks. Happy caching!

Link The Full Monty

To aid you if you’re trying to set this up yourself, here’s the full Nginx virtualhost.conf file used on this website (which is based on the Nginx-Craft setup, and also uses the Craft-Multi-Environment setup). This is the config from Laravel Forge.

Remember that in Nginx, sometimes the order in which directives appear can affect how things work.

fastcgi_cache_path /etc/nginx/cache/nystudio levels=1:2 keys_zone=NYSTUDIO:100m inactive=1d;

# FORGE CONFIG (DOT NOT REMOVE!)
include forge-conf/nystudio107.com/before/*;

server {
    # Listen for both IPv4 & IPv6 requests on port 443 with http2 enabled
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name .nystudio107.com;
    root /home/forge/nystudio107.com/public;
    index index.html index.htm index.php;
    charset utf-8;
    ssi on;

    # 404 error handler
    error_page 404 /index.php?$query_string;

    # A/B Split Testing cookie
    add_header Set-Cookie "abtest=$abtest;Path=/;Max-Age=86400;secure";

    #Cache everything by default
    fastcgi_cache_key "$scheme$request_method$host$request_uri$abtest";
    add_header X-Cache $upstream_cache_status;
    set $no_cache 0;
    if ($request_method = POST)
    {
        set $no_cache 1;
    }
    if ($request_uri ~* "/(admin/|cpresources/)")
    {
        set $no_cache 1;
    }

    # Issue a 301 Redirect for any URL with a trailing /
    rewrite ^/(.*)/$ /$1 permanent;

    # Change // -> / for all URLs, so it works for our php location block, too
    merge_slashes off;
    rewrite (.*)//+(.*) $1/$2 permanent;

    # Access and error logging
    access_log off;
    error_log syslog:server=unix:/dev/log,facility=local7,tag=nginx,severity=error;

    # FORGE SSL (DO NOT REMOVE!)
    ssl_certificate /etc/nginx/ssl/nystudio107.com/171032/server.crt;
    ssl_certificate_key /etc/nginx/ssl/nystudio107.com/171032/server.key;
    
    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:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;

    # FORGE CONFIG (DOT NOT REMOVE!)
    include forge-conf/nystudio107.com/server/*;

    # Load configuration files from nginx-partials
    include /etc/nginx/nginx-partials/*.conf;
    
    # Handle Do Not Track as per https://www.eff.org/dnt-policy
    location /.well-known/dnt-policy.txt {
        try_files /dnt-policy.txt /index.php?p=/dnt-policy.txt;
    }

    # Root directory location handler
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Troll WordPress bots/users
    location ~ ^/(wp-login|wp-admin|wp-config|wp-content|wp-includes|(.*)\.exe) {
        return 301 https://wordpress.com/wp-login.php;
    }

    # Craft-specific location handlers to ensure AdminCP requests route through index.php
    # If you change your `cpTrigger`, change it here as well
    location ^~ /admin {
        try_files $uri $uri/ @phpfpm_nocache;
    }
    location ^~ /index.php/admin {
        try_files $uri $uri/ @phpfpm_nocache;
    }
    location ^~ /cpresources {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Pass our ServiceWorker through Craft so it can work as a template
    location ^~ /sw.js {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # php-fpm configuration
    location ~ [^/]\.php(/|$) {
        try_files $uri $uri/ /index.php?$query_string;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;

        # php-fpm parameters
        fastcgi_param HTTP_PROXY "";
        fastcgi_param CRAFTENV_CRAFT_ENVIRONMENT "live";
        fastcgi_param CRAFTENV_DB_HOST "localhost";
        fastcgi_param CRAFTENV_DB_NAME "nystudio";
        fastcgi_param CRAFTENV_DB_USER "nystudio";
        fastcgi_param CRAFTENV_DB_PASS "XXXX";
        fastcgi_param CRAFTENV_SITE_URL "https://nystudio107.com/";
        fastcgi_param CRAFTENV_BASE_URL "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/";
        fastcgi_param CRAFTENV_BASE_PATH "/home/forge/nystudio107.com/public/";

        # shared php-fpm configuration
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;

        # FastCGI Cache settings
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
        fastcgi_cache NYSTUDIO;
        fastcgi_hide_header Set-Cookie;
        fastcgi_cache_valid 200 1d;
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_cache_bypass $no_cache;
        fastcgi_no_cache $no_cache;
        }

    # php-fpm configuration for non-cached content
    location @phpfpm_nocache {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_param PATH_INFO $query_string;

        # php-fpm parameters
        fastcgi_param HTTP_PROXY "";
        fastcgi_param CRAFTENV_CRAFT_ENVIRONMENT "live";
        fastcgi_param CRAFTENV_DB_HOST "localhost";
        fastcgi_param CRAFTENV_DB_NAME "nystudio";
        fastcgi_param CRAFTENV_DB_USER "nystudio";
        fastcgi_param CRAFTENV_DB_PASS "XXXX";
        fastcgi_param CRAFTENV_SITE_URL "https://nystudio107.com/";
        fastcgi_param CRAFTENV_BASE_URL "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/";
        fastcgi_param CRAFTENV_BASE_PATH "/home/forge/nystudio107.com/public/";

        # shared php-fpm configuration
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;

        # No FastCGI Cache
        fastcgi_cache_bypass 1;
        fastcgi_no_cache 1;
        }

    location ~ /\.ht {
        deny all;
    }
}

# FORGE CONFIG (DOT NOT REMOVE!)
include forge-conf/nystudio107.com/after/*;

${ category } · ${ blog.postDate }

${ blog.title }

#${ tag }