Single Post Page

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

We need to create a new render method that will retrieve one specific post and pass it to the template. Putting this method in HomePresenter is not very neat because it’s about a single post, not the homepage. So, let’s create a new class PostPresenter and place it in app/Presentation/Post/. This presenter will also need a database connection, so we'll add the constructor which will require the database connection.

The PostPresenter could look like this:

<?php
namespace App\Presentation\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 must not forget to specify the correct namespace App\Presentation\Post, which depends on the presenter mapping configuration.

The renderShow method requires one argument – the ID of the post to be displayed. It then loads the post from the database and passes it 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>
...

The {link} tag generates a URL address that points to the Post:show action. It also passes the post ID as an argument.

The same can be written concisely using an n:attribute:

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

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

However, the template for the Post:show action does not exist yet. We can try opening the link to this post. Tracy will show an error because the template Post/show.latte doesn't exist yet. If you see a different error message, you probably need to enable mod_rewrite on your web server.

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>

Now let’s review the individual parts of the template.

The first line starts the definition of a block named “content”, just like on the homepage. This block will again be displayed within the main 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, allowing the user to navigate easily between the post list and a specific post. We use the n:href attribute again, so Nette will take care of generating the URL. The link points to the default action of the Home presenter (you could also write n:href="Home:", as the default action name can be omitted; it will be added automatically).

The third line formats the date using a filter, which we are already familiar with.

The fourth line displays the title of the blog post within an <h1> HTML tag. This tag contains an attribute you might not recognize (n:block="title"). Can you guess what it does? If you’ve read the previous section carefully, you already know it's an n:attribute. This is another example, equivalent to:

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

Simply put, this block redefines the block named title. This block is already defined in the main layout template (/app/Presentation/@layout.latte:11), and just like method overriding in OOP, this block overrides the one in the main template. Therefore, the page's <title> will now contain the title of the displayed post, and all we needed was this simple n:block="title" attribute. Awesome, right?

The fifth and final line of the template displays the full content of the specific post.

Checking Post ID

What happens if someone alters the ID in the URL and inserts a non-existent id? We should show the user a nice “page not found” error. Let’s modify the render method in PostPresenter slightly:

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 display a 404 page with an understandable message. Note that in the development environment (on localhost), you won’t see this error page. Instead, Tracy will show the exception with full details, which is quite convenient for development. If you want to test both modes, simply change the argument passed to the setDebugMode method in Bootstrap.php.

Summary

We have a database with posts and a web application with two views – the first displays an overview of all posts, and the second displays one specific post.