Page content check for markets and solutions (vitec_pagecheck)
Sibling of the product check (decision Harry 2026-11-13): new content element vitec_pagecheck with a FlexForm scope (markets | solutions) and PageCheckService/PageCheckJsonRenderer. Overview = one row per page of the tree (11 markets + 24 sub-markets, 8 solution groups + 39 sub-solutions) with 18 checks taken from the page elements (hero image and subline, lead, intro text/image/CTA, cards with visuals, testimonials, products, insights text/cover/CTA, logos, CTA banner), the page properties (meta description, OG image, page media) and the market/solution record behind the page; columns a page type does not have come as ok null. ?check=<pageUid> delivers the page detail (elements in page order with texts, links, images, FlexForm settings; SEO, media, record, children). Always USER_INT, check is cHash-excluded already. New command vitec:pagecheck-pages created /markets/market-check (608) and /solutions/solution-check (609) from the product-check page properties; both live on DEV, rendering is front end work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
47c8e2902f
commit
20bcbeb731
9 files changed
+913
No files matched your search
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Creates the two internal audit pages for the page content check (decision
|
||||
* Harry 2026-11-13, sibling of /products/product-check):
|
||||
*
|
||||
* /markets/market-check vitec_pagecheck, scope markets
|
||||
* /solutions/solution-check vitec_pagecheck, scope solutions
|
||||
*
|
||||
* Page properties are taken from the product check page (/products/
|
||||
* product-check: doktype, backend layout, no_index, nav_hide, nav_title
|
||||
* handling) so all three audit pages behave alike. Idempotent: an existing
|
||||
* slug is reported and skipped.
|
||||
*
|
||||
* vendor/bin/typo3 vitec:pagecheck-pages # dry run (default)
|
||||
* vendor/bin/typo3 vitec:pagecheck-pages --apply # creates
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:pagecheck-pages',
|
||||
description: 'Create /markets/market-check and /solutions/solution-check with the vitec_pagecheck element (dry-run by default)'
|
||||
)]
|
||||
final class CreateCheckPagesCommand extends Command
|
||||
{
|
||||
private const TEMPLATE_SLUG = '/products/product-check';
|
||||
|
||||
private const PAGES = [
|
||||
['parentSlug' => '/markets', 'title' => 'Market Check', 'slug' => '/markets/market-check', 'header' => 'Market Content Check', 'scope' => 'markets'],
|
||||
['parentSlug' => '/solutions', 'title' => 'Solution Check', 'slug' => '/solutions/solution-check', 'header' => 'Solution Content Check', 'scope' => 'solutions'],
|
||||
];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Create the pages (default: dry run)');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$apply = (bool)$input->getOption('apply');
|
||||
$db = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
|
||||
|
||||
$template = $db->fetchAssociative(
|
||||
'SELECT uid, doktype, backend_layout, backend_layout_next_level, no_index, no_follow, nav_hide, no_search, seo_title, description FROM pages WHERE deleted = 0 AND slug = ?',
|
||||
[self::TEMPLATE_SLUG]
|
||||
);
|
||||
if ($template === false) {
|
||||
$output->writeln('<error>Template page ' . self::TEMPLATE_SLUG . ' not found.</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$output->writeln(sprintf(
|
||||
'Template %s (uid %d): doktype %d, layout "%s", no_index %d, nav_hide %d, no_search %d',
|
||||
self::TEMPLATE_SLUG, $template['uid'], $template['doktype'], $template['backend_layout'], $template['no_index'], $template['nav_hide'], $template['no_search']
|
||||
));
|
||||
$output->writeln($apply ? '<comment>APPLY</comment>' : 'DRY RUN - nothing is written (--apply to create)');
|
||||
|
||||
$created = 0;
|
||||
foreach (self::PAGES as $def) {
|
||||
$existing = $db->fetchAssociative('SELECT uid FROM pages WHERE deleted = 0 AND slug = ?', [$def['slug']]);
|
||||
if ($existing !== false) {
|
||||
$output->writeln(sprintf(' %s exists (uid %d) - skipped', $def['slug'], $existing['uid']));
|
||||
continue;
|
||||
}
|
||||
$parent = $db->fetchAssociative('SELECT uid FROM pages WHERE deleted = 0 AND sys_language_uid = 0 AND slug = ?', [$def['parentSlug']]);
|
||||
if ($parent === false) {
|
||||
$output->writeln(sprintf(' <error>parent %s not found - %s skipped</error>', $def['parentSlug'], $def['slug']));
|
||||
continue;
|
||||
}
|
||||
$output->writeln(sprintf(' create %s "%s" under %s (pid %d) + vitec_pagecheck "%s" (scope %s)', $def['slug'], $def['title'], $def['parentSlug'], $parent['uid'], $def['header'], $def['scope']));
|
||||
$created++;
|
||||
if (!$apply) {
|
||||
continue;
|
||||
}
|
||||
$pageId = 'NEWpage' . $def['scope'];
|
||||
$datamap = [
|
||||
'pages' => [
|
||||
$pageId => [
|
||||
'pid' => -(int)$this->lastChild((int)$parent['uid'], (int)$parent['uid']),
|
||||
'title' => $def['title'],
|
||||
'slug' => $def['slug'],
|
||||
'doktype' => (int)$template['doktype'],
|
||||
'backend_layout' => (string)$template['backend_layout'],
|
||||
'backend_layout_next_level' => (string)$template['backend_layout_next_level'],
|
||||
'no_index' => (int)$template['no_index'],
|
||||
'no_follow' => (int)$template['no_follow'],
|
||||
'nav_hide' => (int)$template['nav_hide'],
|
||||
'no_search' => (int)$template['no_search'],
|
||||
'hidden' => 0,
|
||||
],
|
||||
],
|
||||
'tt_content' => [
|
||||
'NEWce' . $def['scope'] => [
|
||||
'pid' => $pageId,
|
||||
'colPos' => 0,
|
||||
'sys_language_uid' => 0,
|
||||
'CType' => 'vitec_pagecheck',
|
||||
'header' => $def['header'],
|
||||
'pi_flexform' => ['data' => ['sDEF' => ['lDEF' => ['settings.scope' => ['vDEF' => $def['scope']]]]]],
|
||||
],
|
||||
],
|
||||
];
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($datamap, []);
|
||||
$dataHandler->process_datamap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
$output->writeln(' <error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
foreach ($dataHandler->substNEWwithIDs as $new => $uid) {
|
||||
$output->writeln(sprintf(' written: %s -> #%d', $new, $uid));
|
||||
}
|
||||
}
|
||||
$output->writeln(sprintf('%s: %d page(s).', $apply ? 'DONE' : 'DRY RUN - would create', $created));
|
||||
if ($apply && $created > 0) {
|
||||
$output->writeln('Afterwards: vendor/bin/typo3 cache:flush');
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** uid of the last live child page (to append after it), or the parent itself when there is none */
|
||||
private function lastChild(int $parentUid, int $fallback): int
|
||||
{
|
||||
$uid = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages')->fetchOne(
|
||||
'SELECT uid FROM pages WHERE deleted = 0 AND sys_language_uid = 0 AND pid = ? ORDER BY sorting DESC LIMIT 1',
|
||||
[$parentUid]
|
||||
);
|
||||
return $uid === false ? $fallback : (int)$uid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Service;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
|
||||
/**
|
||||
* Page content audit for the market and solution trees (sibling of
|
||||
* ProductCheckService, decision Harry 2026-11-13): which market / sub-market /
|
||||
* solution-group / sub-solution PAGE carries which content, and the full
|
||||
* content of one page for the detail pane.
|
||||
*
|
||||
* Unlike products these are pages built from content elements, so the audit
|
||||
* looks at the elements in page order (hero, intro, cards, testimonials,
|
||||
* products, insights, logos, CTA banner), at the page properties (meta
|
||||
* description, OG image, page media) and at the market / solution record that
|
||||
* points to the page via detail_page.
|
||||
*
|
||||
* Scopes: 'markets' = children of /markets (11 markets, 24 sub-markets)
|
||||
* 'solutions' = children of /solutions (8 groups, 39 sub-solutions)
|
||||
*
|
||||
* Columns carry an `applies` list - a column a page type does not have (e.g.
|
||||
* logos on a sub-market, insights on a main market) is delivered as
|
||||
* {ok: null, count: 0} and counted neither as filled nor as applicable.
|
||||
*
|
||||
* Read fresh on every call, nothing cached (USER_INT in the renderer).
|
||||
*/
|
||||
final class PageCheckService
|
||||
{
|
||||
public const SCOPES = [
|
||||
'markets' => [
|
||||
'label' => 'Markets',
|
||||
'rootSlug' => '/markets',
|
||||
'types' => ['market', 'submarket'],
|
||||
'recordTable' => 'tx_vitec_domain_model_market',
|
||||
],
|
||||
'solutions' => [
|
||||
'label' => 'Solutions',
|
||||
'rootSlug' => '/solutions',
|
||||
'types' => ['solution', 'subsolution'],
|
||||
'recordTable' => 'tx_vitec_domain_model_solution',
|
||||
],
|
||||
];
|
||||
|
||||
private const ALL = ['market', 'submarket', 'solution', 'subsolution'];
|
||||
|
||||
/**
|
||||
* Columns in display order. kind as in the product check (images / video /
|
||||
* text / features / list), applies = page types that have the column.
|
||||
*/
|
||||
private const FIELDS = [
|
||||
['key' => 'hero', 'label' => 'Hero image', 'kind' => 'images', 'applies' => self::ALL],
|
||||
['key' => 'subline', 'label' => 'Hero subline', 'kind' => 'text', 'applies' => self::ALL],
|
||||
['key' => 'lead', 'label' => 'Lead', 'kind' => 'text', 'applies' => ['solution', 'subsolution']],
|
||||
['key' => 'intro', 'label' => 'Intro text', 'kind' => 'text', 'applies' => self::ALL],
|
||||
['key' => 'introImage', 'label' => 'Intro image', 'kind' => 'images', 'applies' => self::ALL],
|
||||
['key' => 'introCta', 'label' => 'Intro CTA', 'kind' => 'text', 'applies' => self::ALL],
|
||||
['key' => 'cards', 'label' => 'Cards', 'kind' => 'features', 'applies' => self::ALL],
|
||||
['key' => 'stories', 'label' => 'Testimonials', 'kind' => 'list', 'applies' => self::ALL],
|
||||
['key' => 'products', 'label' => 'Products', 'kind' => 'list', 'applies' => ['submarket', 'subsolution']],
|
||||
['key' => 'insights', 'label' => 'Insights text', 'kind' => 'text', 'applies' => ['submarket', 'solution', 'subsolution']],
|
||||
['key' => 'insightsCover', 'label' => 'Insights cover', 'kind' => 'images', 'applies' => ['submarket', 'solution', 'subsolution']],
|
||||
['key' => 'insightsCta', 'label' => 'Insights CTA', 'kind' => 'text', 'applies' => ['submarket', 'solution', 'subsolution']],
|
||||
['key' => 'logos', 'label' => 'Logos', 'kind' => 'list', 'applies' => ['market']],
|
||||
['key' => 'cta', 'label' => 'CTA banner', 'kind' => 'text', 'applies' => ['market', 'solution']],
|
||||
['key' => 'seo', 'label' => 'Meta description', 'kind' => 'text', 'applies' => self::ALL],
|
||||
['key' => 'og', 'label' => 'OG image', 'kind' => 'images', 'applies' => self::ALL],
|
||||
['key' => 'media', 'label' => 'Page media', 'kind' => 'images', 'applies' => self::ALL],
|
||||
['key' => 'record', 'label' => 'Record', 'kind' => 'list', 'applies' => self::ALL],
|
||||
];
|
||||
|
||||
private const HERO_COLPOS = [1, 110];
|
||||
private const CONTAINER_PREFIXES = ['vitec_cols_', 'vitec_container', 'vitec_cards_carousel'];
|
||||
|
||||
/** Text fields per element type for the detail pane: column => label. */
|
||||
private const ELEMENT_TEXTS = [
|
||||
'vitec_markethero' => ['vitec_subline' => 'Subline', 'vitec_cta_label' => 'CTA label', 'vitec_video_title' => 'Video title'],
|
||||
'vitec_herosection' => ['header' => 'Header', 'subheader' => 'Subheader', 'bodytext' => 'Text', 'vitec_cta_label' => 'CTA label'],
|
||||
'vitec_introparagraph' => ['header' => 'Header', 'subheader' => 'Subheader', 'bodytext' => 'Text', 'vitec_button_text' => 'Button'],
|
||||
'vitec_marketintro' => ['vitec_intro_headline' => 'Headline', 'vitec_intro_text' => 'Text', 'vitec_learnmore_label' => 'Button'],
|
||||
'vitec_card' => ['vitec_eyebrow' => 'Eyebrow', 'header' => 'Header', 'subheader' => 'Subheader', 'bodytext' => 'Text', 'vitec_back_headline' => 'Back headline', 'vitec_back_text' => 'Back text', 'vitec_primary_cta_label' => 'Button'],
|
||||
'vitec_usecaselist' => ['header' => 'Header'],
|
||||
'vitec_customerlogos' => ['header' => 'Header'],
|
||||
'vitec_marketinsights' => ['vitec_text1' => 'Text 1', 'vitec_text2' => 'Text 2', 'vitec_cta_label' => 'CTA label', 'vitec_video_person' => 'Video person', 'vitec_video_role' => 'Video role', 'vitec_video_title' => 'Video title'],
|
||||
'vitec_ctabanner' => ['header' => 'Header', 'bodytext' => 'Text', 'vitec_primary_cta_label' => 'Primary button', 'vitec_secondary_cta_label' => 'Secondary button'],
|
||||
'vitec_productfeature' => ['header' => 'Header'],
|
||||
'vitec_productlist' => ['header' => 'Header'],
|
||||
];
|
||||
|
||||
/** Link fields per element type: column => label. */
|
||||
private const ELEMENT_LINKS = [
|
||||
'vitec_markethero' => ['vitec_cta_link' => 'CTA link', 'vitec_video_url' => 'Video URL'],
|
||||
'vitec_herosection' => ['vitec_cta' => 'CTA link'],
|
||||
'vitec_introparagraph' => ['vitec_button_link' => 'Button link'],
|
||||
'vitec_marketintro' => ['vitec_learnmore_target' => 'Button target'],
|
||||
'vitec_card' => ['vitec_primary_cta' => 'Button link', 'vitec_card_link' => 'Card link'],
|
||||
'vitec_marketinsights' => ['vitec_cta_target' => 'CTA target', 'vitec_video_url' => 'Video URL'],
|
||||
'vitec_ctabanner' => ['vitec_primary_cta' => 'Primary link', 'vitec_secondary_cta' => 'Secondary link'],
|
||||
];
|
||||
|
||||
private const ELEMENT_LABELS = [
|
||||
'vitec_markethero' => 'Hero',
|
||||
'vitec_herosection' => 'Hero section',
|
||||
'vitec_introparagraph' => 'Intro paragraph',
|
||||
'vitec_marketintro' => 'Intro (text | image)',
|
||||
'vitec_card' => 'Card',
|
||||
'vitec_usecaselist' => 'Testimonials',
|
||||
'vitec_customerlogos' => 'Customer logos',
|
||||
'vitec_marketinsights' => 'Insights',
|
||||
'vitec_ctabanner' => 'CTA banner',
|
||||
'vitec_productfeature' => 'Featured product',
|
||||
'vitec_productlist' => 'Product list',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function overview(string $scope): array
|
||||
{
|
||||
$cfg = self::SCOPES[$scope] ?? throw new \InvalidArgumentException('Unknown scope ' . $scope);
|
||||
$pages = $this->pages($cfg);
|
||||
$pageUids = array_map(static fn(array $p): int => (int)$p['uid'], $pages);
|
||||
$elementsByPage = $this->elementsByPage($pageUids);
|
||||
$allElementUids = [];
|
||||
foreach ($elementsByPage as $els) {
|
||||
foreach ($els as $el) {
|
||||
$allElementUids[] = (int)$el['uid'];
|
||||
}
|
||||
}
|
||||
$elementFiles = $this->referenceCounts('tt_content', $allElementUids);
|
||||
$pageFiles = $this->referenceCounts('pages', $pageUids);
|
||||
$records = $this->records($cfg['recordTable'], $pageUids);
|
||||
$recordFiles = $this->referenceCounts($cfg['recordTable'], array_map(static fn(array $r): int => (int)$r['uid'], $records));
|
||||
// Empty selection = what the testimonial band shows: quoted stories,
|
||||
// at most twelve (UsecaseListJsonRenderer::testimonialRows).
|
||||
$allStories = min(12, $this->countAll('tx_vitec_domain_model_usecase', "deleted = 0 AND hidden = 0 AND hideonwebsite = 0 AND TRIM(COALESCE(quote, '')) <> ''"));
|
||||
$allCustomers = $this->countAll('tx_vitec_domain_model_customer', 'deleted = 0 AND hidden = 0');
|
||||
|
||||
$keys = array_column(self::FIELDS, 'key');
|
||||
$filled = array_fill_keys($keys, 0);
|
||||
$applicable = array_fill_keys($keys, 0);
|
||||
$byType = [];
|
||||
$rows = [];
|
||||
foreach ($pages as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
$type = (string)$p['_type'];
|
||||
$byType[$type] = ($byType[$type] ?? 0) + 1;
|
||||
$els = $elementsByPage[$uid] ?? [];
|
||||
$visible = array_values(array_filter($els, static fn(array $e): bool => (int)$e['hidden'] === 0));
|
||||
$hiddenCount = count($els) - count($visible);
|
||||
$record = $records[$uid] ?? null;
|
||||
$raw = $this->pageChecks($type, $visible, $elementFiles, $pageFiles[$uid] ?? [], $p, $record, $record ? ($recordFiles[(int)$record['uid']] ?? []) : [], $allStories, $allCustomers);
|
||||
|
||||
$checks = [];
|
||||
foreach (self::FIELDS as $field) {
|
||||
$key = $field['key'];
|
||||
if (!in_array($type, $field['applies'], true)) {
|
||||
$checks[$key] = ['ok' => null, 'count' => 0];
|
||||
continue;
|
||||
}
|
||||
$checks[$key] = $raw[$key];
|
||||
$applicable[$key]++;
|
||||
if ($raw[$key]['ok'] === true) {
|
||||
$filled[$key]++;
|
||||
}
|
||||
}
|
||||
$rows[] = $this->base($p, $type, $record, $hiddenCount) + ['checks' => $checks];
|
||||
}
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'scopeLabel' => $cfg['label'],
|
||||
'generatedAt' => date(\DATE_ATOM),
|
||||
'fields' => self::FIELDS,
|
||||
'summary' => ['pages' => count($rows), 'byType' => $byType, 'filled' => $filled, 'applicable' => $applicable],
|
||||
'pages' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null null when the page is not part of any scope */
|
||||
public function detail(int $uid): ?array
|
||||
{
|
||||
foreach (self::SCOPES as $scope => $cfg) {
|
||||
foreach ($this->pages($cfg) as $p) {
|
||||
if ((int)$p['uid'] !== $uid) {
|
||||
continue;
|
||||
}
|
||||
$type = (string)$p['_type'];
|
||||
$records = $this->records($cfg['recordTable'], [$uid]);
|
||||
$record = $records[$uid] ?? null;
|
||||
$els = $this->elementsByPage([$uid])[$uid] ?? [];
|
||||
$hiddenCount = count(array_filter($els, static fn(array $e): bool => (int)$e['hidden'] === 1));
|
||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||
|
||||
$elements = [];
|
||||
foreach ($els as $el) {
|
||||
$cType = (string)$el['CType'];
|
||||
$texts = [];
|
||||
foreach (self::ELEMENT_TEXTS[$cType] ?? ['header' => 'Header', 'bodytext' => 'Text'] as $column => $label) {
|
||||
$value = (string)($el[$column] ?? '');
|
||||
$texts[] = ['key' => $column, 'label' => $label, 'ok' => $this->filled($value), 'html' => $value];
|
||||
}
|
||||
$links = [];
|
||||
foreach (self::ELEMENT_LINKS[$cType] ?? [] as $column => $label) {
|
||||
$value = trim((string)($el[$column] ?? ''));
|
||||
if ($value !== '') {
|
||||
$links[] = ['key' => $column, 'label' => $label, 'target' => $value];
|
||||
}
|
||||
}
|
||||
$settings = [];
|
||||
if (trim((string)($el['pi_flexform'] ?? '')) !== '') {
|
||||
$settings = $flexFormService->convertFlexFormContentToArray((string)$el['pi_flexform'])['settings'] ?? [];
|
||||
$settings = array_filter($settings, static fn($v): bool => is_scalar($v) && (string)$v !== '' && (string)$v !== '0');
|
||||
$settings = array_map(static fn($v): string => (string)$v, $settings);
|
||||
}
|
||||
$elements[] = [
|
||||
'uid' => (int)$el['uid'],
|
||||
'type' => $cType,
|
||||
'label' => self::ELEMENT_LABELS[$cType] ?? $cType,
|
||||
'colPos' => (int)$el['colPos'],
|
||||
'slot' => in_array((int)$el['colPos'], self::HERO_COLPOS, true) ? 'hero' : 'main',
|
||||
'hidden' => (int)$el['hidden'] === 1,
|
||||
'header' => (string)($el['header'] ?? ''),
|
||||
'layoutVariant' => (string)($el['vitec_layout_variant'] ?? ''),
|
||||
'texts' => $texts,
|
||||
'links' => $links,
|
||||
'images' => $this->files('tt_content', (int)$el['uid']),
|
||||
'settings' => $settings,
|
||||
];
|
||||
}
|
||||
|
||||
$children = [];
|
||||
if ((int)$p['_level'] === 1) {
|
||||
foreach ($this->pages($cfg) as $c) {
|
||||
if ((int)$c['pid'] === $uid) {
|
||||
$children[] = ['uid' => (int)$c['uid'], 'title' => (string)$c['title'], 'url' => (string)$c['slug'], 'hidden' => (int)$c['hidden'] === 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->base($p, $type, $record, $hiddenCount) + [
|
||||
'seo' => [
|
||||
'title' => (string)$p['title'],
|
||||
'seoTitle' => (string)($p['seo_title'] ?? ''),
|
||||
'description' => (string)($p['description'] ?? ''),
|
||||
'og' => $this->files('pages', $uid, ['og_image']),
|
||||
],
|
||||
'media' => $this->files('pages', $uid, ['media']),
|
||||
'record' => $record === null ? null : [
|
||||
'uid' => (int)$record['uid'],
|
||||
'title' => (string)$record['title'],
|
||||
'subtitle' => (string)($record['subtitle'] ?? ''),
|
||||
'teaser' => (string)($record['teaser'] ?? ''),
|
||||
'description' => (string)($record['description'] ?? ''),
|
||||
'image' => $this->files($cfg['recordTable'], (int)$record['uid'], ['image']),
|
||||
],
|
||||
'elements' => $elements,
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- checks -------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $visible visible elements in page order
|
||||
* @param array<int, array<string, int>> $elementFiles [elementUid][fieldname] = count
|
||||
* @param array<string, int> $pageFiles [fieldname] = count
|
||||
* @param array<string, mixed> $page
|
||||
* @param array<string, mixed>|null $record
|
||||
* @param array<string, int> $recordFiles
|
||||
* @return array<string, array{ok: bool, count: int, icons?: int}>
|
||||
*/
|
||||
private function pageChecks(string $type, array $visible, array $elementFiles, array $pageFiles, array $page, ?array $record, array $recordFiles, int $allStories, int $allCustomers): array
|
||||
{
|
||||
$hero = $this->firstOf($visible, 'vitec_markethero') ?? $this->firstOf($visible, 'vitec_herosection');
|
||||
$heroFiles = $hero ? ($elementFiles[(int)$hero['uid']] ?? []) : [];
|
||||
$lead = null;
|
||||
foreach ($visible as $el) {
|
||||
if ($el['CType'] === 'vitec_introparagraph' && in_array((int)$el['colPos'], self::HERO_COLPOS, true)) {
|
||||
$lead = $el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$intro = $this->firstOf($visible, 'vitec_marketintro');
|
||||
$introFiles = $intro ? ($elementFiles[(int)$intro['uid']] ?? []) : [];
|
||||
|
||||
$cards = array_values(array_filter($visible, static fn(array $e): bool => $e['CType'] === 'vitec_card'));
|
||||
$cardVisuals = 0;
|
||||
foreach ($cards as $c) {
|
||||
$f = $elementFiles[(int)$c['uid']] ?? [];
|
||||
if (($f['vitec_icon'] ?? 0) > 0 || ($f['vitec_image'] ?? 0) > 0) {
|
||||
$cardVisuals++;
|
||||
}
|
||||
}
|
||||
|
||||
$stories = $this->firstOf($visible, 'vitec_usecaselist');
|
||||
$storyCount = 0;
|
||||
if ($stories) {
|
||||
$sel = $this->flexSettings((string)$stories['pi_flexform']);
|
||||
$selected = GeneralUtility::intExplode(',', (string)($sel['usecases'] ?? ''), true);
|
||||
$storyCount = $selected === [] ? $allStories : count($selected);
|
||||
}
|
||||
|
||||
$productCount = 0;
|
||||
$hasProducts = false;
|
||||
foreach ($visible as $el) {
|
||||
if ($el['CType'] === 'vitec_productfeature') {
|
||||
$hasProducts = true;
|
||||
$productCount += (int)(($this->flexSettings((string)$el['pi_flexform'])['product'] ?? 0) > 0);
|
||||
} elseif ($el['CType'] === 'vitec_productlist') {
|
||||
$hasProducts = true;
|
||||
$sel = $this->flexSettings((string)$el['pi_flexform']);
|
||||
$productCount += count(GeneralUtility::intExplode(',', (string)($sel['selectedproducts'] ?? ''), true));
|
||||
}
|
||||
}
|
||||
|
||||
$insights = $this->firstOf($visible, 'vitec_marketinsights');
|
||||
$insightsFiles = $insights ? ($elementFiles[(int)$insights['uid']] ?? []) : [];
|
||||
|
||||
$logos = $this->firstOf($visible, 'vitec_customerlogos');
|
||||
$logoCount = 0;
|
||||
if ($logos) {
|
||||
$sel = $this->flexSettings((string)$logos['pi_flexform']);
|
||||
$selected = GeneralUtility::intExplode(',', (string)($sel['customers'] ?? ''), true);
|
||||
$logoCount = $selected === [] || ($sel['onlySelected'] ?? '0') !== '1' ? $allCustomers : count($selected);
|
||||
}
|
||||
|
||||
$cta = $this->firstOf($visible, 'vitec_ctabanner');
|
||||
|
||||
$recordFilled = 0;
|
||||
if ($record !== null) {
|
||||
$recordFilled += (($recordFiles['image'] ?? 0) > 0) ? 1 : 0;
|
||||
$recordFilled += $this->filled($record['teaser'] ?? '') ? 1 : 0;
|
||||
$recordFilled += $this->filled($record['description'] ?? '') ? 1 : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'hero' => $this->check(($heroFiles['vitec_bgimage'] ?? 0) + ($heroFiles['vitec_hero_bgimage'] ?? 0) + ($heroFiles['vitec_hero_image'] ?? 0)),
|
||||
'subline' => $this->check($hero && ($this->filled($hero['vitec_subline'] ?? '') || $this->filled($hero['subheader'] ?? '')) ? 1 : 0),
|
||||
'lead' => $this->check($lead && $this->filled($lead['bodytext'] ?? '') ? 1 : 0),
|
||||
'intro' => $this->check($intro && $this->filled($intro['vitec_intro_text'] ?? '') ? 1 : 0),
|
||||
'introImage' => $this->check($introFiles['vitec_image'] ?? 0),
|
||||
'introCta' => $this->check($intro && $this->filled($intro['vitec_learnmore_label'] ?? '') && $this->filled($intro['vitec_learnmore_target'] ?? '') ? 1 : 0),
|
||||
'cards' => $this->check(count($cards)) + ['icons' => $cardVisuals],
|
||||
'stories' => ['ok' => $stories !== null, 'count' => $storyCount],
|
||||
'products' => ['ok' => $hasProducts, 'count' => $productCount],
|
||||
'insights' => $this->check($insights && $this->filled($insights['vitec_text1'] ?? '') ? 1 : 0),
|
||||
'insightsCover' => $this->check($insightsFiles['vitec_video_cover'] ?? 0),
|
||||
'insightsCta' => $this->check($insights && $this->filled($insights['vitec_cta_label'] ?? '') && $this->filled($insights['vitec_cta_target'] ?? '') ? 1 : 0),
|
||||
'logos' => ['ok' => $logos !== null, 'count' => $logoCount],
|
||||
'cta' => $this->check($cta && $this->filled($cta['vitec_primary_cta_label'] ?? '') ? 1 : 0),
|
||||
'seo' => $this->check($this->filled($page['description'] ?? '') ? 1 : 0),
|
||||
'og' => $this->check($pageFiles['og_image'] ?? 0),
|
||||
'media' => $this->check($pageFiles['media'] ?? 0),
|
||||
'record' => ['ok' => $record !== null && ($recordFiles['image'] ?? 0) > 0 && $this->filled($record['teaser'] ?? ''), 'count' => $recordFilled],
|
||||
];
|
||||
}
|
||||
|
||||
// ---- building blocks ----------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $p
|
||||
* @param array<string, mixed>|null $record
|
||||
*/
|
||||
private function base(array $p, string $type, ?array $record, int $hiddenElements): array
|
||||
{
|
||||
$flags = [];
|
||||
if ((int)$p['hidden'] === 1) {
|
||||
$flags[] = 'hidden';
|
||||
}
|
||||
if ((int)$p['nav_hide'] === 1) {
|
||||
$flags[] = 'navHide';
|
||||
}
|
||||
if ((int)$p['no_index'] === 1) {
|
||||
$flags[] = 'noindex';
|
||||
}
|
||||
if ($record === null) {
|
||||
$flags[] = 'noRecord';
|
||||
}
|
||||
if ($hiddenElements > 0) {
|
||||
$flags[] = 'hiddenElements:' . $hiddenElements;
|
||||
}
|
||||
return [
|
||||
'uid' => (int)$p['uid'],
|
||||
'title' => (string)$p['title'],
|
||||
'slug' => trim((string)$p['slug'], '/'),
|
||||
'url' => (string)$p['slug'],
|
||||
'type' => $type,
|
||||
'level' => (int)$p['_level'],
|
||||
'parent' => (int)$p['pid'],
|
||||
'flags' => $flags,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, count: int} */
|
||||
private function check(int $count): array
|
||||
{
|
||||
return ['ok' => $count > 0, 'count' => $count];
|
||||
}
|
||||
|
||||
private function filled(mixed $value): bool
|
||||
{
|
||||
return trim(strip_tags((string)$value)) !== '';
|
||||
}
|
||||
|
||||
/** @param array<int, array<string, mixed>> $elements */
|
||||
private function firstOf(array $elements, string $cType): ?array
|
||||
{
|
||||
foreach ($elements as $el) {
|
||||
if ($el['CType'] === $cType) {
|
||||
return $el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private function flexSettings(string $xml): array
|
||||
{
|
||||
if (trim($xml) === '') {
|
||||
return [];
|
||||
}
|
||||
$settings = GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($xml)['settings'] ?? [];
|
||||
return is_array($settings) ? array_map(static fn($v): string => is_scalar($v) ? (string)$v : '', $settings) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope's pages: level 1 = children of the root page, level 2 = their
|
||||
* children. Standard pages only, in tree order; a page carrying the check
|
||||
* element itself is left out.
|
||||
*
|
||||
* @param array{rootSlug: string, types: array<int, string>} $cfg
|
||||
* @return array<int, array<string, mixed>> rows with _level and _type
|
||||
*/
|
||||
private function pages(array $cfg): array
|
||||
{
|
||||
$conn = $this->connection('pages');
|
||||
$root = $conn->fetchAssociative('SELECT uid FROM pages WHERE deleted = 0 AND sys_language_uid = 0 AND slug = ?', [$cfg['rootSlug']]);
|
||||
if ($root === false) {
|
||||
return [];
|
||||
}
|
||||
$select = 'SELECT uid, pid, title, slug, hidden, nav_hide, no_index, description, seo_title, sorting FROM pages
|
||||
WHERE deleted = 0 AND sys_language_uid = 0 AND doktype = 1 AND pid IN (%s) ORDER BY pid, sorting';
|
||||
$level1 = $conn->fetchAllAssociative(sprintf($select, (int)$root['uid']));
|
||||
$checkPages = $this->pagesWithCheckElement();
|
||||
$out = [];
|
||||
foreach ($level1 as $p1) {
|
||||
if (isset($checkPages[(int)$p1['uid']])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $p1 + ['_level' => 1, '_type' => $cfg['types'][0]];
|
||||
$level2 = $conn->fetchAllAssociative(sprintf($select, (int)$p1['uid']));
|
||||
foreach ($level2 as $p2) {
|
||||
if (isset($checkPages[(int)$p2['uid']])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $p2 + ['_level' => 2, '_type' => $cfg['types'][1]];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @return array<int, true> page uid => true for pages that hold a check element */
|
||||
private function pagesWithCheckElement(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($this->connection('tt_content')->fetchFirstColumn(
|
||||
"SELECT DISTINCT pid FROM tt_content WHERE deleted = 0 AND CType IN ('vitec_pagecheck', 'vitec_productcheck')"
|
||||
) as $pid) {
|
||||
$out[(int)$pid] = true;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* All non-deleted default-language elements of the pages, page order,
|
||||
* containers themselves left out (their children count as page content).
|
||||
*
|
||||
* @param int[] $pageUids
|
||||
* @return array<int, array<int, array<string, mixed>>> [pageUid] = rows
|
||||
*/
|
||||
private function elementsByPage(array $pageUids): array
|
||||
{
|
||||
if ($pageUids === []) {
|
||||
return [];
|
||||
}
|
||||
$rows = $this->connection('tt_content')->fetchAllAssociative(
|
||||
'SELECT * FROM tt_content WHERE deleted = 0 AND sys_language_uid = 0 AND pid IN ('
|
||||
. implode(',', array_map('intval', $pageUids)) . ') ORDER BY pid, colPos, sorting, uid'
|
||||
);
|
||||
// page order: hero slot first, then the main column(s) by colPos and sorting
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
$heroA = in_array((int)$a['colPos'], self::HERO_COLPOS, true) ? 0 : 1;
|
||||
$heroB = in_array((int)$b['colPos'], self::HERO_COLPOS, true) ? 0 : 1;
|
||||
return [(int)$a['pid'], $heroA, (int)$a['colPos'], (int)$a['sorting'], (int)$a['uid']]
|
||||
<=> [(int)$b['pid'], $heroB, (int)$b['colPos'], (int)$b['sorting'], (int)$b['uid']];
|
||||
});
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$cType = (string)$row['CType'];
|
||||
foreach (self::CONTAINER_PREFIXES as $prefix) {
|
||||
if (str_starts_with($cType, $prefix)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$out[(int)$row['pid']][] = $row;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible file references per record and field.
|
||||
*
|
||||
* @param int[] $uids
|
||||
* @return array<int, array<string, int>> [uid][fieldname] = count
|
||||
*/
|
||||
private function referenceCounts(string $table, array $uids): array
|
||||
{
|
||||
if ($uids === []) {
|
||||
return [];
|
||||
}
|
||||
$rows = $this->connection('sys_file_reference')->fetchAllAssociative(
|
||||
'SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
|
||||
WHERE deleted = 0 AND hidden = 0 AND tablenames = ? AND uid_foreign IN (' . implode(',', array_map('intval', $uids)) . ')
|
||||
GROUP BY uid_foreign, fieldname',
|
||||
[$table]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files of one record with original URL and a ~480 px thumbnail.
|
||||
*
|
||||
* @param string[]|null $fieldnames null = all fields
|
||||
* @return array<int, array{field: string, url: string, thumb: string, name: string, mime: string}>
|
||||
*/
|
||||
private function files(string $table, int $uid, ?array $fieldnames = null): array
|
||||
{
|
||||
$sql = "SELECT r.fieldname, r.uid_local, f.identifier, f.name, f.mime_type
|
||||
FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = ? AND r.uid_foreign = ?";
|
||||
$params = [$table, $uid];
|
||||
if ($fieldnames !== null) {
|
||||
$sql .= ' AND r.fieldname IN (' . implode(',', array_fill(0, count($fieldnames), '?')) . ')';
|
||||
$params = array_merge($params, $fieldnames);
|
||||
}
|
||||
$sql .= ' ORDER BY r.fieldname, r.sorting_foreign';
|
||||
$out = [];
|
||||
foreach ($this->connection('sys_file_reference')->fetchAllAssociative($sql, $params) as $r) {
|
||||
$url = '/fileadmin' . $r['identifier'];
|
||||
$out[] = [
|
||||
'field' => (string)preg_replace('/^vitec_/', '', (string)$r['fieldname']),
|
||||
'url' => $url,
|
||||
'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url),
|
||||
'name' => (string)$r['name'],
|
||||
'mime' => (string)$r['mime_type'],
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function thumbnail(int $fileUid, string $mime, string $fallback): string
|
||||
{
|
||||
if (!str_starts_with($mime, 'image/') || $mime === 'image/svg+xml') {
|
||||
return $fallback;
|
||||
}
|
||||
try {
|
||||
$file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileUid);
|
||||
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||
$processed = $imageService->applyProcessingInstructions($file, ['maxWidth' => 480, 'maxHeight' => 480]);
|
||||
return $imageService->getImageUri($processed);
|
||||
} catch (\Throwable) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records pointing to the pages via detail_page (first live record per page).
|
||||
*
|
||||
* @param int[] $pageUids
|
||||
* @return array<int, array<string, mixed>> [pageUid] = record row
|
||||
*/
|
||||
private function records(string $table, array $pageUids): array
|
||||
{
|
||||
if ($pageUids === []) {
|
||||
return [];
|
||||
}
|
||||
$rows = $this->connection($table)->fetchAllAssociative(
|
||||
'SELECT uid, title, subtitle, teaser, description, detail_page FROM ' . $table . '
|
||||
WHERE deleted = 0 AND detail_page IN (' . implode(',', array_map('intval', $pageUids)) . ') ORDER BY hidden, uid'
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[(int)$r['detail_page']] ??= $r;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function countAll(string $table, string $where): int
|
||||
{
|
||||
return (int)$this->connection($table)->fetchOne("SELECT COUNT(*) FROM $table WHERE $where");
|
||||
}
|
||||
|
||||
private function connection(string $table): Connection
|
||||
{
|
||||
return $this->connectionPool->getConnectionForTable($table);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Evomedien\Vitec\Service\PageCheckService;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* JSON for the content element `vitec_pagecheck` (pages /markets/market-check
|
||||
* and /solutions/solution-check): the page content audit of one scope,
|
||||
* rendered by the React front end like the product check.
|
||||
*
|
||||
* Emitted under content.pagecheck:
|
||||
*
|
||||
* without query {mode: "overview", scope, scopeLabel, generatedAt, fields[], summary, pages[]}
|
||||
* ?check=<pageUid> {mode: "detail", page: {…} | null}
|
||||
* on failure {mode: "error", error}
|
||||
*
|
||||
* The scope (markets | solutions) comes from the element's FlexForm
|
||||
* (settings.scope). Always USER_INT - the audit shows the database as it is
|
||||
* right now; `check` is excluded from the cHash (ext_localconf.php, shared
|
||||
* with the product check). The front end loads the detail variant
|
||||
* client-side from /_json, one request per opened row.
|
||||
*/
|
||||
final class PageCheckJsonRenderer
|
||||
{
|
||||
protected ?ContentObjectRenderer $cObj = null;
|
||||
|
||||
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
|
||||
{
|
||||
$this->cObj = $cObj;
|
||||
}
|
||||
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
if ($this->cObj !== null && $this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
|
||||
$this->cObj->convertToUserIntObject();
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
$scope = 'markets';
|
||||
$flex = (string)($this->cObj?->data['pi_flexform'] ?? '');
|
||||
if (trim($flex) !== '') {
|
||||
$settings = GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($flex)['settings'] ?? [];
|
||||
if (isset(PageCheckService::SCOPES[(string)($settings['scope'] ?? '')])) {
|
||||
$scope = (string)$settings['scope'];
|
||||
}
|
||||
}
|
||||
$service = GeneralUtility::makeInstance(PageCheckService::class);
|
||||
$queryParams = $GLOBALS['TYPO3_REQUEST']?->getQueryParams() ?? [];
|
||||
$check = (int)($queryParams['check'] ?? 0);
|
||||
if ($check > 0) {
|
||||
$payload = ['mode' => 'detail', 'scope' => $scope, 'page' => $service->detail($check)];
|
||||
} else {
|
||||
$payload = ['mode' => 'overview'] + $service->overview($scope);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$payload = ['mode' => 'error', 'error' => $e->getMessage()];
|
||||
}
|
||||
return (string)json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Page Content Check</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.scope>
|
||||
<label>Page tree to audit</label>
|
||||
<description>Markets = the 11 markets and their sub-markets below /markets. Solutions = the 8 solution groups and their sub-solutions below /solutions.</description>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>Markets and sub-markets</label>
|
||||
<value>markets</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Solutions and sub-solutions</label>
|
||||
<value>solutions</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>markets</default>
|
||||
</config>
|
||||
</settings.scope>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
@@ -32,6 +32,8 @@ services:
|
||||
# ProductCheckJsonRenderer (TypoScript userFunc, no DI there).
|
||||
Evomedien\Vitec\Service\ProductCheckService:
|
||||
public: true
|
||||
Evomedien\Vitec\Service\PageCheckService:
|
||||
public: true
|
||||
|
||||
# SSR dashboard tiles (the WidgetConfigurationInterface argument is
|
||||
# injected by the dashboard compiler pass via the tag).
|
||||
|
||||
@@ -114,6 +114,21 @@ tt_content {
|
||||
}
|
||||
}
|
||||
|
||||
# Page Content Check - the market / solution page audit as data (pages
|
||||
# /markets/market-check, /solutions/solution-check). Always USER_INT, see PageCheckJsonRenderer.
|
||||
vitec_pagecheck < lib.contentElementWithHeader
|
||||
vitec_pagecheck {
|
||||
fields {
|
||||
content {
|
||||
fields {
|
||||
pagecheck = USER
|
||||
pagecheck.userFunc = Evomedien\Vitec\UserFunc\PageCheckJsonRenderer->render
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# EXT:solr search results as JSON - the search endpoint for the React frontend
|
||||
solr_pi_results < lib.contentElementWithHeader
|
||||
solr_pi_results {
|
||||
|
||||
@@ -37,6 +37,10 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||
'plugins',
|
||||
'Live audit of every product record: one row per product, one check per content field, details per row. Place once on /products/product-check.'
|
||||
);
|
||||
// Page content audit for the market and solution trees (2026-11-13):
|
||||
// scope (markets | solutions) via FlexForm, data from PageCheckService.
|
||||
$registerPluginWithFlexForm('Pagecheck', 'Page Content Check (markets / solutions audit)', 'FILE:EXT:vitec/Configuration/FlexForms/Pagecheck.xml', 'vitec-plugin-productlist');
|
||||
|
||||
$registerPluginWithFlexForm('Simplecard', 'Simple Card', 'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml', 'vitec-plugin-simplecard');
|
||||
$registerPluginWithFlexForm('Productshow', 'Show Single Product', 'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml', 'vitec-plugin-productshow');
|
||||
$registerPluginWithFlexForm('Usecaseshow', 'Single Success Story', 'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml', 'vitec-plugin-usecaseshow');
|
||||
|
||||
@@ -40,6 +40,15 @@ mod {
|
||||
CType = vitec_productcheck
|
||||
}
|
||||
}
|
||||
pagecheck {
|
||||
iconIdentifier = vitec-plugin-productlist
|
||||
title = Page Content Check (markets / solutions audit)
|
||||
description = Live audit of the market or solution page tree - one row per page, one check per content slot. Internal pages /markets/market-check and /solutions/solution-check.
|
||||
tt_content_defValues {
|
||||
CType = vitec_pagecheck
|
||||
}
|
||||
}
|
||||
|
||||
productshow {
|
||||
iconIdentifier = vitec-plugin-productshow
|
||||
title = Show single VITEC Product
|
||||
|
||||
Reference in new issue
Block a user