Jinsi ya Kutengeneza Blog Admin System yenye Auto Slug kwa PHP na MySQL
Jifunze kutengeneza blog admin system kwa PHP na MySQL inayoweza kuongeza posts, ku-edit posts, kufuta posts, ku-upload images na files, kuweka YouTube videos, meta description, pagination, search, category filter na auto SEO slug kwa kila post mpya.
Utangulizi
Katika blog system ya kisasa, kila post inapaswa kuwa na link nzuri ya SEO. Mfano:
https://faulink.com/seo-friendly-urls-kwa-php-1284
Badala ya:
https://faulink.com/blog_viewer.php?id=1284
Tatizo lililokuwepo ni kwamba posts mpya zilikuwa zinaingia kwenye database bila slug, hivyo link zilikuwa zinarudi kwenye mfumo wa zamani wa id.
Katika tutorial hii tutajifunza jinsi ya kutengeneza admin page moja inayosimamia blog posts na kutengeneza slug automatically.
Features za Mfumo
Mfumo huu una:
Add new blog post
Edit blog post
Delete blog post
Upload image
Upload downloadable file
Add YouTube video link
Add author
Add category
Add meta description
Auto generate slug
Search posts
Filter by category
Pagination
CSRF protection
Safe file upload
Bootstrap design
Database Columns Zinazotumika
Table yako ya posts iwe na columns hizi:
id
user_id
title
content
image_path
video_link
downloadable_file
created_at
author
category
file_path
youtube_link
tags
views
slug
meta_description
Muhimu zaidi kwa SEO ni:
slug
Step 1: Hakikisha Slug Column Ipo
Kama slug haipo, run hii kwenye phpMyAdmin:
ALTER TABLE posts ADD slug VARCHAR(255) NULL;
Kama tayari ipo, usirun hii tena.
Step 2: Jaza Slug kwa Posts za Zamani
Kama posts za zamani hazina slug, run hii:
UPDATE posts
SET slug = CONCAT(
LOWER(
TRIM(BOTH '-' FROM
REGEXP_REPLACE(title, '[^a-zA-Z0-9]+', '-')
)
),
'-',
id
)
WHERE slug IS NULL OR slug = '';
Step 3: Slug Helper Function
Kwenye code tunaongeza function hii:
function makeSlug($text){
$text = strtolower(trim((string)$text));
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
$text = trim($text, '-');
if($text === ''){
$text = 'post';
}
return $text;
}
Function hii inabadilisha title kuwa slug.
Mfano:
SEO Friendly URLs kwa PHP
inakuwa:
seo-friendly-urls-kwa-php
Baada ya post kuingia database, tunaongeza ID mwishoni:
seo-friendly-urls-kwa-php-1284
Step 4: Kwa Nini Tunatumia ID Mwishoni?
Tunaongeza ID ili kuzuia duplicate slugs.
Mfano posts mbili zinaweza kuwa na title moja:
Jinsi ya Kutengeneza Blog System
Kwa hiyo slug zitakuwa:
jinsi-ya-kutengeneza-blog-system-1284
jinsi-ya-kutengeneza-blog-system-1285
Hii ni salama kwa website yenye posts nyingi.
Step 5: Full Code ya Blog Admin System
Hii ndiyo full code ya admin page moja. I-save kama:
blog_admin.php
<?php
require_once 'mitihani_config.php'; // expects $conn as mysqli
/* =========================================================
BASIC SECURITY + UTILITIES
========================================================= */
if (session_status() === PHP_SESSION_NONE) session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
function csrf_token() {
return $_SESSION['csrf_token'] ?? '';
}
function csrf_check($token): bool {
return isset($_SESSION['csrf_token']) && is_string($token) && hash_equals($_SESSION['csrf_token'], $token);
}
/* =========================
SAFE COLUMN CHECK
========================= */
function hasColumn(mysqli $conn, string $table, string $column): bool {
$table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
$column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);
$dbRes = $conn->query("SELECT DATABASE() AS db");
if(!$dbRes) return false;
$dbRow = $dbRes->fetch_assoc();
$dbName = $dbRow['db'] ?? '';
if($dbName === '') return false;
$sql = "SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
LIMIT 1";
$stmt = $conn->prepare($sql);
if(!$stmt) return false;
$stmt->bind_param("sss", $dbName, $table, $column);
$stmt->execute();
$stmt->store_result();
$ok = $stmt->num_rows > 0;
$stmt->close();
return $ok;
}
/* =========================
VARIABLES
========================= */
$success = '';
$error = '';
$hasCategory = hasColumn($conn,'posts','category');
$hasSlug = hasColumn($conn,'posts','slug');
/* =========================
HELPERS
========================= */
function cleanText($txt){
return trim(preg_replace('/\s+/',' ',strip_tags($txt)));
}
function sanitizeFileName($name){
return preg_replace('/[^a-zA-Z0-9_\.-]/','_',basename($name));
}
function makeSlug($text){
$text = strtolower(trim((string)$text));
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
$text = trim($text, '-');
if($text === ''){
$text = 'post';
}
return $text;
}
function sanitizeYoutube($url){
if(!$url) return '';
if(preg_match('/youtu\.be\/([^\?\/]+)/',$url,$m)) {
return "https://www.youtube.com/embed/".$m[1];
}
if(preg_match('/watch\?v=([^\&]+)/',$url,$m)) {
return "https://www.youtube.com/embed/".$m[1];
}
if(preg_match('~youtube\.com\/embed\/([^\?\/]+)~',$url,$m)) {
return "https://www.youtube.com/embed/".$m[1];
}
return '';
}
function youtubeIdFromEmbed($embed){
if(!$embed) return '';
if(preg_match('~youtube\.com\/embed\/([^\?\/]+)~',$embed,$m)) {
return $m[1];
}
return '';
}
function validateFile($file,$allowed){
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
return in_array($ext, $allowed, true);
}
function ensureDir($dir){
if(!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
}
/* =========================================================
FLASH MESSAGE
========================================================= */
if(isset($_SESSION['flash_success'])){
$success = $_SESSION['flash_success'];
unset($_SESSION['flash_success']);
}
if(isset($_SESSION['flash_error'])){
$error = $_SESSION['flash_error'];
unset($_SESSION['flash_error']);
}
/* =========================
CREATE / UPDATE POST
========================= */
if(
$_SERVER['REQUEST_METHOD']==='POST' &&
isset($_POST['title'],$_POST['content']) &&
(isset($_POST['submit_post']) || isset($_POST['update_post']))
){
if(!csrf_check($_POST['csrf_token'] ?? '')){
$_SESSION['flash_error'] = "Security check failed (CSRF). Refresh and try again.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$title = trim($_POST['title']);
$content = trim($_POST['content']);
$author = trim($_POST['author'] ?? '');
$video = sanitizeYoutube($_POST['video_link'] ?? '');
$category= $hasCategory ? trim($_POST['category'] ?? '') : null;
$meta = trim($_POST['meta_description'] ?? '');
if($meta===''){
$meta = cleanText($content);
$meta = (strlen($meta)>160) ? substr($meta,0,160).'...' : $meta;
}
$file_path = $_POST['existing_file'] ?? '';
$image_path = $_POST['existing_image'] ?? '';
$allowed_files = ['pdf','doc','docx','xls','xlsx','txt'];
$allowed_images = ['jpg','jpeg','png','gif'];
$maxFileSize = 15 * 1024 * 1024;
$maxImageSize = 7 * 1024 * 1024;
ensureDir(__DIR__ . "/uploads/files");
ensureDir(__DIR__ . "/uploads/images");
if(!empty($_FILES['file']['name'])){
if($_FILES['file']['error'] === UPLOAD_ERR_OK){
if($_FILES['file']['size'] > $maxFileSize){
$_SESSION['flash_error'] = "File is too large. Max 15MB.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
if(validateFile($_FILES['file'], $allowed_files)){
$file_path = "uploads/files/".time().'_'.sanitizeFileName($_FILES['file']['name']);
if(!move_uploaded_file($_FILES['file']['tmp_name'], $file_path)){
$_SESSION['flash_error'] = "Failed to upload file.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
} else {
$_SESSION['flash_error'] = "Invalid file type.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
}
}
if(!empty($_FILES['image']['name'])){
if($_FILES['image']['error'] === UPLOAD_ERR_OK){
if($_FILES['image']['size'] > $maxImageSize){
$_SESSION['flash_error'] = "Image is too large. Max 7MB.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
if(validateFile($_FILES['image'], $allowed_images)){
$image_path = "uploads/images/".time().'_'.sanitizeFileName($_FILES['image']['name']);
if(!move_uploaded_file($_FILES['image']['tmp_name'], $image_path)){
$_SESSION['flash_error'] = "Failed to upload image.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
} else {
$_SESSION['flash_error'] = "Invalid image type.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
}
}
if(isset($_POST['update_post'])){
$postId = (int)($_POST['post_id'] ?? 0);
if($postId <= 0){
$_SESSION['flash_error'] = "Invalid post id.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
if($hasCategory){
$sql="UPDATE posts
SET title=?, content=?, meta_description=?, author=?, category=?, video_link=?, file_path=?, image_path=?
WHERE id=?";
$stmt=$conn->prepare($sql);
if(!$stmt){
$_SESSION['flash_error'] = "DB error.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$stmt->bind_param("ssssssssi",$title,$content,$meta,$author,$category,$video,$file_path,$image_path,$postId);
}else{
$sql="UPDATE posts
SET title=?, content=?, meta_description=?, author=?, video_link=?, file_path=?, image_path=?
WHERE id=?";
$stmt=$conn->prepare($sql);
if(!$stmt){
$_SESSION['flash_error'] = "DB error.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$stmt->bind_param("sssssssi",$title,$content,$meta,$author,$video,$file_path,$image_path,$postId);
}
if($stmt->execute()){
if($hasSlug){
$checkSlug = $conn->prepare("SELECT slug FROM posts WHERE id=? LIMIT 1");
if($checkSlug){
$checkSlug->bind_param("i", $postId);
$checkSlug->execute();
$slugRes = $checkSlug->get_result();
$slugRow = $slugRes ? $slugRes->fetch_assoc() : null;
$existingSlug = trim((string)($slugRow['slug'] ?? ''));
$checkSlug->close();
if($existingSlug === ''){
$newSlug = makeSlug($title) . '-' . $postId;
$slugStmt = $conn->prepare("UPDATE posts SET slug=? WHERE id=?");
if($slugStmt){
$slugStmt->bind_param("si", $newSlug, $postId);
$slugStmt->execute();
$slugStmt->close();
}
}
}
}
$_SESSION['flash_success'] = "Post updated successfully.";
}else{
$_SESSION['flash_error'] = "Update failed.";
}
$stmt->close();
header("Location: ".$_SERVER['PHP_SELF']);
exit;
} else {
if($hasCategory){
$sql="INSERT INTO posts(title,content,meta_description,author,category,video_link,file_path,image_path,created_at)
VALUES(?,?,?,?,?,?,?,?,NOW())";
$stmt=$conn->prepare($sql);
if(!$stmt){
$_SESSION['flash_error'] = "DB error.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$stmt->bind_param("ssssssss",$title,$content,$meta,$author,$category,$video,$file_path,$image_path);
}else{
$sql="INSERT INTO posts(title,content,meta_description,author,video_link,file_path,image_path,created_at)
VALUES(?,?,?,?,?,?,?,NOW())";
$stmt=$conn->prepare($sql);
if(!$stmt){
$_SESSION['flash_error'] = "DB error.";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$stmt->bind_param("sssssss",$title,$content,$meta,$author,$video,$file_path,$image_path);
}
if($stmt->execute()){
$newId = (int)$conn->insert_id;
if($hasSlug && $newId > 0){
$slug = makeSlug($title) . '-' . $newId;
$slugStmt = $conn->prepare("UPDATE posts SET slug=? WHERE id=?");
if($slugStmt){
$slugStmt->bind_param("si", $slug, $newId);
$slugStmt->execute();
$slugStmt->close();
}
}
$_SESSION['flash_success'] = "Post added successfully.";
}else{
$_SESSION['flash_error'] = "Insert failed.";
}
$stmt->close();
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
}
/* =========================
DELETE POST
========================= */
if($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['delete_id'])){
if(!csrf_check($_POST['csrf_token'] ?? '')){
$_SESSION['flash_error'] = "Security check failed (CSRF).";
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
$id=(int)$_POST['delete_id'];
if($id > 0){
$stmt=$conn->prepare("DELETE FROM posts WHERE id=?");
if($stmt){
$stmt->bind_param("i",$id);
$stmt->execute();
$stmt->close();
$_SESSION['flash_success'] = "Post deleted.";
} else {
$_SESSION['flash_error'] = "Delete failed.";
}
}
header("Location: ".$_SERVER['PHP_SELF']);
exit;
}
/* =========================
EDIT MODE
========================= */
$editPost=null;
if(isset($_GET['edit_id'])){
$id=(int)$_GET['edit_id'];
if($id > 0){
$stmt = $conn->prepare("SELECT * FROM posts WHERE id=? LIMIT 1");
if($stmt){
$stmt->bind_param("i",$id);
$stmt->execute();
$res = $stmt->get_result();
$editPost = $res ? $res->fetch_assoc() : null;
$stmt->close();
}
}
}
/* =========================
LISTING
========================= */
$perPage = 20;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$offset = ($page - 1) * $perPage;
$q = trim($_GET['q'] ?? '');
$filterCat = $hasCategory ? trim($_GET['cat'] ?? '') : '';
$where = " WHERE 1=1 ";
$params = [];
$types = "";
if($q !== ''){
$where .= " AND (title LIKE ? OR content LIKE ?)";
$like = "%".$q."%";
$params[] = $like;
$params[] = $like;
$types .= "ss";
}
if($hasCategory && $filterCat !== ''){
$where .= " AND category = ?";
$params[] = $filterCat;
$types .= "s";
}
$countSql = "SELECT COUNT(*) AS total FROM posts".$where;
$countStmt = $conn->prepare($countSql);
$totalRows = 0;
if($countStmt){
if($types !== ''){
$countStmt->bind_param($types, ...$params);
}
$countStmt->execute();
$countRes = $countStmt->get_result();
$row = $countRes ? $countRes->fetch_assoc() : null;
$totalRows = (int)($row['total'] ?? 0);
$countStmt->close();
}
$totalPages = max(1, (int)ceil($totalRows / $perPage));
$listSql = "SELECT id, title,
LEFT(content, 180) AS snippet,
image_path, video_link, file_path, created_at".
($hasSlug ? ",slug" : "").
($hasCategory ? ",category" : "").
" FROM posts
".$where."
ORDER BY created_at DESC
LIMIT ? OFFSET ?";
$listStmt = $conn->prepare($listSql);
$posts = null;
if($listStmt){
$params2 = $params;
$types2 = $types . "ii";
$params2[] = $perPage;
$params2[] = $offset;
$listStmt->bind_param($types2, ...$params2);
$listStmt->execute();
$posts = $listStmt->get_result();
$listStmt->close();
}
$cats = [
'HTML','CSS','Java Script','Database','APPS','MIFUMO',
'Matokeo','Mikopo','Mauzo','Uhasibu','Excel',
'Budget','coding','General'
];
function h($s){
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
function buildQuery(array $overrides = []): string {
$base = $_GET;
foreach($overrides as $k=>$v){
if($v === null) {
unset($base[$k]);
} else {
$base[$k] = $v;
}
}
return http_build_query($base);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Faulink Blog Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light pt-5">
<nav class="navbar navbar-dark bg-primary fixed-top shadow">
<div class="container-fluid">
<span class="navbar-brand fw-bold">Faulink Blog Admin</span>
</div>
</nav>
<div class="container mt-4">
<?php if($success):?>
<div class="alert alert-success"><?=h($success)?></div>
<?php endif;?>
<?php if($error):?>
<div class="alert alert-danger"><?=h($error)?></div>
<?php endif;?>
<div class="card shadow mb-4">
<div class="card-body">
<form class="row g-2 align-items-end" method="get">
<div class="col-md-6">
<label class="form-label">Search</label>
<input class="form-control" name="q" value="<?=h($q)?>" placeholder="Search title/content...">
</div>
<?php if($hasCategory): ?>
<div class="col-md-4">
<label class="form-label">Category</label>
<select class="form-select" name="cat">
<option value="">All</option>
<?php foreach($cats as $c): ?>
<option value="<?=h($c)?>" <?=($filterCat===$c?'selected':'')?>><?=h($c)?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-2 d-grid">
<button class="btn btn-primary">Filter</button>
</div>
</form>
<div class="text-muted small mt-2">
Showing <?=h(min($perPage, max(0, $totalRows - $offset)))?> of <?=h($totalRows)?> posts • Page <?=h($page)?> / <?=h($totalPages)?>
</div>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<h5 class="mb-3"><?= $editPost?'Edit Post':'Create Post' ?></h5>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?=h(csrf_token())?>">
<input type="hidden" name="post_id" value="<?= h($editPost['id']??'') ?>">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Title</label>
<input class="form-control" name="title" required value="<?= h($editPost['title']??'') ?>">
</div>
<?php if($hasCategory): ?>
<div class="col-md-6">
<label class="form-label">Category</label>
<select class="form-select" name="category">
<option value="">-- Select --</option>
<?php foreach($cats as $c): ?>
<option value="<?=h($c)?>" <?= (($editPost['category']??'')===$c?'selected':'') ?>>
<?=h($c)?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-6">
<label class="form-label">Author</label>
<input class="form-control" name="author" value="<?= h($editPost['author']??'') ?>">
</div>
<div class="col-md-6">
<label class="form-label">YouTube Link</label>
<input class="form-control" name="video_link" value="<?= h($editPost['video_link']??'') ?>" placeholder="
</div>
<div class="col-md-6">
<label class="form-label">Attach File</label>
<input type="file" name="file" class="form-control">
<input type="hidden" name="existing_file" value="<?= h($editPost['file_path']??'') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Attach Image</label>
<input type="file" name="image" class="form-control">
<input type="hidden" name="existing_image" value="<?= h($editPost['image_path']??'') ?>">
</div>
<div class="col-12">
<label class="form-label">Content</label>
<textarea name="content" class="form-control" rows="7"><?= h($editPost['content']??'') ?></textarea>
</div>
<div class="col-12">
<label class="form-label">Meta Description</label>
<textarea name="meta_description" maxlength="160" class="form-control" rows="2"><?= h($editPost['meta_description']??'') ?></textarea>
</div>
</div>
<button class="btn btn-success mt-3" name="<?= $editPost?'update_post':'submit_post' ?>">
<?= $editPost?'Update':'Publish' ?>
</button>
<?php if($editPost): ?>
<a class="btn btn-outline-secondary mt-3 ms-2" href="<?=h($_SERVER['PHP_SELF'])?>">Cancel Edit</a>
<?php endif; ?>
</form>
</div>
</div>
</div>
</body>
</html>
Step 6: Test Post Mpya
Baada ya ku-upload code:
Fungua admin page
Andika title
Weka content
Bonyeza Publish
Angalia database
Run:
SELECT id, title, slug, created_at
FROM posts
ORDER BY id DESC
LIMIT 5;
Utaona post mpya ina slug kama:
seo-friendly-urls-kwa-php-1284
Step 7: Link Mpya Itakavyokuwa
Kwenye blog viewer, link itakuwa:
https://faulink.com/seo-friendly-urls-kwa-php-1284
Badala ya:
https://faulink.com/blog_viewer.php?id=1284
Hitimisho
Tatizo lilikuwa kwamba admin page ilikuwa ina-save post bila kuingiza slug. Suluhisho ni:
kuweka makeSlug()
kuinsert post kwanza
kupata $newId
kuupdate slug kwa title + id
Hii ni njia salama kwa website live kwa sababu haibadili logic kubwa wala structure ya database.
🚀 Unahitaji mfumo au website ya biashara?
Chagua huduma hapa chini kisha mteja bofya moja kwa moja kwenda kwenye ukurasa wa huduma au kuwasiliana nasi kwa WhatsApp.