Create a Blog in 30 Minutes Without a Framework

A lot of PHP frameworks like to boast “create a blog in 20 minutes”. Well, I’m going to show you how to create a bare bones blog in about 30, but without the use of a framework. Our blog will include posts, and comments. I won’t bother with HTML headers and footers – you can add those in yourself. I recommend using my XHTML template; most of the pages below can easily be inserted into the body. I also won’t be covering user authentication in this tutorial, I’ll cover that in a later tutorial. We also won’t be doing much error checking, or prevention against users inserting malicious code. We will, however, prevent SQL injection with a very simple function.

What you will need:

  • A web server with PHP and MySQL support
  • Very basic programming knowledge

Topics covered:

  • Using MySQL to insert, delete, and update posts and comments
  • Iterating over MySQL results
  • Using cookies to save commenter information
  • Protection against SQL injection
  • Examples of php’s date() function
  • Using anchors to scroll the page
  • Redirection
  • Heredocs

Not covered:

  • User authentication (coming later)
  • Categories and tags (coming later)
  • Prevention against other (JavaScript) injection attacks
  • Error checking (empty fields, invalid emails, etc.)

The first thing we need to do is create some MySQL tables so that we have some place to store our data. We will be using two tables for our blog: posts, and comments. I will show you how to add categories and users in later tutorials.

If you haven’t already done so, you will need to create a MySQL database for your blog. I cover this in step 4 of my WordPress tutorial. Make note of your database name, username and password; you will need these in a few minutes.

Now create the following tables. You can do so by running the following code in the “SQL” tab of phpMyAdmin if you have it installed.

CREATE TABLE `posts` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL,
  `body` text NOT NULL,
  `num_comments` INT(11) NOT NULL DEFAULT '0',
  `date` INT(11) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE `comments` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `post_id` INT(11) NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `email` VARCHAR(255) NOT NULL,
  `website` VARCHAR(255) DEFAULT NULL,
  `content` text NOT NULL,
  `date` INT(11) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

You may have noticed “date” is an int rather than datetime. I prefer to use a unix timestamp instead because they are easier to work with in PHP, but you can use whatever you choose — just keep in mind you will have to modify some of the code below if you choose to use datetime instead.

Now open your favorite text editor, and get ready to start coding! I use EditPlus, but there are plenty of free alternatives. We will be creating the following pages:

  • index.php
  • post_add.php
  • post_delete.php
  • post_edit.php
  • post_view.php
  • comment_add.php
  • comment_delete.php
  • mysql.php

You can create these files now if you like, and we’ll slowly fill them in. Don’t worry, most of them are just a few lines of code!

Let’s start with the most important file: mysql.php. This file simply connects to our database so that we can run queries on it. I’ve also included a few simple helper functions. I don’t know how people get by without these!

<?php
// mysql.php
function mysql_safe_string($value) {
    $value = trim($value);
    if(empty($value))           return 'NULL';
    elseif(is_numeric($value))  return $value;
    else                        return "'".mysql_real_escape_string($value)."'";
}

function mysql_safe_query($query) {
    $args = array_slice(func_get_args(),1);
    $args = array_map('mysql_safe_string',$args);
    return mysql_query(vsprintf($query,$args));
}

function redirect($uri) {
    header('location:'.$uri);
    exit;
}

mysql_connect('localhost','USERNAME','PASSWORD');
mysql_select_db('DATABASE');

If you hadn’t noticed, you will need to fill in your username, password and database name. mysql_safe_query is basically a combination of mysql_query, sprintf, and some sanitization. It converts empty strings to NULL, leaves numbers as they are, and quotes and properly escapes strings as necessary so that you don’t have to worry about SQL injection attacks.

Now let’s add the ability to make some blog posts!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
// post_add.php
if(!empty($_POST)) {
    include 'mysql.php';
    if(mysql_safe_query('INSERT INTO posts (title,body,date) VALUES (%s,%s,%s)', $_POST['title'], $_POST['body'], time()))
        echo 'Entry posted. <a href="post_view.php?id='.mysql_insert_id().'">View</a>';
    else
        echo mysql_error();
}
?>

<form method="post">
    <table>
        <tr>
            <td><label for="title">Title</label></td>
            <td><input name="title" id="title" /></td>
        </tr>
        <tr>
            <td><label for="body">Body</label></td>
            <td><textarea name="body" id="body"></textarea></td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" value="Post" /></td>
        </tr>
    </table>
</form>

At the bottom is a simple form with title and body fields, and a submit button. Pretty self explanatory. If you omit the “action” attribute in the form element it will simply post back to the current page; this is what we want.

The PHP at the top simply checks if anything has been posted, and if it has it inserts it into the “posts” table in our database. It doesn’t check that both “title” and “body” have been properly filled out, nor does it prevent users from entering malicious JavaScript code. If you want to check against these things, do it before you insert the data into the database (line 5). However, in a proper implementation this page would be protected with some sort of user authentication, so you may want to allow it if it’s from a trusted user.

The “if” statement checks if there were any (usually syntax) errors in the query. If there weren’t any, and the post was successfully inserted into the database, it notifies the user and adds a link to view the newly created post. Otherwise, it spits out the error for debugging.

Since we now have a link to post_view.php, we might as well create that page next.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
// post_view.php
include 'mysql.php';
$result = mysql_safe_query('SELECT * FROM posts WHERE id=%s LIMIT 1', $_GET['id']);

if(!mysql_num_rows($result)) {
    echo 'Post #'.$_GET['id'].' not found';
    exit;
}

$row = mysql_fetch_assoc($result);
echo '<h2>'.$row['title'].'</h2>';
echo '<em>Posted '.date('F j<\s\up>S</\s\up>, Y', $row['date']).'</em><br/>';
echo nl2br($row['body']).'<br/>';
echo '<a href="post_edit.php?id='.$_GET['id'].'">Edit</a> | <a href="post_delete.php?id='.$_GET['id'].'">Delete</a> | <a href="index.php">View All</a>';

echo '<hr/>';
$result = mysql_safe_query('SELECT * FROM comments WHERE post_id=%s ORDER BY date ASC', $_GET['id']);
echo '<ol id="comments">';
while($row = mysql_fetch_assoc($result)) {
    echo '<li id="post-'.$row['id'].'">';
    echo (empty($row['website'])?'<strong>'.$row['name'].'</strong>':'<a href="'.$row['website'].'" target="_blank">'.$row['name'].'</a>');
    echo ' (<a href="comment_delete.php?id='.$row['id'].'&post='.$_GET['id'].'">Delete</a>)<br/>';
    echo '<small>'.date('j-M-Y g:ia', $row['date']).'</small><br/>';
    echo nl2br($row['content']);
    echo '</li>';
}
echo '</ol>';

echo <<<HTML
<form method="post" action="comment_add.php?id={$_GET['id']}">
    <table>
        <tr>
            <td><label for="name">Name:</label></td>
            <td><input name="name" id="name" value="{$_COOKIE['name']}"/></td>
        </tr>
        <tr>
            <td><label for="email">Email:</label></td>
            <td><input name="email" id="email" value="{$_COOKIE['email']}"/></td>
        </tr>
        <tr>
            <td><label for="website">Website:</label></td>
            <td><input name="website" id="website" value="{$_COOKIE['website']}"/></td>
        </tr>
        <tr>
            <td><label for="content">Comments:</label></td>
            <td><textarea name="content" id="content"></textarea></td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" value="Post Comment"/></td>
        </tr>
    </table>
</form>
HTML
;

This one’s a little more complicated. It displays the blog entry, all comments on the entry, and adds a form to post new comments.

Line 4 gets the post given by the “id” variable in the URL. We use “LIMIT 1” to let MySQL know we only want 1 result back, and it can stop searching after it has found it. Lines 6-9 check that we actually got a result back. Lines 11-15 will display the post. Notice how in the date script I used <sup> to display the suffix on the date – we need to escape the “s” and the “u” so that the date function doesn’t convert them, but the “p” isn’t an accepted letter so it doesn’t need to be escaped. On line 14 we convert new lines into <br/>s so that you don’t need to type them in your post. Line 15 prints out some options we’ll talk about later.

Lines 17-28 will display all the comments in an ordered list. The while loop on line 20 is used to iterate over all the results/comments. We give each comment an “id” so that we can use anchor links, as you will see later.

Lines 30-55 print uses heredoc syntax to print out the form. Heredocs behave the same as double-quoted strings, meaning you can use variables in them. Just remember to use squiggly brackets {} around array variables. We include $_COOKIE variables to automatically fill out the user’s name, email and website so that he doesn’t have to retype them every time he leaves a comment. I will show you how to set these next. Note that if the variable does not exist/isn’t set, the value will just be left blank, like we want. We also include the post ID in the action URL on line 31. We could have just as easily included this in a hidden form field, as many people like to do, but I like to put it in the URL for a little more consistency. If we later decide we want to put the comment form on the actual comment_add page, then we can link directly to it with the id in the URL, whereas otherwise we would have to somehow post the id over.

Let’s add the comment_add.php page next.

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// comment_add.php
include 'mysql.php';

$expire = time()+60*60*24*30;
setcookie('name', $_POST['name'], $expire, '/');
setcookie('email', $_POST['email'], $expire, '/');
setcookie('website', $_POST['website'], $expire, '/');

mysql_safe_query('INSERT INTO comments (post_id,name,email,website,content,date) VALUES (%s,%s,%s,%s,%s,%s)',
    $_GET['id'], $_POST['name'], $_POST['email'], $_POST['website'], $_POST['content'], time());
mysql_safe_query('UPDATE posts SET num_comments=num_comments+1 WHERE id=%s LIMIT 1', $_GET['id']);
redirect('post_view.php?id='.$_GET['id'].'#post-'.mysql_insert_id());

First we save the user’s input to some cookie variables. We set to expiry date to one month from now. This will be refreshed every time the user comments on something. Lines 9 and 10 simply insert the comment into the database. Line 11 increments the comment counter on the post. We could have just retrieved the number of comments with MySQL’s COUNT(*) command, but this puts extra strain on the database if it’s used a lot. It probably won’t make a difference until you’re getting thousands of visitors per day, but it’s always a good idea to do it right the first time around. Line 12 redirects back the post we were just on, and scrolls the page down to the comment we just inserted – people like to see that their comment was actually posted.

Deleting comments is pretty easy, so we’ll do that next.

<?php
// comment_delete.php
include 'mysql.php';
mysql_safe_query('DELETE FROM comments WHERE id=%s LIMIT 1', $_GET['id']);
mysql_safe_query('UPDATE posts SET num_comments=num_comments-1 WHERE id=%s LIMIT 1', $_GET['post']);
redirect('post_view.php?id='.$_GET['post']);

Not much needs explaining here. Same deal as before. Just remember the decrement the comment counter or it will be off.

Let’s delete some posts too while we’re at it.

<?php
// post_delete.php
include 'mysql.php';
mysql_safe_query('DELETE FROM posts WHERE id=%s LIMIT 1', $_GET['id']);
mysql_safe_query('DELETE FROM comments WHERE post_id=%s', $_GET['id']);
redirect('index.php');

You may have noticed that I haven’t been using closing PHP tags – you don’t need them, and it’s actually probably a good idea not to include them so that you don’t accidentally output some whitespace. Notice we also delete all the comments associated with the post. It won’t hurt much to leave them there, but we might as well save some disk space and give MySQL less to dig through.

On to post_edit.php. I always find the edit pages to be the biggest pain to write.

<?php
// post_edit.php
include 'mysql.php';

if(!empty($_POST)) {
    if(mysql_safe_query('UPDATE posts SET title=%s, body=%s, date=%s WHERE id=%s', $_POST['title'], $_POST['body'], time(), $_GET['id']))
        redirect('post_view.php?id='.$_GET['id']);
    else
        echo mysql_error();
}

$result = mysql_safe_query('SELECT * FROM posts WHERE id=%s', $_GET['id']);

if(!mysql_num_rows($result)) {
    echo 'Post #'.$_GET['id'].' not found';
    exit;
}

$row = mysql_fetch_assoc($result);

echo <<<HTML
<form method="post">
    <table>
        <tr>
            <td><label for="title">Title</label></td>
            <td><input name="title" id="title" value="{$row['title']}" /></td>
        </tr>
        <tr>
            <td><label for="body">Body</label></td>
            <td><textarea name="body" id="body">{$row['body']}</textarea></td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" value="Save"/></td>
        </tr>
    </table>
</form>
HTML
;

This one’s not so bad. We simply retrieve the post, and then fill the form with it. We use MySQL’s UPDATE command to update the post, then redirect back to the post so the user can see his changes. You might notice that this page is quite similar to the post_add.php page. You can merge these two pages if you like, so that if you add any more fields to the post, you don’t have to edit two pages, but then you end up with a lot of conditionals just to change a bit of wording and it can get quite messy.

Now, back to the beginning, let’s write the index page. The first page your users will see when the view your blog. It should probably display only the last few entries, but for simplicity, we’re going to make it display all entries on one page.

<?php
// index.php
include 'mysql.php';

echo '<h1>My First Blog</h1>';
echo "<em>Not just another WordPress web log</em><hr/>";

$result = mysql_safe_query('SELECT * FROM posts ORDER BY date DESC');

if(!mysql_num_rows($result)) {
    echo 'No posts yet.';
} else {
    while($row = mysql_fetch_assoc($result)) {
        echo '<h2>'.$row['title'].'</h2>';
        $body = substr($row['body'], 0, 300);
        echo nl2br($body).'...<br/>';
        echo '<a href="post_view.php?id='.$row['id'].'">Read More</a> | ';
        echo '<a href="post_view.php?id='.$row['id'].'#comments">'.$row['num_comments'].' comments</a>';   
        echo '<hr/>';
    }
}

echo <<<HTML
<a href="post_add.php">+ New Post</a>
HTML
;

The loop here is quite similar to the comment loop you saw earlier, so there isn’t much to explain. I’ve shortened the posts to 300 characters so that you don’t end up with too much text on one page. Note that you need a line break after the heredoc if it’s at the end of the page, otherwise PHP will complain.

You should now have a fully functional blog! We will add more features to it in the future, so stay tuned. Please leave some comments below and let me know if this tutorial was too difficult to follow, or too wordy 🙂

download-button

It’s better if you work through the tutorial on your own, but for convenience, I’ve included a zip file with all the above files. I wouldn’t use this on a live site! It’s meant for learning purposes only.

How To Create a Database and Install WordPress

This tutorial isn’t just about installing WordPress. It will cover how to set up a database using cpanel, and optionally, how to unzip files directly on your web server.

WordPress has documentation on their site about installation too, but I’m going to share with you how I like to do it.

The Easy Way

Estimated Time: 15 minutes
Skill Level Required: Beginner
Requirements:

  • Access to a web server (FTP and cpanel)
  • An FTP client

Steps:

  1. Download WordPress. Go to wordpress.org/download and click Download WordPress. Save the zip file to somewhere on your computer. The desktop works well (you can delete it in a minute).
  2. Unzip it. Find the file on your computer, right-click it and click “extract here”. If you don’t have an option like that, you might need a file (un)archiver. I like WinRar, but 7-Zip is free. This will create a folder called “wordpress” on your desktop.
  3. Upload it. Open your FTP client (FileZilla is free if you need one), and connect to your web server. You will need a server address (your domain), a username, and password to do this. After you’ve connected, upload all the files inside the WordPress folder (don’t upload the folder itself unless you want WordPress to be located at yoursite.com/wordpress). You probably want to put these files inside the “www” or “public_html” folder on your web server. If there are a bunch of files in there already, they might conflict with WordPress; consider deleting them or moving them to a subfolder.
    Note:
    I’m not responsible if your web server explodes or anything else bad happens.
  4. Create a configuration file. Go to yoursite.com. You should be presented with a screen that looks something like this (from WordPress 2.7)
    wp-create-config-fileIgnore the warning and click the big round “Create a Configuration File” button. It should work for most setups (if it doesn’t, you’d better follow their instructions, not mine).
  5. Next. Ignore the pretty text on the next page, and click “Let’s go!”
    wp-lets-go
  6. Setup the database. Now it’s going to ask you for a bunch of info. If you don’t know what to put here, I can tell you how to set it up if your web host is using cpanel. Open yoursite.com/cpanel in a new tab/window. If you get a 404 or something like that, talk to someone else 🙂 If it asks you for your username and password, enter it. Depending on the version of cpanel, you should be presented with a bunch of icons. Find the one that says “MySQL Databases” and click it. We’re going to create a new database for WordPress so that we don’t accidentally muck anything else up. Near the top of the page there should an area to “create a new database”. Enter something clever like “wordpress” or “mysite” and create the database.
    cpanel-new-database
    Scroll down to where it says “add new user”. Choose a username and password, and create a user. You might as well make your password a bunch of random upper and lowercase letters, numbers, and symbols. You should only need to enter this password once after it’s created. Now, scroll past “add new user” to “add user to database”. Choose the username and database you just created, then click “add”. If the username and database have another username before it, make note of this; you will need to enter these into the WordPress installation form.
    cpanel-add-user
  7. Fill in the WordPress form. Great, your database is set up. Now you can fill in that old form. Enter the database name, user name and password you just chose (with the prefixes if it added them). You shouldn’t need to change the database host or table prefix. Click “Submit”.
    wp-database-connection-details
  8. You’re done. You can listen to WordPress now 🙂 It will guide you through the rest.

The Faster Way

WordPress 2.7 is 1.76 MB compressed, or 5.38 MB uncompressed with 603 files. I find it considerably faster to upload just the zip file.

Estimated Time: 10 minutes
Skill Level Required: Novice
Requirements:

  • Access to a web server (FTP, cpanel, and shell)
  • An FTP client

Assumptions:

  • You can follow the steps above
  • You’re on a linux server, but have no knowledge of linux

Steps:

  1. Download WordPress.
  2. Don’t unzip it.
  3. Upload the zip file.
  4. Open up PuTTY or your favorite SSH program.
  5. Connect to your web server. If you’re on HostGator like me, you can find the IP Address on the left sidebar of your cpanel, under “Account Information”. The Port is 2222. You may need to ask Tech Support to enable Jailed SSH for you though.
  6. Enter your username and password. If you’re new to this, your password won’t appear as you type (no asterisks, nothing).
  7. Type the following commands. You can use tab to auto-complete folder and filenames. Adjust the file/folder names below as needed. Use the “ls” (list directory contents) and “pwd” (print working directory) commands to figure out where you are, if you get lost. Also, “man <command>” will give you more information about a particular command.
    cd public_html/<folder-where-you-uploaded-wordpress>
    unzip wordpress-2.7.zip
    mv wordpress/* .
    rm wordpress-2.7.zip
    rmdir wordpress
  8. Follow steps 4 onward, above.

That’s it for this tutorial, I hope you enjoyed it!