How to extend Typo3 Blog-Extension with Previous/Next-Navigation?
In this short tutorial, I explain the steps to add a plugin to the Typo3 blog extension. The plugin inherits from the post model and its repository and also uses the templates from the blog extension.
To extend the blog extension I use my template extension for this project. The PHP namespace is Drea\Template and it is installed in vendor/drea/template/. The current Typo3 version is 12.4.11.
1. Define the classes
Extend the Blog-Model
<?php
declare(strict_types=1);
namespace Drea\Template\Domain\Model;
use T3G\AgencyPack\Blog\Domain\Model\Post as T3gPost;
class Post extends T3gPost
{}
Extend the Blog-Repository
<?php
declare(strict_types=1);
namespace Drea\Template\Domain\Repository;
use T3G\AgencyPack\Blog\Domain\Repository\PostRepository as T3gPostRepository;
class PostRepository extends T3gPostRepository
{
public function initializeObject(): void
{
parent::initializeObject();
}
public function findPreviousAndNextByCurrentPost(): array
{
$return = [
'previous' => null,
'next' => null
];
$currentPost = $this->findCurrentPost();
if (!$currentPost) {
return $return;
}
// Iterate all posts and find the previous and next post
// The previous post is the one with the higher index b/c the posts are sorted by date descending.
$allPosts = $this->findAll();
foreach ($allPosts as $index => $post) {
if ($post->getUid() === $currentPost->getUid()) {
if (isset($allPosts[$index - 1])) {
$return['next'] = $allPosts[$index - 1];
}
if (isset($allPosts[$index + 1])) {
$return['previous'] = $allPosts[$index + 1];
}
break;
}
}
return $return;
}
}
Bring them together with a controller action
<?php
declare(strict_types=1);
namespace Drea\Template\Controller;
use Psr\Http\Message\ResponseInterface;
use Drea\Template\Domain\Repository\PostRepository;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class PostController extends ActionController
{
protected PostRepository $postRepository;
public function __construct(
PostRepository $postRepository,
) {
$this->postRepository = $postRepository;
}
/**
* Shows the previous and next post.
*/
public function previousNextAction(): ResponseInterface
{
$this->view->assignMultiple($this->postRepository->findPreviousAndNextByCurrentPost());
$content = $this->view->render();
return $this->htmlResponse($content ? $content : '');
}
}
Note the last two lines (26+27). When there is no previous and next post the returning content from the fluid template might not be set (null instead of empty string). This would lead to a failure in the response. Therefore it is tested here before returning.
Map the model to the database table
<?php
declare(strict_types=1);
return [
\Drea\Template\Domain\Model\Post::class => [
'tableName' => 'pages',
],
];
The model has the name Drea\Template\Domain\Model\Post and therefore the database layer expects a table name tx_template_domain_model_post. This does not exist because the pages table is used for blog posts. Let's tell the system.
2. Define the plugin
<?php
declare(strict_types=1);
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use Drea\Template\Controller\PostController;
defined('TYPO3') or die();
// [...] other stuff for template extension
// define plugins
ExtensionUtility::configurePlugin(
'Template',
'PreviousNext',
[
PostController::class => 'previousNext',
]
);
The plugin is not selectable by editors, so we've to define it only in ext_localconf.php (and not also in Configuration/TCA/Overrides/tt_content.php).
3. Configure TypoScript
We have to extend the blog`s template, partial and layout paths to extend and overwrite files by the template extension who modifies this all (the so called extension extending extension?). You sure know what I mean. The plugin is an Extbase plugin which uses the FLUIDTEMPLATE content object which uses definitions for the template paths, etc.
Here I do a little special because Extbase expects the template for the plugin in template/Resources/Private/Templates/Post/PreviousNext.html which would be the default location and is defined through template extension, PostController and previousNextAction. Instead, and because I'm overwriting other blog extension stuff anyway, I extend the blog extension path here pointing to my template extension into the Blog subfolders. Later in the plugin call I reference to the blog stuff and not to the default Extbase path. The path to the template file is then: template/Resources/Private/Template/Blog/Post/PreviousNext.html.
File: template/Configuration/TypoScript/setup.typoscript# Extend blog template paths
plugin.tx_blog.view {
templateRootPaths {
10 = EXT:template/Resources/Private/Templates/Blog/
}
partialRootPaths {
10 = EXT:template/Resources/Private/Partials/Blog/
}
layoutRootPaths {
10 = EXT:template/Resources/Private/Layouts/Blog/
}
}
The plan is to call the new plugin from the Fluid template with the cObject-ViewHelper. So we have to define the content object.
Please note line 8 and 9 in the code below where I reference to the blog extension templates and settings like I mentioned in the previous section (plugin.tx_blog and not plugin.tx_template).
Same file: template/Configuration/TypoScript/setup.typoscriptlib.blogPreviousNext = USER
lib.blogPreviousNext {
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
extensionName = Template
pluginName = PreviousNext
vendorName = Drea
controller = Post
view < plugin.tx_blog.view
settings < plugin.tx_blog.settings
}
4. Call the plugin
The blog extension uses a page template to show the details page of a blog post. This has to be modified to call our new plugin. I use the blog extension with its whole page configuration - also called Standalone. This may differ from the Shared installation where you've already set your BlogPost.html.
In my case I want to modify the blog extension template and I've to extend the page object template path.
File: template/Configuration/TypoScript/setup.typoscript# Extend page template paths
page.10 {
templateRootPaths {
10 = EXT:template/Resources/Private/Templates/Page/
}
partialRootPaths {
10 = EXT:template/Resources/Private/Partials/Page/
}
layoutRootPaths {
10 = EXT:template/Resources/Private/Layouts/Page/
}
}
In my case the template is located in template/Resources/Private/Templates/Page/BlogPost.html and looks like this:
<html
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:tmpl="http://typo3.org/ns/Drea/Template/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default" />
<f:section name="Main">
<tmpl:asset extension="template" entry="Scss/Forms.scss" config="vite.config.ts" forceOnTop="true" />
<div id="page-grid">
<div id="page-grid__header">
<f:render partial="Header" section="Default" arguments="{_all}" />
</div>
<main id="page-grid__main" role="main">
<f:render partial="Navigation/MenuBreadcrumb" section="Default" arguments="{items:menu_breadcrumb}" />
<!--TYPO3SEARCH_begin-->
<f:render section="renderPlugin" arguments="{listType: 'blog_header'}" />
<f:cObject typoscriptObjectPath="lib.dynamicContent" data="{colPos: '0'}" />
<f:render section="renderPlugin" arguments="{listType: 'blog_footer'}" />
<f:comment><!--
<f:render section="renderPlugin" arguments="{listType: 'blog_authors'}" />
--></f:comment>
<!--TYPO3SEARCH_end-->
<f:render section="renderPlugin" arguments="{listType: 'blog_comments'}" />
<f:render section="renderPlugin" arguments="{listType: 'blog_commentform'}" />
<f:render section="renderPlugin" arguments="{listType: 'blog_relatedposts'}" />
<f:cObject typoscriptObjectPath="lib.blogPreviousNext"/>
</main>
<aside id="page-grid__aside">
<div id="page__aside">
<f:render section="renderPlugin" arguments="{listType: 'blog_sidebar'}" />
</div>
<f:render partial="Navigation/MenuAsideButton" section="Default" arguments="{_all}" />
</aside>
<div id="page-grid__footer">
<f:render partial="Footer" section="Default" arguments="{_all}" />
</div>
</div>
</f:section>
<f:section name="renderPlugin">
{blogvh:data.contentListOptions(listType: listType)}
<f:cObject typoscriptObjectPath="tt_content.list" data="{contentObjectData}" table="tt_content"/>
</f:section>
</html>
There are some heavy modifications to use in this project. These are only relevant for my page layout. Don't get confused.
Important is only line 27 which calls the new plugin:
<f:cObject typoscriptObjectPath="lib.blogPreviousNext"/>
5. The plugin template
Finally our new previous/next plugin needs a template.
File: template/Resources/Private/Templates/Blog/Post/PreviousNext.html<html
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default" />
<f:section name="Content">
<f:if condition="{previous}||{next}">
<div class="previousnextpost">
<f:if condition="{previous}">
<div class="previousnextpost__item previousnextpost__item--previous">
<h3 class="hs-sm">Previous post</h3>
{f:variable(name:'post', value:previous)}
<f:render partial="Teaser/Post" arguments="{_all}" />
</div>
</f:if>
<f:if condition="{next}">
<div class="previousnextpost__item previousnextpost__item--next">
<h3 class="hs-sm text-end">Next post</h3>
{f:variable(name:'post', value:next)}
<f:render partial="Teaser/Post" arguments="{_all}" />
</div>
</f:if>
</div>
</f:if>
</f:section>
</html>
Here some starting styles for the plugin:
_previousnextpost.scss.previousnextpost {
@extend %container;
margin-top: var(--cs-md);
margin-bottom: var(--cs-xs);
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
&__item {
display: flex;
flex-direction: column;
.postteaser__post {
// enlarge the postteaser
// justify-self: stretch; // does not work?!
height: 100%; // works
}
}
&__label {
margin-bottom: var(--cs-xs);
}
@include media-breakpoint-up(sm) {
grid-template-columns: 1fr 1fr;
&__item--next {
// move next to second column (also when there is no previous post)
grid-column: 2;
}
}
@include media-breakpoint-up(md) {
grid-template-columns: 1fr 1fr 1fr;
&__item--next {
// move next to third column
grid-column: 3;
}
}
}
In theory, all you need to do now is empty the large Typo3 cache (vendor/bin/typo3 cache:flush or ddev typo3 cache:flush) and call up a blog post. In reality, you'll probably get error messages as usual. I just wrote the instructions off the top of my head. It should work, but there's always something wrong. Also, your installation is probably different from mine. Let me know what doesn't work.
Comments
No Comments