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:
- PHP Markdown, which I mentioned above;
- RSS Writer, which will output the RSS feed for the blog.
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.
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
Great flat-file tutorial, Martin. Markdown is great for simple blogs just like this tutorial's. I didn't know Dispatch, looks cool.
Thank you! Happy you like it :)
Talking about Markdown, I found this useful flat-file wiki. I use it as a local knowledgebase synced with Dropbox: http://wikitten.vizuina.com/
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 ?
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
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?)
Make sure that you supply a correct site.url in config.ini. The other possibility is that you don't have any posts.
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.
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.
I wouldn't find it simpler, but you are free to change the code and do it however you would like.
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?
btw, i want to add:
This works for me when i run it local
You are probably running an old version of PHP that doesn't support anonymous functions. You need PHP 5.3 or later.
Thanks,
It's working fine now :)
Very good Tutorial!! Can you make a similar tutorials on slim PHP micro framework.
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
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
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
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
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:
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>
Nginx rewrite:
The correct NGINX rewrite is as follows:
if (!-e $request_filename) {
rewrite ^/(.*)$ /index.php?q=$1 last;
}
Cheers!
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
Thanks for the tutorial. Very simple to understand.