RT Cunningham


RTCMS - My Headless Content Management System

RTCMSVersion 1.1, released on October 21, 2019, can be downloaded here. Edit it and then copy it to your web server root, keeping the structure intact.

RTCMS is a headless content management system I designed for me and me alone. It replaced WordPress, which I used for years. Now I’m sharing it with you. It’s not perfect because I’m not perfect. I’m not an expert because I’m self-taught.

Since I’ve released what I’ve created into the public domain, you can do anything you want with it. The “PHP SmartyPants” library isn’t public domain, but it’s free to use.

The RTCMS structure and concept is simple (and I say simple a lot). Every entry or page (or anything like them that you create) is a type of post. The post files are PHP ini-type files. Except for the robots.txt, sitemap.xml and SmartyPants.txt files, the rest are PHP files.

Server Setup - The Apache and Nginx Web Servers

You should be able to use RTCMS on any web server. The only web servers I’m familiar with are Apache and Nginx, and I haven’t used Apache in years. Caddy is relatively new, but it seems promising.

The pertinent Apache setup is in the .htaccess file you put in the root directory of the website. It should look like this:

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

This batch of conditions and rules checks to see if the file or directory exists and then passes it off to the index.php file in the root directory for processing.

The pertinent Nginx setup is in the “server” context:

location / {
    try_files     $uri /index.php;
    expires       3600s;
    add_header    Cache-Control "max-age=3600, public";
    add_header    Vary Accept-Encoding always;

The try_files directive looks for the URI and if it’s not found, sends it to the index.php file in the root directory for processing.

The web browser expiration of “3600” should be whatever you feel comfortable with. The default file cache time is 3600 seconds (one hour) as well. The only time it should be an issue is when you’re waiting for the cache to update while editing a post.

The cache contains complete static pages in the cache directory. The original posts are built “on the fly”, just like a lot of familiar CMS platforms.

There is a slightly more advanced way of reading cache files with Nginx, which I’ll explain in a minute.

Site Structure - Keeping It Simple

This is the current site structure. The first five lines are directories and subdirectories. The rest of the lines are the files in the root directory.

I don’t think I can make it any simpler without putting everything in the root directory. That could get messy quickly.


The cache.php and sitemap.php files need to be moved somewhere outside the web root directory.

The “rtcx” theme subdirectory contains four files:


The theme has the styles in the document head, in the header.php file. I minified the styles using CSS Minifier. They can be unminified with Unminify.

Caching and Advanced Caching with Nginx

The cache code, contained in the index.php file, checks for the existence of a cache file and if it doesn’t exist, it creates one. It also regenerates the file if its cache time has expired. I included a script in the archive (“cache.php” in the root directory) that you can run at intervals with a cron job. I recommend you move it outside of the web root directory, or delete it if you don’t want to use it.

I don’t know about the Apache web server, but the Nginx web server can read the cache files directly, without relying on the PHP code. The key is a new variable in front of the $uri variable, which much be checked first. Examine this code:

# you only need to include the first two lines if your posts don't have filename extensions:
include           /etc/nginx/mime.types;
default_type      text/html;
set $cache_uri    /cache$uri;
if ($remote_addr = {
    set $cache_uri $uri;
location / {
    try_files     $cache_uri $uri /index.php;
    expires       3600s;
    add_header    Cache-Control "max-age=3600, public";
    add_header    Vary Accept-Encoding always;

You can create other conditions to bypass the cache, but it depends on your particular situation. Although I’ve used this type of code in the past, with another CMS, I don’t recommend it. PHP versions 7.0 and above process the code faster with every point release. If a cache file exists, only the index.php file is processed and it’s almost as fast as the advanced caching method.

The “3600” above should match the number in the config.php file in the root directory. The cron job for the script should also match. You can use any interval you want, but it should be at least 3600 seconds, which is one hour. One hour is a reasonable amount of time to wait when you’re editing existing posts.

The Configuration File and Everything Related to It

The configuration file in the root directory, config.php, contains all the global variables. The variables for the posts are contained in the post files themselves. First, I’ll explain the configuration file and then I’ll explain a typical post file.

The Configuration File

The paths and URLs are obtained programmatically. Anything you change here will change the structure of your website. You then have to be careful the directories and files actually exist.

The site settings have to be edited. If you want to use a site description, you need to make sure that variable is used in the theme. I don’t use it. The timezone has to be one of the timezones provided here. The date format and the time format items can be edited to comply with the formats here. The site author URI can be any page or entry you’ve already created. I use the home page for mine.

The site schema settings have to be edited. If you don’t belong to an organization, your name is sufficient. The logo needs to be larger than 160 pixels wide and 90 pixels high. I created a 200x200 image using Free Logo Design. The “sameas” links are the social profile links for your social sites. If you don’t want any in the website schema that search engines look at, change it to two single quotes (”).

The favicons are supposed to work with most web browsers and the Windows 8 and 10 start menus.

The Typical Post File

Each post file uses the same format whether it’s a page, an entry or something else.

The post date is the date using the UTC timezone. I have multiple timezones, including UTC, set up with the date and calendar display on my desktop. The post time is the time using the UTC timezone. The “entry” theme file will convert both to the timezone you enter in the config.php file.

The post type must be either “entry” or “page”. That is, unless you create another type. You can copy the entry.php file in the theme directory to make it work the same way, with the author, the date and the category. Or, you can copy the page.php file to follow that format. Or, you can do something else.

The post author and post image information is the same as what’s in the config.php file in the root directory unless you enter new information on the post. Although multiple people don’t have access to your site, you can have multiple authors.

The post category name and the post category slug can be anything as long as the slug matches the name without superfluous characters.

The posts robots should be blank unless you’re dealing with category or 404 pages. In those cases, it should be “noindex,follow”.

The post title should be less than 70 characters so all the characters will fit when displayed by the Google search engine. Double quotes within the title must be escaped by backslashes.

The post description should be a synopsis of your post content, using less than 157 characters. Double quotes within the description must be escaped by backslashes.

The post image name, width, height and caption pertain to the primary image in the post. I use thumbnails 150 pixels wide, but anything up to 300 pixels wide should work on all mobile devices.

The post content should be 300 or more words, which is easily counted in any text editor. Use standard HTML5 tags. Feel free to look at the source posts for these articles to see how it’s done. Double quotes within the content must be escaped by backslashes.

When working with post file names (which are also the post slugs) and image file names, use lowercase alphanumeric characters and dashes only (a-z, 1-9 and -). Other characters may or may not be interpreted by various web browsers correctly.

Do not delete lines without anything to the right of the = sign. You need to be able to easily change post information when necessary.

The Theme Files

There are four theme files in the default theme directory (“rtcx”). If you change nothing, your website will look exactly like mine and you probably don’t want that.

Changing an existing theme to use the variables I use shouldn’t be too difficult. There are plenty of websites where you can get free themes or templates. “HTML5 UP” is one such site. All the variables you need to know about are either in the config.php file in the root directory or in a post file.

I’ll go over my theme for you, and then you can decide if you want to use the same things in yours.


The header.php file contains everything that displays for each entry or page up to the top of the content area, including navigation. There are multiple conditions for controlling the information included in it. It includes a meta robots line if that variable is filled in on a post. It doesn’t include a canonical link line if a 404 response is produced.

The post author information is copied from the site author information unless those items are filled in on a post. One schema script is included on pages and another is included on entries. The entries version includes author information.

page.php and entry.php

The page.php file displays less than the entry.php file. An entry will display author, date and category information. Entries include previous and next post navigation items.

The page file includes routines to produce a list of articles and a list of categories. It also produces independent category lists. You must create the pages in advance, without the lists. The lists will be appended to them.


The footer.php file contains everything that displays for each entry or page below the bottom of the content area. It contains a JavaScript routine to hide and display the menu on mobile devices and it also contains a JavaScript routine that causes external links to open in a new tab or window.

Comments on Entry Posts

There are no native comment routines built into RTCMS. You’ll have to use a third-party service if you want comments at the bottom of your entry pages.

I use Disqus and the service hasn’t failed me yet. If you want to use the same service, you’ll have to set it up on their site. Then, instead of any code they give you, paste this at the bottom of the theme’s entry.php file, substituting “website” with your site information:

<div id="disqus_thread">
    <br><button onclick="disqus();return false;">Add or View Comments - No Login Required</button>
    var disqus_url = "<?php echo $url ?>";
    var discus_identifier = "<?php echo $url ?>";
    var discus_title = "<?php echo $post_title ?>";
    var disqus_loaded = false;
    function disqus() {
        if (!disqus_loaded) {
            disqus_loaded = true;
            var e = document.createElement("script");
            e.type = "text/javascript";
            e.async = true;
            e.src = "//";
            e.setAttribute("data-timestamp", +new Date());
            (document.getElementsByTagName("head")[0] ||

Creating and Editing Posts

The only software required for creating and editing posts is a plain text editor and a web browser. You need the web browser to check your work. The intensity of your work will determine if you need anything more.

I have a web development environment on my laptop, with my local website set up identically to my online website. I test my posts in my web browser before I upload them.

The post title should be less than 70 characters and double quotes within the string have to be escaped with a backslash. The post description should be less than 157 characters and double quotes within have to be escaped. The post content should be 300 words or more (preferably more than 500) and double quotes within have to be escaped.

If you include examples of HTML or PHP code, remember to convert the “<” character to “<” or the web browser will try to interpret it as HTML. This trips me up more often than anything.

Image sizes have to consider cell phone screen sizes. I like to use 150 pixel widths linking to images of 1024 pixels wide (or less). 300 pixel widths are as wide as I like to go, so that the text wraps around them properly. My theme uses three alignment styles: aligncenter, alignleft and alignright. As you can see, I prefer to align to the right.

Images aren’t even required. If you don’t use one and don’t fill in the information for one, the schema will use the site image information from the config.php file in the root directory.

Standard Pages

Every website has some standard pages. As far as I know, only two are required: The home page (index page) and the privacy policy page. Anything else is up to you.

As you can see, I have pages linked in the top bar and I have pages linked at the bottom of each post. You can use the comments policy and privacy policy pages as examples (don’t copy anything verbatim).

If you don’t have an outgoing mail server, you should change the contact page. My contact page is comprised of the page with a form on it, a script called contact-form.php (in the root directory) and another post with “Message Sent” as the post title. MailtoUI is a good alternative.

The lists shown on the article list and category list pages, which I’ve already mentioned, are generated by routines in the theme’s page.php file.

Copyright Information

The “PHP SmartyPants” library contains its own copyright information. To the extent possible under the laws of the United States and the Philippines, I (RT Cunningham) have waived all copyright and related or neighboring rights to “RTCMS” under a CC0 1.0 Universal Public Domain Dedication.