Make a Website

How to Make a Blog Website in PHP with an Admin Panel (No WordPress)

Build your own blog in PHP without WordPress: a complete one-file blog with a password-protected admin page you can upload today, how to switch it to MySQL, the security it needs, and what a full blog script adds.

A PHP blog with a list of posts and an admin page for writing a new post

You do not need WordPress to run a blog. A blog is a list of posts, a page for each post, and a private page where you write new ones. That fits in a single PHP file, and building it yourself teaches you how every blog works underneath, WordPress included.

This guide gives you a complete one-file PHP blog with an admin page that you can upload today, then shows how to switch it to MySQL, the security every PHP blog needs, and what a full blog script adds for a site you want to grow.

Try it: a blog in one PHP file

  1. Save the code below as index.php.
  2. Change ADMIN_PASSWORD to your own long password.
  3. On your computer, put it in the web folder of XAMPP or Laragon (for example htdocs/blog/) and open http://localhost/blog/. On hosting, upload it to public_html.
  4. Open ?admin (for example yourdomain.com/?admin), log in, and publish your first post.

It uses SQLite, a database stored in a single file, so there is nothing to set up: the file blog.sqlite is created on the first visit, one folder above the website, where nobody can download it.

<?php
/* A one-file blog with an admin page. Save it as index.php. Needs PHP 8 with SQLite (pdo_sqlite). */

const BLOG_NAME = 'My Blog';
const ADMIN_PASSWORD = 'change-this-password';   // change it before you upload

session_start();

// The database file sits one folder above the website, where nobody can download it
$db = new PDO('sqlite:' . dirname(__DIR__) . '/blog.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    slug TEXT UNIQUE NOT NULL,
    body TEXT NOT NULL,
    created TEXT NOT NULL
)');

function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }

if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));
$message = '';

// Admin: log in, log out, publish
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) exit('Please reload the page and try again.');
    $action = $_POST['action'] ?? '';

    if ($action === 'login' && hash_equals(ADMIN_PASSWORD, $_POST['password'] ?? '')) {
        session_regenerate_id(true);
        $_SESSION['admin'] = true;
    } elseif ($action === 'login') {
        sleep(1);   // slows down anyone guessing passwords
        $message = 'Wrong password.';
    } elseif ($action === 'logout') {
        $_SESSION = [];
    } elseif ($action === 'publish' && !empty($_SESSION['admin'])) {
        $title = trim($_POST['title'] ?? '');
        $body  = trim($_POST['body'] ?? '');
        $slug  = trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($title)), '-') ?: 'post';
        if ($title !== '' && $body !== '') {
            $taken = $db->prepare('SELECT COUNT(*) FROM posts WHERE slug = ?');
            $taken->execute([$slug]);
            if ($taken->fetchColumn()) $slug .= '-' . time();
            $db->prepare('INSERT INTO posts (title, slug, body, created) VALUES (?, ?, ?, ?)')
               ->execute([$title, $slug, $body, date('Y-m-d H:i')]);
            header('Location: ?post=' . urlencode($slug));
            exit;
        }
        $message = 'A title and some text are both needed.';
    }
}

$post = null;
if (isset($_GET['post'])) {
    $find = $db->prepare('SELECT * FROM posts WHERE slug = ?');
    $find->execute([$_GET['post']]);
    $post = $find->fetch(PDO::FETCH_ASSOC);
    if (!$post) http_response_code(404);
}
$adminPage = isset($_GET['admin']);
$pageTitle = $post ? $post['title'] . ' | ' . BLOG_NAME : BLOG_NAME;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($pageTitle) ?></title>
<?php if ($post): ?><meta name="description" content="<?= e(mb_substr(preg_replace('/\s+/', ' ', $post['body']), 0, 155)) ?>"><?php endif; ?>
<?php if ($adminPage): ?><meta name="robots" content="noindex"><?php endif; ?>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #fafaf9; color: #1c1917; }
  header { padding: 16px 20px; background: #1c1917; }
  header a { color: #fff; font-size: 20px; font-weight: 800; text-decoration: none; }
  main { max-width: 720px; margin: 0 auto; padding: 24px 20px; line-height: 1.6; }
  article { margin-bottom: 28px; }
  .date { color: #78716c; font-size: 14px; }
  input, textarea { width: 100%; box-sizing: border-box; font: inherit; padding: 10px; margin: 6px 0 12px;
                    border: 1px solid #d6d3d1; border-radius: 8px; }
  textarea { min-height: 220px; }
  button { font: inherit; font-weight: 700; padding: 10px 20px; border: 0; border-radius: 8px;
           background: #ea580c; color: #fff; cursor: pointer; }
  .msg { color: #b91c1c; }
</style>
</head>
<body>
<header><a href="?"><?= e(BLOG_NAME) ?></a></header>
<main>
<?php if ($message): ?><p class="msg"><?= e($message) ?></p><?php endif; ?>

<?php if ($adminPage && empty($_SESSION['admin'])): ?>
  <h1>Log in</h1>
  <form method="post">
    <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
    <input type="hidden" name="action" value="login">
    <input type="password" name="password" placeholder="Admin password" required>
    <button>Log in</button>
  </form>

<?php elseif ($adminPage): ?>
  <h1>New post</h1>
  <form method="post">
    <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
    <input type="hidden" name="action" value="publish">
    <input name="title" placeholder="Title" required>
    <textarea name="body" placeholder="Write your post. A blank line starts a new paragraph." required></textarea>
    <button>Publish</button>
  </form>
  <form method="post" style="margin-top: 16px">
    <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
    <input type="hidden" name="action" value="logout">
    <button style="background: #57534e">Log out</button>
  </form>

<?php elseif ($post): ?>
  <article>
    <h1><?= e($post['title']) ?></h1>
    <div class="date"><?= e($post['created']) ?></div>
    <?php foreach (preg_split('/\R{2,}/', $post['body']) as $paragraph): ?>
      <p><?= nl2br(e($paragraph)) ?></p>
    <?php endforeach; ?>
  </article>
  <a href="?">&larr; All posts</a>

<?php elseif (isset($_GET['post'])): ?>
  <h1>Post not found</h1>
  <a href="?">&larr; All posts</a>

<?php else: ?>
  <?php $posts = $db->query('SELECT title, slug, body, created FROM posts ORDER BY id DESC')->fetchAll(PDO::FETCH_ASSOC); ?>
  <?php if (!$posts): ?><p>No posts yet. <a href="?admin">Write the first one</a>.</p><?php endif; ?>
  <?php foreach ($posts as $p): ?>
    <article>
      <h2><a href="?post=<?= e(urlencode($p['slug'])) ?>"><?= e($p['title']) ?></a></h2>
      <div class="date"><?= e($p['created']) ?></div>
      <p><?= e(mb_substr($p['body'], 0, 200)) ?>&hellip;</p>
    </article>
  <?php endforeach; ?>
<?php endif; ?>
</main>
</body>
</html>
PartWhat it does
new PDO('sqlite:...')Opens (or creates) the database file outside the public folder
CREATE TABLE IF NOT EXISTSMakes the posts table the first time; after that it does nothing
e() with htmlspecialcharsEvery piece of text is escaped before it is shown, so a title like <script> appears as text and never runs
prepare() and execute()Prepared statements: what users type can never change your SQL
The csrf tokenEvery form carries a secret from the session, so another website cannot post to your admin
session_regenerate_id and sleep(1)A fresh session after login, and a pause after a wrong password that slows down guessing
The slugTurns “My First Post” into my-first-post for the address, and adds a number if it is already taken
http_response_code(404)A missing post returns a real 404, which Google needs

Use MySQL instead of SQLite

SQLite is fine for a personal blog. If your hosting gives you MySQL, or you expect many writers, change the database lines like this and keep the rest of the file:

// Instead of the SQLite line, connect to MySQL (create the database in cPanel first):
$db = new PDO('mysql:host=localhost;dbname=myblog;charset=utf8mb4', 'db_user', 'db_password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE IF NOT EXISTS posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    slug VARCHAR(220) NOT NULL UNIQUE,
    body MEDIUMTEXT NOT NULL,
    created VARCHAR(20) NOT NULL
)');

Store a password hash, not the password

The one-file blog keeps the admin password in the file to stay short. For a real site, store only a hash of it, so even someone who reads the file does not learn the password:

// Make a hash once (for example in a temporary file), then delete that file:
echo password_hash('your-long-password', PASSWORD_DEFAULT);

// In the blog, keep only the hash and check against it:
const ADMIN_HASH = '$2y$10$...the long hash...';
if ($action === 'login' && password_verify($_POST['password'] ?? '', ADMIN_HASH)) { ... }

What a real blog adds

The one-file blog works, but a blog you want to grow in Google needs more:

  • Clean addresses like /my-first-post/ instead of ?post=, with .htaccess rewrite rules
  • Editing and deleting posts, drafts, and a preview before publishing
  • Categories, a search box and a sidebar or menu
  • Cover pictures, safely uploaded and converted to WebP
  • SEO: a title and description per post, canonical links, a sitemap, Article and FAQ structured data
  • Pages: About, Contact (with spam protection) and Privacy Policy, which AdSense needs
  • Stronger security: hashed passwords, locking a login after several wrong passwords, safe uploads and hidden error messages
  • An installer, so the site can be set up without editing code

Your own PHP blog or WordPress?

Own PHP blogWordPress
SpeedVery fast: only the code you needSlower without caching plugins
UpdatesOnly when you change somethingCore, theme and plugin updates every few weeks
SecuritySmall code, small attack surfaceThe most attacked CMS; plugins are the usual way in
FeaturesWhat you build or buyThousands of plugins
Best forLearning, speed, a simple focused blogSites that need many ready-made features

Common problems

ProblemCause and fix
could not find driverThe SQLite (or MySQL) extension is off. Turn on pdo_sqlite or pdo_mysql in your hosting's PHP settings.
unable to open database filePHP cannot write in the folder above the website. Use __DIR__ instead of dirname(__DIR__) and protect that file from downloads, or use MySQL.
headers already sentSomething is printed before <?php, often a space or a blank line at the top of the file. Remove it.
“Please reload the page” after a long waitThe session expired, so the form's CSRF token no longer matches. Reload and try again.
Bold or links show as code in postsOn purpose: everything is escaped for safety. A real blog uses an editor that saves cleaned HTML.

The full version: a ready blog CMS

Our PHP blog script with admin panel is a fast blog with no WordPress and no plugins. It is the same kind of blog you are reading now:

  • Articles with a cover picture, summary, level badge, reading time and a table of contents made from the headings
  • An FAQ section on every article, sent to Google as FAQ structured data
  • Categories with sub-categories and a drop-down menu, live search, and editable About, Contact, Privacy, Terms and Disclaimer pages
  • Draft preview links, and automatic 301 redirects when you change an address
  • Logo, favicon, colours (6 presets or your own) and home page texts from the admin
  • SEO and AI search: canonical links, Open Graph, structured data, a sitemap and llms.txt
  • AdSense with ads.txt and three ad places that follow the placement rules
  • Security: 4 wrong admin passwords lock the login for 15 minutes, CSRF protection, prepared statements, safe WebP uploads and a spam-protected contact form

Where to go next

Questions people ask

How do I make a blog website in PHP?
You need a posts table, a page that lists posts, a page that shows one post, and a password-protected admin page that saves new posts. The one-file PHP blog in this guide does all of that with SQLite, prepared statements, escaped output and a CSRF token.
Can I make a PHP blog without a database setup?
Yes. SQLite stores the whole database in one file and PHP creates it on the first visit, so there is nothing to set up. Most hosting has the pdo_sqlite extension turned on.
How do I connect my PHP blog to MySQL?
Create a database and user in cPanel, then open the connection with new PDO('mysql:host=localhost;dbname=yourdb;charset=utf8mb4', 'user', 'password') and create the posts table with INT AUTO_INCREMENT and VARCHAR or MEDIUMTEXT columns.
How do I make the admin page of a PHP blog secure?
Store only a password hash and check it with password_verify, start a fresh session after login, put a CSRF token in every form, use prepared statements, escape all output with htmlspecialchars, and lock the login after several wrong passwords.
Is a PHP blog better than WordPress?
It is faster, needs fewer updates and has a smaller attack surface, but you only get the features you build or buy. WordPress is better when you need many ready-made plugins.
Why do I get could not find driver in PHP?
The PDO extension for your database is not enabled. Turn on pdo_sqlite or pdo_mysql in your hosting's PHP extensions page and reload.
How do I get clean URLs in a PHP blog?
Add an .htaccess rewrite rule that sends /post-slug/ to index.php?post=post-slug, and print links in that form. On Nginx the same is done with try_files.
Where should the SQLite database file go?
Outside the public web folder, for example one folder above public_html, so nobody can download it. If PHP cannot write there, keep it in the site folder and block downloads of .sqlite files.
Is there a ready PHP blog script with an admin panel?
Yes. Our PHP blog script has articles with covers and FAQs, categories, pages, search, draft previews, automatic redirects, SEO and AdSense places, and admin security with a login lockout, with no WordPress or plugins.

Written by Habib Baloch

I build Android apps, websites and Ubuntu servers, and write down exactly how I did it.

Ask me about this guide →