Batch of 2026-10-28/29 plus 2026-11-03: - NEW vitec:import-datapath-stories: 22 story packages from the agency dump (docx + Links/ assets) as usecase records with content, quotes, images, customer logos, download cards; --repair-quotes normalizes attribution lines; asset backfill for existing stories - /products grid incident repaired (fix_products_grid, fix_graphics_grid, check_grid_page): containers unhidden, 46 duplicate cards soft-deleted, section order restored; BuildProductPortfolio and ImportProductCategoryPages now GUARD against re-runs on pages that already carry vitec_cols_% containers - ' double-encoding fixed: render-pipeline importers write literal apostrophes (ENT_NOQUOTES), fix_double_entities literalizes existing rows - ticket quick wins (ticket_quickwins_20261028), broadcast market page cleanup, AttachCardImages market-card handling, SubmarketWorkbook META REPAIR, story content/video fixes (fix_story_*) - ProvideProductCatalog: transitional v1 fields removed (categories uid/link, legacyGroupFields, item products teaser) - pure v2 shape (ticket 0024-FE); search renderer keeps its teaser - BuildProductOverviews: heroes get the numbered placeholder background (placeholders/pages/<uid>-01.png, wide grey fallback) - backfill on existing pages with empty hero_bgimage, attached at creation for new pages; existing references always win (ticket 0026-FE); 40 placeholder PNGs shipped to fileadmin (not tracked) - AttachPageMedia: --depth=3 with sysfolders as pass-through parents for the overview pages' -08 menu tiles - NEW migrations/fec_isolation_test.sh: two-user live test proving fe_comment ticket isolation (backend verified tight, cross-user sighting traced to the FE page-JSON cache, ticket 0027-BE) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
286 lines
17 KiB
PHP
286 lines
17 KiB
PHP
<?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;
|
|
|
|
/**
|
|
* Fills the /markets main page from the agency workbook "Markets Hero
|
|
* Page.xlsx" (Dropbox delivery 2026-10-23, status "Ready for review").
|
|
* One page, fixed copy - the content is embedded below instead of parsing
|
|
* the workbook at runtime (same idea as BackfillRedirectsCommand).
|
|
*
|
|
* The page already has the workbook's structure (template vitec__2):
|
|
* hero (vitec_herosection, colPos 1)
|
|
* markets intro 50/50 (vitec_introparagraph, colPos 0)
|
|
* overview container (vitec_container: header + vitec_marketlist)
|
|
* prefooter container (vitec_container: vitec_introparagraph)
|
|
* so this is a CONTENT update plus three structural touches:
|
|
* - the marketlist flexform selection moves from the old 9-record set
|
|
* (incl. the dropped "Security & Surveillance") to the 11 main-market
|
|
* records in workbook order
|
|
* - the old prefooter container is HIDDEN and a vitec_ctabanner with the
|
|
* two workbook CTAs (contact / solutions) takes its place - the same
|
|
* centered/orange prefooter the 11 market hero pages use
|
|
* - the 11 market card bodies land in the market RECORDS' teaser field
|
|
* (the marketlist cards render from the records)
|
|
* Page properties follow the established set (og/twitter/canonical/
|
|
* abstract/keywords).
|
|
*
|
|
* vendor/bin/typo3 vitec:import-markets-main
|
|
* vendor/bin/typo3 vitec:import-markets-main --apply
|
|
*/
|
|
#[AsCommand(
|
|
name: 'vitec:import-markets-main',
|
|
description: 'Fill the /markets main page from "Markets Hero Page.xlsx" (dry-run by default)'
|
|
)]
|
|
final class ImportMarketsMainPageCommand extends Command
|
|
{
|
|
private const HERO_H1 = 'Video solutions for every operational environment';
|
|
private const HERO_COPY = 'From passenger communications and live production to critical infrastructure and command centers, every environment depends on information reaching the right people at the right time. VITEC brings together secure video, visualization and communications technologies to help organizations maintain visibility, communicate clearly and operate with confidence.';
|
|
private const HERO_CTA = 'Explore our markets';
|
|
|
|
private const INTRO_H2 = 'Built around the way your industry operates';
|
|
private const INTRO_COPY = 'Different industries place different demands on video and visual information. Some need to engage audiences across thousands of screens. Others need operators to see critical data, coordinate teams and respond quickly. VITEC combines video distribution, streaming, visualization and display technologies to support these distinct operational needs across commercial, public-sector and mission-critical environments.';
|
|
private const INTRO_CTA = 'Explore solutions';
|
|
|
|
private const OVERVIEW_H2 = 'Explore our markets';
|
|
|
|
/** workbook order; titles match the record taxonomy per norm() */
|
|
private const CARDS = [
|
|
'Hospitality' => 'Create connected guest experiences with IPTV, digital signage and video distribution that support entertainment, communications and consistent service across hospitality environments.',
|
|
'Transport' => 'Keep passengers informed and operations visible with real-time information, video distribution and visualization across airports, stations and complex transport environments.',
|
|
'Sports & Entertainment' => 'Connect fans, guests and operators with live video, digital signage, data visualization and control-room technology across venues, events and entertainment environments.',
|
|
'Broadcast & Telecoms' => 'Support contribution, production, distribution and network operations with reliable video workflows designed for demanding broadcast, media and telecom environments.',
|
|
'Education' => 'Deliver live and on-demand video, campus communications and digital information across learning spaces, shared facilities and student accommodation.',
|
|
'Corporation' => 'Improve communication and operational awareness across workplaces with IPTV, digital signage, video distribution, dashboards and control-room visualization.',
|
|
'Industrial' => 'Give industrial teams clearer oversight of sites, processes and operational data through centralized monitoring, visualization and secure video distribution.',
|
|
'Critical National Infrastructure' => 'Support resilient infrastructure operations with real-time monitoring, secure communications and visualization that help teams maintain awareness across critical systems and assets.',
|
|
'Government' => 'Enable secure communications, public information delivery and coordinated operations across government environments with reliable video, signage and control-room solutions.',
|
|
'Public Safety' => 'Bring live video, operational data and incident information into a shared view to support situational awareness, coordination and faster response.',
|
|
'Defense & Security' => 'Deliver secure, low-latency video and mission-critical visualization for ISR, command and control, tactical distribution and high-security operational environments.',
|
|
];
|
|
|
|
private const PREFOOTER_H2 = 'Find the right approach for your environment';
|
|
private const PREFOOTER_COPY = 'Your operational priorities determine the technology you need. Talk to VITEC about your environment, workflows and users, and we can help identify the video, visualization and communications approach that fits your requirements.';
|
|
private const PREFOOTER_PRIMARY = 'Talk to our team';
|
|
private const PREFOOTER_SECONDARY = 'Explore solutions';
|
|
|
|
private const SEO_TITLE = 'Video Solutions by Market | VITEC';
|
|
private const SEO_DESCRIPTION = 'Explore VITEC video, visualization, streaming and communications solutions for transport, broadcast, industry, government, public safety and more.';
|
|
/** Curated page keywords (Claude, 2026-10-26), same convention as the hero pages. */
|
|
private const KEYWORDS = 'video solutions by market, IPTV, digital signage, video distribution, visualization, control room, broadcast, transport, government, public safety, defense, critical infrastructure';
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
Bootstrap::initializeBackendAuthentication();
|
|
$apply = (bool)$input->getOption('apply');
|
|
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
|
$pages = $pool->getConnectionForTable('pages');
|
|
$content = $pool->getConnectionForTable('tt_content');
|
|
|
|
$marketsUid = $this->pageUid($pages, '/markets');
|
|
$solutionsUid = $this->pageUid($pages, '/solutions');
|
|
$contactUid = $this->pageUid($pages, '/contact');
|
|
if (!$marketsUid || !$solutionsUid || !$contactUid) {
|
|
$output->writeln(sprintf('<error>Page lookup failed: /markets=%d /solutions=%d /contact=%d</error>', $marketsUid, $solutionsUid, $contactUid));
|
|
return Command::FAILURE;
|
|
}
|
|
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · /markets #%d · /solutions #%d · /contact #%d', $marketsUid, $solutionsUid, $contactUid));
|
|
$datamap = [];
|
|
|
|
// --- resolve the existing elements ---------------------------------------
|
|
$hero = $this->one($content, $marketsUid, "CType = 'vitec_herosection' AND colPos = 1", 'hero');
|
|
$intro = $this->one($content, $marketsUid, "CType = 'vitec_introparagraph' AND colPos = 0 AND tx_container_parent = 0", 'markets intro');
|
|
$containers = $content->fetchAllAssociative(
|
|
"SELECT uid, hidden, sorting FROM tt_content WHERE deleted = 0 AND sys_language_uid IN (0,-1) AND pid = ? AND CType = 'vitec_container' AND colPos = 0 AND tx_container_parent = 0 ORDER BY sorting",
|
|
[$marketsUid]
|
|
);
|
|
if ($hero === null || $intro === null || count($containers) !== 2) {
|
|
$output->writeln('<error>Unexpected page structure (' . count($containers) . ' top-level containers) - aborting.</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
$overviewContainer = $prefooterContainer = null;
|
|
$overviewHeader = $marketlist = null;
|
|
foreach ($containers as $container) {
|
|
$children = $content->fetchAllAssociative(
|
|
'SELECT uid, CType, header, hidden, sorting FROM tt_content WHERE deleted = 0 AND tx_container_parent = ? ORDER BY sorting',
|
|
[(int)$container['uid']]
|
|
);
|
|
foreach ($children as $child) {
|
|
if ($child['CType'] === 'vitec_marketlist') {
|
|
$overviewContainer = $container;
|
|
$marketlist = $child;
|
|
}
|
|
if ($child['CType'] === 'header') {
|
|
$overviewHeader = $child;
|
|
}
|
|
}
|
|
if ($overviewContainer === null || (int)$container['uid'] !== (int)$overviewContainer['uid']) {
|
|
$prefooterContainer = $container;
|
|
}
|
|
}
|
|
if ($overviewContainer === null || $prefooterContainer === null || $overviewHeader === null || $marketlist === null) {
|
|
$output->writeln('<error>Could not identify overview/prefooter containers - aborting.</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
// --- hero -----------------------------------------------------------------
|
|
$datamap['tt_content'][(int)$hero['uid']] = [
|
|
'header' => self::HERO_H1,
|
|
'bodytext' => $this->p(self::HERO_COPY),
|
|
'vitec_cta_label' => self::HERO_CTA,
|
|
];
|
|
$output->writeln(sprintf(' ~ hero #%d: H1 "%s" + copy + CTA label "%s" (anchor target kept)', (int)$hero['uid'], self::HERO_H1, self::HERO_CTA));
|
|
|
|
// --- markets intro 50/50 ----------------------------------------------------
|
|
$datamap['tt_content'][(int)$intro['uid']] = [
|
|
'header' => self::INTRO_H2,
|
|
'bodytext' => $this->p(self::INTRO_COPY),
|
|
'vitec_button_text' => self::INTRO_CTA,
|
|
'vitec_button_link' => 't3://page?uid=' . $solutionsUid,
|
|
];
|
|
$output->writeln(sprintf(' ~ intro #%d: H2 "%s" + copy + CTA "%s" -> /solutions', (int)$intro['uid'], self::INTRO_H2, self::INTRO_CTA));
|
|
|
|
// --- overview header + marketlist selection --------------------------------
|
|
$datamap['tt_content'][(int)$overviewHeader['uid']] = ['header' => self::OVERVIEW_H2];
|
|
$output->writeln(sprintf(' ~ overview header #%d: "%s" -> "%s"', (int)$overviewHeader['uid'], (string)$overviewHeader['header'], self::OVERVIEW_H2));
|
|
|
|
$markets = $pool->getConnectionForTable('tx_vitec_domain_model_market')->fetchAllAssociative(
|
|
'SELECT uid, title, teaser FROM tx_vitec_domain_model_market WHERE deleted = 0 AND sys_language_uid IN (0,-1)'
|
|
);
|
|
$selection = [];
|
|
$missing = [];
|
|
foreach (self::CARDS as $title => $body) {
|
|
$record = null;
|
|
foreach ($markets as $row) {
|
|
if ($this->norm((string)$row['title']) === $this->norm($title)) {
|
|
$record = $row;
|
|
break;
|
|
}
|
|
}
|
|
if ($record === null) {
|
|
$missing[] = $title;
|
|
continue;
|
|
}
|
|
$selection[] = (int)$record['uid'];
|
|
$oldTeaser = trim((string)$record['teaser']);
|
|
if ($oldTeaser !== $body) {
|
|
$datamap['tx_vitec_domain_model_market'][(int)$record['uid']] = ['teaser' => $body];
|
|
$output->writeln(sprintf(' ~ market record #%d "%s": teaser %s', (int)$record['uid'], (string)$record['title'],
|
|
$oldTeaser === '' ? 'set (was empty)' : 'OVERWRITTEN ("' . mb_substr($oldTeaser, 0, 50) . '...")'));
|
|
} else {
|
|
$output->writeln(sprintf(' = market record #%d "%s": teaser already current', (int)$record['uid'], (string)$record['title']));
|
|
}
|
|
}
|
|
if ($missing !== []) {
|
|
$output->writeln('<error>No market record for: ' . implode(', ', $missing) . ' - aborting.</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
$datamap['tt_content'][(int)$marketlist['uid']]['pi_flexform'] = [
|
|
'data' => ['sDEF' => ['lDEF' => ['settings.markets' => ['vDEF' => implode(',', $selection)]]]],
|
|
];
|
|
$output->writeln(sprintf(' ~ marketlist #%d: selection -> 11 records in workbook order (%s)', (int)$marketlist['uid'], implode(',', $selection)));
|
|
|
|
// --- prefooter: hide the old container, add the two-CTA banner -------------
|
|
if ((int)$prefooterContainer['hidden'] !== 1) {
|
|
$datamap['tt_content'][(int)$prefooterContainer['uid']]['hidden'] = 1;
|
|
$output->writeln(sprintf(' ~ old prefooter container #%d: HIDDEN (kept for reference)', (int)$prefooterContainer['uid']));
|
|
}
|
|
$datamap['tt_content']['NEWPREFOOTER'] = [
|
|
'pid' => $marketsUid,
|
|
'CType' => 'vitec_ctabanner',
|
|
'colPos' => 0,
|
|
'header' => self::PREFOOTER_H2,
|
|
'bodytext' => $this->p(self::PREFOOTER_COPY),
|
|
'vitec_primary_cta' => 't3://page?uid=' . $contactUid,
|
|
'vitec_primary_cta_label' => self::PREFOOTER_PRIMARY,
|
|
'vitec_secondary_cta' => 't3://page?uid=' . $solutionsUid,
|
|
'vitec_secondary_cta_label' => self::PREFOOTER_SECONDARY,
|
|
'vitec_layout_variant' => 'centered',
|
|
'vitec_background_variant' => 'orange',
|
|
];
|
|
$output->writeln(sprintf(' + prefooter vitec_ctabanner (centered/orange, as on the hero pages): "%s" -> /contact, "%s" -> /solutions',
|
|
self::PREFOOTER_PRIMARY, self::PREFOOTER_SECONDARY));
|
|
|
|
// --- page properties --------------------------------------------------------
|
|
$datamap['pages'][$marketsUid] = [
|
|
'seo_title' => self::SEO_TITLE,
|
|
'og_title' => self::SEO_TITLE,
|
|
'twitter_title' => self::SEO_TITLE,
|
|
'description' => self::SEO_DESCRIPTION,
|
|
'og_description' => self::SEO_DESCRIPTION,
|
|
'twitter_description' => self::SEO_DESCRIPTION,
|
|
'abstract' => self::SEO_DESCRIPTION,
|
|
'canonical_link' => 't3://page?uid=' . $marketsUid,
|
|
'keywords' => self::KEYWORDS,
|
|
];
|
|
$output->writeln(' ~ page meta: seo/og/twitter title + description, canonical, abstract, keywords');
|
|
|
|
if (!$apply) {
|
|
$output->writeln("\nDRY RUN - nothing written. Add --apply to write.");
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
// DataHandler puts NEW records at the top - move the banner to the end,
|
|
// right where the hidden old prefooter container sits.
|
|
$newUid = (int)($dataHandler->substNEWwithIDs['NEWPREFOOTER'] ?? 0);
|
|
if ($newUid > 0) {
|
|
$content->update('tt_content', ['sorting' => (int)$prefooterContainer['sorting'] + 8], ['uid' => $newUid]);
|
|
$output->writeln(sprintf('<info>Done. Banner #%d sorted after the old container. Flush the frontend cache.</info>', $newUid));
|
|
} else {
|
|
$output->writeln('<error>New prefooter banner uid not resolved.</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
private function pageUid($pages, string $slug): int
|
|
{
|
|
$row = $pages->fetchAssociative('SELECT uid FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)', [$slug]);
|
|
return (int)($row['uid'] ?? 0);
|
|
}
|
|
|
|
/** @return array<string,mixed>|null exactly one matching element, else null (reported) */
|
|
private function one($content, int $pid, string $where, string $label): ?array
|
|
{
|
|
$rows = $content->fetchAllAssociative(
|
|
'SELECT uid, header, hidden, sorting FROM tt_content WHERE deleted = 0 AND sys_language_uid IN (0,-1) AND pid = ? AND ' . $where,
|
|
[$pid]
|
|
);
|
|
return count($rows) === 1 ? $rows[0] : null;
|
|
}
|
|
|
|
private function p(string $text): string
|
|
{
|
|
return '<p>' . htmlspecialchars($text, ENT_NOQUOTES | ENT_HTML5) . '</p>';
|
|
}
|
|
|
|
private function norm(string $value): string
|
|
{
|
|
return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], mb_strtolower($value)));
|
|
}
|
|
}
|