diff --git a/VERSION b/VERSION index 0eed1a2..f8f4f03 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.0 +1.12.1 diff --git a/docs/QUALITY.md b/docs/QUALITY.md index 3713a2e..e84d265 100644 --- a/docs/QUALITY.md +++ b/docs/QUALITY.md @@ -1,5 +1,13 @@ # Qualitätsbericht – erster ausführbarer Prototyp +## Ergänzung: Elektro Bernhard gemessen, 14.09.2026 (1.12.1) + +Lighthouse **13** (`scripts/audit.mjs`, je drei Läufe Mobil und Desktop, Mediane) gegen den lokalen Produktionsmodus der Referenzwebsite Elektro Bernhard (`var/elektro-bernhard.db`, Port 8090), vier Adressen: `/`, `/leistungen`, `/leistungen/notdienst`, `/kontakt`. Ergebnis nach den Korrekturen: **Barrierefreiheit, Best Practices, SEO und agentisches Browsing überall 100**, Leistung überall 100 bis auf die mobile Startseite mit **99** (LCP 2,1 s durch das Hero-Foto als Hintergrund unter Mobildrosselung; CLS 0, TBT 30 ms). Berichte unter `reports/lighthouse-elektro-bernhard-2/`; der erste Lauf davor (`reports/lighthouse-elektro-bernhard/`) zeigte Barrierefreiheit 95–97. + +axe (WCAG 2.0/2.1 A+AA und Best-Practice-Regeln, Playwright, 1440 und 390 px) auf sechs Seiten: **null Verstöße**. Vorher gab es drei echte Befunde, alle behoben: Markennamen `#9aa0a6` auf Weiß (2,6:1 → `#6b7178`, 4,7:1), der rote Link „Kant-IT Solutions“ im dunklen Fuß (3,0:1 → hell), und auf der Leistungsübersicht sprang die Gliederung von H1 auf H3 (jetzt eine H2 vor den Karten). Ein vierter Befund lag im CMS: der Pause-Knopf des Sliders hatte den sichtbaren Text „Pause“, aber den zugänglichen Namen „Automatische Wiedergabe pausieren“ (WCAG 2.5.3); der Name beginnt jetzt mit dem sichtbaren Text. Achtung beim Messen: unmittelbar nach dem Laden meldet axe Kontrastverstöße an Elementen, die gerade eingeblendet werden (halb transparente Zwischenzustände); erst nach dem Durchscrollen und zwei Sekunden Wartezeit sind die Befunde belastbar. + +Maschinenlesbar: `llms.txt`, `robots.txt`, `sitemap.xml` werden ausgeliefert, `LocalBusiness` mit drei `sameAs`-Profilen auf der Startseite, `BreadcrumbList` auf Unterseiten, `og:image` auf allen Inhaltsseiten. Nicht gemessen: Felddaten (LCP/INP/CLS am 75. Perzentil), Screenreader, Firefox/Safari, das tatsächliche Hosting. + ## Ergänzung: Sichtbare leere Layouts, 14.09.2026 (1.6.2) 30 JavaScript-Tests und 27 PHP-Editor-/Sicherheitstests mit 312 Assertions bestanden. Vollständiger isolierter Chromium-Editortest einschließlich axe ohne Verstöße: direkte Tastatureinfügung in leere Sektionen/Spalten, genaues Drag-and-drop-Ziel und Scrollen am Rand, Spaltenreihenfolge auf Desktop und Mobil, Undo/Redo, Speichern/Neuladen, Veröffentlichung und Konflikte. Die Tests prüfen ausdrücklich, dass die neuen Editorhilfen weder im gespeicherten Blockbaum noch im öffentlichen HTML stehen. Desktop-, Mobil- und Drag-Screenshots visuell geprüft; Bericht unter `reports/editor/result.json`. diff --git a/docs/STATUS.md b/docs/STATUS.md index 271f8d8..814cb12 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,13 @@ # Projektstatus +## 1.12.1 – Elektro Bernhard gemessen: Lighthouse und axe (14.09.2026) + +**Anlass:** Umut fragte nach Google-Bewertung, SEO, llms.txt und Barrierefreiheit der neuen Website. + +**Ergebnis:** Lighthouse-Mediane auf vier Seiten, Mobil und Desktop: alles 100, nur die mobile Startseite hat Leistung 99 (LCP 2,1 s durch das Hero-Foto). axe auf sechs Seiten in zwei Breiten: null Verstöße. Details und Messbedingungen in `docs/QUALITY.md`. + +**Behoben:** drei Kontrast-/Gliederungsbefunde in der Website (Skript) und einer im CMS: der Pause-Knopf des Sliders verletzte WCAG 2.5.3 (sichtbarer Text „Pause“, Name „Automatische Wiedergabe pausieren“); `slider.js` bildet den Namen jetzt aus dem sichtbaren Text, `scripts/admin-quality-check.mjs` folgt. + ## 1.12.0 – Soziale Profile als Baustein; Elektro Bernhard neu gestaltet (14.09.2026) **Anlass:** Umut fand den ersten Nachbau „grauenhaft“, fragte nach dem transparenten Menü und vermisste Social-Icons im CMS – „gescheit“, also einmal zentral gepflegt und überall mit eigener Größe und Farbe einsetzbar. diff --git a/public/assets/slider.js b/public/assets/slider.js index b5add42..ad53506 100644 --- a/public/assets/slider.js +++ b/public/assets/slider.js @@ -76,7 +76,8 @@ export function initSliders(doc = document) { dots.forEach((dot, i) => { dot.hidden = !visible.includes(i); dot.setAttribute('aria-pressed', String(i === index)); }); if (play) { play.textContent = playing ? 'Pause' : 'Wiedergabe'; play.dataset.playing = String(playing); - play.setAttribute('aria-label', playing ? 'Automatische Wiedergabe pausieren' : 'Automatische Wiedergabe starten'); + // Der zugängliche Name beginnt mit dem sichtbaren Text (WCAG 2.5.3): wer „Pause“ liest, kann „Pause“ sagen. + play.setAttribute('aria-label', playing ? 'Pause: automatische Wiedergabe anhalten' : 'Wiedergabe: automatisch weiterblättern'); play.disabled = reduced(); play.title = reduced() ? 'Automatik ist bei reduzierter Bewegung deaktiviert.' : ''; } }; diff --git a/scripts/admin-quality-check.mjs b/scripts/admin-quality-check.mjs index a87bb8d..9267872 100644 --- a/scripts/admin-quality-check.mjs +++ b/scripts/admin-quality-check.mjs @@ -72,11 +72,11 @@ try { await page.mouse.move(1, 1); await page.waitForTimeout(7200); console.log('Paused slider:', JSON.stringify(await geometry()), await controls.innerText()); assert.ok(Math.abs((await geometry()).left - manual.left) < 2, 'manual navigation pauses autoplay'); - await controls.getByRole('button', { name: 'Automatische Wiedergabe starten' }).click(); + await controls.getByRole('button', { name: 'Wiedergabe: automatisch weiterblättern' }).click(); await page.mouse.move(1, 1); await page.waitForTimeout(7700); assert.ok(Math.abs((await geometry()).left - manual.left) > 20, 'play resumes after manual operation'); await page.emulateMedia({ reducedMotion: 'reduce' }); - assert.equal(await controls.getByRole('button', { name: 'Automatische Wiedergabe starten' }).isDisabled(), true); + assert.equal(await controls.getByRole('button', { name: 'Wiedergabe: automatisch weiterblättern' }).isDisabled(), true); const reduced = await geometry(); await page.waitForTimeout(7100); assert.ok(Math.abs((await geometry()).left - reduced.left) < 2, 'reduced motion stops autoplay'); await controls.getByRole('button', { name: 'Vorherige Folie', exact: true }).click(); diff --git a/scripts/seed-elektro-bernhard.php b/scripts/seed-elektro-bernhard.php index da22f57..1ff516a 100644 --- a/scripts/seed-elektro-bernhard.php +++ b/scripts/seed-elektro-bernhard.php @@ -382,7 +382,7 @@ $fuss = [ $column('fuss-schluss-links', [$text('fuss-copyright', '© 2026 Elektro Bernhard GmbH', ['desktop' => ['color' => '#8f959c', 'fontSize' => '13px', 'lineHeight' => 1.8]])]), $column('fuss-schluss-rechts', [ ['id' => 'fuss-rechtliches', 'type' => 'legal', 'props' => ['label' => 'Rechtliches'], 'style' => ['desktop' => ['color' => '#b3b8bf', 'fontSize' => '13px', 'justifyContent' => 'flex-end', 'padding' => ['right' => '72px']], 'mobile' => ['justifyContent' => 'flex-start', 'padding' => ['right' => '0px']]]], - $text('fuss-urheber', 'Website von Kant-IT Solutions', ['desktop' => ['color' => '#8f959c', 'fontSize' => '13px', 'textAlign' => 'right', 'margin' => ['top' => '6px'], 'padding' => ['right' => '72px']], 'mobile' => ['textAlign' => 'left', 'padding' => ['right' => '0px']]]), + [...$text('fuss-urheber', 'Website von Kant-IT Solutions', ['desktop' => ['color' => '#8f959c', 'fontSize' => '13px', 'textAlign' => 'right', 'margin' => ['top' => '6px'], 'padding' => ['right' => '72px']], 'mobile' => ['textAlign' => 'left', 'padding' => ['right' => '0px']]]), 'css' => '& a{color:#e8eaed}'], ]), ], ['desktop' => ['columns' => 'two', 'gap' => 'medium'], 'mobile' => ['columns' => 'one']], ['desktop' => ['margin' => ['top' => '0px']]]), ], ['desktop' => ['backgroundColor' => '@nacht', 'color' => '#b3b8bf', 'padding' => ['top' => '72px', 'bottom' => '24px']], 'mobile' => ['padding' => ['top' => '52px', 'bottom' => '24px']]]), @@ -632,7 +632,7 @@ $startseite = [ [$column('marken-label-spalte', [$text('marken-label', 'Wir arbeiten mit bewährten Marken', ['desktop' => ['color' => '@gedaempft', 'fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '0.12em', 'textTransform' => 'uppercase']])])], array_map(static fn (string $name, int $i): array => [ 'id' => 'marke-'.($i + 1), 'type' => 'column', - 'children' => [['id' => 'marke-name-'.($i + 1), 'type' => 'text', 'props' => ['text' => $name], 'style' => ['desktop' => ['fontSize' => '24px', 'fontWeight' => '900', 'letterSpacing' => '-0.04em', 'color' => '#9aa0a6', 'textAlign' => 'center'], 'mobile' => ['fontSize' => '20px']]]], + 'children' => [['id' => 'marke-name-'.($i + 1), 'type' => 'text', 'props' => ['text' => $name], 'style' => ['desktop' => ['fontSize' => '24px', 'fontWeight' => '900', 'letterSpacing' => '-0.04em', 'color' => '#6b7178', 'textAlign' => 'center'], 'mobile' => ['fontSize' => '20px']]]], ], $marken, array_keys($marken)), ), [], ['desktop' => ['display' => 'flex', 'flexWrap' => 'wrap', 'alignItems' => 'center', 'justifyContent' => 'space-between', 'gap' => '40px', 'margin' => ['top' => '0px']], 'mobile' => ['gap' => '22px', 'justifyContent' => 'center']]), ], ['desktop' => ['padding' => ['top' => '40px', 'bottom' => '40px'], 'borderWidth' => ['top' => '1px', 'right' => '0px', 'bottom' => '1px', 'left' => '0px'], 'borderStyle' => 'solid', 'borderColor' => '@linie'], 'mobile' => ['padding' => ['top' => '32px', 'bottom' => '32px']]]), @@ -713,6 +713,7 @@ $leistungenId = $anlegen('leistungen', null, [ 'blocks' => [ $seitenkopf('leistungen-seite', 'Unsere Leistungen', 'Elektrotechnik aus einer Hand', 'Wir übernehmen Beratung, Planung und Installation. Auch nach Abschluss der Arbeiten erreichen Sie uns bei Fragen oder Störungen.'), $section('leistungen-seite-raster-abschnitt', [ + $fx($heading('leistungen-seite-h2', 'Vier Leistungen, ein Ansprechpartner', 2, ['desktop' => [...$h2Stil['desktop'], 'margin' => ['bottom' => '36px']], 'mobile' => $h2Stil['mobile']]), 'rise'), $row('leistungen-seite-raster', array_map( static fn (array $l, int $i): array => [...$karte('leistungen-seite-'.$l[0], $l[1], $l[2], $l[3], '/leistungen/'.$l[0]), 'layout' => ['effects' => ['shadow' => 'soft', 'hover' => 'lift', 'entrance' => 'rise', 'delay' => ['none', 'short', 'medium', 'long'][$i]]]], $leistungen, array_keys($leistungen),