RT Cunningham

Semi Static WordPress Sites with Nginx [Updated for 2019]

Semi-Static WordPress Sites with Nginx You can make a WordPress-driven website behave like a static website without using a static site generator. You don’t have to rely on a plugin either. I call it a semi static WordPress site and it’s exactly how my site (this site) is currently set up.

Unlike page caching, where the cached pages expire and have to be regenerated, the pages for a semi static WordPress site are permanent until you change or remove the source pages.

The static.zip file contains the PHP files I reference in this article. Please download it, unzip it and load the PHP files in a text editor. Certain items will make more sense if you follow along.

Semi Static WordPress and the Command Line

You should be comfortable using PHP from the command line. You can get away with not using the command line if you trust the PHP scripts are perfect. I never will. It’s so much easier to start an SSH terminal connection to your server and debug any PHP script you’re using.

A lot of the code is lifted from my WordPress Static Site Generator. If you read the article about that first, you’ll see some repetition. There are two scripts: A “full” script is what I use when I want to regenerate all the static files. A “single” script is what I use to generate a single static file when I publish a post or edit one after publication.

I invoke the “full” script from the command line, usually a few minutes after the “single” script runs (and generates a static page for a new post). The “single” script works with WordPress functions and a server cron job, which invokes it every minute of the day.

What You Give Up with a Semi Static WordPress Site

There are features you have to give up if you want to keep your sanity in check. Mainly, it’s the pagination of posts pages, pagination of taxonomy pages (categories and tags) and pagination of archive pages. Tag pages will take a lot of work to keep up, even as static lists. That’s why I don’t recommend them. Category pages are fine as long as you don’t create too many.

You have to avoid anything that’s dynamic. If you want related posts lists, use a third-party service if you can (I use Google AdSense with a matched content unit). Otherwise, you’ll have to run the “full” script every time you publish or edit a post. That’s not really an issue, however, because regenerating the pages only takes a minute and I always do it anyway.

What You Gain with a Semi Static WordPress Site

Speed and ease of use are the main advantages. Nothing is faster than when you serve plain text (or HTML) from RAM. The next fastest is when you serve it from a solid-state drive. The best web hosting companies use solid-state drives.

You don’t have to worry about caching. The PHP script generates a static page within a minute of being published. Therefore, the database can only get hits from visitors within that 60-second period and it’s highly unlikely. With only one page at a time like that, it won’t make much of an impact on the database anyway.

I have a one-gigabyte droplet at DigitalOcean (affiliate link). Looking at it now with the “free -m” command shows me I have more than 400 megabytes available. I have done nothing to optimize the database settings. The number fluctuates in both directions by only a few megabytes at a time, before and after publishing an article.

Nginx Server Configuration

At the Nginx server level, you’ll need to insert “default_type text/html;” and “include /etc/nginx/mime.types;” before any location blocks. For whatever reason, when changing the default type, the “types” directive stops working.

That isn’t a problem with WordPress. It’s a problem at the online web server. The web server won’t know what file type to use.

You need two directories along with the directories WordPress creates: “/static” and “/temp”. If you’ve set up Nginx for WordPress correctly, you’ll have a location block like this:

location / {
try_files $uri $uri/ /index.php;

You need to make it look at a new location, the static files location. Look at this:

set $cache_uri "/static$uri";
if ($http_cookie ~* "wordpress_logged_in") {
set $cache_uri $uri;
location / {
try_files $cache_uri $cache_uri/ /index.php;
expires 3600s;
add_header Cache-Control "max-age=3600, public";
add_header Vary Accept-Encoding always;

Replace your location block with what I’m showing you. There’s one thing you need to know - your robots.txt and sitemap.xml file both have to be in the static directory, instead of the WordPress root directory. (It doesn’t hurt to have them in both places.)

The WordPress Configuration File

You’ll have to change the media location by adding a line like this just below the other “define” lines:

define('UPLOADS', 'uploads');

You can use anything in place of the second “uploads”. I use “ifiles” on my sites. From the root directory, create your new uploads directory. Making your pages static works a lot better when you use a static location.

The WordPress Settings

In Settings/Reading, select static post and use the “Sample Page” for it. You can always change it later. WordPress will automatically use it as the front/index page. Leave the posts page as is, with nothing selected. Leave search engine visibility checked. Check it if it’s unchecked for some reason.

In Settings/Discussion, make sure every check box is unchecked. You won’t be notifying other blogs and you can’t receive link notifications. You won’t be using the WordPress comment system at all. Third-party comment systems are available and I recommend Disqus.

In Settings/Media, make sure the check boxes are unchecked. That’s the way they should be by default but aren’t.

In Settings/Permalinks, click on the radio button for “Post name”. Then, click on the radio button for “Custom Structure”. Finally, remove the trailing slash in that input box. The input box should contain “/%postname%” only. You’ll understand this later on.


You only need a few plugins:

If you’re concerned about search engine optimization, you also need “Broken Link Checker” and “Yoast SEO”.

Your Theme’s Function File

The functions I’m referencing are to be added to your theme’s “functions.php” file, if one already exists (remove the PHP opening tag). Otherwise, use it as a new file. These are the sections:

Remove Lines from Head. These functions remove some lines WordPress automatically adds to most theme files.

Output Buffering. This section lets you change the output before it even appears. Use it to remove other lines and change existing lines.

Change Taxonomy Separators. WordPress uses the slash character (“/”) to make permalinks pretty. It also uses it to separate taxonomy bases names from the taxonomies (like /author/ and /category/). This section changes the separator to a dash (or hyphen). I did not include a line for /tag/. Since the pages have to created manually, you would be spending more time creating blank pages than anything else.

Page Priority Over Taxonomy. This section lets you create one page for each taxonomy page. A page starting with “category-” will take priority over the automatically generated page that corresponds to it. With this ability, you can include anything you want on the taxonomy pages, including various shortcodes.

Article List Shortcode. This section provides the [article-list] shortcode. You can see the results by looking at my Article List page (other than the text and the title).

Category List Shortcode. This section provides the [category-list] shortcode. You can see the results by looking at my Category List page (other than the text and the title).

Category Page Shortcode. This section provides the [category-page] shortcode to be used for every category page. You have to create one page for each category and place this shortcode on it. The slug for a category page must start with “category-“. It won’t work correctly if the category isn’t created before the page.

Extra Functions

The functions are mostly identical to the functions I used for my WordPress Static Site Generator, but these two are also needed for doing it this way.

Publish Post. This section creates a “touched” file in the “/temp” directory.

Edit Post. This section also creates a “touched” file in the “/temp” directory.

Semi Static WordPress Scripts - Single and Full

You can probably understand most of both scripts without me explaining every detail. The single script will handle a newly published or edited page. The full script will handle all of them. A cron job should invoke the single script every minute of every day. The full script should be run from the command line but you can probably have it run once an hour with a cron job.

The user agent can be anything you want it to be. SSWP just tells my logs what it is. The “real_index_page” is the page slug you use for the front page (“sample-page” in the script). It won’t hurt anything for it to exist, but it’s wasted space.

The scripts will create files without any extensions, except for “index.html”.

Semi Static WordPress Procedures

When the single script executes, it looks for files in the “/temp” directory. It will create/regenerate files in the static directory for each one them and then delete the temporary files. When the full script executes, it creates/regenerates all the files in the “/static” directory.

Use FeedBurner for a semi static experience with feeds. You don’t want other people hitting your database for anything. Here are a few rewrites to redirect all feeds to FeedBurner (you need an account):

if ($http_user_agent !~* feedburner) {
rewrite ^/atom$ https://feeds.feedburner.com/your_account permanent;
rewrite ^/comments/feed https://feeds.feedburner.com/your_account permanent;
rewrite ^/feed$ https://feeds.feedburner.com/your_account permanent;
rewrite ^/feed/ https://feeds.feedburner.com/your_account permanent;
rewrite ^/rss https://feeds.feedburner.com/your_account permanent;

The full script creates the “index.html” file and the “sitemap.xml” file to go with the rest of the new files. The single script only adds the “sitemap.xml” file.

Disqus Comments

A static website doesn’t include a commenting system. You can add Disqus by creating an account there and adding this code to your “functions.php” file:

add_filter('the_content', 'add_disqus');
function add_disqus($text) {
$text .= '

return $text;

Change “your-site-url” to your site URL and change “your-account” to your Disqus account.

An Updated Article

To be clear, this is an update. I wrote the original tutorial in multiple articles last year (2018). This is all of them combined and corrected.

The code will probably have to be tweaked to be 100 percent correct for your specific website. You should view the output before relying on it. Make sure there are no references to “wp-” anything, even if that means copying theme support files (even the jQuery script) into an “assets” directory like I use (feel free to look at the source of this page).

If you have any issues with anything, feel free to contact me.


RT Cunningham
April 16, 2019
Web Development