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.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments