Finally migrate RSS Feed

Misusing the sitemap implementation was a bad idea and broke for every
major update.

I now finally migrate to custom implementation which should remove the
issues for future.
This commit is contained in:
Daniel Siepmann 2024-02-07 18:04:55 +01:00
parent 02493e9540
commit 8bcdec5d94
7 changed files with 197 additions and 386 deletions

View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\DsSite\Frontend\RssFeed;
use Exception;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor;
final class BlogPostsDataProvider implements DataProcessorInterface
{
public function __construct(
private readonly DatabaseQueryProcessor $databaseQueryProcessor,
private readonly ConnectionPool $connectionPool
) {
}
public function process(
ContentObjectRenderer $contentObjectRenderer,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
) {
$processedData = $this->databaseQueryProcessor->process(
$contentObjectRenderer,
$contentObjectConfiguration,
array_merge($processorConfiguration, [
'where' => 'AND no_index = 0' . $this->getAdditionalWhere($contentObjectRenderer->getRequest()),
]),
$processedData
);
foreach ($processedData['pages'] as &$page) {
$page['description'] = $page['data']['abstract']
. $this->getContent(
$contentObjectRenderer,
$page['data']['uid']
);
}
return $processedData;
}
private function getAdditionalWhere(ServerRequestInterface $request): string
{
$categoryUid = intval($request->getQueryParams()['category_uid'] ?? 0);
if ($categoryUid === 0) {
return '';
}
$pageUids = $this->getPageUidsWithRelationToCategory($categoryUid);
$where = $this->createAdditionalWhereForPageUids($pageUids);
if ($where !== '') {
return '';
}
return ' ' . $where;
}
private function getPageUidsWithRelationToCategory(int $categoryUid): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->select('uid');
$queryBuilder->from('pages');
$queryBuilder->leftJoin(
'pages',
'sys_category_record_mm',
'mm',
'pages.uid = mm.uid_foreign'
);
$queryBuilder->where(
$queryBuilder->expr()->eq(
'mm.tablenames',
$queryBuilder->createNamedParameter('pages')
),
$queryBuilder->expr()->eq(
'mm.fieldname',
$queryBuilder->createNamedParameter('categories')
),
$queryBuilder->expr()->in(
'mm.uid_local',
$queryBuilder->createNamedParameter($categoryUid)
)
);
return array_map(function (array $row) {
if (is_numeric($row['uid'])) {
return (int) $row['uid'];
}
throw new Exception('UID was not numeric: ' . var_export($row['uid'], true), 1707325559);
}, $queryBuilder->executeQuery()->fetchAllAssociative());
}
private function createAdditionalWhereForPageUids(array $pageUids): string
{
return ' AND uid IN(' . implode(',', $pageUids) . ')';
}
private function getContent(
ContentObjectRenderer $contentObjectRenderer,
int $pageUid
): string {
$colPositions = [
50,
0,
100,
200,
];
$content = '';
foreach ($colPositions as $colPos) {
$content .= $contentObjectRenderer->cObjGetSingle('CONTENT', [
'table' => 'tt_content',
'select.' => [
'orderBy' => 'sorting',
'where' => '{#colPos}=' . $colPos,
'pidInList' => $pageUid,
],
]);
}
return $content;
}
}

View file

@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\DsSite\Frontend\RssFeed;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
class ContentRendering
{
public function __construct(
private readonly ContentObjectRenderer $contentObjectRenderer
) {
}
public function extend(array $row): array
{
$row['description'] = $row['data']['abstract'] . $this->getContent($row['data']['uid']);
return $row;
}
private function getContent(int $pageUid): string
{
$colPositions = [
50,
0,
100,
200,
];
$content = '';
foreach ($colPositions as $colPos) {
$content .= $this->contentObjectRenderer->cObjGetSingle('CONTENT', [
'table' => 'tt_content',
'select.' => [
'orderBy' => 'sorting',
'where' => '{#colPos}=' . $colPos,
'pidInList' => $pageUid,
],
]);
}
return $content;
}
}

View file

@ -1,105 +0,0 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\DsSite\Frontend\RssFeed;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider;
/**
* Provides dynamic for additionalWhere,
* to only list pages for current requested category.
*/
class SitemapDataProvider extends RecordsXmlSitemapDataProvider
{
public function __construct(
ServerRequestInterface $request,
string $key,
array $config = [],
ContentObjectRenderer $cObj = null
) {
$categoryUid = intval($request->getQueryParams()['category_uid'] ?? 0);
if ($categoryUid > 0) {
$pageUids = $this->getPageUidsWithRelationToCategory($categoryUid);
$config['additionalWhere'] = ($config['additionalWhere'] ?? '')
. $this->createAdditionalWhereForPageUids($pageUids)
;
}
parent::__construct($request, $key, $config, $cObj);
}
public function generateItems(): void
{
parent::generateItems();
$contentRendering = new ContentRendering($this->cObj);
foreach ($this->items as $key => $item) {
$this->items[$key] = $contentRendering->extend($item);
}
}
private function getPageUidsWithRelationToCategory(int $categoryUid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
/* @var QueryBuilder $queryBuilder */
$queryBuilder->select('uid');
$queryBuilder->from('pages');
$queryBuilder->leftJoin(
'pages',
'sys_category_record_mm',
'mm',
'pages.uid = mm.uid_foreign'
);
$queryBuilder->where(
$queryBuilder->expr()->eq(
'mm.tablenames',
$queryBuilder->createNamedParameter('pages')
),
$queryBuilder->expr()->eq(
'mm.fieldname',
$queryBuilder->createNamedParameter('categories')
),
$queryBuilder->expr()->in(
'mm.uid_local',
$queryBuilder->createNamedParameter($categoryUid)
)
);
return array_map(function (array $row) {
return (int) $row['uid'];
}, $queryBuilder->executeQuery()->fetchAllAssociative());
}
private function createAdditionalWhereForPageUids(array $pageUids): string
{
return ' AND uid IN(' . implode(',', $pageUids) . ')';
}
}

View file

@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\DsSite\Frontend\RssFeed;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3Fluid\Fluid\View\TemplateView;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Seo\XmlSitemap\Exception\InvalidConfigurationException;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface;
class XmlSitemapRenderer
{
private array $typoScriptConfiguration = [];
protected array $configuration;
private ContentObjectRenderer $contentObjectRenderer;
protected TemplateView $view;
public function __construct(
protected TypoScriptService $typoScriptService,
protected RenderingContextFactory $renderingContextFactory,
) {
}
public function setContentObjectRenderer(ContentObjectRenderer $contentObjectRenderer): void
{
$this->contentObjectRenderer = $contentObjectRenderer;
}
protected function initialize(array $fullConfiguration): void
{
$this->configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($fullConfiguration['plugin.']['tx_seo.'] ?? []);
$renderingContext = $this->renderingContextFactory->create();
$templatePaths = $renderingContext->getTemplatePaths();
$templatePaths->setTemplateRootPaths($this->configuration['view']['templateRootPaths']);
$templatePaths->setLayoutRootPaths($this->configuration['view']['layoutRootPaths']);
$templatePaths->setPartialRootPaths($this->configuration['view']['partialRootPaths']);
$templatePaths->setFormat('xml');
$this->view = GeneralUtility::makeInstance(TemplateView::class, $renderingContext);
$this->view->assign('settings', $this->getSettings());
}
public function render(string $_, array $typoScriptConfiguration, ServerRequestInterface $request): string
{
$this->typoScriptConfiguration = $typoScriptConfiguration;
$this->initialize($GLOBALS['TSFE']->tmpl->setup);
$this->view->assign('type', $request->getAttribute('routing')?->getPageType() ?? 0);
$sitemapType = $typoScriptConfiguration['sitemapType'] ?? 'xmlSitemap';
if (!empty($sitemap = ($request->getQueryParams()['sitemap'] ?? null))) {
return $this->renderSitemap($request, $sitemap, $sitemapType);
}
return $this->renderIndex($request, $sitemapType);
}
protected function renderIndex(ServerRequestInterface $request, string $sitemapType): string
{
$sitemaps = [];
foreach ($this->configuration['config'][$sitemapType]['sitemaps'] ?? [] as $sitemap => $config) {
if (!empty($config['provider']) && is_string($config['provider'])
&& class_exists($config['provider'])
&& is_subclass_of($config['provider'], XmlSitemapDataProviderInterface::class)
) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance(
$config['provider'],
$request,
$sitemap,
(array)($config['config'] ?? [])
);
$pages = $provider->getNumberOfPages();
for ($page = 0; $page < $pages; $page++) {
$sitemaps[] = [
'key' => $sitemap,
'page' => $page,
'lastMod' => $provider->getLastModified(),
];
}
}
}
$this->view->assign('sitemapType', $sitemapType);
$this->view->assign('sitemaps', $sitemaps);
return $this->view->render('Index');
}
protected function renderSitemap(ServerRequestInterface $request, string $sitemap, string $sitemapType): string
{
if (!empty($sitemapConfig = $this->configuration['config'][$sitemapType]['sitemaps'][$sitemap] ?? null)) {
if (class_exists($sitemapConfig['provider']) &&
is_subclass_of($sitemapConfig['provider'], XmlSitemapDataProviderInterface::class)) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance(
$sitemapConfig['provider'],
$request,
$sitemap,
(array)($sitemapConfig['config'] ?? [])
);
$items = $provider->getItems();
$this->view->assign('items', $items);
$this->view->assign('sitemapType', $sitemapType);
$template = ($sitemapConfig['config']['template'] ?? false) ?: 'Sitemap';
return $this->view->render($template);
}
throw new InvalidConfigurationException('No valid provider set for ' . $sitemap, 1535578522);
}
throw new PropagateResponseException(
GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'No valid configuration found for sitemap ' . $sitemap
),
1535578569
);
}
private function getSettings(): array
{
$settings = [];
foreach (array_keys($this->typoScriptConfiguration['userFunc.']['variables.'] ?? []) as $variableName) {
if (!is_string($variableName) || substr($variableName, -1) === '.') {
continue;
}
$settings[$variableName] = $this->contentObjectRenderer->cObjGetSingle(
$this->typoScriptConfiguration['userFunc.']['variables.'][$variableName] ?? '',
$this->typoScriptConfiguration['userFunc.']['variables.'][$variableName . '.'] ?? []
);
}
return $settings;
}
}

View file

@ -18,7 +18,7 @@ services:
arguments:
- 'pages'
DanielSiepmann\DsSite\Frontend\RssFeed\XmlSitemapRenderer:
DanielSiepmann\DsSite\Frontend\RssFeed\BlogPostsDataProvider:
public: true
DanielSiepmann\DsSite\Backend\PreviewRenderer\Video:

View file

@ -1,30 +1,3 @@
plugin.tx_seo {
view {
templateRootPaths {
20 = EXT:ds_site/Resources/Private/Templates/Sitemaps/
}
}
config {
xmlSitemap {
sitemaps {
blog-posts {
provider = DanielSiepmann\DsSite\Frontend\RssFeed\SitemapDataProvider
config {
table = pages
sortField = lastUpdated
sortOrder = DESC
lastModifiedField = tstamp
additionalWhere = AND no_index = 0
pid = {$pageUids.blogPosts}
recursive = 3
template = RssFeed
}
}
}
}
}
}
page {
headerData {
10 = TEXT
@ -32,7 +5,7 @@ page {
wrap = <link rel="alternate" title="RSS Feed of all blog posts" type="application/rss+xml" href="|" />
typolink {
parameter = t3://page?uid=1
additionalParams = &type=1533906435&sitemap=blog-posts
additionalParams = &type=1707321482&feed=blog-posts
returnLast = url
}
}
@ -58,7 +31,7 @@ page {
additionalParams.stdWrap.cObject = COA
additionalParams.stdWrap.cObject {
10 = TEXT
10.value = &type=1533906435&sitemap=blog-posts
10.value = &type=1707321482&feed=blog-posts
11 = TEXT
11.value = &category_uid=
12 = TEXT
@ -71,18 +44,44 @@ page {
}
}
}
rssFeed = PAGE
rssFeed {
typeNum = 1707321482
seo_sitemap {
10 {
userFunc = DanielSiepmann\DsSite\Frontend\RssFeed\XmlSitemapRenderer->render
userFunc {
variables {
categoryId = TEXT
categoryId.data = GP:category_uid
categoryId.intval = 1
categoryTitle = TEXT
categoryTitle.data.dataWrap = DB : sys_category:{GP:category_uid}:title
config {
admPanel = 0
debug = 0
disableAllHeaderCode = 1
additionalHeaders {
10 {
header = Content-Type: application/xml;charset=utf-8
}
}
}
10 = FLUIDTEMPLATE
10 {
dataProcessing {
10 = DanielSiepmann\DsSite\Frontend\RssFeed\BlogPostsDataProvider
10 {
as = pages
table = pages
orderBy = lastUpdated DESC
selectFields = pages.uid, pages.abstract, pages.title, pages.lastUpdated
where = AND no_index = 0
pidInList = {$pageUids.blogPosts}
recursive = 3
}
}
variables {
categoryId = TEXT
categoryId.data = GP:category_uid
categoryId.intval = 1
categoryTitle = TEXT
categoryTitle.data.dataWrap = DB : sys_category:{GP:category_uid}:title
}
file = EXT:ds_site/Resources/Private/Templates/RssFeed.xml
}
}

View file

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<f:if condition="{settings.categoryId}">
<f:if condition="{categoryId}">
<f:then>
<title>Daniel Siepmann - Coding is Art - Blog Posts {settings.categoryTitle}</title>
<description>List of {settings.categoryTitle} blog posts at daniel-siepmann.de</description>
<link>{f:uri.typolink(parameter: 't3://page?uid=11', additionalParams: '&topic_uid={settings.categoryId}', absolute: 1)}</link>
<atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1533906435', additionalParams: '&sitemap=blog-posts&category_uid={settings.categoryId}', absolute: 1)}" rel="self" type="application/rss+xml" />
<title>Daniel Siepmann - Coding is Art - Blog Posts {categoryTitle}</title>
<description>List of {categoryTitle} blog posts at daniel-siepmann.de</description>
<link>{f:uri.typolink(parameter: 't3://page?uid=11', additionalParams: '&topic_uid={categoryId}', absolute: 1)}</link>
<atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1707321482', additionalParams: '&feed=blog-posts&category_uid={categoryId}', absolute: 1)}" rel="self" type="application/rss+xml" />
</f:then>
<f:else>
<title>Daniel Siepmann - Coding is Art - All Blog Posts</title>
<description>List of blog posts at daniel-siepmann.de</description>
<link>{f:uri.typolink(parameter: 't3://page?uid=1', absolute: 1)}</link>
<atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1533906435', additionalParams: '&sitemap=blog-posts', absolute: 1)}" rel="self" type="application/rss+xml" />
<atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1707321482', additionalParams: '&feed=blog-posts', absolute: 1)}" rel="self" type="application/rss+xml" />
</f:else>
</f:if>
<lastBuildDate>{f:format.date(date: 'now', format: 'D, d M Y H:i:s O')}</lastBuildDate>
<ttl>1800</ttl>
<f:for each="{items}" as="item">
<f:if condition="{item.data.doktype} < 200">
<f:for each="{pages}" as="post">
<f:if condition="{post.data.doktype} < 200">
{f:render(section: 'Item', arguments: {
item: item.data,
description: item.description
item: post.data,
description: post.description
})}
</f:if>
</f:for>