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

Download

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.

simple-php-blogging-system.png

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 = https://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.

Bootstrap Studio

The revolutionary web design tool for creating responsive websites and apps.

Learn more

Related Articles

Dex Barrett

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

Martin Angelov

Thank you! Happy you like it :)

Dex Barrett

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

Martin Angelov

Cool! I am bookmarking this.

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 ?

Martin Angelov

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

OK Martin thanks !

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

Rizqy Hidayat

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!

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?)

Martin Angelov

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

Bob Rockefeller

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

That wouldn't be very lightweight, still worth checking.

Besides working with composer it's pretty comfy as is.

Max Requiao

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

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.

Hi Martin,

Awsome tutorial!

Thanks!

Daniel

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.

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!

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.

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

Martin Angelov

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

Velosofy

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?

Velosofy

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

Martin Angelov

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

Velosofy

Thanks,
It's working fine now :)

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

Hassan Yousuf

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

George French

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

Hassan Yousuf

Hello.

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

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.

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

Warren Clyde

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.

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

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

This would be a big help?

Thanx

Josh Chapman

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!

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

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

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]

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>

Laszlo Espadas

Nginx rewrite:

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

The correct NGINX rewrite is as follows:

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

Cheers!

Luy Tran

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

I'm having this issue described in an earlier comment... When I click a blog post from the homepage it gives me an error saying "No input file specified". Someone mentioned checking permissions and mine are all correct, but I'm still having the issue. Any other suggestions would be great... thanks

Paste the code in your blog .htaccess file
DirectoryIndex index.php
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond $1 !^(index.php|robots.txt)
RewriteRule ^(.*)$ index.php?/$1 [L]

Hello
First of all, thanks for the Tutorial and the great minimal Blog System!
I have a 4-language website and would like to customize the Blog System so that when a user changes the language, then the respective .md files from a to be included under the folder.
I would be for any hint or solution proposal grateful! ... and please excuse the language skills (With greetings from Google Translator).

Thank You!
Andreas

Hi Martin,
great Blog and Tutorial! Is it possible to add a "catogrization" to the blogposts and a search function? Yet, I don't know how to do that... would be great to have a tutorial on that too :)
Cheers,Kevin

Stephanie

Thanks for the tutorial. Very simple to understand.