fix: remove automatically appended footer legal links (1.8.1)
This commit is contained in:
1 parent
ab86312e87
commit
8f475d1374
12 files changed
+40
-62
No files matched your search
@@ -30,6 +30,8 @@ Ohne veröffentlichte Auswahl erscheint automatisch eine Standard-404 im Website
|
||||
|
||||
## Anmelden und bearbeiten
|
||||
|
||||
**Footer-Links (ab 1.8.1):** Datenschutz, Impressum und weitere Links werden selbst im Footer oder Seiteninhalt platziert. Das CMS hängt keine zusätzliche Rechtszeile an. Der optionale Baustein „Rechtliches“ verwendet die Adressen aus den Einstellungen und erscheint nur an seiner eingefügten Position.
|
||||
|
||||
Für eine neue Installation im interaktiven Terminal ein Konto anlegen:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Stand: 14.09.2026. Arbeitstitel KantSites; Produktname offen.
|
||||
|
||||
## ADR 083 – Footer-Links werden redaktionell platziert (14.09.2026)
|
||||
|
||||
Auf ausdrücklichen Wunsch von Umut entfällt die automatisch angehängte Rechtszeile vollständig. Seiten, Beiträge, Archive, Fehlerseiten und Editorvorschauen geben ausschließlich die selbst platzierten Footer-/Inhaltslinks aus. Die frühere Erkennung des speziellen `legal`-Blocks konnte normale Text- oder Menülinks nicht berücksichtigen; dadurch erschien unter einem vollständig gestalteten Footer ein zusätzlicher heller Streifen mit doppelten Links. Die Template-Ausgabe, ihre Erkennung im Controller und ungenutztes CSS sind entfernt.
|
||||
|
||||
Normale Links, Menüs und bewusst eingefügte „Rechtliches“-Blöcke bleiben erhalten. Der optionale Block nutzt weiterhin die konfigurierten Seitenadressen und den Link zur Einwilligung. Einwilligungsdialog, Einstellungen und der Endpunkt `/einwilligung` behalten ihre Funktion. Keine Inhalte werden umgeschrieben, keine neue Schema- oder Datenbankmigration. Bei einem Rückweg auf ältere Versionen erscheint deren automatische Leiste wieder.
|
||||
|
||||
Diese Entscheidung ersetzt die automatische Verlinkung aus ADR 033 und die bedingte Zusatzleiste aus ADR 062; Position und Gestaltung der Links liegen beim Autor.
|
||||
|
||||
## ADR 082 – Zuweisbare 404-Seite im gemeinsamen Seitenrenderer (14.09.2026)
|
||||
|
||||
`website.settings.notFoundPageId` verweist auf eine normale Seite über deren stabile ID. Eine eigene Einstellung mit Livesuche weist sie zu oder entfernt die Zuordnung; sie benötigt Veröffentlichungsrechte und CSRF-Schutz. Der erwartete bisherige Wert schützt gegen ein veraltetes Formular. Die Änderung liest die aktuellen Einstellungen innerhalb einer Transaktion, unter MySQL/MariaDB mit Zeilensperre, und schreibt nur die geänderte Zuordnung in den erhaltenen JSON-Inhalt. Das normale Unternehmensformular erhält diesen Wert unabhängig von seinen eigenen Feldern. Startseite und 404-Seite dürfen nicht dieselbe Seite sein.
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Projektstatus
|
||||
|
||||
## 1.8.1 – Keine automatische Rechtszeile unter dem Footer (14.09.2026)
|
||||
|
||||
**Behoben:** Das CMS hängt Datenschutz, Impressum und Einwilligungslink nicht mehr automatisch an den Footer an. Entfernt aus Seiten-/Archiv-/Fehlervorlagen und damit auch aus Seiten- und Bereichsvorschauen. Normale Textlinks, Menüs und ausdrücklich eingefügte „Rechtliches“-Bausteine bleiben erhalten; der Editorhinweis beschreibt die freie Platzierung. Keine Inhaltsänderung oder Migration (ADR 083).
|
||||
|
||||
**Geprüft:** PHP-Vollsuite 477 Tests / 3815 Assertions und 32 JavaScript-Tests erfolgreich. Vorhandene Tests an die redaktionelle Platzierung angepasst; freiwilliger Rechtsblock und Einwilligungsablauf weiterhin geprüft. 47 Twig-Dateien und Produktionscontainer gültig, PHP-Stilprüfung der drei betroffenen PHP-Dateien ohne Änderungen, Git-Diff ohne Whitespacefehler.
|
||||
|
||||
**Browser:** Isolierte Kant-IT-Kopie mit bestehenden Rechtsblöcken sowie einer Variante mit normalen Footer-Textlinks wie im Screenshot. Acht Prüfgruppen erfolgreich: Startseite, Beitragsarchiv und 404 auf Desktop 1600/Mobil 390 sowie Seiten- und Footer-Editor. Eigene Links erhalten, keine Zusatzleiste, kein horizontaler Überlauf. Screenshots unter `reports/footer-links/` visuell geprüft. Gegenprobe mit dem unveränderten Release 1.8.0 reproduziert die doppelte Leiste auf allen sechs öffentlichen Ansichten (`reports/footer-links-before/`). Originaldaten bleiben unverändert.
|
||||
|
||||
**Bekannte Prüfbefunde:** Die Variante mit gewöhnlichen, unformatierten Textlinks meldet Kontrastbefunde an diesen Links; bei sichtbarem Nach-oben-Knopf zusätzlich einen Landmark-Befund. Das Archiv meldet außerdem Überschriftenreihenfolge und doppelte Landmark-Benennung. Alle Befunde betreffen dieselben Elemente wie in der Gegenprobe mit 1.8.0. Keine fehlerfreie Gesamt-Barrierefreiheit oder neuen Lighthouse-Werte behauptet. Keine Wiederholung der gesamten Editor-Bedienprüfung für diese Template-Entfernung.
|
||||
|
||||
**Veröffentlichung:** Release 1.8.1 wird aus dem geprüften Commit gebaut und auf dem vorhandenen Git bereitgestellt. Danach über die Systemverwaltung aktualisieren; Kundenhostings werden hier nicht verändert.
|
||||
|
||||
## 1.8.0 – Eigene 404-Seite mit Website-Design (14.09.2026)
|
||||
|
||||
**Umgesetzt:** Einstellungen → 404-Seite mit Livesuche, bestehende Seite zuweisen, Auswahl zurücksetzen, direktem Bearbeiten und öffentlicher 404-Vorschau. „404-Vorlage anlegen“ erstellt und verknüpft einen bearbeitbaren Entwurf, ohne vorhandene Seiten zu überschreiben. Bis zur Veröffentlichung erscheint eine automatische Standard-404. Normaler Seitenrenderer übernimmt Standard-Header/-Footer, Theme, Schriften, Menüs und verknüpfte Sektionen. Die ID-Verknüpfung übersteht Umbenennen; Löschen führt auf den Standard zurück. Nur veröffentlichte Revisionen werden öffentlich ausgeliefert.
|
||||
|
||||
@@ -328,7 +328,7 @@ function renderProperties() {
|
||||
field('Beim Laden geöffnet', 'open', block.props.open, { checkbox: true });
|
||||
}
|
||||
if (block.type === 'legal') {
|
||||
field('Beschriftung für Vorlesesoftware', 'label', block.props?.label || 'Rechtliches', { maxLength: 60, help: 'Der Baustein zeigt Impressum, Datenschutz und – sobald jemand gefragt wurde – den Weg zurück zur Einwilligung. Die Adressen stehen in den Einstellungen. Steht er im Fußbereich, entfällt die eingebaute Zeile am Seitenende.' });
|
||||
field('Beschriftung für Vorlesesoftware', 'label', block.props?.label || 'Rechtliches', { maxLength: 60, help: 'Der Baustein zeigt Impressum, Datenschutz und – sobald jemand gefragt wurde – den Weg zurück zur Einwilligung. Die Adressen stehen in den Einstellungen. Platziere ihn dort, wo die Links erscheinen sollen, zum Beispiel im Fußbereich.' });
|
||||
}
|
||||
if (block.type === 'postnav') {
|
||||
field('Beschriftung zurück', 'previousLabel', block.props.previousLabel, { maxLength: 60 });
|
||||
|
||||
@@ -162,12 +162,6 @@ body.consent-open{padding-bottom:260px}
|
||||
.site-footer .block-column{border:0;border-radius:0;padding:0}
|
||||
.site-header .block-text,
|
||||
.site-footer .block-text{margin:0}
|
||||
/* One landmark for the three links that must be on every page. Loose text outside a landmark is
|
||||
also what axe objects to, and rightly: a screen reader then has no way to skip to it. */
|
||||
.legal-bar{padding:16px 20px;text-align:center;font-size:12px;color:var(--muted);background:color-mix(in srgb,var(--ink) 6%,var(--paper))}
|
||||
.legal-bar a{color:var(--accent);text-decoration:none}
|
||||
.legal-bar a:hover{text-decoration:underline}
|
||||
|
||||
/* An image takes the rounding of its figure. Without this a radius on an image block rounded the
|
||||
figure and left the picture square - which looked like the setting did nothing. */
|
||||
.block-image img, .block-gallery img, .block-logos img { border-radius: inherit }
|
||||
|
||||
@@ -131,11 +131,6 @@ final class PageController extends AbstractController
|
||||
'googleFonts' => $googleFonts,
|
||||
'localFonts' => $localFonts,
|
||||
'styleBundle' => $this->bundle->url([] !== $localFonts),
|
||||
// Steht die Rechtszeile als Baustein im Dokument oder in einem Bereich, entfaellt
|
||||
// die eingebaute Leiste am Seitenende - zweimal dasselbe waere schlechter als gar
|
||||
// nichts, und die Pflicht ist mit dem Baustein erfuellt.
|
||||
'hasLegalBlock' => $this->hasType($page['document'], 'legal')
|
||||
|| (bool) array_filter(array_values($areas), fn (array $area): bool => $this->hasType($area, 'legal')),
|
||||
], new Response(status: $status));
|
||||
|
||||
// Die Pruefung braucht ein Skript von Cloudflare und einen Rahmen dafuer. Beides wird nur
|
||||
|
||||
@@ -53,17 +53,6 @@
|
||||
{% for block in areas.footer.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: true, richHeadings: areas.footer.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{# Privacy statement, imprint and the way back to the consent decision. All three have to be
|
||||
reachable from every page, so they are part of the template and not of the footer area,
|
||||
which someone could delete. One labelled landmark, so they are not loose text either. #}
|
||||
{% set legal = consentGate.legalLinks() %}
|
||||
{% set revocable = not editing|default(false) and consentGate.askedGroups is not empty and not consentGate.needsBanner(app.request) %}
|
||||
{% if legal is not empty or revocable %}
|
||||
<nav class="legal-bar" aria-label="Rechtliches">
|
||||
{% for path, label in legal %}{% if not loop.first %} · {% endif %}<a href="/{{ path }}">{{ label }}</a>{% endfor %}
|
||||
{% if revocable %}{% if legal is not empty %} · {% endif %}<a href="/einwilligung">Einwilligung ändern</a>{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% if not editing|default(false) and consentGate.needsBanner(app.request) %}
|
||||
{% include 'consent-banner.html.twig' with {consent: consentGate.state(app.request), back: app.request.pathInfo} only %}
|
||||
{% endif %}
|
||||
|
||||
@@ -2,17 +2,6 @@
|
||||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>Seite nicht gefunden · KantSites</title><link rel="stylesheet" href="/assets/site.css?v={{ kant_version() }}">{% include 'favicon.html.twig' %}</head>
|
||||
{% set consentOpen = not editing|default(false) and consentGate.needsBanner(app.request) %}
|
||||
<body class="{{ consentOpen ? 'consent-open' : '' }}"><main class="shell error-page"><p>404 · KantSites</p><h1>Hier ist noch keine Seite.</h1><p>Die Adresse existiert nicht oder die Seite ist noch nicht veröffentlicht.</p><a class="button" href="/">Zur Startseite</a></main>
|
||||
{# Privacy statement, imprint and the way back to the consent decision. All three have to be
|
||||
reachable from every page, so they are part of the template and not of the footer area,
|
||||
which someone could delete. One labelled landmark, so they are not loose text either. #}
|
||||
{% set legal = consentGate.legalLinks() %}
|
||||
{% set revocable = not editing|default(false) and consentGate.askedGroups is not empty and not consentGate.needsBanner(app.request) %}
|
||||
{% if legal is not empty or revocable %}
|
||||
<nav class="legal-bar" aria-label="Rechtliches">
|
||||
{% for path, label in legal %}{% if not loop.first %} · {% endif %}<a href="/{{ path }}">{{ label }}</a>{% endfor %}
|
||||
{% if revocable %}{% if legal is not empty %} · {% endif %}<a href="/einwilligung">Einwilligung ändern</a>{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% if not editing|default(false) and consentGate.needsBanner(app.request) %}
|
||||
{% include 'consent-banner.html.twig' with {consent: consentGate.state(app.request), back: app.request.pathInfo} only %}
|
||||
{% endif %}
|
||||
|
||||
@@ -91,18 +91,6 @@
|
||||
{% for block in areas.footer.blocks %}{% include 'blocks/block.html.twig' with {block: block, editing: editing|default(false), postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: areas.footer.schemaVersion|default(1) >= 12, richHeadings: areas.footer.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{# Privacy statement, imprint and the way back to the consent decision. All three have to be
|
||||
reachable from every page, so they are part of the template and not of the footer area,
|
||||
which someone could delete. One labelled landmark, so they are not loose text either. #}
|
||||
{% set legal = consentGate.legalLinks() %}
|
||||
{% set revocable = not editing|default(false) and consentGate.askedGroups is not empty and not consentGate.needsBanner(app.request) %}
|
||||
{# Nur, wenn die Seite die Zeile nicht selbst enthaelt: sonst stuende sie zweimal da. #}
|
||||
{% if (legal is not empty or revocable) and not hasLegalBlock|default(false) %}
|
||||
<nav class="legal-bar" aria-label="Rechtliches">
|
||||
{% for path, label in legal %}{% if not loop.first %} · {% endif %}<a href="/{{ path }}">{{ label }}</a>{% endfor %}
|
||||
{% if revocable %}{% if legal is not empty %} · {% endif %}<a href="/einwilligung">Einwilligung ändern</a>{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% set oben = siteSettings.load() %}
|
||||
{% if oben.scrollTop|default(false) and not editing|default(false) %}
|
||||
{# Ein Anker, kein Skript. Er springt an den Anfang - das kann ein Browser allein. #}
|
||||
|
||||
+12
-11
@@ -1118,12 +1118,9 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString("script-src 'none'", (string) $zweite->headers->get('Content-Security-Policy'));
|
||||
}
|
||||
|
||||
public function testTheLegalLineCanLiveInsideTheFooterInsteadOfUnderneathIt(): void
|
||||
public function testLegalLinksAppearOnlyWhereAnAuthorPlacesThem(): void
|
||||
{
|
||||
// Umut: „ganz unten Einwilligung ändern, Datenschutz usw. ist links, komplett bitte besser
|
||||
// lösen." Die eingebaute Leiste klebte als heller Streifen unter einem dunklen Fuß. Als
|
||||
// Baustein steht sie dort, wo sie hingehört, und erbt die Farben ihres Bereichs.
|
||||
// Zwei verschiedene Seiten: die Zeile fuehrt zu Datenschutz und Impressum.
|
||||
// Die hinterlegten Adressen erzeugen keine zusätzliche Leiste unter dem Footer.
|
||||
$datenschutz = $this->pages->createPage('datenschutz', null, 'page', DemoContent::starter('Datenschutz'));
|
||||
$seite = $this->pages->findForEditor($datenschutz);
|
||||
$this->pages->publish($datenschutz, $seite['draft_revision_id'], $seite['version']);
|
||||
@@ -1132,9 +1129,10 @@ final class CmsTest extends TestCase
|
||||
$document['schemaVersion'] = 19;
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
|
||||
// Ohne Baustein: die eingebaute Leiste am Seitenende.
|
||||
// Ohne selbst platzierte Links bleibt die Seite ohne Rechtszeile.
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('<nav class="legal-bar"', $html);
|
||||
self::assertStringNotContainsString('<nav class="legal-bar"', $html);
|
||||
self::assertStringNotContainsString('<a href="/datenschutz">', $html);
|
||||
|
||||
// Mit Baustein: nur noch dieser, und zwar an seiner Stelle im Dokument.
|
||||
$document['blocks'][] = ['id' => 'rechtszeile', 'type' => 'legal', 'props' => []];
|
||||
@@ -1142,7 +1140,7 @@ final class CmsTest extends TestCase
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, $version), $version + 1);
|
||||
$mit = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('<nav id="rechtszeile" class="block-legal"', $mit);
|
||||
self::assertStringNotContainsString('<nav class="legal-bar"', $mit, 'Zweimal dieselbe Zeile ist schlechter als einmal.');
|
||||
self::assertStringNotContainsString('<nav class="legal-bar"', $mit);
|
||||
self::assertStringContainsString('Datenschutz', $mit);
|
||||
}
|
||||
|
||||
@@ -3610,12 +3608,12 @@ final class CmsTest extends TestCase
|
||||
}
|
||||
self::assertSame('', $this->site->load()[$key]);
|
||||
}
|
||||
// A page that exists is accepted and then linked on every page.
|
||||
// A page that exists is accepted; placing its link remains an editorial choice.
|
||||
$this->publishedPage('datenschutz', 'Datenschutz');
|
||||
$this->site->save(['homePath' => 'home', 'privacyPath' => 'datenschutz']);
|
||||
self::assertSame('datenschutz', $this->site->load()['privacyPath']);
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::pages()['home'], 0), 1);
|
||||
self::assertStringContainsString('<a href="/datenschutz">Datenschutz</a>', $this->request('/')->getContent());
|
||||
self::assertStringNotContainsString('<a href="/datenschutz">Datenschutz</a>', $this->request('/')->getContent());
|
||||
}
|
||||
|
||||
public function testADeliveredPageIncrementsOneCounterAndTheAdministrationDoesNot(): void
|
||||
@@ -3650,7 +3648,10 @@ final class CmsTest extends TestCase
|
||||
|
||||
public function testTheQuestionAppearsUntilItIsAnsweredAndCanBeChangedAgain(): void
|
||||
{
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::pages()['home'], 0), 1);
|
||||
$document = DemoContent::pages()['home'];
|
||||
$document['schemaVersion'] = 19;
|
||||
$document['blocks'][] = ['id' => 'consent-links', 'type' => 'legal', 'props' => []];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
$this->askConsent();
|
||||
|
||||
$response = $this->request('/');
|
||||
|
||||
@@ -718,7 +718,7 @@ final class EditorSecurityTest extends WebTestCase
|
||||
self::assertResponseRedirects('/admin/media?q=0');
|
||||
}
|
||||
|
||||
public function testTheMandatoryLegalPagesAreCreatedPublishedAndLinkedEverywhere(): void
|
||||
public function testLegalPagesCanBeCreatedWithoutInjectingFooterLinks(): void
|
||||
{
|
||||
$client = $this->login('editor');
|
||||
$crawler = $client->request('GET', '/admin/settings');
|
||||
@@ -745,11 +745,11 @@ final class EditorSecurityTest extends WebTestCase
|
||||
self::assertStringContainsString('NOCH AUSZUFÜLLEN', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
// And they are linked from an ordinary page, without anyone editing the footer.
|
||||
// Creating the pages does not alter the author's footer or append a link bar.
|
||||
$client->request('GET', '/');
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertStringContainsString('<a href="/datenschutz">Datenschutz</a>', $content);
|
||||
self::assertStringContainsString('<a href="/impressum">Impressum</a>', $content);
|
||||
self::assertStringNotContainsString('<a href="/datenschutz">Datenschutz</a>', $content);
|
||||
self::assertStringNotContainsString('<a href="/impressum">Impressum</a>', $content);
|
||||
|
||||
// Running it twice keeps the existing pages: they may already carry the real text.
|
||||
$before = $this->db->fetchOne('SELECT COUNT(*) FROM revision');
|
||||
|
||||
Reference in new issue
Block a user