7 days of unlimited WordPress themes, plugins & graphics - for free!* Unlimited asset downloads! Start 7-Day Free Trial
  1. Web Design
  2. Complete Websites

Introducing Stacey: the Lightweight CMS Alternative

Scroll to top
Read Time: 30 mins

There's more to the world of content management systems than Wordpress, Squarespace and Joomla. I'm going to show you a lightweight and database free alternative, perfect for just straight-up blogging. Her name is Stacey..

Who is Stacey?

Stacey is a lightweight php CMS developed by Anthony Kolber. Unlike most CMS's there's no database involved, no admin interface, just a small collection of php files. Throw the application folder onto a php server and you're good to go.

Stacey stores content in the form of directories and text files. For each entry (be it blog post, portfolio, tutorial, whatever) you add a folder, within that you place an accompanying text file, plus any other extras you may want. Stacey trawls the folders and generates a front end just like any other CMS.

You don't need to be familiar with php to build a theme either. Stacey makes use of a templating language which allows you to output data and perform some basic tricks, but nothing which will make your head spin. If you're comfortable with HTML and CSS, you're in good hands.

Why Even Flirt With the Idea?

Sometimes, the CMS packages we're all used to seeing (think WordPress, Drupal) are just too powerful. They've become hugely capable and versatile platforms, but often overly complex if all you want is to write the occasional blog post.

Stacey, however, is very focused. There's also an extra element of security as she has no database to hack (security is a factor which has led to a few database-free CMS approaches; such as Jekyll, which runs locally and generates static HTML for deployment onto your web server.)

You might think that working with and organizing text files is a recipe for disaster, but it's actually pretty effective. Stacey's templating makes it manageable and surprisingly scalable.

Lastly, it's always good fun to just play with different things..

Enough talk, let's introduce ourselves so you can make up your own mind.

First Date

In order to demonstrate the features and inner workings of Stacey, we first need to design ourselves a simple blog. I won't go into the whole HTML/CSS build, but let's just break down the files I'll be working with.

Note: if you're not interested in this bit, feel free to jump to the meat of the tutorial where we start building our CMS..

So, to begin with, I need to think about the pages (and therefore templates) I'll need. Standard blog stuff; an index.html where I can list recent blog posts, a single post, plus a page for displaying things like "About Me", "Contact" etc. About as generic as you can get!

Let's start with a blanco HTML page:

All clear so far. We'll need a styles.css (along with normalize.css in this case) and I've included a couple of favicons too.

You'll notice that, besides the HTML5 shiv, there's no reference to jQuery or any other JavaScripts. We don't really need any, so why bother including the assets? Keep things lean and mean. Our file structure looks a little like this at the moment:

Second Base

Let's quickly wrap up this static HTML page, then we can concentrate on getting Stacey involved. I've thrown a <header> in, with a logo and some main navigation links:

Under that, on the homepage at least, there's a hero section to draw visitors in:

You'll notice that each of these sections so far contain a wrapper within them. This wrapper will determine the width of our content.

Here's another section, our main content area, starting off with an <aside> where we'll house our secondary navigation:

Next to the aside, our taster articles to lead people into the blog posts. There are a few of these <article> elements:

After all our articles, we'll close all the parents off and add a simple footer which contains a couple of links:

Phew, we're done with the markup! Now the CSS..

Closing the Deal

Our CSS is straight forward. There are no images to embed, no gradients, border radius values or obscure CSS3 effects. We begin with some fundamentals:

I always include these - clearfix is just very useful and * { box-sizing: border-box } allows for a brilliantly intuitive way of manipulating elements.

The rest of the CSS is just styling. It features the awesome tomato named color value, which I'm currently in love with. The layout follows a mobile first approach, with just one media query at 700px to shift things into columns and bump the base font-size.

So then, end result?

The other pages follow more or less the same structure, giving us a decent basis from which to work. On with the actual tutorial!

Step 1: Collecting Stacey

This bit's easy; head on over to Stacey's GitHub repository and download the latest (3.0 at time of writing) source files.

This is more or less what you'll have downloaded

Much of what you've downloaded is demo stuff, so we can clear out a lot of the surplus. We need to keep:

  • The "app" folder and its contents.
  • The "extensions" folder and its contents.
  • The "htaccess" file.
  • The "index.php" file.

We also need the following folders, but they can be completely emptied for now:

  • The "content" folder.
  • The "public" folder.
  • The "templates" folder.

Step 2: Make Acquaintance

In order for things to actually run, you'll need to have your files hosted on a php5+ server. Whether you do this on your local machine, or on a live web server is up to you. In any case, once you have your project in the right place, visit the root url in a browser and..


Fair enough, we pulled all the demo content out, so let's now add some requisite files and integrate our blog theme to get things actually working.

Firstly, place the index.html we've built into the "templates" folder of our Stacey project. Secondly, we need to set some global variables, so place a file in the "content" folder, call it "_shared.yml" or "_shared.txt" and fill it with the following:

All will become clear in a moment..

Lastly, in order to make use of our index.html and display a page, add a folder to "content" called "index". In that folder, add a file called "index.yml".

Now if you refresh the page, you'll see that our index.html is being pointed to (even though it's still completely lacking in styles).

Step 3: How Stacey's Mind Works

It's only fair I explain what happened there. Upon opening a page Stacey looks for content in the content folder. She'll always look at the "_shared.yml" first, that's where we've set some global variables such as the site name etc. If you're not familiar with YAML think of it like human-readable markup. We could also use a .txt file here, in fact Stacey recognizes and parses all kinds of file formats.

She then looks for a folder to match the url. We've opened the root up in the browser, so she's used the folder "index" to determine the contents. Once the contents folder has been found, she'll look for a .txt or .yml file within that folder. The name of that particular file is very important, because that corresponds to the name of the template she'll then grab out of the "templates" folder. She'll mash the whole lot together and display the result in the browser.

Renaming our .yml file would mean using a different template file:

With that cleared up, let's now get our index.html looking more presentable.

Step 4: Linking up Assets

We've already prepared our CSS files and our favicons, but our template index.html can't find them at the moment. Let's put them in the "public" folder, then make sure the paths are all pointing in the right place.

The "public" folder is made available to us, thanks to some .htaccess wizardry, directly at the root of the project. That means we can point to "/css/styles.css" instead of "/public/css/styles.css". In order to do this, we need to firstly rename the "htaccess" file ".htaccess" and make sure that our server allows ReWrites (in most cases your server will have this feature as default).

Stacey makes a few variables always available for us to use. For example we can output {{ page.root_path }} which will give us the root of our project. Let's update our paths to make sure the CSS and favicons are hooked up:

If you now reload the page, you'll see the index.html displaying exactly as we made it. Excellent.

Step 5: What's She Talking About?

Now then, those curly braces I just used to output a variable? They're part of the Twig templating language which Stacey uses to communicate. You don't need to be a developer genius to use Stacey, but there are a couple of ways to output things which you need to be aware of.

There are two kinds of delimiters: {% ... %} and {{ ... }}. The first one is used to execute statements such as for-loops, the latter prints the result of an expression to the template. - Twig Documentation for Designers

Let's use the same value again, to make our logo link to the root url:

Now let's look at some other variables; we set a few in our "_shared.yml", using key value pairs, remember? Let's use those values in our metatags.

These values will be taken from the "_shared.yml" file, unless we overwrite the variables in our content files. If we define "title: Margarine" within "index.yml" that's the value that will be outputted.

In this way, we can define variables and have them outputted in the HTML templates. For example, let's place a key value pair for "page_title" within the "index.yml". We can have that output within the title metatag too:

If that value isn't available, it won't be outputted, simple as that. We can now define a specific page title for each page in our project (but we'll get to that in a bit). Is it all starting to come together now?

Step 6: Splitting Up

Already?! Don't panic, I'm talking about splitting our template files up. If you've ever built a dynamic website, you'll have split common assets into files of their own. The head of our index.html, for example, will be used in exactly the same way for all of our template files, so there's no point in having multiple copies of it. Let's split it off into its own file:

Everything up to the closing </header> is going to be common across all the templates. Cut it out, paste it into a new file and save that file as "head.html" within a new folder "partials" in the templates directory.

Now we need to include that partial into our index.html template. Where you snipped it out, add this line of code:

Refresh your homepage and all should appear exactly as it was, completely unchanged. You can do this for a few areas of the index.html of course; the footer (including the closing </body> and </html> tags) should go in a "footer.html" partial, the contents of the aside, plus the looping article tasters.

Once you're done, you should have a few snippets in your "partials" folder, and a somewhat lighter index.html template which looks like:

Step 7: Getting Serious

So far what we've produced boils down to an overly complicated version of a static html page, not much to write home about. Let's now add some actual content to our directories so we can see about outputting it dynamically.

As I mentioned earlier Stacey grabs content from the "content" folder. She accesses the "_shared.yml" file, then she checks out whichever folder matches the current url. We have friendly urls enabled (assuming you correctly renamed your .htaccess file) so if we were to visit "stacey-master/cheese-burger" she would look for a YAML file within "content/cheese-burger".

Failing that, she'd look for "content/1.cheese-burger" or "content/2.cheese-burger" because we can also prepend our folder names with numbers. Doing this allows us to index them if ever we need to loop through the folders (which we'll look at in a bit).

Further clarification of how Stacey pulls content into the browser

We're going to add some folders to our "content" folder. We have one for "contact" and another for "about". You'll notice they're preceded by a number, which means we can index them and loop through them for navigation purposes.

We then have "articles" which doesn't contain a .yml file of its own (to help us prevent it being listed along with the other two) and a series of numbered folders within it. These will be our articles, the paths for which will be:

  • {root}/articles/leather
  • {root}/articles/rolling-cheese
  • {root}/articles/taxi-driver
  • {root}/articles/snake-skin

Step 8: Content

We have folders to give us some content, so now let's actually put that content within the folders. As we've discussed before, we need a .yml or .txt file in each folder, in order that Stacey knows which template to use and which content to pump into it. Without such a file, Stacey will return a 404 error (which reminds me, we need to pull together a 404.html at some point too, don't let me forget..)

In each of our "articles" sub folders, place an empty file: "post.yml". This is going to point to a "post.html" template, so duplicate the "index.html" template and rename it accordingly. Good start - visiting any of the article urls will now at least generate a page, albeit a duplicate of our home page.

Within our post.yml we can specify a number of variables which we can use to populate our post.html template. Whatever values you like, as long as they're key value pairs, like so:

The values can contain html syntax, or (even more useful) markdown. Adding a "|" and a line break after the key will automatically render the content as markdown, wrapping the content in <p> tags once it's parsed. For example, the "post.yml" for our "articles/1.leather" folder looks like this:

You'll recognize the page_title as being the variable we injected into the meta title of the page earlier on. We've specified a date string, an author, an intro paragraph, plus the content. Both the intro and the content will be parsed as markdown owing to the "|" and the line break.

Step 9: Putting Out

Let's now actually output these variables into our post.html template. Firstly, we'll remove that whole <section class="hero"> element; we don't need it anywhere other than the homepage. Next we'll remove the {% include 'partials/list_articles.html' %} statement within our tasters element.

In the hole that's left, let's paste some static html back in, which we can fill with dynamic content.

Now replace the values with the variables we've set out:

The comment count, for the time being, will have to remain as static data, but we'll sort that out later. With any luck, you'll have something a bit like this:

Now, repeat for all the "post.yml" files within your articles folder!

Step 10: Conditions

Okay, so we've covered setting and outputting variables. Now let's have a look at Stacey's conditional (if) statements.

We're going to add some links to the bottom of each post, to navigate to the next and previous entries. To begin with, we need to work out if there are any entries (sibling folders) to link to. Happily, Stacey makes a wide array of variables available to us at any given point; we've already seen and used a number of variables within the "page." scope. Whilst within a page we can also use page.siblings to perform this particular check for us:

This is what our templating engine's if statement looks like - much like an if statement in any scripting language. What we're saying here is "if there are sibling entries present, do something". Let's include some partials if there are entries to link to; one partial for a previous link, another for a next:

We'll now need to make the partials which we've just instructed Stacey to include (if they're not there, she'll throw a tantrum). link_prev.html will contain this:

and link_next.html this:

Reload your page, you should get something like this:

In order for these to be dynamically generated, display the correct titles and link to the correct posts, we need to dive into Stacey's variable scope again. We'll use a for/endfor loop, displaying each value present in the page.previous_sibling and page.next_sibling variables. There can only be one of each, so we generate one link with the correct details:


As you can see, Stacey has prepared all the values we need, giving us all the page details for the sibling folders of our current entry. Brilliant.

Step 11: Primary Navigation

We're getting pretty good at this, right? How about we loop through some more values and output some links for our top level pages? We'll place these in the header strip at the top of each page. Here's what we have in partials/head.html:

How do we loop through the folders in our root? Which are those folders? We're focusing on 1.about and 2.contact - these are the two which will be listed. In order to be looped through they need to be numbered - we're also going to make it necessaryu for there to be a .yml file present. So, we're looking at children within the root, and that's how Stacey has prepared them for us too. Replace the list items with this loop:

You'll be able to work out what's going on from what we've previously done with for/endfor loops. We've said that for each child present in the root ("content" folder) if there's a page_title variable present fot that child, output the details. Our "3.articles" folder is ignored in this case.

What may take you by surprise is that folders are looped in reverse numerical order. This is logical when you consider that more recent blog entries will have the higher numbers, the earliest of course having 1,2,3 etc. Most recent folders are output first. I think I'll switch the numbers on my "about" and "contact" folders round, so they display more favorably.

In order for this to actually output anything, we'll need to have the necessary .yml files present in the 1.about and 2.contact folders - so make sure you have them. Again, you'll need to name them "page.yml", or something similar, making sure there's a template of that name present in the templates folder. This is all coming naturally now!

Step 12: Secondary Navigation

We're making good progress, now we need to output a similar set of links for our subnav, so we'll output the subfolders present. Place this in the "nav_articles.html" partial:

We say that for every indexable child ("1.about, 2.contact, 3.articles") if that child has children of its own, churn out an <ul> element. For each of those children, output a list item with the title and url.

We only have one folder with children, our "articles" folder, but if we were to have more, perhaps a portfolio for example, then that too would be outputted. In those situations it might be wise to clearly separate the submenus, but it's not something we need to worry about in this case.

You should now have a dynamically outputted submenu, linking you to each and every article. More importantly, you almost have a fully functioning site! I'm just going to clean up the "page.html" template by getting rid of the previous and next links, plus all the metadata we don't need. The body of the template looks like this now:

Pretty straight-forward.

Step 13: Homepage Content

The posts on the homepage are more or less the same as the secondary navigation. Let's output those in the same way, adding extra variables and bits of markup as needed. In partials/list_articles.html we use the same for/if/for arrangement:

Now let's add the markup, along with the correct variables to the loop:

Done! Let's now look at some extras to jazz the site up a bit.

Step 14: No Comment

Discussion and community is often present on blogs, but as we're not running a database it's difficult for us to manage our own comments system. Instead, we're going to lean on a third party service and have them do the hard work for us. There are many options to choose here, Facebook allow you to use their comments API enabling you to leverage all the power behind the world's largest social network. One caveat in opting for thier service though, is that you don't technically own the content produced via this method. Your comments aren't your own. Who can say what will happen in the future and what sort of rights you'll have or want to have over the content?

Instead, today we're going for Disqus, which you'll notice we've also implemented on Tuts+ recently.

Head on over, sign up for a free account and you'll be able to enter the details of your domains.

After you've filled in all the necessary details, you'll find options for implementing Disqus in your site. There are plugins for all kinds of familiar platforms, but we'll need the universal embedding snippet:

We need to embed this in our pages where comments are needed. It's entirely up to you how you go about this - you can simply copy and paste this where the comments will go (in "post.html"), or you can split things up into a separate js file, which keeps things a bit cleaner.

You'll need the element where comments are injected to be placed in the "post.html" template:

I've just stuck it underneath the next and previous links. Now separate off the javascript and place in a file in a new js folder within the public directory:

I've named this file disqus.js and I'll pull it into the footer partial.

Note: On some pages, the script will throw a small warning (check web inspector) if it's searching for the #disqus_thread div and it can't find it. Not a massive problem, but if you're a js genius and you want to sort it out, feel free!

Cool! We now have comments on each post! Disqus looks after the whole lot for us, recording comments against the url of each post (so if you alter the url for some reason, expect your comments to vanish). It's also nice that the markup which has been injected is fully fluid (so it fits well into our fluis layout) and adopts some of the simple styles we've set, such as link colors etc.

Leave some comments, we'll need them to test the next bit..

Step 15: Counting Comments

Having made space for each post's comment count amongst the meta data, let's now populate it with some real data. Disqus allows us to do that with another js snippet (which you'll find in your disqus.com dashboard). Again, you can paste it in as is, or just take what we need and add it to the disqus.js file:

This pulls in the count.js file. Now we need to appeand any links where we want the comment count displayed with #disqus_thread. For example, in our post.html:

and in partials/list_article.html:

The "Comments" will be replaced, once everything's been loaded, with the comment count associated with the url:

Step 16: 404

Thanks to the .htaccess file we're all set up use 404 redirects if someone enters a broken url. Now all we need to do is place a 404.html file in the public folder. As this file isn't parsed by Stacey in any way, we can't make use of variables or any of the content we have elsewhere (shame). For this reason, it's best to have a standalone file, fully self-contained, with all its styles and content right there in the html.

Here's what I came up with (quickly), notice the embedded styles in the head of the document:

Go ahead and create this file. Throw it in the public folder, then visit a nonsense url within your project..

Step 17: Friends with Benefits

Stacey offers plenty more, straight out of the box, so it's a good idea to jump back into the original source files and take a look what's on offer. For example, there's an RSS template, which we can link to and which generates a feed from all the content. It looks like this:

We'll add this "feed.atom" to our templates folder. We also then need a few partials, which you'll find in the source, so throw this whole "feed" folder into the partials directory too:

You can edit these atom files in any text or code editor, so feel free to alter the way your feed is generated. For example, in "feed-entry.atom" you might want to change the page title:

That's assuming you changed the variable at the beginning of the tutorial. Now we need to link to the feed, so folk can grab it. We need a .yml file in the "content" folder, then we can point to that. Make a file content/feed/feed.yml and place this within it:

and now we need to update the link in our partials/footer.html:

Done! Again, you can alter the way your Atom feed is displayed, so feel free to play with variables within these files.

Note: You might also want to check out the json feed and the sitemap.xml which work in much the same fashion.

Step 18: Additional Media

The content of our blog is pretty unimaginative at the moment, just a few snippets of dummy text. We can use markdown of course, but we can display even more than that. Stacey provides us with some additional templates and functionality which help us output images, files and other media. To begin with, we need to include the partials which you'll find in the source:

These belong in templates/partials/assets. Each one handles a different file type (you can alter precisely how the markup is churned out) so how does Stacey actually make use of them? Stacey once again trawls the contents of the post folders, adding any extra files she finds to her bank of page information.

For example, let's place a few images in our content/3.articles/1.leather post folder.

With that done, Stacey will find the additional assets and index them. Include the media.html partial in the post.html template and you're good to go!

What you do with the images now is up to you; you could easily include a js slideshow script (in the public folder) and have these images rotate. Or display them differently with CSS. Up to you.

What about other media types? We can embed video, but how about a YouTube embed? That would be done with an extra chunk of html. Grab your embed code:

Paste it into a new html file within the folder where you want it, then let Stacey parse the contents and generate the page!

Note: The extra figure I've wrapped around the iframe there is to give us a fluid video. With this extra CSS:

we get (almost) bomb-proof fluidity.


Well, we've reached the end of a lovely evening together and I hope to have at least opened your eyes to the world of CMS alternatives. There's plenty more you can achieve with a system like this - I'm curious to see what you come up with.

Stacey is simple, but powerful enough to manage fundamental blogging needs. She's flexible and open to suggestion. Maybe there's a romance here waiting to blossom? For any questions or thoughts, get busy in the comments. Thanks for reading!

Additional Resources

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.