Blog Example
A complete working PHP Blog application built with Tplix. Covers MVC structure, form validation, PDO database integration, and clean separation of presentation from business logic.
Building a Simple Blog
A minimal but complete blog application that demonstrates Tplix's core features — template inheritance, loops, filters, and conditional rendering — all in a real MVC setup.
What We'll Build
A two-page blog: a post listing page and a single-post detail page. No frameworks, no Composer dependencies beyond Tplix — just plain PHP and PDO.
Post Listing
Loop over posts, show title, excerpt, date, and a "Read more" link.
Single Post
Display full content, formatted date, and author with proper escaping.
Shared Layout
One base template, two child templates — Tplix template inheritance at work.
Project Structure
Database Schema
A single posts table is all we need.
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
excerpt TEXT,
content TEXT NOT NULL,
author VARCHAR(100) NOT NULL DEFAULT 'Admin',
published TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO posts (title, slug, excerpt, content, author) VALUES
('Getting Started with Tplix',
'getting-started-tplix',
'Learn how to install and configure Tplix in minutes.',
'Tplix is a fast, Smarty-compatible template engine...', 'Jane'),
('Template Inheritance Explained',
'template-inheritance',
'How {extends} and {block} keep your layouts DRY.',
'Template inheritance lets child templates override...', 'Tom');
The Model — Post.php
Plain PHP class backed by PDO. Two methods: one for the list, one for a single post.
<?php
class Post
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/** Return all published posts, newest first */
public function all(): array
{
$stmt = $this->pdo->query(
"SELECT id, title, slug, excerpt, author, created_at
FROM posts
WHERE published = 1
ORDER BY created_at DESC"
);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/** Return one post by slug, or null if not found */
public function findBySlug(string $slug): ?array
{
$stmt = $this->pdo->prepare(
"SELECT * FROM posts WHERE slug = ? AND published = 1 LIMIT 1"
);
$stmt->execute([$slug]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
}
Front Controller — index.php
Bootstraps PDO, Tplix, and routes to the right controller action.
<?php
require 'vendor/autoload.php';
require 'Post.php';
require 'PostController.php';
// Database connection
$pdo = new PDO('mysql:host=localhost;dbname=blog;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Tplix setup
$tplix = new Tplix\Engine([
'template_dir' => __DIR__ . '/templates',
'compile_dir' => __DIR__ . '/cache',
'caching' => false,
]);
// Simple router
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$slug = trim($path, '/');
$controller = new PostController($pdo, $tplix);
if ($slug === '' || $slug === 'blog') {
$controller->index();
} else {
$controller->show($slug);
}
The Controller — PostController.php
Fetches data from the model and passes it to the template. Notice we pass raw data — Tplix handles escaping in the view.
<?php
class PostController
{
public function __construct(
private PDO $pdo,
private Tplix\Engine $tplix
) {}
/** GET / — list all posts */
public function index(): void
{
$model = new Post($this->pdo);
$posts = $model->all();
$this->tplix->assign('posts', $posts);
$this->tplix->assign('page_title', 'Blog');
$this->tplix->display('index.tpl');
}
/** GET /{slug} — single post */
public function show(string $slug): void
{
$model = new Post($this->pdo);
$post = $model->findBySlug($slug);
if (!$post) {
http_response_code(404);
echo '<h1>Post not found</h1>';
return;
}
$this->tplix->assign('post', $post);
$this->tplix->assign('page_title', $post['title']);
$this->tplix->display('post.tpl');
}
}
The Templates
⬡ layout.tpl — shared base template
Defines the HTML skeleton. Child templates override content and optionally title.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{block name="title"}{$page_title|escape} — My Blog{/block}</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header>
<a href="/">My Blog</a>
</header>
<main>
{block name="content"}{/block}
</main>
<footer>
<p>© {$.now|date_format:"%Y"} My Blog</p>
</footer>
</body>
</html>
⬡ index.tpl — post listing
Extends the layout. Uses {foreach} to loop posts and {foreachelse} for the empty state.
{extends file="layout.tpl"}
{block name="content"}
<h1>Latest Posts</h1>
{foreach $posts as $post}
<article>
<h2><a href="/{$post.slug|escape}">{$post.title|escape}</a></h2>
<p class="meta">
By {$post.author|escape}
on {$post.created_at|date_format:"%B %e, %Y"}
</p>
<p>{$post.excerpt|escape}</p>
<a href="/{$post.slug|escape}">Read more →</a>
</article>
{foreachelse}
<p>No posts yet. Check back soon!</p>
{/foreach}
{/block}
⬡ post.tpl — single post
Overrides both title and content blocks to display the full article.
{extends file="layout.tpl"}
{block name="title"}{$post.title|escape} — My Blog{/block}
{block name="content"}
<article>
<h1>{$post.title|escape}</h1>
<p class="meta">
By {$post.author|escape}
•
{$post.created_at|date_format:"%B %e, %Y"}
</p>
<div class="content">
{$post.content}
</div>
<a href="/">← Back to all posts</a>
</article>
{/block}
Tplix Features Used in This Example
{extends} + {block}
Child templates inherit the full layout and selectively override named sections — no repeated HTML.
{foreach} + {foreachelse}
Iterate over post arrays and gracefully handle the empty-list case in one clean block.
|escape filter
Every user-controlled value is escaped at the template layer, keeping the controller clean of HTML concerns.
|date_format filter
Raw MySQL datetime strings are formatted for display directly in the template — no strtotime in the controller.
$.now
Built-in variable for the current Unix timestamp — used here for the dynamic copyright year in the footer.
Array dot-notation
{$post.title} accesses associative array keys — cleaner than PHP's $post['title'] inline.
Where to Go Next
$page and $totalPages from the controller and use {if} / {math} in the template.
'caching' => true in the Tplix engine config and watch repeated renders become near-instant.
tags column, assign an array to Tplix, and use a nested {foreach} for the tag list.
truncate_words modifier so excerpts are auto-trimmed without touching the model or controller.
Ready to build?
Install Tplix, create the three template files above, and you have a running blog in under an hour.