Tutorial: Let’s Build a Lightweight Blog System (part 1)

Tutorial: Let’s Build a Lightweight Blog System (part 1)

In this tutorial, we will be making a lightweight blog system with PHP and CSS3. The blog posts will be stored in a folder as files (in the markdown format). The system will not use a database and will not have an admin panel, which makes it perfect for small blogs with only one author. It will have an RSS feed and a JSON read-only API, and will feature a completely responsive design that adapts to the screen size of the device.

In this first part, we are going to write the PHP backend, and in part 2, we will code the HTML and CSS. Let’s begin!

The posts

Posts will be named in the following pattern: 2013-03-14_url-of-the-post.md and they will be stored in the posts/ folder. The first part is the date of the post (you can optionally include a time as well if you have more than one post for that day), followed by an underscore, and the title of the post. The underscore is used by the system to separate the title from the date. This convention also has the benefit that posts can easily be sorted by publication date. When displayed on your blog, this post will have an URL like http://example.com/2013/03/url-of-the-post

Each post will be in the markdown format (hence the extension). The only requirement is that the first line of the markdown document is formatted as a H1 heading. For example:

# The title of the blog post

Body of the blog post.

This will make the first line a H1 heading. If this is the first time you’ve seen markdown, the next section is for you.

What is markdown

Markdown is a lightweight format for writing text. It has a minimal and intuitive syntax, which is transformed into HTML. This makes it perfect for our simple blog system, as it won’t have an admin panel nor database – all the posts will be stored in a folder as files.

We can’t simply display the raw markdown directly, first we have to transform it into HTML. There are a number of libraries that can do this (in every major language), but for this tutorial I chose the PHP Markdown library, as it integrates nicely with composer.

Composer

Composer is a dependency manager for PHP. With it, you can declare what libraries your project depends on, and they will be downloaded automatically for you. We will use it to include two libraries in this project:

You could simply download these libraries and use them straight away, but let’s see how we can do the same with composer (note that you need to follow these steps only if you are starting from scratch; the zip accompanying the tutorial already includes all the necessary files).

First we have to declare which packages the current project depends on, in a file named composer.json:

{
    "require": {
	"dflydev/markdown": "v1.0.2",
	"suin/php-rss-writer": ">=1.0"
    }
}

You can obtain the specific identifiers for the libraries and their versions from the composer package repository, or by following the instructions that each library included on its github repo.

The next steps are to install composer into your project (follow these instructions), and to run the install command. This will download all the appropriate libraries and place them in the vendor/ folder. All that is left is to include the composer autoloader in your php scripts:

require 'vendor/autoload.php';

This will let you create instances of the libraries without having to include their PHP files individually.

Lightweight PHP Blog System

Lightweight PHP Blog System

The configuration file

I am using one additional PHP library - Dispatch. This is a tiny routing framework that I’ve mentioned before. It will let us listen for requests on specific URLs and render views. Unfortunately it doesn’t have Composer support at the moment, so we have to download it separately and include it into the project.

A neat feature of this framework is that it lets you write configuration settings in an INI file, which you can access with the config() function call. There are a number of settings you need to fill in for your blog to work:

app/config.ini

; The URL of your blog
site.url = http://demo.tutorialzine.com/2013/03/simple-php-blogging-system/

; Blog info

blog.title = "Tutorialzine Demo Blog"
blog.description = "This is a lightweight and responsive blogging system.."
blog.authorbio = "Created by ..."

posts.perpage = 5

; Framework config. No need to edit.
views.root = app/views
views.layout = layout

These settings are used throughout the views, so when you are setting up a new blog you won’t need to edit anything else.

The PHP code

Here is our main PHP file:

index.php

// This is the composer autoloader. Used by
// the markdown parser and RSS feed builder.
require 'vendor/autoload.php';

// Explicitly including the dispatch framework,
// and our functions.php file
require 'app/includes/dispatch.php';
require 'app/includes/functions.php';

// Load the configuration file
config('source', 'app/config.ini');

// The front page of the blog.
// This will match the root url
get('/index', function () {

	$page = from($_GET, 'page');
	$page = $page ? (int)$page : 1;

	$posts = get_posts($page);

	if(empty($posts) || $page < 1){
 		// a non-existing page
 		not_found();
 	}

     render('main',array(
     	'page' => $page,
		'posts' => $posts,
		'has_pagination' => has_pagination($page)
	));
});

// The post page
get('/:year/:month/:name',function($year, $month, $name){

	$post = find_post($year, $month, $name);

	if(!$post){
		not_found();
	}

	render('post', array(
		'title' => $post->title .' ⋅ ' . config('blog.title'),
		'p' => $post
	));
});

// The JSON API
get('/api/json',function(){

	header('Content-type: application/json');

	// Print the 10 latest posts as JSON
	echo generate_json(get_posts(1, 10));
});

// Show the RSS feed
get('/rss',function(){

	header('Content-Type: application/rss+xml');

	// Show an RSS feed with the 30 latest posts
	echo generate_rss(get_posts(1, 30));
});

// If we get here, it means that
// nothing has been matched above

get('.*',function(){
	not_found();
});

// Serve the blog
dispatch();

The get() function of Dispatch creates a pattern that is matched against the currently visited URL. If they match, the callback function is executed and no more patterns are matched. The last get() call sets up a pattern that matches every URL in which case we show a 404 error.

Some of the functions you see above are not part of the Dispatch framework. They are specific to the blog and are defined in the functions.php file:

  • get_post_names() uses the glob function to find all the posts and to sort them in alphabetical order (as the date is the first part of the file name, this effectively sorts them by their publishing date);
  • get_posts() takes the list of names returned by get_post_names() and parses the files. It uses the Markdown library to convert them to HTML;
  • find_post() searches for a post by month, day and name;
  • has_pagination() is a helper function that is used in the views (we will look at them in the next part);
  • not_found() is a wrapper around Dispatch’s error function but renders a view as the message;
  • generate_rss() uses the RSS Library we mentioned above, and turns an array of posts into a valid RSS feed;
  • generate_json() is only a wrapper around json_encode, but I included it for consistency with the generate_rss function.

And here is the source:

app/includes/functions.php

use dflydev\markdown\MarkdownParser;
use \Suin\RSSWriter\Feed;
use \Suin\RSSWriter\Channel;
use \Suin\RSSWriter\Item;

/* General Blog Functions */

function get_post_names(){

	static $_cache = array();

	if(empty($_cache)){

		// Get the names of all the
		// posts (newest first):

		$_cache = array_reverse(glob('posts/*.md'));
	}

	return $_cache;
}

// Return an array of posts.
// Can return a subset of the results
function get_posts($page = 1, $perpage = 0){

	if($perpage == 0){
		$perpage = config('posts.perpage');
	}

	$posts = get_post_names();

	// Extract a specific page with results
	$posts = array_slice($posts, ($page-1) * $perpage, $perpage);

	$tmp = array();

	// Create a new instance of the markdown parser
	$md = new MarkdownParser();

	foreach($posts as $k=>$v){

		$post = new stdClass;

		// Extract the date
		$arr = explode('_', $v);
		$post->date = strtotime(str_replace('posts/','',$arr[0]));

		// The post URL
		$post->url = site_url().date('Y/m', $post->date).'/'.str_replace('.md','',$arr[1]);

		// Get the contents and convert it to HTML
		$content = $md->transformMarkdown(file_get_contents($v));

		// Extract the title and body
		$arr = explode('</h1>', $content);
		$post->title = str_replace('<h1>','',$arr[0]);
		$post->body = $arr[1];

		$tmp[] = $post;
	}

	return $tmp;
}

// Find post by year, month and name
function find_post($year, $month, $name){

	foreach(get_post_names() as $index => $v){
		if( strpos($v, "$year-$month") !== false && strpos($v, $name.'.md') !== false){

			// Use the get_posts method to return
			// a properly parsed object

			$arr = get_posts($index+1,1);
			return $arr[0];
		}
	}

	return false;
}

// Helper function to determine whether
// to show the pagination buttons
function has_pagination($page = 1){
	$total = count(get_post_names());

	return array(
		'prev'=> $page > 1,
		'next'=> $total > $page*config('posts.perpage')
	);
}

// The not found error
function not_found(){
	error(404, render('404', null, false));
}

// Turn an array of posts into an RSS feed
function generate_rss($posts){

	$feed = new Feed();
	$channel = new Channel();

	$channel
		->title(config('blog.title'))
		->description(config('blog.description'))
		->url(site_url())
		->appendTo($feed);

	foreach($posts as $p){

		$item = new Item();
		$item
			->title($p->title)
			->description($p->body)
			->url($p->url)
			->appendTo($channel);
	}

	echo $feed;
}

// Turn an array of posts into a JSON
function generate_json($posts){
	return json_encode($posts);
}

At the top of the file, we have a number of use statements, which import the necessary namespaces (read more about PHP’s namespaces).

The last thing we need to do, is to rewrite all requests so they hit index.php. Otherwise our fancy routes wouldn’t work. We can do this with an .htaccess file:

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteCond $1 !^(index\.php)
RewriteRule ^(.*)$ index.php/$1 [L]

If a file or folder does not exist, the request will be redirected to index.php.

It’s a wrap!

Continue with the second part, where we are creating the views and the responsive CSS design.

Join our newsletter and get our PSDs!19,476 people learn about HTML5, JS and more. Join them!

by Martin Angelov

Martin is a web developer with an eye for design from Bulgaria. He founded Tutorialzine in 2009 and it still is his favorite side project.

41 Comments

  1. Dex Barrett says:

    Great flat-file tutorial, Martin. Markdown is great for simple blogs just like this tutorial's. I didn't know Dispatch, looks cool.

    1. Martin Angelov says:

      Thank you! Happy you like it :)

      1. Dex Barrett says:

        Talking about Markdown, I found this useful flat-file wiki. I use it as a local knowledgebase synced with Dropbox: http://wikitten.vizuina.com/

        1. Martin Angelov says:

          Cool! I am bookmarking this.

  2. amin says:

    hello martin that's a great tutorial
    may i translate this tutorial to Persian language and publish it in my own website width back link of your website ?

    1. Martin Angelov says:

      Thank you! No, unfortunately the tutorials can't be translated. See here.

      1. amin says:

        OK Martin thanks !

  3. iman says:

    HI martin
    your works very nice
    I think these things should be published in all the world
    Your site is blocked in my country
    You should let your work In my country spread...
    Of course for Education NOT GET MONEY
    sorry for Language

  4. great write Martin. I'd been read your latest article about small php framework and thought to write my own too, but you came first. Nice job!

  5. cloudox says:

    im not understand this not work with me ? why im getin this > (THIS PAGE DOESN'T EXIST!

    Would you like to try our homepage instead?)

    1. Martin Angelov says:

      Make sure that you supply a correct site.url in config.ini. The other possibility is that you don't have any posts.

  6. Good stuff here! Out of curiosity, why didn't you start with Kirby as the backend rather than creating your own? Just as a tutorial?

    Bob

  7. Max Requiao says:

    I enjoyed seeing, being of congratulations, very good tutorial, be very successful!

  8. Robert says:

    Amazing and great looking blog man
    It also wold be nice with an infinite-scroll at the posts, hope you teach how to add it next week.

  9. Daniel says:

    Hi Martin,

    Awsome tutorial!

    Thanks!

    Daniel

  10. rubin says:

    Not Found
    The requested URL /blog/2013/03/image-picker-jquery-plugin was not found on this server.

    I'm using php version 5.3 now, and I can display the index page , but whan I click the title of a post, I get the error above.

    Please kindly help me fix it.

    Thanks.

  11. Stephen says:

    I just found this after looking for a very simple blog platform (I have no major use for WordPress on my personal blog anymore), can I use this freely on my website (with credit)? Also, is there an easy way to display all posts in a list on the sidebar? I'm sure with a bit of tweaking I could get it working, but just checking if there would be a simpler way.

    Thanks!

  12. Mark says:

    What part of the code displays the calendar icon next to the post date? it keeps disappearing when I modify the code to my liking.

  13. Bukla says:

    Why don't you simply work with XML files and the according PHP functions? No need to explode text here and there.

    1. Martin Angelov says:

      I wouldn't find it simpler, but you are free to change the code and do it however you would like.

  14. Velosofy says:

    I'm trying to host this but im getting

    Parse error: syntax error, unexpected T_FUNCTION in /home/velosofy/public_html/index.php on line 17

    wich is this code: http://gyazo.com/c3012b7e06e99971c6de1eea39b43bee
    Could you tell me how this comes?

    1. Velosofy says:

      btw, i want to add:
      This works for me when i run it local

    2. Martin Angelov says:

      You are probably running an old version of PHP that doesn't support anonymous functions. You need PHP 5.3 or later.

      1. Velosofy says:

        Thanks,
        It's working fine now :)

  15. Mith says:

    Very good Tutorial!! Can you make a similar tutorials on slim PHP micro framework.

  16. Hassan Yousuf says:

    Hello. Awesome job with the blog system (Y)

    How do I create a new post and how will it appear on the top of the site?
    I created another new post just like you said:

    # The title of the blog post

    Body of the blog post.

    I wrote the blog then what do I save the file in? What should I rename? Which extension? This part is really making me sick.

    Thanks! :D

  17. George French says:

    Hi, great tut, I uploaded it to my development Web Server but ran across a problem, whenever I try to access a post, I get 404.

    You can take a look: http://berty.org

  18. Hassan Yousuf says:

    Hello.

    When I click a blog post from the homepage it gives me an error saying "No input file specified"
    Please help.

    1. myc says:

      Same here, everything works fine except for viewing single post.(The requested URL was not found on this server)

      Anyway, thanks for this awesome tutorial.

      1. Stephen says:

        I had this same problem, is the server you're running it on use SuPHP? If so, that's the problem. You need to set the permissions correctly for all the files/directories.

        Directories: 755
        Files: 644

  19. Warren Clyde says:

    Hello.

    I want to create a search engine like whenever they search something from the site that relates a blog it should show the result.

    Like when they want to search something that is on my blog. It should show it. How do I do it?

    Please reply.

    1. deron says:

      Something more than Google custom search (google.com/cse)?

  20. dpuk44 says:

    Hi, does anyone know how to auto index the posts only on page one (newest posts) ?

    This would be a big help?

    Thanx

  21. Josh Chapman says:

    How would I go about moving the blog system so that it shows up and runs based on 'blog.php' rather than 'index.php'? In other words, how do I get a cover/home page to show up instead of the blog?

    Love the concept of a simple non-admin blog system. Great work!

  22. deron says:

    I really like this format, but for the life of me none of the solutions have addressed the single post issue. I even uploaded the download example to a test folder only modifying the .ini file, and the results are that the single posts still errors out. If tried all the .htaccess modifications mentioned so far with no love. Any ideas on where else the issue might be?

    http://www.getgoingsolutions.com/test/simple-php-blogging-system

  23. Dave says:

    I am having the single post issue as well. Anyone have ideas where I should look in the code?

  24. Aaron B says:

    If you are seeing blank pages when clicking on an article, and the rest of the fixes don't work, open the .htaccess file and replace the contents with this code:

    DirectoryIndex index.php
    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond $1 !^(index\.php|robots\.txt)
    RewriteRule ^(.*)$ index.php?/$1 [L]
  25. Dave says:

    I got it working!!! The issus is with Apache2 and what you need to do to get apache2 to follow the .htaccess is to to edit /etc/apache2/sites-available/default from this

    <Directory /var/www/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride None
    Order allow,deny
    allow from all
    </Directory>

    to

    <Directory /var/www/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
    </Directory>

  26. Laszlo Espadas says:

    Nginx rewrite:

    location / {
        if (!-e $request_filename){
            rewrite ^(.*)$ /index.php/$1 last;
        }
    }
    
    1. dlnandrsn says:

      The correct NGINX rewrite is as follows:

      if (!-e $request_filename) {
      rewrite ^/(.*)$ /index.php?q=$1 last;
      }

      Cheers!

  27. Luy Tran says:

    Recommend this routing system: https://github.com/c9s/Pux

Add Comment

Add a Reply

HTML is escaped automatically. Surround code blocks with <pre></pre> for readability.
Perks:   **bold**   __italics__   [some text](http://example.com) for links