From 7833e1b6682e2b5fdc977afdf44cb2820bc2f4a7 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Fri, 28 Aug 2026 13:57:54 +0200 Subject: [PATCH] Product import --- composer.lock | 330 +++++++++--------- migrations/check_workbook.php | 7 + .../Aligo_Product Page Content.xlsx | Bin 0 -> 9237 bytes .../Arqa_Product Page Content.xlsx | Bin 0 -> 9190 bytes migrations/reset_product_mapping.php | 51 +++ .../Backend/ProductTextImportController.php | 119 ++++++- .../Classes/Import/ProductXlsxReader.php | 172 ++++++++- .../vitec/Classes/Seo/VitecMetaHandler.php | 35 ++ .../Configuration/Backend/AjaxRoutes.php | 11 + packages/vitec/Configuration/Services.yaml | 6 + .../ContentElements/columns/config.yaml | 5 + .../columns/language/labels.xlf | 3 + .../featured-content/config.yaml | 6 + .../featured-content/language/labels.xlf | 6 + .../ContentElements/hero-section/config.yaml | 12 + .../hero-section/language/labels.xlf | 12 + .../intro-paragraph/config.yaml | 6 + .../intro-paragraph/language/labels.xlf | 2 + .../Templates/Import/ProductTexts.html | 13 +- .../Public/Javascript/product-inline-edit.js | 148 ++++++++ public/_frontend/index.html | 4 +- 21 files changed, 763 insertions(+), 185 deletions(-) create mode 100644 migrations/products_xlsx/Aligo_Product Page Content.xlsx create mode 100644 migrations/products_xlsx/Arqa_Product Page Content.xlsx create mode 100644 migrations/reset_product_mapping.php create mode 100644 packages/vitec/Classes/Seo/VitecMetaHandler.php create mode 100644 packages/vitec/Resources/Public/Javascript/product-inline-edit.js diff --git a/composer.lock b/composer.lock index c5568a7..6f59f97 100644 --- a/composer.lock +++ b/composer.lock @@ -338,16 +338,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.13", + "version": "1.5.14", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5" + "reference": "0c8abba0634f637bd78c4e451981da368d403463" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/c008272789979f709f7fcb32c2ecf1d2db5e84e5", - "reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c8abba0634f637bd78c4e451981da368d403463", + "reference": "0c8abba0634f637bd78c4e451981da368d403463", "shasum": "" }, "require": { @@ -394,7 +394,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.13" + "source": "https://github.com/composer/ca-bundle/tree/1.5.14" }, "funding": [ { @@ -406,7 +406,7 @@ "type": "github" } ], - "time": "2026-07-18T12:35:13+00:00" + "time": "2026-08-21T14:57:29+00:00" }, { "name": "composer/class-map-generator", @@ -479,16 +479,16 @@ }, { "name": "composer/composer", - "version": "2.10.2", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc" + "reference": "f0de0bf90226853b841672f086d8b58b02332504" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/8d4439f572a97670a9edc039eb3b093cc976b4bc", - "reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc", + "url": "https://api.github.com/repos/composer/composer/zipball/f0de0bf90226853b841672f086d8b58b02332504", + "reference": "f0de0bf90226853b841672f086d8b58b02332504", "shasum": "" }, "require": { @@ -499,6 +499,8 @@ "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "justinrainbow/json-schema": "^6.5.1", "php": "^7.2.5 || ^8.0", @@ -576,7 +578,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.10.2" + "source": "https://github.com/composer/composer/tree/2.10.3" }, "funding": [ { @@ -588,7 +590,7 @@ "type": "github" } ], - "time": "2026-07-01T09:24:45+00:00" + "time": "2026-08-27T11:34:23+00:00" }, { "name": "composer/metadata-minifier", @@ -1801,22 +1803,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.3", + "version": "7.15.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", - "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.2", - "guzzlehttp/psr7": "^2.13", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -1909,7 +1911,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.3" + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" }, "funding": [ { @@ -1925,20 +1927,20 @@ "type": "tidelift" } ], - "time": "2026-08-05T19:48:21+00:00" + "time": "2026-08-24T09:21:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.2", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", - "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", "shasum": "" }, "require": { @@ -1993,7 +1995,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.2" + "source": "https://github.com/guzzle/promises/tree/2.5.3" }, "funding": [ { @@ -2009,20 +2011,20 @@ "type": "tidelift" } ], - "time": "2026-08-05T19:30:54+00:00" + "time": "2026-08-24T09:11:28+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.13.0", + "version": "2.13.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" + "reference": "95e7828100de18b4e269fb1703be530082d5166d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", - "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", "shasum": "" }, "require": { @@ -2112,7 +2114,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.13.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.1" }, "funding": [ { @@ -2128,7 +2130,7 @@ "type": "tidelift" } ], - "time": "2026-07-16T22:23:49+00:00" + "time": "2026-08-24T09:13:11+00:00" }, { "name": "halaxa/json-machine", @@ -2194,19 +2196,20 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.10.0", + "version": "6.11.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" + "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", + "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", "shasum": "" }, "require": { + "ext-filter": "*", "ext-json": "*", "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" @@ -2239,20 +2242,9 @@ ], "authors": [ { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" } ], "description": "A library to validate a json schema.", @@ -2263,9 +2255,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.11.0" }, - "time": "2026-06-16T20:50:26+00:00" + "time": "2026-08-21T10:30:42+00:00" }, { "name": "lolli42/finediff", @@ -2592,24 +2584,24 @@ }, { "name": "masterminds/html5", - "version": "2.10.1", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fd5018f6815fff903946d0564977b44ce8010e29" + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", - "reference": "fd5018f6815fff903946d0564977b44ce8010e29", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", "shasum": "" }, "require": { "ext-dom": "*", - "php": ">=5.3.0" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { @@ -2653,9 +2645,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" + "source": "https://github.com/Masterminds/html5-php/tree/2.11.0" }, - "time": "2026-06-23T18:43:15+00:00" + "time": "2026-08-18T06:18:41+00:00" }, { "name": "netzbewegung/nb-headless-content-blocks", @@ -4563,16 +4555,16 @@ }, { "name": "symfony/config", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e896f5e874e6983d0a098f554ca82a08a924c298" + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e896f5e874e6983d0a098f554ca82a08a924c298", - "reference": "e896f5e874e6983d0a098f554ca82a08a924c298", + "url": "https://api.github.com/repos/symfony/config/zipball/696e12da8eea497a1a3808d714b43ff67a7df43e", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e", "shasum": "" }, "require": { @@ -4618,7 +4610,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.16" + "source": "https://github.com/symfony/config/tree/v7.4.17" }, "funding": [ { @@ -4638,20 +4630,20 @@ "type": "tidelift" } ], - "time": "2026-07-30T15:04:16+00:00" + "time": "2026-08-20T09:55:18+00:00" }, { "name": "symfony/console", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d" + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d", - "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d", + "url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca", + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca", "shasum": "" }, "require": { @@ -4716,7 +4708,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.16" + "source": "https://github.com/symfony/console/tree/v7.4.17" }, "funding": [ { @@ -4736,20 +4728,20 @@ "type": "tidelift" } ], - "time": "2026-07-31T12:37:14+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "7f59a843c1fbcceafecdf6863d3e38ea6ecd5061" + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7f59a843c1fbcceafecdf6863d3e38ea6ecd5061", - "reference": "7f59a843c1fbcceafecdf6863d3e38ea6ecd5061", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", "shasum": "" }, "require": { @@ -4800,7 +4792,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.16" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.17" }, "funding": [ { @@ -4820,7 +4812,7 @@ "type": "tidelift" } ], - "time": "2026-08-06T09:45:22+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5204,16 +5196,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.15", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79" + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79", - "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", "shasum": "" }, "require": { @@ -5250,7 +5242,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.15" + "source": "https://github.com/symfony/filesystem/tree/v7.4.17" }, "funding": [ { @@ -5270,20 +5262,20 @@ "type": "tidelift" } ], - "time": "2026-07-22T07:36:05+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/finder", - "version": "v7.4.14", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "13b38720174286f55d1761152b575a8d1436fc25" + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", - "reference": "13b38720174286f55d1761152b575a8d1436fc25", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", "shasum": "" }, "require": { @@ -5318,7 +5310,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.14" + "source": "https://github.com/symfony/finder/tree/v7.4.17" }, "funding": [ { @@ -5338,20 +5330,20 @@ "type": "tidelift" } ], - "time": "2026-06-27T08:31:18+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b676451bb638e99a7d34d8a2be90406822e301eb" + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b676451bb638e99a7d34d8a2be90406822e301eb", - "reference": "b676451bb638e99a7d34d8a2be90406822e301eb", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2ebe78c083501dfb9509b31a7aedcae4d60a391f", + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f", "shasum": "" }, "require": { @@ -5400,7 +5392,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.16" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.17" }, "funding": [ { @@ -5420,20 +5412,20 @@ "type": "tidelift" } ], - "time": "2026-08-07T11:50:27+00:00" + "time": "2026-08-20T09:55:18+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.15", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "68c1f27c97edd0222eb8d440a6c8c4da5354ab46" + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/68c1f27c97edd0222eb8d440a6c8c4da5354ab46", - "reference": "68c1f27c97edd0222eb8d440a6c8c4da5354ab46", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b17c9bf3a551d5f635638a3b6c05f06c4dc87584", + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584", "shasum": "" }, "require": { @@ -5484,7 +5476,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.15" + "source": "https://github.com/symfony/mailer/tree/v7.4.17" }, "funding": [ { @@ -5504,20 +5496,20 @@ "type": "tidelift" } ], - "time": "2026-07-28T07:33:02+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/messenger", - "version": "v7.4.15", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "6338eed8f65c593469fdbdbe043d2a7552264403" + "reference": "3976018381c403e369d3027592e911b979eaeed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/6338eed8f65c593469fdbdbe043d2a7552264403", - "reference": "6338eed8f65c593469fdbdbe043d2a7552264403", + "url": "https://api.github.com/repos/symfony/messenger/zipball/3976018381c403e369d3027592e911b979eaeed9", + "reference": "3976018381c403e369d3027592e911b979eaeed9", "shasum": "" }, "require": { @@ -5578,7 +5570,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v7.4.15" + "source": "https://github.com/symfony/messenger/tree/v7.4.17" }, "funding": [ { @@ -5598,20 +5590,20 @@ "type": "tidelift" } ], - "time": "2026-07-22T12:46:20+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/mime", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5" + "reference": "bf328d82105831db3e409195db0540ff57f27c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/20094b76a7106dbe978d31cd3bd7aa1ed248d0e5", - "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5", + "url": "https://api.github.com/repos/symfony/mime/zipball/bf328d82105831db3e409195db0540ff57f27c80", + "reference": "bf328d82105831db3e409195db0540ff57f27c80", "shasum": "" }, "require": { @@ -5635,7 +5627,7 @@ "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" }, "type": "library", "autoload": { @@ -5667,7 +5659,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.16" + "source": "https://github.com/symfony/mime/tree/v7.4.17" }, "funding": [ { @@ -5687,7 +5679,7 @@ "type": "tidelift" } ], - "time": "2026-08-07T14:56:57+00:00" + "time": "2026-08-22T09:04:42+00:00" }, { "name": "symfony/options-resolver", @@ -5927,16 +5919,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.38.1", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "dc21118016c039a66235cf93d96b435ffb282412" + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", - "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/51b5ff5ba85452b31ec6f55490b08148612339d9", + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9", "shasum": "" }, "require": { @@ -5990,7 +5982,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.42.0" }, "funding": [ { @@ -6010,20 +6002,20 @@ "type": "tidelift" } ], - "time": "2026-05-25T15:22:23+00:00" + "time": "2026-08-24T10:51:20+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { @@ -6075,7 +6067,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -6095,7 +6087,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-08-07T06:33:24+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -6751,16 +6743,16 @@ }, { "name": "symfony/process", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f5804be144caceb570f6747519999636b664f24c" + "reference": "058d17fc284cce14efb2385783b55014a461b176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", - "reference": "f5804be144caceb570f6747519999636b664f24c", + "url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176", + "reference": "058d17fc284cce14efb2385783b55014a461b176", "shasum": "" }, "require": { @@ -6792,7 +6784,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.13" + "source": "https://github.com/symfony/process/tree/v7.4.17" }, "funding": [ { @@ -6812,7 +6804,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/property-access", @@ -6897,16 +6889,16 @@ }, { "name": "symfony/property-info", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "c79afdb2e720db8ee4f7050a07b3116b92cb896b" + "reference": "a37837b52ed5dd7474c251d15694007344c6462d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/c79afdb2e720db8ee4f7050a07b3116b92cb896b", - "reference": "c79afdb2e720db8ee4f7050a07b3116b92cb896b", + "url": "https://api.github.com/repos/symfony/property-info/zipball/a37837b52ed5dd7474c251d15694007344c6462d", + "reference": "a37837b52ed5dd7474c251d15694007344c6462d", "shasum": "" }, "require": { @@ -6963,7 +6955,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.16" + "source": "https://github.com/symfony/property-info/tree/v7.4.17" }, "funding": [ { @@ -6983,7 +6975,7 @@ "type": "tidelift" } ], - "time": "2026-08-07T14:56:57+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/rate-limiter", @@ -7061,16 +7053,16 @@ }, { "name": "symfony/routing", - "version": "v7.4.15", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b" + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/80c0a93d3f8e7499f716204a1fb38ead942a7a2b", - "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b", + "url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3", + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3", "shasum": "" }, "require": { @@ -7122,7 +7114,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.15" + "source": "https://github.com/symfony/routing/tree/v7.4.17" }, "funding": [ { @@ -7142,7 +7134,7 @@ "type": "tidelift" } ], - "time": "2026-07-21T15:13:06+00:00" + "time": "2026-08-17T13:12:36+00:00" }, { "name": "symfony/service-contracts", @@ -7323,16 +7315,16 @@ }, { "name": "symfony/translation", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "501e0ff4553c744209ca1a68790d8a4541563710" + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/501e0ff4553c744209ca1a68790d8a4541563710", - "reference": "501e0ff4553c744209ca1a68790d8a4541563710", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", "shasum": "" }, "require": { @@ -7399,7 +7391,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.16" + "source": "https://github.com/symfony/translation/tree/v7.4.17" }, "funding": [ { @@ -7419,7 +7411,7 @@ "type": "tidelift" } ], - "time": "2026-07-30T12:37:26+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/translation-contracts", @@ -7587,16 +7579,16 @@ }, { "name": "symfony/uid", - "version": "v7.4.9", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2676b524340abcfe4d6151ec698463cebafee439" + "reference": "69d732355a139c6f8881337d28515aa01f12b8be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", - "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be", + "reference": "69d732355a139c6f8881337d28515aa01f12b8be", "shasum": "" }, "require": { @@ -7641,7 +7633,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.9" + "source": "https://github.com/symfony/uid/tree/v7.4.17" }, "funding": [ { @@ -7661,20 +7653,20 @@ "type": "tidelift" } ], - "time": "2026-04-30T15:19:22+00:00" + "time": "2026-08-11T07:38:58+00:00" }, { "name": "symfony/validator", - "version": "v7.4.16", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "3aee77358ab14090337183657e554668bc94ab4a" + "reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/3aee77358ab14090337183657e554668bc94ab4a", - "reference": "3aee77358ab14090337183657e554668bc94ab4a", + "url": "https://api.github.com/repos/symfony/validator/zipball/b1cbb758c005fbe0d7b2b8d1561869d318628a10", + "reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10", "shasum": "" }, "require": { @@ -7745,7 +7737,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.16" + "source": "https://github.com/symfony/validator/tree/v7.4.17" }, "funding": [ { @@ -7765,7 +7757,7 @@ "type": "tidelift" } ], - "time": "2026-08-06T09:41:15+00:00" + "time": "2026-08-22T09:04:42+00:00" }, { "name": "symfony/var-exporter", @@ -7849,16 +7841,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.15", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e101850ded5d2c0d44bf32abb8996404afec2dec" + "reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e101850ded5d2c0d44bf32abb8996404afec2dec", - "reference": "e101850ded5d2c0d44bf32abb8996404afec2dec", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0b040d7b66ceb10b7bb24c8e6656257932693a12", + "reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12", "shasum": "" }, "require": { @@ -7901,7 +7893,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.15" + "source": "https://github.com/symfony/yaml/tree/v7.4.17" }, "funding": [ { @@ -7921,7 +7913,7 @@ "type": "tidelift" } ], - "time": "2026-07-21T15:13:06+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "typo3/class-alias-loader", diff --git a/migrations/check_workbook.php b/migrations/check_workbook.php index 533f682..0f364b5 100644 --- a/migrations/check_workbook.php +++ b/migrations/check_workbook.php @@ -40,6 +40,13 @@ foreach ($parsed['components'] as $c) { ); } +echo "\n--- Composed sources (g:) ---\n"; +foreach (($reader->sources($parsed)['Composed'] ?? []) as $entry) { + echo $entry['selector'] . "\n"; + echo " label: " . $entry['label'] . "\n"; + echo " html : " . $entry['value'] . "\n\n"; +} + echo "\n--- Related Product Cards, raw ---\n"; foreach ($parsed['components'] as $c) { if ($c['component'] !== 'Related Product Card') { diff --git a/migrations/products_xlsx/Aligo_Product Page Content.xlsx b/migrations/products_xlsx/Aligo_Product Page Content.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bae58777984cf7e009fbc46097328843ebc69111 GIT binary patch literal 9237 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 literal 0 HcmV?d00001 diff --git a/migrations/reset_product_mapping.php b/migrations/reset_product_mapping.php new file mode 100644 index 0000000..19bb37d --- /dev/null +++ b/migrations/reset_product_mapping.php @@ -0,0 +1,51 @@ + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] +); + +$row = $pdo->query("SELECT uid, mapping, record_aliases FROM tx_vitec_import_mapping WHERE model = 'product_xlsx'")->fetch(); +if (!$row) { + exit("No row for model 'product_xlsx' - nothing to clear, defaults apply already.\n"); +} + +echo "Row uid " . $row['uid'] . "\n"; +echo " mapping (cleared) : " . ($row['mapping'] ?: '(empty)') . "\n"; +echo " aliases (KEPT) : " . ($row['record_aliases'] ?: '(empty)') . "\n"; + +$pdo->prepare("UPDATE tx_vitec_import_mapping SET mapping = '', tstamp = ? WHERE model = 'product_xlsx'") + ->execute([time()]); + +echo "Done - the saved field mapping is cleared, the corrected defaults apply on the next preview.\n"; diff --git a/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php b/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php index f1a2a48..4292fed 100644 --- a/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php +++ b/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php @@ -15,6 +15,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\DataHandling\DataHandler; +use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; @@ -50,28 +51,50 @@ final class ProductTextImportController /** * Never offered as import targets. `slug` changes URLs - that is redirect * territory, not a text import; the workbook URL is checked against the - * record's slug informationally instead. + * record's slug informationally instead. The flags and the shortcut pid are + * editorial switches that will never come out of a workbook (decision + * 2026-08-28) - a text import writing "1" into hideonwebsite would silently + * unpublish a product. Only this XLSX flow filters them; the CSV flow keeps + * the full field list. */ - private const EXCLUDED_TARGETS = ['slug']; + private const EXCLUDED_TARGETS = [ + 'slug', + 'hideonapp', + 'hideonwebsite', + 'hideonproducts', + 'hideondatasheets', + 'shortcutpid', + 'shortcut', + 'legacy', + 'supportproduct', + 'subproduct', + ]; /** * Pre-seed for the very first mapping (no row in tx_vitec_import_mapping * yet). Editors change everything in the dropdowns; this only saves the * first manual pass. Keys are product columns, values source selectors as * ProductXlsxReader defines them. + * + * Verified against the finished MGW-Diamond-H page (2026-08-28): the hero + * one-liner lives in `teaser` (subtitle is unused there), the ~45-70-word + * introduction in `description`, the ~80-120-word overview in + * `description2`, and the three Key Capability Groups land as ONE HTML + * block in `capabilities` - hence the composed `g:` selector. Why-Choose + * cards, resources, pre-footer and hero CTA are page content elements or + * template copy on the finished page, not record fields, and therefore + * have no defaults here. An earlier version of this seed mapped onto + * `subtitle` and onto columns that exist only in the DB but not in the + * TCA (`cta`, `key1-3`, `apptext1-3`) - both corrected. */ private const DEFAULT_MAPPING = [ - 'subtitle' => 'c:Hero — H1#1:body', - 'teaser' => 'c:Product Introduction#1:body', - 'description' => 'c:Product Overview — H2#1:body', - 'cta' => 'c:Pre-footer CTA — H2#1:cta', + 'seotitle' => 'x:seotitle', + 'teaser' => 'c:Hero — H1#1:body', + 'description' => 'c:Product Introduction#1:body', + 'description2' => 'c:Product Overview — H2#1:body', + 'capabilities' => 'g:Key Capability Group', + 'textrelatedproducts' => 'p:Related Products — H2#1', 'keywords' => 'm:primaryKeyword', - 'key1' => 'c:Why Choose Card#1:title', - 'apptext1' => 'c:Why Choose Card#1:body', - 'key2' => 'c:Why Choose Card#2:title', - 'apptext2' => 'c:Why Choose Card#2:body', - 'key3' => 'c:Why Choose Card#3:title', - 'apptext3' => 'c:Why Choose Card#3:body', ]; private const SORTABLE = ['title', 'slug', 'tstamp']; @@ -252,6 +275,16 @@ final class ProductTextImportController $map = array_map('strval', (array)($body['map'] ?? [])); $apply = array_map('strval', (array)($body['apply'] ?? [])); + // A per-row "Save" button writes exactly its own row; the checkbox + // state of every other row is deliberately ignored then. Only one + // submit button ever posts its value, so the two can't both be set. + $applySingle = trim((string)($body['applySingle'] ?? '')); + $applyRelatedSingle = trim((string)($body['applyRelatedSingle'] ?? '')); + if ($applySingle !== '') { + $apply = [$applySingle]; + } elseif ($applyRelatedSingle !== '') { + $apply = []; + } $targets = $this->targetFields(); $data = []; @@ -271,6 +304,11 @@ final class ProductTextImportController // cannot be related to itself. $relatedChoices = array_map('intval', (array)($body['related'] ?? [])); $applyRelated = array_map('intval', (array)($body['applyRelated'] ?? [])); + if ($applySingle !== '') { + $applyRelated = []; + } elseif ($applyRelatedSingle !== '') { + $applyRelated = [(int)$applyRelatedSingle]; + } $relatedCards = $this->relatedCards($parsed); $relatedTodo = []; foreach ($applyRelated as $index) { @@ -352,6 +390,60 @@ final class ProductTextImportController return $this->renderTexts($request, $this->loadProduct($uid), $parsed, $map); } + // ------------------------------------------------- inline field editing + + /** + * AJAX (vitec_product_field_get): the FULL raw value of one product field + * for the click-to-edit cell - the cell itself only shows a truncated + * preview. RTE fields return their stored HTML; editing is deliberately + * source-level (decision 2026-08-28: no inline WYSIWYG). + */ + public function fieldGet(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + $uid = (int)($params['uid'] ?? 0); + $field = (string)($params['field'] ?? ''); + $product = $this->loadProduct($uid); + $targets = $this->targetFields(); + if ($product === null || !isset($targets[$field])) { + return new JsonResponse(['success' => false, 'message' => 'Unknown product or field.']); + } + return new JsonResponse([ + 'success' => true, + 'value' => (string)($product[$field] ?? ''), + ]); + } + + /** + * AJAX (vitec_product_field_save): write one product field. Same target + * whitelist and the same DataHandler path as the form apply - RTE + * transformations and record history included. Responds with the freshly + * re-read value so the cell preview shows what was actually stored. + */ + public function fieldSave(ServerRequestInterface $request): ResponseInterface + { + $body = (array)$request->getParsedBody(); + $uid = (int)($body['uid'] ?? 0); + $field = (string)($body['field'] ?? ''); + $value = (string)($body['value'] ?? ''); + $targets = $this->targetFields(); + if ($this->loadProduct($uid) === null || !isset($targets[$field])) { + return new JsonResponse(['success' => false, 'message' => 'Unknown product or field.']); + } + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start([self::TABLE => [(string)$uid => [$field => $value]]], []); + $dataHandler->process_datamap(); + if ($dataHandler->errorLog !== []) { + return new JsonResponse(['success' => false, 'message' => implode(' | ', $dataHandler->errorLog)]); + } + $fresh = (string)($this->loadProduct($uid)[$field] ?? ''); + return new JsonResponse([ + 'success' => true, + 'value' => $fresh, + 'preview' => $this->preview($fresh), + ]); + } + // ------------------------------------------------------------ internals /** @@ -439,6 +531,9 @@ final class ProductTextImportController $view = $this->moduleTemplateFactory->create($request); $this->pageRenderer->addCssFile('EXT:vitec/Resources/Public/Css/backend-import.css'); + // Click-to-edit for the "Current value" column; a module because the + // backend CSP blocks inline handlers. + $this->pageRenderer->loadJavaScriptModule('@evomedien/vitec/product-inline-edit.js'); $view->assignMultiple([ 'product' => $product, 'parsed' => $parsed, diff --git a/packages/vitec/Classes/Import/ProductXlsxReader.php b/packages/vitec/Classes/Import/ProductXlsxReader.php index 1880861..fe9c58f 100644 --- a/packages/vitec/Classes/Import/ProductXlsxReader.php +++ b/packages/vitec/Classes/Import/ProductXlsxReader.php @@ -144,7 +144,7 @@ final class ProductXlsxReader */ public function sources(array $parsed): array { - $groups = ['Meta' => [], 'Components' => [], 'Page SEO Check' => []]; + $groups = ['Meta' => [], 'Components' => [], 'Composed' => [], 'Page SEO Check' => []]; foreach ($parsed['meta'] ?? [] as $key => $value) { $groups['Meta'][] = $this->entry('m:' . $key, 'Meta · ' . $key, (string)$value); @@ -169,6 +169,47 @@ final class ProductXlsxReader $value ); } + + // Section-intro rows ("... — H2") additionally as ONE ready RTE + // value: heading plus paragraph in the exact markup the finished + // MGW-Diamond product stores in `textrelatedproducts` - imported + // and hand-written intros then render identically. + if (str_ends_with($name, '— H2')) { + $pair = $this->composePair($component); + if ($pair !== null) { + $groups['Composed'][] = $this->entry( + 'p:' . $name . '#' . $component['occurrence'], + $name . $suffix . ' · Title+Body as HTML', + $pair + ); + } + } + } + + // Composed sources: a component occurring several times can be pulled + // as ONE value - every occurrence as

+ bullets/paragraph, prefixed + // with the section heading. Built for the Key Capability Groups, whose + // three rows land in the single `capabilities` field; that is the shape + // the finished product pages already use. + $counts = []; + foreach ($parsed['components'] ?? [] as $component) { + $counts[(string)$component['component']] = ((int)($component['occurrence'] ?? 1)); + } + foreach ($counts as $name => $count) { + if ($count < 2 || in_array($name, self::HIDDEN_COMPONENTS, true)) { + continue; + } + $html = $this->composeGroup($parsed, $name); + if ($html !== null) { + $groups['Composed'][] = $this->entry('g:' . $name, $name . ' · all ' . $count . ' as HTML', $html); + } + } + + // Cross-component composite for the SEO title: hero title, em dash, + // introduction heading - "Aligo — High-performance AV over IP and KVM". + $seoTitle = $this->composeSeoTitle($parsed); + if ($seoTitle !== null) { + $groups['Composed'][] = $this->entry('x:seotitle', 'Hero title — Intro heading (SEO title)', $seoTitle); } foreach ($parsed['seo'] ?? [] as $header => $value) { @@ -191,6 +232,20 @@ final class ProductXlsxReader $value = $parsed['meta'][substr($selector, 2)] ?? null; return is_string($value) && $value !== '' ? $value : null; } + if (str_starts_with($selector, 'g:')) { + return $this->composeGroup($parsed, substr($selector, 2)); + } + if (str_starts_with($selector, 'p:') && preg_match('/^p:(.+)#(\d+)$/', $selector, $m)) { + foreach ($parsed['components'] ?? [] as $component) { + if ((string)$component['component'] === $m[1] && (int)$component['occurrence'] === (int)$m[2]) { + return $this->composePair($component); + } + } + return null; + } + if ($selector === 'x:seotitle') { + return $this->composeSeoTitle($parsed); + } if (str_starts_with($selector, 's:')) { $value = $parsed['seo'][substr($selector, 2)] ?? null; return is_string($value) && $value !== '' ? $value : null; @@ -206,6 +261,121 @@ final class ProductXlsxReader return null; } + /** + * SEO title composite: the hero title (the product name), an em dash, and + * the introduction heading. Plain text - `seotitle` is a plain input, no + * escaping wanted here. + * + * @param array $parsed + */ + private function composeSeoTitle(array $parsed): ?string + { + $hero = null; + $intro = null; + foreach ($parsed['components'] ?? [] as $component) { + $name = (string)($component['component'] ?? ''); + if ($name === 'Hero — H1' && (int)($component['occurrence'] ?? 1) === 1) { + $hero = trim((string)($component['title'] ?? '')); + } elseif ($name === 'Product Introduction' && (int)($component['occurrence'] ?? 1) === 1) { + $intro = trim((string)($component['title'] ?? '')); + } + } + if ($hero === null || $hero === '' || $intro === null || $intro === '') { + return null; + } + return $hero . ' — ' . $intro; + } + + /** + * One component row as a heading/paragraph pair. The markup - centred h3 + * with an inner span, centred p - is copied verbatim from what the + * finished MGW-Diamond product carries in `textrelatedproducts`, so the + * front end renders imported intros exactly like the hand-made one. + * + * @param array $component + */ + private function composePair(array $component): ?string + { + $title = trim((string)($component['title'] ?? '')); + $body = trim((string)($component['body'] ?? '')); + if ($body === '') { + $body = trim((string)($component['cta'] ?? '')); + } + $body = trim((string)preg_replace('/\|\s*CTA:.*$/su', '', $body)); + if ($title === '' || $body === '') { + return null; + } + return '

' . htmlspecialchars($title, ENT_QUOTES) . '

' . "\n" + . '

' . htmlspecialchars($body, ENT_QUOTES) . '

'; + } + + /** + * All occurrences of one component, composed into a single HTML block in + * the exact markup the hand-made MGW-Diamond `capabilities` field uses + * (the front end styles only that shape): the immediately preceding + * "... — H2" row (found by ORDER, not by name - "Key Capability Group" vs + * "Key Capabilities — H2" makes name matching fragile) becomes + *

, each occurrence's title a + *

group heading, and a body whose text uses "•" bullets + * becomes a

    . Plain text otherwise. + * + * @param array $parsed + */ + private function composeGroup(array $parsed, string $name): ?string + { + $components = $parsed['components'] ?? []; + $members = array_values(array_filter( + $components, + static fn(array $c): bool => (string)($c['component'] ?? '') === $name + )); + if ($members === []) { + return null; + } + + $html = ''; + $firstOrder = (int)($members[0]['order'] ?? 0); + foreach ($components as $component) { + if ((int)($component['order'] ?? 0) === $firstOrder - 1 + && str_ends_with((string)($component['component'] ?? ''), '— H2') + && trim((string)($component['title'] ?? '')) !== '' + ) { + $html .= '

    ' + . htmlspecialchars(trim((string)$component['title']), ENT_QUOTES) + . '

    ' . "\n"; + break; + } + } + + foreach ($members as $member) { + $title = trim((string)($member['title'] ?? '')); + $body = trim((string)($member['body'] ?? '')); + if ($body === '') { + $body = trim((string)($member['cta'] ?? '')); + } + // Card copy carries a "| CTA: