Tutorial: a blog

Build a blog section: a listing page, a post view, dated folders and pagination.

On this page

This builds on the Quickstart. By the end, /blog/ lists posts newest first, ten to a page, and each post lives in a dated folder.

1. Fields

The post view needs a title, a summary, a body and a cover image. title and body exist from the Quickstart's note view; add the other two as site fields:

phlat make:field summary --label=Summary
phlat make:field cover --type=@phlat/image --label=Cover

2. The post view

// site/views/post/_index.json
{
    "fields": {
        "title": { "type": "@phlat/text", "label": "Title", "required": true },
        "summary": {},
        "cover": {},
        "body": { "type": "@phlat/markdown", "label": "Body" }
    }
}
{* site/views/post/index.latte *}
{layout 'layout.latte'}

{block main}
<article>
    <h1 n:edit="$page->title">{$page->title}</h1>
    <p class="text-sm">{$page->created|date}</p>

    {if !$page->cover->empty()}
        <img src="{$page->cover->src('w=1200,h=600,crop')}" alt="">
    {/if}

    <div n:edit="$page->body" class="prose">{$page->body|render}</div>
</article>
{/block}

3. The blog view

The blog page holds the posts. children.views limits new children to post, and children.path files each one under its year and month, so the folder never grows flat:

// site/views/blog/_index.json
{
    "fields": {
        "title": { "type": "@phlat/text", "label": "Title" }
    },
    "children": {
        "views": ["post"],
        "path": "{year}/{month}/{slug}"
    }
}

A post made in May 2026 lands at site/pages/blog/2026/05/hello/ and is served at /blog/2026/05/hello/. The year and month folders are made for you as hidden pages.

4. The listing

A controller reads the page number from the query string and asks the registry for one page of posts. children() would return the year folders, so the query goes through find() at the depth of the posts:

<?php
// site/views/blog/controller.php

declare(strict_types=1);

use Phlat\App;
use Phlat\Page;

return function (App $app, Page $page): array {
    $perPage = 10;
    $current = max(1, (int) ($app->request->query('page') ?? 1));
    $query = 'view=post, depth=' . $page->childDepth();

    return [
        'posts' => $page->find("{$query}, sort=-created, limit={$perPage}, offset=" . ($current - 1) * $perPage),
        'current' => $current,
        'total' => max(1, (int) ceil($page->find($query)->count() / $perPage)),
    ];
};
{* site/views/blog/index.latte *}
{layout 'layout.latte'}

{block main}
<h1>{$page->title}</h1>

{include '@ui/list.latte', items: $posts}

{if $total > 1}
    {include '@ui/pagination.latte', current: $current, total: $total}
{/if}
{/block}

@ui/list takes the page collection as it is: each post shows its title, link, date and description. See the phlat/ui addon's README for the other components.

5. Pages

Create the blog page, then a post under it:

phlat make:page blog --view=blog --title=Blog --publish

In the admin, add a child page to Blog. It gets the post view and a dated folder. Write the post, save it and publish it on the Settings tab.

Next