Saturday, April 06, 2013

Tutorial Let’s Build a Lightweight Blog System Part 1

Do you want to share?

Do you like this story?

DEMO      -     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:

1# The title of the blog post
2 
3Body 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:

1{
2    "require": {
3    "dflydev/markdown": "v1.0.2",
4    "suin/php-rss-writer": ">=1.0"
5    }
6}

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:
1require '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

01; The URL of your blog
03 
04; Blog info
05 
06blog.title = "Tutorialzine Demo Blog"
07blog.description = "This is a lightweight and responsive blogging system.."
08blog.authorbio = "Created by ..."
09 
10posts.perpage = 5
11 
12; Framework config. No need to edit.
13views.root = app/views
14views.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

01// This is the composer autoloader. Used by
02// the markdown parser and RSS feed builder.
03require 'vendor/autoload.php';
04 
05// Explicitly including the dispatch framework,
06// and our functions.php file
07require 'app/includes/dispatch.php';
08require 'app/includes/functions.php';
09 
10// Load the configuration file
11config('source', 'app/config.ini');
12 
13// The front page of the blog.
14// This will match the root url
15get('/index', function () {
16 
17    $page = from($_GET, 'page');
18    $page = $page ? (int)$page : 1;
19 
20    $posts = get_posts($page);
21 
22    if(empty($posts) || $page < 1){
23        // a non-existing page
24        not_found();
25    }
26 
27     render('main',array(
28        'page' => $page,
29        'posts' => $posts,
30        'has_pagination' => has_pagination($page)
31    ));
32});
33 
34// The post page
35get('/:year/:month/:name',function($year, $month, $name){
36 
37    $post = find_post($year, $month, $name);
38 
39    if(!$post){
40        not_found();
41    }
42 
43    render('post', array(
44        'title' => $post->title .' ⋅ ' . config('blog.title'),
45        'p' => $post
46    ));
47});
48 
49// The JSON API
50get('/api/json',function(){
51 
52    header('Content-type: application/json');
53 
54    // Print the 10 latest posts as JSON
55    echo generate_json(get_posts(1, 10));
56});
57 
58// Show the RSS feed
59get('/rss',function(){
60 
61    header('Content-Type: application/rss+xml');
62 
63    // Show an RSS feed with the 30 latest posts
64    echo generate_rss(get_posts(1, 30));
65});
66 
67// If we get here, it means that
68// nothing has been matched above
69 
70get('.*',function(){
71    not_found();
72});
73 
74// Serve the blog
75dispatch();

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

001use dflydev\markdown\MarkdownParser;
002use \Suin\RSSWriter\Feed;
003use \Suin\RSSWriter\Channel;
004use \Suin\RSSWriter\Item;
005 
006/* General Blog Functions */
007 
008function get_post_names(){
009 
010    static $_cache = array();
011 
012    if(empty($_cache)){
013 
014        // Get the names of all the
015        // posts (newest first):
016 
017        $_cache = array_reverse(glob('posts/*.md'));
018    }
019 
020    return $_cache;
021}
022 
023// Return an array of posts.
024// Can return a subset of the results
025function get_posts($page = 1, $perpage = 0){
026 
027    if($perpage == 0){
028        $perpage = config('posts.perpage');
029    }
030 
031    $posts = get_post_names();
032 
033    // Extract a specific page with results
034    $posts = array_slice($posts, ($page-1) * $perpage, $perpage);
035 
036    $tmp = array();
037 
038    // Create a new instance of the markdown parser
039    $md = new MarkdownParser();
040 
041    foreach($posts as $k=>$v){
042 
043        $post = new stdClass;
044 
045        // Extract the date
046        $arr = explode('_', $v);
047        $post->date = strtotime(str_replace('posts/','',$arr[0]));
048 
049        // The post URL
050        $post->url = site_url().date('Y/m', $post->date).'/'.str_replace('.md','',$arr[1]);
051 
052        // Get the contents and convert it to HTML
053        $content = $md->transformMarkdown(file_get_contents($v));
054 
055        // Extract the title and body
056        $arr = explode('</h1>', $content);
057        $post->title = str_replace('<h1>','',$arr[0]);
058        $post->body = $arr[1];
059 
060        $tmp[] = $post;
061    }
062 
063    return $tmp;
064}
065 
066// Find post by year, month and name
067function find_post($year, $month, $name){
068 
069    foreach(get_post_names() as $index => $v){
070        if( strpos($v, "$year-$month") !== false && strpos($v, $name.'.md') !== false){
071 
072            // Use the get_posts method to return
073            // a properly parsed object
074 
075            $arr = get_posts($index+1,1);
076            return $arr[0];
077        }
078    }
079 
080    return false;
081}
082 
083// Helper function to determine whether
084// to show the pagination buttons
085function has_pagination($page = 1){
086    $total = count(get_post_names());
087 
088    return array(
089        'prev'=> $page > 1,
090        'next'=> $total > $page*config('posts.perpage')
091    );
092}
093 
094// The not found error
095function not_found(){
096    error(404, render('404', null, false));
097}
098 
099// Turn an array of posts into an RSS feed
100function generate_rss($posts){
101 
102    $feed = new Feed();
103    $channel = new Channel();
104 
105    $channel
106        ->title(config('blog.title'))
107        ->description(config('blog.description'))
108        ->url(site_url())
109        ->appendTo($feed);
110 
111    foreach($posts as $p){
112 
113        $item = new Item();
114        $item
115            ->title($p->title)
116            ->description($p->body)
117            ->url($p->url)
118            ->appendTo($channel);
119    }
120 
121    echo $feed;
122}
123 
124// Turn an array of posts into a JSON
125function generate_json($posts){
126    return json_encode($posts);
127}

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:

1RewriteEngine On
2 
3RewriteCond %{REQUEST_FILENAME} !-f
4RewriteCond %{REQUEST_FILENAME} !-d
5 
6RewriteCond $1 !^(index\.php)
7RewriteRule ^(.*)$ index.php/$1 [L]

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

by Martin Angelov
source: http://tutorialzine.com/2013/03/simple-php-blogging-system/

YOU MIGHT ALSO LIKE

0 komentar:

Post a Comment

Advertisements

Advertisements