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

File: template/Classes/Domain/Model/Post.php
<?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

File: template/Classes/Domain/Repository/PostRepository.php
<?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

File: template/Classes/Controller/PostController.php
<?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

File: template/Configuration/Extbase/Persistence/Classes.php
<?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

File: template/ext_localconf.php
<?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.typoscript
lib.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

Write comment

Fields marked with * are mandatory.

By using this form you agree with the storage and handling of your data by this website. Name and comment will be published. Your email address will not be published and will only be used to get in touch with you if there is a need to do so.

You can find more information in the privacy policy.

Related posts

Previous

Next