horta.dev.br

February 28, 2026 01:56pm

This very website is a project - from ground up a lightweight, frameworkless CMS written in PHP

Overview

This website is more than a portfolio — it’s a custom content management system (CMS) built entirely from scratch using PHP and MySQL.
Every page, article, and category is dynamically generated via secure backend logic developed over multiple iterations.

The site powers not only the public-facing portfolio but also an internal admin system for managing content efficiently and safely. But it's not only a simple CMS: each project hosted have their own Classes, Database and custom frontend styling, being different where they have to be, while maintaining the same classy presentation matching the main theme.

To check the progress, every commit and what was changed, please read the changelog.


⚙️ Technical Stack

Area Technologies
Frontend HTML5, CSS3, JavaScript -base design by HTML5 Up.
Backend PHP 8, PDO, MySQL
Security Layers Sessions, CSRF Tokens, Rate-Limiting, Prepared Statements
Infrastructure Apache with custom .htaccess, hosting service
Admin Tools TinyMCE (locally-hosted), Image Manager, Category Relations, Blocks Management
WYSIWYG page and category editor

Architecture & Design

The project adopts a two-connection model for database access — a read-only user for public data and a write-privileged one for admin functions — enforcing least-privilege principles.

The backend is structured into layers for modularity:

  • Articles, Categories, Users and Blocks classes handle their respective classes and methods in a OOP design.

  • Database handles both connections.

  • auth enforces session login.

  • csrf manages secure form submissions.

Screenshot of admin main page as of October 7th, 2025
The editor to write pages like this one :)

Features

  • ✍️ Admin Panel for creating and managing pages, categories, blocks, users and media.

  • WYSIWYG Editor with image upload and browsing to make my life easier.

  • Category Relationships between posts.

  • Directory Protection using recursive .htaccess rules.

  • Lightweight and Secure — minimal dependencies, pure PHP logic - no frameworks, no strings attached, no limitations.


Challenges & Solutions

Challenge: Mixing production and local configurations without breaking sessions or DB connections.
Solution: Split database configuration into distinct connection function, and validated current environment before connecting.

Challenge: Implementing safe image uploads and previews.
Solution: Added a dedicated directory with server-side validation and controlled MIME-type handling.


Security & Best Practices

  • Enforced secure cookies (httponly, samesite=lax,secure).

  • CSRF token validation for all POST operations.

  • Login rate limiting to prevent brute-force attempts.

  • Recursive .htaccess rule to disable directory listing in all folders.

[CSRF Validation Snippet]

function csrf_check(): void {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $token = $_POST['csrf_token'] ?? '';
        if (empty($token) || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
            http_response_code(401);
            die('Invalid credentials');
        }
    }
}


 

 Results & Learnings

This project became my personal playground for modern PHP practices — blending classic procedural roots with secure, modular architecture, OOP design.
It demonstrates myability to design, secure, and maintain a real-world system end-to-end — from database schema to user facing structures.


Code Snippets

[Category class]
class Article {
    public int $id;
    public string $title;
    public string $slug;
    public string $description;
    public ?string $longdescription;
    public ?string $image;
    public ?string $link;
    public int $enabled;
    public string $created_at;
    public string $updated_at;

    public function __construct(array $data) {
        $this->id             = (int)($data['id'] ?? 0);
        $this->title          = $data['title'] ?? '';
        $this->slug           = $data['slug'] ?? '';
        $this->description    = $data['description'] ?? '';
        $this->longdescription = $data['longdescription'] ?? null;
        $this->image          = $data['image'] ?? null;
        $this->link           = $data['link'] ?? null;
        $this->enabled        = (int)($data['enabled'] ?? 1);
        $this->created_at     = $data['created_at'] ?? '';
        $this->updated_at     = $data['updated_at'] ?? '';
    }
 
[Slug generation method]
function generateSlug($text) {
    // Convert to lowercase
    $text = mb_strtolower($text, 'UTF-8');

    // Replace non-alphanumeric characters (and non-hyphen) with a hyphen
    // \pL matches any kind of letter from any language
    // \d matches any digit
    $text = preg_replace('~[^\pL\d]+~u', '-', $text);

    // Transliterate (convert accented characters to their ASCII equivalents)
    // This requires the iconv extension to be enabled in php.ini
    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

    // Remove any remaining unwanted characters (non-word characters except hyphen)
    $text = preg_replace('~[^-\w]+~', '', $text);

    // Trim leading/trailing hyphens
    $text = trim($text, '-');

    // Replace multiple hyphens with a single hyphen
    $text = preg_replace('~-+~', '-', $text);

    // Handle empty slug case
    if (empty($text)) {
        return 'n-a'; // or another default slug
    }

    return $text.'-'.rand(0,999999);
}