From c4ec979f78d01a824bdab71fcba7e97089c20f0a Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 27 Oct 2026 10:15:02 +0100 Subject: [PATCH] Fill the /markets main page from "Markets Hero Page.xlsx" - new vitec:import-markets-main (dry-run by default): one page with fixed approved copy, so the workbook content is embedded in the command instead of parsed at runtime; every target is resolved at runtime (pages by slug, elements by CType/colPos/container, market records by normalised title) and the command aborts on an unexpected structure - the page already had the workbook structure (template vitec__2), so this updates the hero, the 50/50 markets intro (CTA -> /solutions) and the overview header in place - the marketlist flexform selection moves from the old 9-record set (which still included the dropped "Security & Surveillance") to the 11 main-market records in workbook order - the 11 market card bodies land in the market records' teaser field (all were empty; the marketlist cards render from the records - frontend briefing asks for teaser on the cards) - the old single-button prefooter container is hidden, replaced by a vitec_ctabanner (centered/orange, as on the 11 market hero pages) with both workbook CTAs: "Talk to our team" -> /contact and "Explore solutions" -> /solutions; explicit sorting after DataHandler - page properties per the established set: seo/og/twitter title and description, canonical, abstract, curated keywords - applied and verified live in the JSON (cards, order, teasers, CTA targets, meta tags); no spec bump - no new JSON keys involved Co-Authored-By: Claude Fable 5 --- .../Command/ImportMarketsMainPageCommand.php | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 packages/vitec/Classes/Command/ImportMarketsMainPageCommand.php diff --git a/packages/vitec/Classes/Command/ImportMarketsMainPageCommand.php b/packages/vitec/Classes/Command/ImportMarketsMainPageCommand.php new file mode 100644 index 0000000..13ebdc8 --- /dev/null +++ b/packages/vitec/Classes/Command/ImportMarketsMainPageCommand.php @@ -0,0 +1,285 @@ + '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('Page lookup failed: /markets=%d /solutions=%d /contact=%d', $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('Unexpected page structure (' . count($containers) . ' top-level containers) - aborting.'); + 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('Could not identify overview/prefooter containers - aborting.'); + 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('No market record for: ' . implode(', ', $missing) . ' - aborting.'); + 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('DataHandler: ' . implode(' | ', $dataHandler->errorLog) . ''); + 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('Done. Banner #%d sorted after the old container. Flush the frontend cache.', $newUid)); + } else { + $output->writeln('New prefooter banner uid not resolved.'); + 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|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 '

' . htmlspecialchars($text, ENT_QUOTES | ENT_HTML5) . '

'; + } + + private function norm(string $value): string + { + return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], mb_strtolower($value))); + } +}