Project Guide: Build a CMS with PHP

Building your own Content Management System (CMS) using PHP and MySQL is one of the most rewarding projects for any aspiring web developer. This PHP project not only helps you understand backend logic but also improves your understanding of how professional platforms like WordPress and Joomla operate under the hood.


Why Build a CMS in PHP?

Creating a CMS from scratch gives you hands-on experience with database design, CRUD operations, authentication, and page rendering. PHP’s flexibility and MySQL’s reliability make them the perfect combination for this project.

Benefits include:

  • Strengthening your backend development skills
  • Understanding MVC structure and database relationships
  • Learning security and validation techniques
  • Building a portfolio-ready PHP project

💡 Explore other PHP projects at phponline.in/projects to enhance your skills.


Step-by-Step Guide: How to Build a CMS with PHP and MySQL

1. Plan the CMS Architecture

Before coding, define what your CMS will manage — pages, posts, users, and categories. Create a database schema for better data flow and performance.


2. Set Up Your Development Environment

Use XAMPP or WAMP to create a local server. Ensure PHP and MySQL are configured correctly.

Recommended Tools:

  • XAMPP / WAMP
  • VS Code or Sublime Text
  • phpMyAdmin for database management


3. Create the Database

Design tables such as:

  • users – for admin login
  • posts – for articles or pages
  • categories – for organizing content

Example SQL:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(100),
  password VARCHAR(255),
  role VARCHAR(50)
);

4. Build the Admin Dashboard in PHP

The admin panel is where content gets created and managed.
Include features like:

  • Add/Edit/Delete posts
  • Upload images
  • Manage categories and users


5. Create the Frontend UI

Display dynamic pages using PHP loops and queries. Use Bootstrap or Tailwind CSS for a modern design. Optimize for SEO with meta tags and friendly URLs.


6. Implement Authentication and Security

Add login and logout systems using PHP sessions. Always sanitize form inputs to prevent SQL injection.


7. Add SEO-Friendly URLs

Use .htaccess to convert URLs like
example.com/view.php?id=5example.com/post/hello-world


8. Finalize and Test

Test every module — post creation, login, image upload, and category filtering. Debug using PHP error logs and validate inputs.


Advanced Features You Can Add Later

  • Role-based access control
  • Comment system
  • REST API for headless CMS
  • File manager for uploads
  • Dashboard analytics using Chart.js

Small PHP CMS

// Small PHP CMS 
// --------------------------------
// Add these files to your project root. Keep folder structure as shown.
// Minimal, procedural PHP using PDO, prepared statements, and password hashing.
// NOT for production without additional hardening (CSRF tokens, file upload checks, HTTPS).

/*
Folder structure (recommended):

/ (project root)
  - config.php
  - init.php
  - index.php
  - view.php
  - assets/
      - uploads/ (writable by web server)
  - admin/
      - login.php
      - logout.php
      - index.php        (list posts)
      - edit.php         (create & edit)
      - delete.php

SQL to create database (MySQL):

CREATE DATABASE cms_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE cms_db;

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(100) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  slug VARCHAR(255) NOT NULL UNIQUE,
  body TEXT NOT NULL,
  image VARCHAR(255) DEFAULT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP
);

-- Insert an admin user (run in PHP or use the prepared statement below to set a hashed password)
-- Example (run in PHP or replace PASSWORD_HASH with your own hashed password):
-- INSERT INTO users (username, password) VALUES ('admin', '<password_hash_here>');


// -----------------------------
// File: config.php
// -----------------------------
<?php
// Database settings - change these to match your environment
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'cms_db');
define('DB_USER', 'root');
define('DB_PASS', '');

// Base URL (use trailing slash)
define('BASE_URL', '/'); // adjust if installed in a subfolder, e.g. '/cms/'

// Uploads folder
define('UPLOAD_DIR', __DIR__ . '/assets/uploads/');
?>


// -----------------------------
// File: init.php
// -----------------------------
<?php
require_once __DIR__ . '/config.php';

// Start session
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// Setup PDO
try {
    $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
} catch (Exception $e) {
    die('Database connection failed: ' . htmlspecialchars($e->getMessage()));
}

// Helper: redirect
function redirect($url) {
    header('Location: ' . $url);
    exit;
}

// Helper: slugify
function slugify($text) {
    $text = preg_replace('~[^\pL\d]+~u', '-', $text);
    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
    $text = preg_replace('~[^-\w]+~', '', $text);
    $text = trim($text, '-');
    $text = preg_replace('~-+~', '-', $text);
    $text = strtolower($text);
    if (empty($text)) {
        return 'n-a';
    }
    return $text;
}

// Auth helpers
function is_admin() {
    return !empty($_SESSION['user_id']);
}

function require_admin() {
    if (!is_admin()) {
        redirect(BASE_URL . 'admin/login.php');
    }
}
?>


// -----------------------------
// File: index.php (public listing)
// -----------------------------
<?php
require_once __DIR__ . '/init.php';

// Fetch latest posts
$stmt = $pdo->query('SELECT id, title, slug, LEFT(body, 300) AS excerpt, image, created_at FROM posts ORDER BY created_at DESC');
$posts = $stmt->fetchAll();
?><!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Simple PHP CMS</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/modern-css-reset/dist/reset.min.css">
  <style> body{font-family:Arial,Helvetica,sans-serif;max-width:900px;margin:24px auto;padding:0 12px} .post{margin-bottom:24px} .post img{max-width:200px;height:auto} .meta{color:#666;font-size:0.9rem} </style>
</head>
<body>
  <h1>Simple PHP CMS</h1>
  <p><a href="admin/login.php">Admin login</a></p>

  <?php if (count($posts) === 0): ?>
    <p>No posts yet.</p>
  <?php else: ?>
    <?php foreach ($posts as $post): ?>
      <article class="post">
        <h2><a href="view.php?slug=<?php echo urlencode($post['slug']); ?>"><?php echo htmlspecialchars($post['title']); ?></a></h2>
        <p class="meta">Posted on <?php echo htmlspecialchars($post['created_at']); ?></p>
        <?php if ($post['image']): ?>
          <img src="assets/uploads/<?php echo htmlspecialchars($post['image']); ?>" alt="<?php echo htmlspecialchars($post['title']); ?>">
        <?php endif; ?>
        <p><?php echo nl2br(htmlspecialchars($post['excerpt'])); ?>...</p>
        <p><a href="view.php?slug=<?php echo urlencode($post['slug']); ?>">Read more</a></p>
      </article>
    <?php endforeach; ?>
  <?php endif; ?>
</body>
</html>


// -----------------------------
// File: view.php (single post)
// -----------------------------
<?php
require_once __DIR__ . '/init.php';

$slug = $_GET['slug'] ?? '';
if (!$slug) {
    redirect(BASE_URL);
}

$stmt = $pdo->prepare('SELECT * FROM posts WHERE slug = ? LIMIT 1');
$stmt->execute([$slug]);
$post = $stmt->fetch();
if (!$post) {
    http_response_code(404);
    echo 'Post not found';
    exit;
}
?><!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title><?php echo htmlspecialchars($post['title']); ?></title>
</head>
<body>
  <a href="<?php echo BASE_URL; ?>">← Back</a>
  <h1><?php echo htmlspecialchars($post['title']); ?></h1>
  <p class="meta">Posted on <?php echo htmlspecialchars($post['created_at']); ?></p>
  <?php if ($post['image']): ?>
    <img src="assets/uploads/<?php echo htmlspecialchars($post['image']); ?>" style="max-width:400px;height:auto">
  <?php endif; ?>
  <div><?php echo nl2br(htmlspecialchars($post['body'])); ?></div>
</body>
</html>


// -----------------------------
// File: admin/login.php
// -----------------------------
<?php
require_once __DIR__ . '/../init.php';

// If already logged in
if (is_admin()) {
    redirect(BASE_URL . 'admin/index.php');
}

$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';

    if ($username && $password) {
        $stmt = $pdo->prepare('SELECT id, password FROM users WHERE username = ? LIMIT 1');
        $stmt->execute([$username]);
        $user = $stmt->fetch();
        if ($user && password_verify($password, $user['password'])) {
            // login success
            session_regenerate_id(true);
            $_SESSION['user_id'] = $user['id'];
            redirect(BASE_URL . 'admin/index.php');
        } else {
            $error = 'Invalid credentials.';
        }
    } else {
        $error = 'Enter username and password.';
    }
}
?><!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Admin Login</title>
</head>
<body>
  <h1>Admin Login</h1>
  <?php if ($error): ?><p style="color:red"><?php echo htmlspecialchars($error); ?></p><?php endif; ?>
  <form method="post">
    <label>Username<br><input name="username"></label><br>
    <label>Password<br><input type="password" name="password"></label><br>
    <button type="submit">Login</button>
  </form>
  <p><a href="<?php echo BASE_URL; ?>">Back to site</a></p>
</body>
</html>


// -----------------------------
// File: admin/logout.php
// -----------------------------
<?php
require_once __DIR__ . '/../init.php';
session_unset();
session_destroy();
redirect(BASE_URL);
?>


// -----------------------------
// File: admin/index.php (admin dashboard/list)
// -----------------------------
<?php
require_once __DIR__ . '/../init.php';
require_admin();

// Fetch posts
$stmt = $pdo->query('SELECT id, title, slug, created_at FROM posts ORDER BY created_at DESC');
$posts = $stmt->fetchAll();
?><!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Admin - Posts</title>
</head>
<body>
  <h1>Admin - Posts</h1>
  <p><a href="edit.php">+ New Post</a> | <a href="logout.php">Logout</a></p>

  <?php if (count($posts) === 0): ?>
    <p>No posts yet.</p>
  <?php else: ?>
    <table border="1" cellpadding="6" cellspacing="0">
      <tr><th>ID</th><th>Title</th><th>Created</th><th>Actions</th></tr>
      <?php foreach ($posts as $p): ?>
        <tr>
          <td><?php echo $p['id']; ?></td>
          <td><?php echo htmlspecialchars($p['title']); ?></td>
          <td><?php echo htmlspecialchars($p['created_at']); ?></td>
          <td>
            <a href="edit.php?id=<?php echo $p['id']; ?>">Edit</a> |
            <a href="delete.php?id=<?php echo $p['id']; ?>" onclick="return confirm('Delete this post?')">Delete</a>
          </td>
        </tr>
      <?php endforeach; ?>
    </table>
  <?php endif; ?>
</body>
</html>


// -----------------------------
// File: admin/edit.php (create / update)
// -----------------------------
<?php
require_once __DIR__ . '/../init.php';
require_admin();

$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$error = '';

if ($id) {
    $stmt = $pdo->prepare('SELECT * FROM posts WHERE id = ? LIMIT 1');
    $stmt->execute([$id]);
    $post = $stmt->fetch();
    if (!$post) {
        redirect(BASE_URL . 'admin/index.php');
    }
} else {
    $post = ['title'=>'', 'body'=>'', 'image'=>null];
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $title = trim($_POST['title'] ?? '');
    $body = trim($_POST['body'] ?? '');
    $slug = slugify($title ?: uniqid('post-'));

    if (!$title || !$body) {
        $error = 'Title and body are required.';
    } else {
        // Handle image upload
        $imageName = $post['image'] ?? null;
        if (!empty($_FILES['image']['name'])) {
            if (!is_dir(UPLOAD_DIR)) {
                mkdir(UPLOAD_DIR, 0755, true);
            }
            $tmp = $_FILES['image']['tmp_name'];
            $orig = basename($_FILES['image']['name']);
            $ext = pathinfo($orig, PATHINFO_EXTENSION);
            $imageName = time() . '-' . preg_replace('/[^a-z0-9._-]/i', '', $orig);
            move_uploaded_file($tmp, UPLOAD_DIR . $imageName);
        }

        if ($id) {
            $stmt = $pdo->prepare('UPDATE posts SET title = ?, slug = ?, body = ?, image = ? WHERE id = ?');
            $stmt->execute([$title, $slug, $body, $imageName, $id]);
        } else {
            $stmt = $pdo->prepare('INSERT INTO posts (title, slug, body, image) VALUES (?, ?, ?, ?)');
            $stmt->execute([$title, $slug, $body, $imageName]);
            $id = $pdo->lastInsertId();
        }

        redirect(BASE_URL . 'admin/index.php');
    }
}
?>
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title><?php echo $id ? 'Edit' : 'New'; ?> Post</title>
</head>
<body>
  <h1><?php echo $id ? 'Edit' : 'New'; ?> Post</h1>
  <?php if ($error): ?><p style="color:red"><?php echo htmlspecialchars($error); ?></p><?php endif; ?>
  <form method="post" enctype="multipart/form-data">
    <label>Title<br><input name="title" value="<?php echo htmlspecialchars($post['title']); ?>" style="width:100%"></label><br><br>
    <label>Body<br><textarea name="body" rows="10" style="width:100%"><?php echo htmlspecialchars($post['body']); ?></textarea></label><br><br>
    <?php if (!empty($post['image'])): ?>
      <p>Current image:<br><img src="<?php echo BASE_URL; ?>assets/uploads/<?php echo htmlspecialchars($post['image']); ?>" style="max-width:200px"></p>
    <?php endif; ?>
    <label>Image (optional)<br><input type="file" name="image"></label><br><br>
    <button type="submit">Save</button>
  </form>
  <p><a href="index.php">← Back to posts</a></p>
</body>
</html>


// -----------------------------
// File: admin/delete.php
// -----------------------------
<?php
require_once __DIR__ . '/../init.php';
require_admin();

$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id) {
    // Optionally delete image file
    $stmt = $pdo->prepare('SELECT image FROM posts WHERE id = ? LIMIT 1');
    $stmt->execute([$id]);
    $r = $stmt->fetch();
    if ($r && $r['image']) {
        $file = UPLOAD_DIR . $r['image'];
        if (is_file($file)) @unlink($file);
    }
    $stmt = $pdo->prepare('DELETE FROM posts WHERE id = ?');
    $stmt->execute([$id]);
}
redirect(BASE_URL . 'admin/index.php');
?>


/*
Installation & quick start:
1. Create database and run SQL from top of this file.
2. Place files into the folder structure shown.
3. Update config.php with DB credentials and BASE_URL.
4. Make assets/uploads writable by web server: chmod 755 (or 775/777 in dev only).
5. Create an admin user. Example to create hashed password with PHP CLI:

   <?php
   echo password_hash('yourpassword', PASSWORD_DEFAULT);
   ?>

   Then insert into DB:
   INSERT INTO users (username, password) VALUES ('admin', '<paste_hash>');

6. Visit the site at index.php and login at admin/login.php.

Security notes:
- Use HTTPS in production.
- Add CSRF tokens to forms.
- Validate file uploads (size, mime type) before moving them.
- Limit admin user access and use strong passwords.
- Consider switching to prepared frameworks (Laravel) for larger projects.

License: This code is provided copyright-free (public domain-style). You may reuse and modify it.
*/

“Learn more in our complete PHP Tutorials for Beginners


Frequently Asked Questions (FAQ)

Q1. What is a CMS in PHP?

A CMS (Content Management System) in PHP is a web application that allows users to manage website content — such as text, images, and pages — without coding manually.

Q2. Is PHP good for building a CMS?

Yes. PHP is widely used for CMS development. WordPress, Drupal, and Joomla are all built with PHP.

Q3. Do I need a framework to build a CMS in PHP?

No, you can build it using pure PHP and MySQL. However, frameworks like Laravel can help you scale later.

Q4. How long does it take to build a simple PHP CMS?

For beginners, a basic version can be built in 7–10 days, depending on your familiarity with PHP and MySQL.

Q5. Can I host my PHP CMS project online?

Yes. You can deploy your CMS on shared hosting platforms that support PHP and MySQL, such as Hostinger or GoDaddy.

Related Article
50+ PHP Interview Questions and Answers 2023

1. Differentiate between static and dynamic websites. Static Website The content cannot be modified after the script is executed The Read more

All We Need to Know About PHP Ecommerce Development

  Many e-commerce sites let you search for products, show them off, and sell them online. The flood of money Read more

PHP Custom Web Development: How It Can Be Used, What Its Pros and Cons Are,

PHP is a scripting language that runs on the server. It uses server resources to process outputs. It is a Read more

PHP Tutorial

PHP Tutorial – Complete Guide for Beginners to Advanced Welcome to the most comprehensive PHP tutorial available online at PHPOnline.in Read more

Introduction of PHP

Introduction to PHP – Learn PHP from Scratch with Practical Examples Welcome to your complete beginner's guide to PHP. Whether Read more

Syntax Overview of PHP

Syntax Overview of PHP (2025 Edition) Welcome to phponline.in, your one-stop platform for mastering PHP. This comprehensive, SEO-rich tutorial on Read more

Environment Setup in PHP

Setting Up PHP Environment (Beginner’s Guide) If you’re planning to learn PHP or start developing websites using PHP, the first Read more

Variable Types in PHP

PHP Variable Types: Complete Beginner's Guide to PHP Data Types Welcome to phponline.in, your trusted source for beginner-to-advanced level PHP Read more

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Prove your humanity: 2   +   2   =