Single Post Page

Let’s add another page to our blog, which will display the content of one particular blog post.

We need to create a new render method, that will fetch one specific blog post and pass it to the template. Having this view in HomePresenter is not nice because it’s about a blog post, not the homepage. So, let’s create a new class PostPresenter and place it to app/UI/Post/. It will need a database connection, so put the database injection code there again.

The PostPresenter should look like this:

<?php
namespace App\UI\Post;

use Nette;
use Nette\Application\UI\Form;

final class PostPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Nette\Database\Explorer $database,
	) {
	}

	public function renderShow(int $id): void
	{
		$this->template->post = $this->database
			->table('posts')
			->get($id);
	}
}

We have to set a correct namespaces App\UI\Post for our presenter. It depends on presenter mapping.

The renderShow method requires one argument – the ID of the post to be displayed. Then, it loads the post from the database and passes the result to the template.

In the Home/default.latte template we add a link to the Post:show action:

...
<h2><a href="{link Post:show $post->id}">{$post->title}</a></h2>
...

Tag {link} generates URL address which points to the action Post:show. This tag also forwards the ID of the post as an argument.

The same we can write shortly using n:attribute:

...
<h2><a n:href="Post:show $post->id">{$post->title}</a></h2>
...

Attribute n:href is similar to the {link} tag.

The template for Post:show action does not yet exist. We can open a link to this post. Tracy will show an error, why Post/show.latte doesn't exist. If you see any other error report you probably have to turn on mod_rewrite in your webserver.

So we'll create Post/show.latte with this content:

{block content}

<p><a n:href="Home:default">← back to posts list</a></p>

<div class="date">{$post->created_at|date:'F j, Y'}</div>

<h1 n:block="title">{$post->title}</h1>

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

Let’s have a look at the individual parts.

The first line starts the definition of a named block called “content” that we saw earlier. It will be displayed in a layout template. As you can see, the end tag {/block} is missing. It is optional.

The second line provides a backlink to the list of blog posts, so the user can navigate smoothly back and forth on our blog. We use n:href attribute again, therefore Nette will take care of generating the URL for us. The link points to the default action of the Home presenter (you could also write n:href="Home:" as the default action can be omitted).

The third line formats the publication timestamp with a filter, as we already know.

The fourth line displays the title of the blog post as a <h1> heading. There is a part that you may not be familiar with, and that is n:block="title". Can you guess what it does? If you’ve read the previous parts carefully, we've mentioned n: attributes. This is another example. It is equivalent to:

{block title}<h1>{$post->title}</h1>{/block}

In simple words, it re-defines a block called title. The block is defined in layout template (/app/UI/@layout.latte:11) and like with OOP overriding, it gets overridden here. Therefore, the page’s <title> will contain the title of the displayed post. We’ve overridden the title of the page and all we needed was n:block="title". Great, huh?

The fifth and the last line of the template displays full content of your post.

Checking Post ID

What happens if someone alters the URL and inserts id which does not exist? We should provide the user with a nice “page not found” error. Let’s update the render method in PostPresenter:

public function renderShow(int $id): void
{
	$post = $this->database
		->table('posts')
		->get($id);
	if (!$post) {
		$this->error('Post not found');
	}

	$this->template->post = $post;
}

If the post cannot be found, calling $this->error(...) will show a 404 page with a nice and understandable message. Note, that in your development environment (on your laptop), you won’t see the error page. Instead, Tracy will show the exception with full details, which is pretty convenient for development. You can check both modes, just change the value passed to setDebugMode in Bootstrap.php.

Summary

We have a database with blog posts and a web app with two views – the first one displays the summary of all recent posts and the second one displays one specific post.