Prepare the Kantarci Markt opening page with offers from the app
- /eroeffnung (noindex, unlinked): opening hero, offers embedded from app.kantarci-markt.de without title and app button, app store buttons, visit section with route, hours and contact placeholders. - The height script on the embed asks the app for its height once it runs (it is deferred and missed the first report) and scrolls to the offer popup when the app reports it outside the viewport. - audit.mjs takes CMS_HOST_RULES, because the app allows framing only from localhost while the server listens on 127.0.0.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
668287f640
commit
0ba82b7d33
4 files changed
+290
-2
No files matched your search
+3
-1
@@ -18,7 +18,9 @@ const portProbe = net.createServer();
|
||||
await new Promise(resolve => portProbe.listen(0, '127.0.0.1', resolve));
|
||||
const port = portProbe.address().port;
|
||||
await new Promise(resolve => portProbe.close(resolve));
|
||||
const chrome = await chromium.launch({ args: [`--remote-debugging-port=${port}`] });
|
||||
// CMS_HOST_RULES, etwa "MAP localhost 127.0.0.1": für eingebettete Seiten, die das Einbetten nur
|
||||
// von localhost erlauben, während der Server auf 127.0.0.1 lauscht (Kantarci-Markt-App).
|
||||
const chrome = await chromium.launch({ args: [`--remote-debugging-port=${port}`, ...(process.env.CMS_HOST_RULES ? [`--host-resolver-rules=${process.env.CMS_HOST_RULES}`] : [])] });
|
||||
const summary = [];
|
||||
try {
|
||||
for (const path of paths) {
|
||||
|
||||
@@ -518,6 +518,274 @@ $startseite = $einblenden($startseite, [
|
||||
|
||||
$anlegen('home', $startseite, 'Kantarci Markt – Neueröffnung in Baden-Baden', $medien['markt']['id']);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Eröffnungsseite (Demo, 24.09.2026)
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//
|
||||
// So sieht die Startseite aus, wenn der Markt geöffnet hat. Sie liegt vorerst unter
|
||||
// /eroeffnung, ist nicht verlinkt und steht auf noindex: Umut soll sie ansehen können, ohne dass
|
||||
// die Vorab-Seite verschwindet. Zur Eröffnung wandert ihr Inhalt als Entwurf auf die Startseite
|
||||
// (geplante Veröffentlichung), /eroeffnung wird danach gelöscht.
|
||||
//
|
||||
// Die Angebote kommen aus der Web-Ansicht der eigenen App (app.kantarci-markt.de). Die erlaubt
|
||||
// das Einbetten selbst (frame-ancestors) und meldet ihre Höhe per postMessage. Das Skript am
|
||||
// Baustein ist Umuts Gegenstück dazu (24.09.2026). Eine Einwilligung braucht es nicht, solange die
|
||||
// Website keine Einwilligungsleiste hat: Consent::granted() lässt dann jede Gruppe durch.
|
||||
//
|
||||
// Platzhalter, bis Umut die echten Werte schickt: Öffnungszeiten, Telefon, Store-Adressen (die
|
||||
// führen vorerst auf die Suche im jeweiligen Store) und die Store-Badges (eigene Knöpfe statt der
|
||||
// offiziellen Grafiken von Apple und Google).
|
||||
|
||||
// Ohne eigene Überschrift und ohne Knopf zur App: Die Seite hat ihre Überschrift, und Umut will keine Links nach
|
||||
// außen (24.09.2026). Ein Klick auf ein Angebot öffnet ein Popup in der Ansicht. Heller Ton, weil der Abschnitt Sand ist.
|
||||
$angeboteUrl = 'https://app.kantarci-markt.de/embed/angebote.php?titel=0&app=0';
|
||||
$appStoreUrl = 'https://apps.apple.com/de/search?term=Kantarci%20Markt';
|
||||
$playStoreUrl = 'https://play.google.com/store/search?q=Kantarci%20Markt&c=apps';
|
||||
$oeffnungszeiten = [['Montag bis Freitag', '8:00 – 20:00 Uhr'], ['Samstag', '8:00 – 20:00 Uhr'], ['Sonntag und Feiertage', 'geschlossen']];
|
||||
$routeUrl = 'https://www.google.com/maps/dir/?api=1&destination='.rawurlencode($strasse.', '.$plz.' '.$ort);
|
||||
|
||||
// Umuts Gegenstück aus der App, um zwei Dinge ergänzt. Erstens läuft es als eigene Datei (defer)
|
||||
// und damit oft erst, nachdem die App ihre Höhe schon einmal gemeldet hat - danach meldet sie nur
|
||||
// noch Änderungen. Deshalb fragt es nach dem Start nach ('km-angebote-height-request'); antwortet
|
||||
// die App nicht, lädt es den Rahmen als Rückfall einmal neu. Zweitens meldet die App beim Öffnen
|
||||
// eines Angebots, wo ihr Popup steht ('km-angebote-focus'); liegt das außerhalb des Sichtbaren,
|
||||
// scrollt die Seite dorthin.
|
||||
$hoehenSkript = <<<'JS'
|
||||
(function () {
|
||||
var gemeldet = false;
|
||||
function passend(f) {
|
||||
try {
|
||||
var src = new URL(f.src, location.href);
|
||||
return src.pathname.indexOf('/embed/angebote') !== -1 ? src : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
window.addEventListener('message', function (ev) {
|
||||
if (!ev.data || (ev.data.type !== 'km-angebote-height' && ev.data.type !== 'km-angebote-focus')) return;
|
||||
var frames = document.querySelectorAll('iframe');
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
var f = frames[i];
|
||||
if (f.contentWindow !== ev.source) continue;
|
||||
var src = passend(f);
|
||||
if (!src || src.origin !== ev.origin) return;
|
||||
if (ev.data.type === 'km-angebote-focus') {
|
||||
var oben = f.getBoundingClientRect().top + Math.max(0, Math.min(20000, Number(ev.data.top) || 0));
|
||||
var hoehe = Math.max(0, Math.min(20000, Number(ev.data.height) || 0));
|
||||
if (oben < 16 || oben + Math.min(hoehe, window.innerHeight - 32) > window.innerHeight - 16) {
|
||||
var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
window.scrollTo({ top: window.scrollY + oben - 24, behavior: still ? 'auto' : 'smooth' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
var h = Math.max(150, Math.min(20000, Number(ev.data.height) || 0));
|
||||
f.style.height = (h + 2) + 'px';
|
||||
f.style.minHeight = '0';
|
||||
f.style.background = 'transparent';
|
||||
f.setAttribute('scrolling', 'no');
|
||||
gemeldet = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
var frames = document.querySelectorAll('iframe');
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
(function (f) {
|
||||
var src = passend(f);
|
||||
if (!src) return;
|
||||
f.contentWindow.postMessage({ type: 'km-angebote-height-request' }, src.origin);
|
||||
setTimeout(function () { if (!gemeldet) f.src = f.src; }, 600);
|
||||
})(frames[i]);
|
||||
}
|
||||
})();
|
||||
JS;
|
||||
|
||||
/** Ändert Bausteine eines Baums nach ID, ohne ihn neu zu schreiben. */
|
||||
$aendern = static function (array $block, array $plan) use (&$aendern): array {
|
||||
if (isset($plan[$block['id'] ?? ''])) {
|
||||
$block = $plan[$block['id']]($block);
|
||||
}
|
||||
foreach ($block['children'] ?? [] as $i => $kind) {
|
||||
$block['children'][$i] = $aendern($kind, $plan);
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
$setzeText = static fn (string $inhalt): Closure => static function (array $b) use ($inhalt): array {
|
||||
$b['props']['text'] = $inhalt;
|
||||
|
||||
return $b;
|
||||
};
|
||||
|
||||
/** Ein Store-Knopf: schwarz, zweizeilig, wie die Badges der Stores, aber ohne deren Logos. */
|
||||
$storeKnopf = static fn (string $id, string $oben, string $unten, string $href): array => $text($id,
|
||||
'<a href="'.htmlspecialchars($href, ENT_QUOTES).'">'.$oben.'<br><strong>'.$unten.'</strong></a>',
|
||||
['desktop' => ['margin' => ['bottom' => '0px']]],
|
||||
"& a{display:inline-block;min-width:176px;padding:9px 20px 10px;border-radius:9px;background:#000;border:1px solid #a6a6a6;color:#fff;font-size:12px;line-height:1.25;text-decoration:none}\n"
|
||||
."& a:hover{background:#123b26;border-color:#d2b978}\n"
|
||||
."& a:focus-visible{outline:3px solid #ad893b;outline-offset:3px}\n"
|
||||
.'& strong{display:block;font-size:21px;font-weight:500;letter-spacing:-0.01em}');
|
||||
|
||||
$abschnittTitel = static fn (string $id, string $klein, string $titel, string $farbeKlein, string $farbeTitel, string $strich = '@gold', string $farbeBetont = '#8c6b27') => [
|
||||
$kleinzeile($id.'-label', $klein, $farbeKlein, $strich),
|
||||
$heading($id.'-h2', $titel, 2, [
|
||||
'desktop' => ['fontSize' => '64px', 'fontWeight' => '400', 'lineHeight' => 1.06, 'letterSpacing' => '-0.035em', 'color' => $farbeTitel, 'margin' => ['bottom' => '22px']],
|
||||
'tablet' => ['fontSize' => '52px'],
|
||||
'mobile' => ['fontSize' => '40px'],
|
||||
], '& em{font-style:italic;color:'.$farbeBetont.'}'),
|
||||
];
|
||||
|
||||
$eroeffnung = [
|
||||
'schemaVersion' => $SCHEMA,
|
||||
'title' => 'Jetzt geöffnet',
|
||||
'description' => 'Kantarci Markt in der Wilhelm-Drapp-Straße in Baden-Baden: frische Produkte, echte Vielfalt, faire Preise und jede Woche neue Angebote.',
|
||||
'blocks' => [
|
||||
// 1 – Hero: wie die Vorab-Seite, aber mit Öffnungszeiten und zwei Wegen weiter.
|
||||
$aendern($startseite['blocks'][0], [
|
||||
'hero-label-text' => $setzeText('Jetzt geöffnet'),
|
||||
'hero-titel' => $setzeText('Frische hat jetzt<br><em>eine Adresse.</em>'),
|
||||
'hero-intro' => $setzeText('Sorgfältig ausgewählte Produkte, echte Vielfalt und faire Preise – ab sofort in der Wilhelm-Drapp-Straße in Baden-Baden.'),
|
||||
'hero-termin-icon' => static function (array $b): array {
|
||||
$b['props']['icon'] = 'clock';
|
||||
|
||||
return $b;
|
||||
},
|
||||
'hero-termin-klein' => $setzeText('Heute geöffnet'),
|
||||
'hero-termin-satz' => $setzeText($oeffnungszeiten[0][1]),
|
||||
'hero-adresse' => $setzeText('So finden Sie uns<br><strong>'.$strasse.'</strong><br>'.$plz.' '.$ort),
|
||||
'hero-markt' => static function (array $b): array {
|
||||
$b['props']['caption'] = 'Frische Vielfalt, jeden Tag.';
|
||||
|
||||
return $b;
|
||||
},
|
||||
'hero-text' => static function (array $b) use ($zeile, $button): array {
|
||||
// Die beiden Wege stehen zwischen Einleitung und Öffnungszeiten.
|
||||
array_splice($b['children'], 3, 0, [$zeile('hero-wege', [
|
||||
$button('hero-angebote', 'Aktuelle Angebote', '#angebote'),
|
||||
['id' => 'hero-besuch', 'type' => 'button', 'props' => ['text' => 'Anfahrt & Öffnungszeiten', 'href' => '#besuch'],
|
||||
'css' => "background:transparent;color:#123b26;box-shadow:inset 0 0 0 1px #123b26\n& span{display:none}\n&:hover{background:#123b26;color:#fff}"],
|
||||
], '14px', ['flexWrap' => 'wrap', 'margin' => ['top' => '0px', 'bottom' => '30px']])]);
|
||||
|
||||
return $b;
|
||||
},
|
||||
]),
|
||||
|
||||
// 2 – Angebote aus der App, auf Sand: Die Web-Ansicht ist für hellen Grund gestaltet
|
||||
// (dunkle Überschrift, weiße Karten); `ton` ändert daran derzeit nichts (24.09.2026).
|
||||
$section('angebote-bereich', [
|
||||
$row('angebote-kopf', [
|
||||
$column('angebote-kopf-titel', $abschnittTitel('angebote', 'Diese Woche', 'Aktuelle<br><em>Angebote.</em>', '@gold-text', '@forst')),
|
||||
$column('angebote-kopf-text', [
|
||||
$text('angebote-intro', 'Direkt aus unserer App – jede Woche neu. Die Angebote gelten im Markt, solange der Vorrat reicht.', [
|
||||
'desktop' => ['maxWidth' => '420px', 'fontSize' => '17px', 'lineHeight' => 1.65, 'color' => '@text', 'margin' => ['bottom' => '0px']],
|
||||
]),
|
||||
], ['desktop' => ['alignSelf' => 'flex-end']]),
|
||||
], ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '40px', 'alignItems' => 'flex-end', 'margin' => ['top' => '0px', 'bottom' => '44px']],
|
||||
'mobile' => ['gap' => '8px', 'margin' => ['bottom' => '32px']],
|
||||
]),
|
||||
['id' => 'angebote-app', 'type' => 'embed',
|
||||
'props' => ['group' => 'external', 'title' => 'Aktuelle Angebote von Kantarci Markt', 'provider' => 'Kantarci-Markt-App', 'url' => $angeboteUrl, 'hint' => '', 'height' => 900],
|
||||
// Die Höhe setzt das Skript, sobald die App sie meldet; bis dahin gelten die 900 px.
|
||||
'css' => '& iframe{border-radius:0;background:transparent}',
|
||||
'script' => $hoehenSkript],
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@sand', 'padding' => ['top' => '110px', 'bottom' => '110px'], 'borderWidth' => ['top' => '1px', 'right' => '0px', 'bottom' => '1px', 'left' => '0px'], 'borderStyle' => 'solid', 'borderColor' => '@gold'],
|
||||
'tablet' => ['padding' => ['top' => '84px', 'bottom' => '84px']],
|
||||
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '64px']],
|
||||
], 'angebote'),
|
||||
|
||||
// 3 – Die App: Text und Store-Knöpfe links, die Bildmarke als App-Symbol rechts.
|
||||
$section('app-bereich', [
|
||||
$row('app-zeile', [
|
||||
$column('app-text', [
|
||||
...$abschnittTitel('app', 'Die Kantarci-Markt-App', 'Angebote immer<br>in der Tasche.', '@gold-text', '@forst'),
|
||||
$text('app-intro', 'Mit unserer App haben Sie die aktuellen Angebote jederzeit dabei – auch unterwegs und vor dem Einkauf.', [
|
||||
'desktop' => ['maxWidth' => '520px', 'fontSize' => '18px', 'lineHeight' => 1.65, 'color' => '@text', 'margin' => ['bottom' => '30px']],
|
||||
'mobile' => ['fontSize' => '17px'],
|
||||
]),
|
||||
$zeile('app-stores', [
|
||||
$storeKnopf('app-ios', 'Laden im', 'App Store', $appStoreUrl),
|
||||
$storeKnopf('app-android', 'Jetzt bei', 'Google Play', $playStoreUrl),
|
||||
], '14px', ['flexWrap' => 'wrap']),
|
||||
]),
|
||||
$column('app-symbol', [
|
||||
$bild('app-symbol-bild', $medien['symbol'], 'App-Symbol von Kantarci Markt', [
|
||||
'desktop' => ['maxWidth' => '200px', 'width' => '200px', 'margin' => $seiten('0px'), 'rotate' => -6,
|
||||
'shadowY' => '24px', 'shadowBlur' => '50px', 'shadowColor' => '#062f2440', 'borderRadius' => $rund('44px'), 'overflow' => 'hidden'],
|
||||
'mobile' => ['maxWidth' => '140px', 'width' => '140px', 'borderRadius' => $rund('31px')],
|
||||
], null, '& img{display:block;width:100%;height:auto}'),
|
||||
], ['desktop' => ['display' => 'flex', 'justifyContent' => 'center', 'alignItems' => 'center']]),
|
||||
], ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '60px', 'alignItems' => 'center', 'margin' => ['top' => '0px'], 'columnWidths' => '3 2'],
|
||||
'mobile' => ['gap' => '44px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@creme', 'padding' => ['top' => '110px', 'bottom' => '110px']],
|
||||
'tablet' => ['padding' => ['top' => '84px', 'bottom' => '84px']],
|
||||
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '72px']],
|
||||
], 'app'),
|
||||
|
||||
// 4 – Das Versprechen, wie auf der Vorab-Seite.
|
||||
$startseite['blocks'][1],
|
||||
|
||||
// 5 – Besuch: Adresse mit Route, Öffnungszeiten, Kontakt.
|
||||
$section('besuch-bereich', [
|
||||
$row('besuch-kopf', [
|
||||
$column('besuch-titel', $abschnittTitel('besuch', 'Besuchen Sie uns', 'Wir freuen uns<br>auf Sie.', '@gold-text', '@forst')),
|
||||
], [], ['desktop' => ['margin' => ['top' => '0px', 'bottom' => '40px']]]),
|
||||
$row('besuch-spalten', [
|
||||
$column('besuch-adresse', [
|
||||
$heading('besuch-adresse-h3', 'Adresse', 3, ['desktop' => ['fontFamily' => 'body', 'fontSize' => '14px', 'fontWeight' => '700', 'letterSpacing' => '0.18em', 'textTransform' => 'uppercase', 'color' => '@gold-text', 'margin' => ['bottom' => '14px']]]),
|
||||
$text('besuch-adresse-text', $name.'<br>'.$strasse.'<br>'.$plz.' '.$ort, ['desktop' => ['fontSize' => '18px', 'lineHeight' => 1.6, 'color' => '@forst', 'margin' => ['bottom' => '22px']]]),
|
||||
['id' => 'besuch-route', 'type' => 'button', 'props' => ['text' => 'Route planen', 'href' => $routeUrl, 'newTab' => true], 'css' => '& span{display:none}'],
|
||||
]),
|
||||
$column('besuch-zeiten', [
|
||||
$heading('besuch-zeiten-h3', 'Öffnungszeiten', 3, ['desktop' => ['fontFamily' => 'body', 'fontSize' => '14px', 'fontWeight' => '700', 'letterSpacing' => '0.18em', 'textTransform' => 'uppercase', 'color' => '@gold-text', 'margin' => ['bottom' => '14px']]]),
|
||||
$text('besuch-zeiten-text', implode('<br>', array_map(static fn (array $z): string => '<strong>'.$z[0].'</strong>'.$z[1], $oeffnungszeiten)), [
|
||||
'desktop' => ['fontSize' => '17px', 'lineHeight' => 1.6, 'color' => '@text', 'margin' => ['bottom' => '0px']],
|
||||
], "& strong{display:block;margin-top:12px;color:#123b26;font-weight:700}\n& strong:first-child{margin-top:0}"),
|
||||
]),
|
||||
$column('besuch-kontakt', [
|
||||
$heading('besuch-kontakt-h3', 'Kontakt', 3, ['desktop' => ['fontFamily' => 'body', 'fontSize' => '14px', 'fontWeight' => '700', 'letterSpacing' => '0.18em', 'textTransform' => 'uppercase', 'color' => '@gold-text', 'margin' => ['bottom' => '14px']]]),
|
||||
$text('besuch-kontakt-text', 'Telefon folgt<br>E-Mail: <a href="mailto:'.$mail.'">'.$mail.'</a>', [
|
||||
'desktop' => ['fontSize' => '17px', 'lineHeight' => 1.9, 'color' => '@text', 'margin' => ['bottom' => '0px']],
|
||||
], $linkCss),
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'three'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '48px', 'alignItems' => 'flex-start', 'margin' => ['top' => '0px'], 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#e4dcc8', 'padding' => ['top' => '36px']],
|
||||
'mobile' => ['gap' => '36px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@creme', 'padding' => ['top' => '110px', 'bottom' => '110px']],
|
||||
'tablet' => ['padding' => ['top' => '84px', 'bottom' => '84px']],
|
||||
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '72px']],
|
||||
], 'besuch'),
|
||||
|
||||
// 6 – Abschluss wie auf der Vorab-Seite, mit neuem Gruß.
|
||||
$aendern($startseite['blocks'][2], [
|
||||
'abschluss-gruss-text' => $setzeText('Danke, dass Sie da sind.'),
|
||||
'abschluss-gruss-satz' => $setzeText('Bis gleich im Kantarci Markt.'),
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$eroeffnung = $einblenden($eroeffnung, [
|
||||
'hero-label' => ['fade'],
|
||||
'hero-titel' => ['rise', 'short'],
|
||||
'hero-intro' => ['rise', 'medium'],
|
||||
'hero-wege' => ['rise', 'long'],
|
||||
'hero-termin' => ['rise', 'long'],
|
||||
'hero-adresse' => ['fade', 'long'],
|
||||
'app-text' => ['rise'],
|
||||
'app-symbol' => ['rise', 'short'],
|
||||
'versprechen-intro' => ['rise'],
|
||||
'versprechen-liste' => ['rise', 'short'],
|
||||
'abschluss-spruch' => ['rise'],
|
||||
'abschluss-gruss' => ['rise', 'short'],
|
||||
]);
|
||||
|
||||
$eroeffnungId = $anlegen('eroeffnung', $eroeffnung, 'Kantarci Markt – frisch, vielfältig, fair in Baden-Baden', $medien['markt']['id']);
|
||||
// Nur zum Ansehen: nicht in Suchmaschinen, nicht in der Sitemap, nirgends verlinkt.
|
||||
$db->executeStatement('UPDATE page SET robots = ? WHERE id = ?', ['noindex', $eroeffnungId]);
|
||||
|
||||
/** Eine Rechtsseite: Kleinzeile, Titel und Vorspann auf Waldgrün, dann Absätze mit Zwischenüberschriften. */
|
||||
$rechtsseite = static function (string $praefix, string $label, string $titel, string $vorspann, array $absaetze) use ($section, $row, $column, $kleinzeile, $heading, $text, $nurOben, $linkCss): array {
|
||||
$kopf = $section($praefix.'-kopf', [
|
||||
|
||||
Reference in new issue
Block a user