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

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

As you remember from part 1, in this tutorial we are creating a lightweight blog system that stores posts as markdown files. It uses a number of libraries: for parsing markdown, for creating the RSS feed, and most importantly the Dispatch micro framework for giving the application structure.

In this part, we will create the views and the CSS that will make the site responsive on mobile devices.

The Views

As you remember from the first part, we used a configuration file – config.ini – to write down the configuration settings of the blog. The last two lines of the file hold the name of the file that holds the layout, and the path to the views:

app/config.ini (lines 12-14)

views.root = app/views
views.layout = layout

These settings are used internally by the Dispatch framework. The first line tells it where to find the views for the site (so that you only need to specify a name when rendering them) and the second is the layout. This is an HTML template that is included with every view that you render. This is a standard HTML5 document with a few additional meta tags to make it mobile-friendly:

app/views/layout.html.php

<!DOCTYPE html>
<html>
<head>
	<title><?php echo isset($title) ? _h($title) : config('blog.title') ?></title>

	<meta charset="utf-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" user-scalable="no" />
	<meta name="description" content="<?php echo config('blog.description')?>" />

	<link rel="alternate" type="application/rss+xml" title="<?php echo config('blog.title')?>  Feed" href="<?php echo site_url()?>rss" />

	<link href="<?php echo site_url() ?>assets/css/style.css" rel="stylesheet" />
	<link href="http://fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,cyrillic-ext" rel="stylesheet" />

	<!--[if lt IE 9]>
		<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
	<![endif]-->

</head>
<body>

	<aside>

		<h1><a href="<?php echo site_url() ?>"><?php echo config('blog.title') ?></a></h1>

		<p class="description"><?php echo config('blog.description')?></p>

		<ul>
			<li><a href="<?php echo site_url() ?>">Home</a></li>
			<li><a href="http://tutorialzine.com/members/">Members area</a></li>
			<li><a href="http://tutorialzine.com/contact/">Contact Us</a></li>
		</ul>

		<p class="author"><?php echo config('blog.authorbio') ?></p>

	</aside>

	<section id="content">

		<?php echo content()?>

	</section>

</body>
</html>

In the head section I am also including a font from Google Web Fonts, and a HTML5 shim that will make it work on browsers as old as IE8. Throughout this view, I am printing the settings we put in config.ini, by using the config() function provided by Dispatch. This will let you customize the blog solely from the config file. Another function provided by dispatch is site_url() which gives you the absolute URL of your blog, so that you can include stylesheets, images or use it as the base for links.

Notice the call to the content() function. What this does, is print the HTML generated by one of the other views. This allows for other views to be “nested” inside it. For example running this code:

index.php (lines 29-33)

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

Will embed the main.html.php template you see below in the view. The page, posts and has_pagination members of the array will be available as variables:

app/views/main.html.php

<?php foreach($posts as $p):?>
	<div class="post">
		<h2><a href="<?php echo $p->url?>"><?php echo $p->title ?></a></h2>

		<div class="date"><?php echo date('d F Y', $p->date)?></div>

		<?php echo $p->body?>
	</div>
<?php endforeach;?>

<?php if ($has_pagination['prev']):?>
	<a href="?page=<?php echo $page-1?>" class="pagination-arrow newer">Newer</a>
<?php endif;?>

<?php if ($has_pagination['next']):?>
	<a href="?page=<?php echo $page+1?>" class="pagination-arrow older">Older</a>
<?php endif;?>

This will present a list of posts on the screen, with optional back/forward arrows. The other template that also operates with posts, is post.html.php, which shows a single post:

app/views/post.html.php

<div class="post">

	<h2><?php echo $p->title ?></h2>

	<div class="date"><?php echo date('d F Y', $p->date)?></div>

	<?php echo $p->body?>

</div>

This is the view that you would need to edit if you want to add social sharing buttons or comment widgets from Facebook or Disqus.

And lastly, here is the 404 page:

app/views/404.html.php

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" />

	<title>404 Not Found | <?php echo config('blog.title') ?></title>
	<link href="<?php echo site_url() ?>assets/css/style.css" rel="stylesheet" />

	<!-- Include the Open Sans font -->
	<link href="http://fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,cyrillic-ext" rel="stylesheet" />

</head>
<body>

	<div class="center message">
		<h1>This page doesn't exist!</h1>
		<p>Would you like to try our <a href="<?php echo site_url() ?>">homepage</a> instead?</p>
	</div>

</body>
</html>

Note that this view is a complete HTML5 document. This is because it doesn’t follow the same layout as the rest of the blog. In the Dispatch framework, you can render a view that does not get embedded in the site layout, by passing false as the last argument to render:

app/includes/functions.php (lines 95-97)

function not_found(){
	error(404, render('404', null, false));
}

Now that we have all the fancy HTML printed to the screen, let’s style it!

The CSS

In this part of the tutorial, we will write the CSS that will make the blog pretty and responsive. All these styles are located in the file assets/css/styles.css that you can edit to your liking. One of the reason this blog is so fast to load is because the design is entirely CSS-based, and I haven’t used any images (thanks to a data url trick that you will see in a moment).

Let’s start with the headings:

h1{
	font: 14px 'Open Sans Condensed', sans-serif;
	text-transform:uppercase;
	margin-bottom: 24px;
}

h2{
	font: 48px 'Open Sans Condensed', sans-serif;
}

h1 a, h2 a{
	color:#4f4f4f !important;
	text-decoration: none !important;
}

Next is the content and posts. Notice the background image to .post .date:before.

#content{
	position: absolute;
	font: 16px/1.5 'PT Serif', Times, Cambria, serif;
	width: 580px;
	left: 50%;
	margin-left: -100px;
}

#content p,
#content ul{
	margin-bottom:25px;
}

#content ul{
	padding-left:20px;
}

#content li{
	margin-bottom:5px;
}

/* The actual post styles */

.post{
	padding: 50px 0 15px 0;
	border-bottom: 1px solid #dfdfdf;
}

.post .date{
	font: bold 12px 'Open Sans Condensed', sans-serif;
	text-transform: uppercase;
	color: #a7a7a7;
	margin: 24px 0 30px 20px;
	position: relative;
}

/* The calendar icon is set as a data url */

.post .date:before{
	width:18px;
	height:18px;
	position:absolute;
	content:'';
	left: -22px;
	top: -1px;
	background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA..')
}

This is the Data URI scheme, which allows for data which would normally be a separate file to to be presented inline. So by encoding the icon in base64 and placing it directly in the background property, I am saving a request to the server.

Next, we have the left static bar, which always takes up the full height of the page:

aside{
	position: fixed;
	width: 250px;
	height: auto;
	top: 0;
	left: 50%;
	bottom: 0;
	margin-left: -460px;
	padding-top: 65px;
}

aside p.description{
	font-size: 13px;
	line-height: 1.8;
	margin-bottom: 40px;
	color:#787878;
}

/* The main navigation links */
aside ul{
	font: bold 18px 'Open Sans Condensed', sans-serif;
	text-transform: uppercase;
	list-style:none;
}

aside ul li a{
	text-decoration:none !important;
	display:inline-block;
	padding:0 3px;
	margin:2px 0 2px 10px;
}

aside ul li a:hover{
	background-color:#389dc1;
	color:#fff;
}

aside p.author{
	position: absolute;
	bottom: 20px;
	height: 30px;
	font-size: 12px;
	color: #888;
}
Lightweight PHP Blog System

The Main Bar

After this, we will style the pagination arrows:

.pagination-arrow{
	display: inline-block;
	font: bold 16px/1 'Open Sans Condensed', sans-serif;
	border: 1px solid #ccc;
	border-radius: 3px;
	margin: 20px 15px;
	color: #555 !important;
	padding: 8px 12px;
	text-decoration: none !important;
	text-transform: uppercase;
	position: relative;
}

.pagination-arrow.newer{
	padding-left: 20px;
}

.pagination-arrow.older{
	padding-right: 20px;
	float:right;
}

.pagination-arrow.newer:before,
.pagination-arrow.older:before{
	content: '';
	border: 5px solid #555;
	border-color: transparent #555 transparent transparent;
	width: 0;
	height: 0;
	position: absolute;
	left: 3px;
	top: 12px;
}

.pagination-arrow.older:before{
	left:auto;
	right:3px;
	border-color: transparent transparent transparent #555;
}

I am using an old border trick to create triangular arrows with CSS. Next are the styles, specific to the 404 page:

.message{
	padding-top:50px;
}

.message h1{
	font-size:36px;
	margin-bottom: 18px;
}

.message p{
	font-size:13px;
}

.center{
	text-align:center;
}

And lastly, we have the media queries, that will tweak the layout depending on the resolution of the device:

/* Small tablets */

@media all and (max-width: 1024px) {
	aside{
		left: 5%;
		margin-left: 0;
		width: 25%;
	}

	#content{
		left: 35%;
		margin-left: 0;
		width: 60%;
	}
}

/* Smartphones */

@media all and (max-width: 840px) {

	h2{
		font-size:36px;
	}

	aside{
		margin-left: 0;
		position: static;
		width: 90%;
		padding: 5% 5% 0 5%;
		text-align: center;
	}

	aside .description{
		margin-bottom: 25px;
	}

	aside li {
		display: inline-block;
	}

	aside ul{
		text-align: center;
	}

	aside .author{
		display: none;
	}

	#content{
		position: static;
		padding: 5%;
		padding-top: 0;
		width: 90%;
	}
}

With this our lightweight flat-file blog is ready!

Done!

One benefit of using a flat file system is that it is much simpler to set up and deploy. Thanks to the markdown format, it is also incredibly easy to write blog posts. As everything is a file in the blog folder, you can use git to manage your blog’s revisions and to deploy it. The system is also highly customizable and with some CSS you can adapt it to your existing website.

Join our newsletter and get our PSDs!19,399 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.

46 Comments

  1. Dex Barrett says:

    Great as usual. One thing I don't fully understand despite the explanation is Dispatch's content() function. Where does it take the content if you don't pass any parameters?

    1. Martin Angelov says:

      If your application has a layout (like in this case), when you call render() with the name of a view, the generated HTML is kept internally and then the layout is rendered automatically. In the layout, the content() function will give you the HTML rendered by the original view, so you can print it in the correct place.

      Reading the source of dispatch will make things much more clear - the entire library is less than 500 lines long.

  2. Mark says:

    Hi total noob here but I'm getting "no input file specified" when I click on posts from the index.

    1. Martin Angelov says:

      Strange, there isn't anything in the code that would print such an error. Maybe there is something else at play. Does it work when the blog is standalone (not in a subfolder), or directly from localhost? Also make sure that you've filled in the correct settings in config.ini.

      1. Mark says:

        I must have made an error somewhere, http://testing2.marhol1.alumnos.upv.es

        I tried on a local MAMP server as well but with the same result

      2. Mark says:

        It was the .htaccess file, my bad, I put this in it in case anyone encounters the same problem

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

        Thanks for the great tutorial!

        1. Dude, you are an absolute LIFE SAVER!! Thanks a whole freaking bunch; that just helped me sooo much!

  3. Sam Wightwick says:

    Nice tutorial!
    I am a big fan of WordPress so nice to see how to make a real simple version. Thanks will definitely try making this

  4. PonyRider says:

    Would it be difficult to host the pages folder on Dropbox or some other cloud service and then pull the pages in from there? Would be a nice part 3!

  5. geedmo says:

    Not sure why but if running from localhost, in config.ini is neccesary replace the site.url by http://localhost/subfolder/ because using a blog entry as home apache says page not found.

    Anyway, pretty nice tutorial, I was looking for an easy way to make a posts based site.
    Thanks for share!

  6. Crazyhunk says:

    I don't know why but breaks with the new version of Dispatch, though I could only find small changes.. not much experienced to know more, any thoughts on that.
    Thank You

  7. Theo says:

    Martin, i have 404 Not Found (The requested URL /index.php was not found on this server.) when visiting each post. Any suggestion?

    tamankota.com/catatan

    1. Martin Angelov says:

      Lots of things might be causing this. Try adding a RewriteBase declaration to the .htaccess like this:

      RewriteBase / 
      

      If you already have this line, try removing it, or try setting the absolute path to your index.php file.

      1. John Dodson says:

        Remove RewriteBase /, leaving the following:

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

        Also make sure that you have the 'rewrite_module' enabled in Apache server

  8. carl says:

    That's just awesome!

    Finally a tutorial which deserved the name "minimal". And it still has a great and clean structure for much more!

    I would like to see more of this kind of coding starter kit ;-)

  9. Gabriel says:

    Hi Martin, I'm a beginner web developer and I'm trying to undestand step-by-step this tutorial because it seems to be really great.

    I have a question, how can I make something like an "Admin panel", in wich I just could enter the new post title and the body, and upload it to the blog system? I mean, grab the data in the title and body and create a new post file with that. Would be very helpful if someone give me a little orientation..

    Thanks Martin.

    1. Martin Angelov says:

      Admin panels complicate things. If you want to have an admin panel, you will have to keep login names and passwords somewhere, handle registrations, password changes. Also you will have to use a database otherwise plain text files might get corrupted. This will make the lightweight blog system not so lightweight. So my advice is to simply use WordPress.

      1. Gabriel says:

        Hmm you´re right. I better forget about the admin panel and DB stuff so I can keep the lightweight part of the system.

        Thanks man!!

        1. Protocol says:

          I disagree completely. You do not need a DB of anysort of house an admin panel. I realize that this is nearly 6 months late, but I don't think Gabriel's question should be brushed off so easily.

          A great example of a blog type system, that has a full featured admin panel, stores posts in files without ever using any type of database would be CuteNews (cutephp.com). To be perfectly honest, and this is purely my opinion, but I hate everything that WordPress has done. I was using CuteNews for a while, but I reached as far as I could manipulate the core code without breaking their license agreement, so I decided to write my own. However, I eventually just gave up on the store posts in files thing, and opted for MySQL database. However Gabriel, I think you might enjoy CuteNews over WordPress, especially as CuteNews can just be inserted into a pre-existing website.

  10. Noob says:

    Total noob here, after following the tutorial and downloading how do I even get what the demo shows? I'm just getting blank white screen.

    1. Martin Angelov says:

      Inspect your Apache error log file. One thing that I can think of is that the rewrite module might not be enabled, so make sure that it is.

  11. Dede says:

    Hi Martin. Thanks for the tutorial.
    i wish, you will post how to create a comment box for the blog.
    i am totally new for web programming :)
    thanks before.

    1. Martin Angelov says:

      The easiest way to add comments would be to use Facebook's comment widget, or disqus. You will only have to add the necessary HTML/JS code to the post view.

  12. lakshit says:

    hey
    This tutorial is awesome .
    But can you please tell me what can i do about images.
    if i have some images to be inserted along with a post .

    1. Martin Angelov says:

      Images are up to you - you can add them to a folder in the assets directory and link to them from the posts with a regular HTML image tag, or you can use markdown's version for that.

  13. Tiago says:

    Martin, ty so much for this tutorial.

    What would be the macro steps to build up a tag cloud?

    Greetins!

    1. Martin Angelov says:

      You should find a way to assign tags to posts (probably with a database), and record how many times each tag has been used. This will determine the size of the tag links. But my suggestion would be to go with WordPress if you need such features.

      1. Tiago says:

        Martin,

        I came across with a solution: every post file was JSON encoded. The meta section had tags, author, title and so on.

        I made a "get_posts" function but "get_tags" named and combined both arrays. I could have gone further and have made counts for each tag but for me /tags/yyyyy was good enough!

        Once again, ty for your lines!!

  14. Mark says:

    Hi Martin,

    Sweet tutorial, I am using it on my portfolio and it works great. However, I would like to know if it is possible to specify more than one url in the config file? The reason being, my portfolio loads content dynamically and when this happens the page url changes which causes the blog to display "this page does not exist" - this only happens when the new page is refreshed. I want to make sure that if the new pages are refreshed the blog remains fully functional.

    Any ideas would very much appreciated.

    Cheers.

  15. Aurel says:

    Is it possible to have only a excerpt on the first page ? I've been trying to change in functions.php but no result. I have very long posts (including images) and even to have 2 posts on page I have to scroll loads.

    @Gabriel - you could try using pagedown (https://github.com/ujifgc/pagedown) with htaccess auth (if only 1 admin) and write files with php. Just an idea.

    Thanks.

    1. Martin Angelov says:

      For excerpts you will need to edit the main template so that instead of printing the body, you print a truncated version. It is not as easy as it sounds, because you have to be careful not to break the HTML markup. See here.

    2. Jimmy says:

      @Martin Angelov - Thanks for this very simple piece of code!

      @Aurel and @Benson - Maybe you've already sorted it out by now, but just in case. I have just been playing around with this and came up with a quick hack to display a trimmed (excerpt) of the post on the main template, by just displaying everything right up to the first paragraph inclusively.

      In app/views/main.html.php replace

      <?php echo $p->body ?>

      with this code

      <?php 
      $text = $p->body;
      $excerpt = explode("</p>", $text);
      echo $excerpt[0],'</p>';
      ?>
      

      Not a real solution but rather a quick fix for simple posts, hope this helps.

  16. Benson says:

    Hi Martin! Its a wonderful light weight app you have created. I appreciate it. I would like to know whether if it is possible to display the trimmed version instead of full article in the main page. Article can be fully viewed after clicking on the link.

    Thanks in advance buddy. :)

  17. Hassan Yousuf says:

    I want to create a search engine like when a person searches something, it should show the results.

  18. Thank you sooo much for such a wonderful tutorial and such a great idea . though i was stuck on "No input file specified" , But marks comment helped me By this :
    DirectoryIndex index.php
    RewriteEngine on
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond $1 !^(index\.php|robots\.txt)
    RewriteRule ^(.*)$ index.php?/$1 [L]

  19. Roger says:

    Great tutorial - really loved this for many reasons. Thanks.
    I have some PHP errors about about the date function :
    Warning: strtotime() [function.strtotime]: It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Europe/Berlin' for 'CEST/2.0/DST' instead in /path-to/app/includes/functions.php on line 48

    Warning: date() [function.date]: It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Europe/Berlin' for 'CEST/2.0/DST' instead in /path-to/app/includes/functions.php on line 51

    I'm sure I've done something stupid - I'm guessing its the date formatting of the .md posts ? Any thoughts ?

    Thanks again for your tutorials !!

    1. Jesus A. Domingo says:

      Hi Roger,

      To fix your issue, as stated by the warnings, simply add a call to date_default_timezone_set('Europe/Berlin') in the upper part of your script. You can change 'Europe/Berline' to whichever timezone you're application is supposed to use.

    2. Roger says:

      Okay. I fixed this. Added :

      date_default_timezone_set('Europe/Paris');

      at the beginning of dispatch.php and the error went away !!

      Hope this helps someone else !

      Cheers

  20. Johnie says:

    What about static pages? Let's say I want to create a contact page. I could create it manually, but I want to manage the content via a Markdown file as well. Although if I create a contact.md in /posts then contact.md will appear among the blogposts. Any idea on how to filter this? Thanks for a great tutorial!

  21. Benson says:

    Hi martin, Could please guide how cache can be included in this tiny app?

  22. Deron says:

    I had the .htaccess issues with single blog posts as well and the changes supplied by Mark and then modified by John didn't help.

    In my case, I set the blog up under a sub-directory on my site site.com/blog. What happened was that I had an .htaccess file at the root of the site - site.com -, which was interfering with the .htaccess in the /blog subdirectory. It seems, what you do at the top level will flow down hill, so conflicts need to be avoided.

    I was able to delete the upstream .htaccess file and everythign was fine. But, this might cause some issues as I move foward with plans for a mobile site, which will use .htaccess. to redirect detected mobile devices. That might be a new adventure.

    The next issue I found was that the rewrite would cause the browser to find the domain root directory - site.com - and try to rewrite from there, which would end up displaying my index.html files without any styling. That wasn't so hot either.

    That's where that "RewriteBase /" instruction comes in handy. It needed to be modified in my case to RewriteBase /blog/ so that the rewrite is targeted to the location of the blog, which may or may not be at the top level.

    All's good now. Really like this particular idea.

  23. Jon says:

    Hi Martin,
    I've just found this and it looks like it will serve my purpose very well.
    Are you intending on taking it further? Some ideas I'd like to see:
    1) It seems like it would be possible to strip out the post-titles, as you've already done and then build a 'recent posts' list.
    2) It's possible by filtering the exploded date information that a list of chronological posts could also be built. I'll have a look at it but integrating it into the aside might be beyond me.
    3) Also, most of my posts are short but building a (safe) trimmed version would also be good.
    4) Some more guidance on how to integrate this with the rest of a site would be handy too. I'm using the foundation 4 css framework.

    Specifically, you state that every post must start with h1 tag, yet the title are printed to the page with h2 tags. Why? (for what its worth I reserve h1 for my site header, so h2 is good - I'm just trying to understand the code.

    Thanks for what you have done so far.

    Jon

  24. Lauro Moraes says:

    Hello Martin Angelov, he congratulations the script is very good, I would like to know how to encode UTF-8?!? I have tried change .htaccess and it did not work. My language is Portuguese Brazilian all special characters of my language appear wrong.
    Could you help me? Since now, thank you.

  25. Deron says:

    Here's a question for the community and Martin (don't make him do all the work)....

    It seems to me that this could be tweaked to schedule posts. Something along the lines of...

    get posts by date that are less than or equal to today.

    I think I can work it out, but if anyone could help point me in the direction of where this should go it would save me a ton of reading through the code. I seem to have a special ADD for reading code.

  26. Laszlo Espadas says:

    How to add page support? (Page use markdown)

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