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.

Sample Application

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

Directory layout
blog/
├── templates/
│ ├── layout.tpl # base template
│ ├── index.tpl # post listing
│ └── post.tpl # single post
├── cache/ # Tplix compiled templates
├── Post.php # model (PDO)
├── PostController.php # controller
└── index.php # front controller
1

Database Schema

A single posts table is all we need.

schema.sql
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');
2

The Model — Post.php

Plain PHP class backed by PDO. Two methods: one for the list, one for a single post.

Post.php
<?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;
    }
}
3

Front Controller — index.php

Bootstraps PDO, Tplix, and routes to the right controller action.

index.php
<?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);
}
4

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.

PostController.php
<?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');
    }
}
5

The Templates

⬡ layout.tpl — shared base template

Defines the HTML skeleton. Child templates override content and optionally title.

templates/layout.tpl
<!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>&copy; {$.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.

templates/index.tpl
{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 &rarr;</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.

templates/post.tpl
{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}
      &bull;
      {$post.created_at|date_format:"%B %e, %Y"}
    </p>

    <div class="content">
      {$post.content}
    </div>

    <a href="/">&larr; 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

→
Add pagination — pass $page and $totalPages from the controller and use {if} / {math} in the template.
→
Enable caching — set 'caching' => true in the Tplix engine config and watch repeated renders become near-instant.
→
Tag cloud / categories — add a tags column, assign an array to Tplix, and use a nested {foreach} for the tag list.
→
Custom Tplix plugins — register a 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.