Back up unsaved editor work locally, structure the settings, fix archive accessibility
- The editor keeps unsaved changes in the browser and offers them after a crash, a closed tab or an expired session. No timed save on the server: every save is a full revision. - The save status stays visible in narrow windows. - Settings get a sticky section bar, two folded groups and a save bar that stays in view. - The admin post list fetches terms and images once, not per post. - Archive post titles are second-level headings, the archive head keeps full text contrast, the skip link sits above a sticky header, a linked image without alternative text names its link. - The editor check starts through the dev router like the others. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e46c98825
commit
b08862b139
17 files changed
+182
-26
No files matched your search
@@ -6,6 +6,19 @@
|
||||
{% block styles %}<link rel="stylesheet" href="/assets/media-dialog.css?v={{ kant_version() }}">{% endblock %}
|
||||
{% block scripts %}<script type="module" src="/assets/page-picker.js?v={{ kant_version() }}"></script><script type="module" src="/assets/favicon-field.js?v={{ kant_version() }}"></script><script type="module" src="/assets/logo-field.js?v={{ kant_version() }}"></script>{% endblock %}
|
||||
{% block content %}
|
||||
{# Die Seite ist lang. Die Leiste bleibt beim Scrollen stehen und führt zu jedem Abschnitt. #}
|
||||
<nav class="jump-nav" aria-label="Abschnitte dieser Seite">
|
||||
<a href="#not-found">404-Seite</a>
|
||||
<a href="#sprachen">Sprachen</a>
|
||||
<a href="#unternehmen">Unternehmen</a>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<a href="#favicon-title">Symbol</a>
|
||||
<a href="#nach-oben">Knopf „nach oben“</a>
|
||||
<a href="#spamschutz">Spamschutz</a>
|
||||
<a href="#einwilligung">Einwilligung</a>
|
||||
{% endif %}
|
||||
<a href="#weiterleitungen">Weiterleitungen</a>
|
||||
</nav>
|
||||
|
||||
{% if missingLegal is not empty %}
|
||||
{# Not a hint, a warning: in Germany both pages are required, and they have to be reachable from
|
||||
@@ -99,7 +112,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="stack-section">
|
||||
<section class="stack-section" id="unternehmen">
|
||||
<div class="section-head"><h2>Unternehmensdaten</h2><span class="muted">Grundlage für Organization und LocalBusiness</span></div>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<form action="{{ path('admin_settings_save') }}" method="post" class="card card-body">
|
||||
@@ -146,8 +159,9 @@
|
||||
<label for="sameAs">Profile</label>
|
||||
<textarea id="sameAs" name="sameAs" rows="3" placeholder="https://www.linkedin.com/company/…">{{ settings.sameAs|join('\n') }}</textarea>
|
||||
<p class="field-help">Eine HTTPS-Adresse je Zeile, höchstens zwanzig. Die sozialen Profile unten stehen automatisch mit in den strukturierten Daten.</p>
|
||||
<fieldset class="field-set">
|
||||
<legend>Soziale Profile</legend>
|
||||
{% set profile = settings.social|default({})|filter(v => v)|length %}
|
||||
<details class="field-set field-fold"{{ profile ? ' open' : '' }}>
|
||||
<summary>Soziale Profile <span class="muted">{{ profile ? profile ~ ' eingetragen' : 'keine eingetragen' }}</span></summary>
|
||||
<p class="field-help">Einmal hier eintragen, dann mit dem Element „Soziale Profile“ an jeder Stelle einsetzen – im Kopf, im Fuß, in einer Kontaktsektion. Größe, Form und Farbe bestimmt das Element. Leere Felder erscheinen nicht.</p>
|
||||
<div class="fields">
|
||||
{% for key, name in socialNetworks %}
|
||||
@@ -157,9 +171,10 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="field-set">
|
||||
<legend>Öffnungszeiten</legend>
|
||||
</details>
|
||||
{% set zeiten = settings.openingHours|default([])|filter(h => h.opens|default('') or h.closes|default(''))|length %}
|
||||
<details class="field-set field-fold"{{ zeiten ? ' open' : '' }}>
|
||||
<summary>Öffnungszeiten <span class="muted">{{ zeiten ? zeiten ~ ' Tag(e) eingetragen' : 'keine eingetragen' }}</span></summary>
|
||||
<p class="field-help">Als Uhrzeit wie 07:30. Ein Tag ohne Angabe gilt als geschlossen. Erscheint als <code>openingHoursSpecification</code> in den strukturierten Daten – der Text auf der Website ist davon unabhängig.</p>
|
||||
<div class="fields">
|
||||
{% for day, label in weekdays %}
|
||||
@@ -170,7 +185,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
</details>
|
||||
<label for="areaServed">Einsatzgebiet</label>
|
||||
<textarea id="areaServed" name="areaServed" rows="3" placeholder="Baden-Baden Rastatt Karlsruhe">{{ settings.areaServed|join('\n') }}</textarea>
|
||||
<p class="field-help">Ein Ort je Zeile, höchstens vierzig. Erscheint als <code>areaServed</code> in den strukturierten Daten.</p>
|
||||
@@ -217,7 +232,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-help">Das Bild kommt aus der Mediathek, dort kann es auch gleich hochgeladen werden. Danach das Quadrat auf den gewünschten Ausschnitt ziehen und die Größe einstellen. Beim Speichern entstehen daraus PNG-Dateien in 32 und 180 Pixeln.</p>
|
||||
<div class="section-head"><h2>Knopf „nach oben"</h2><span class="muted">Erscheint beim Scrollen, in der Ecke</span></div>
|
||||
<div class="section-head"><h2 id="nach-oben">Knopf „nach oben"</h2><span class="muted">Erscheint beim Scrollen, in der Ecke</span></div>
|
||||
<label class="check-label" for="scrollTop"><input id="scrollTop" name="scrollTop" type="checkbox" value="1"{{ settings.scrollTop ? ' checked' : '' }}> Knopf „nach oben" anzeigen</label>
|
||||
<p class="field-help">Springt an den Seitenanfang. Er braucht kein JavaScript: In Browsern mit Scroll-Zeitachse blendet er sich beim Scrollen ein, in den übrigen steht er von Anfang an da.</p>
|
||||
<div class="fields">
|
||||
@@ -260,7 +275,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-help">Farben entweder als <code>@schlüssel</code> aus der Palette oder als Wert wie <code>#1a2b3c</code>. Leer bedeutet: Akzentfarbe des Designs auf Weiß.</p>
|
||||
<div class="section-head"><h2>Spamschutz für Formulare</h2><span class="muted">Optional, von Cloudflare</span></div>
|
||||
<div class="section-head"><h2 id="spamschutz">Spamschutz für Formulare</h2><span class="muted">Optional, von Cloudflare</span></div>
|
||||
<p class="field-help">Eingebaut sind bereits drei Prüfungen ohne Dritte: ein verstecktes Feld, ein signierter Zeitstempel und höchstens fünf Absendungen je Absender und Stunde. Das hält automatisch ausgefüllte Formulare ab. Gegen jemanden, der einen echten Browser fernsteuert, hilft <strong>Cloudflare Turnstile</strong> – die Prüfung ohne Bilderrätsel. Der Preis: das Widget braucht JavaScript (ohne Skript ist das Formular dann nicht absendbar), und die IP-Adresse des Absenders geht an Cloudflare, was in die Datenschutzerklärung gehört. Schlüssel gibt es kostenlos im Cloudflare-Konto unter „Turnstile“.</p>
|
||||
<label class="check-label" for="turnstile"><input id="turnstile" name="turnstile" type="checkbox" value="1"{{ settings.turnstile ? ' checked' : '' }}> Turnstile-Prüfung in Formularen anzeigen</label>
|
||||
<label for="turnstileSiteKey">Öffentlicher Schlüssel (Site Key)</label>
|
||||
@@ -271,7 +286,7 @@
|
||||
<label for="robotsExtra">Zusätzliche Zeilen für robots.txt</label>
|
||||
<textarea id="robotsExtra" name="robotsExtra" rows="8">{{ settings.robotsExtra }}</textarea>
|
||||
<p class="field-help">Nur Kommentarzeilen und einfache Anweisungen wie <code>Disallow: /pfad</code>, <code>Allow:</code>, <code>User-agent:</code> und <code>Crawl-delay:</code>. Leerzeilen trennen Blöcke, höchstens 200 Zeilen. Hier stehen zum Beispiel eigene Regeln je Bot – etwa welche KI-Crawler die Website lesen dürfen und welche nicht.</p>
|
||||
<div class="section-head"><h2>Einwilligung</h2><span class="muted">Nur nötig, wenn die Website etwas von Dritten lädt</span></div>
|
||||
<div class="section-head"><h2 id="einwilligung">Einwilligung</h2><span class="muted">Nur nötig, wenn die Website etwas von Dritten lädt</span></div>
|
||||
<label class="check-label" for="stickyHeader"><input id="stickyHeader" name="stickyHeader" type="checkbox" value="1"{{ settings.stickyHeader ? ' checked' : '' }}> Kopfbereich beim Scrollen oben halten</label>
|
||||
<label class="check-label" for="headerOverlay"><input id="headerOverlay" name="headerOverlay" type="checkbox" value="1"{{ settings.headerOverlay ? ' checked' : '' }}> Kopfbereich über dem Inhalt schweben lassen</label>
|
||||
<p class="field-help">Der Kopfbereich liegt dann über der ersten Sektion, wie der transparente Header in Flatsome. Seine Farben für den gescrollten Zustand stellst du im Editor des Kopfbereichs unter Design → Hintergrund „beim Scrollen“ ein; zusammen mit „oben halten“ bleibt er beim Scrollen sichtbar.</p>
|
||||
@@ -349,14 +364,19 @@
|
||||
</div>
|
||||
<p class="field-help">Die Entscheidung liegt in einem Cookie dieser Website, ein halbes Jahr lang, ohne Kennung und ohne Übermittlung. Unter <code>/einwilligung</code> kann sie jederzeit geändert werden; auf jeder Seite steht dafür ein Link.</p>
|
||||
|
||||
<button class="primary" type="submit">Einstellungen speichern</button>
|
||||
{# Bleibt am unteren Rand stehen, solange das Formular zu sehen ist: Der Knopf lag vorher
|
||||
mehrere Bildschirmhöhen unter dem Feld, das man gerade geändert hatte. #}
|
||||
<div class="form-save-bar">
|
||||
<button class="primary" type="submit">Einstellungen speichern</button>
|
||||
<span class="muted">Gilt für alles von „Unternehmensdaten“ bis „Einwilligung“.</span>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="card card-body muted">Unternehmensdaten, Knopf „nach oben“, Spamschutz und Einwilligung ändert nur ein Administrator.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="stack-section">
|
||||
<section class="stack-section" id="weiterleitungen">
|
||||
<div class="section-head"><h2>Weiterleitungen <span class="count">{{ redirects.total }}</span></h2><span class="muted">Werden vor dem 404 geprüft.</span></div>
|
||||
<form action="{{ path('admin_redirect_save') }}" method="post" class="card card-body">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||||
|
||||
Reference in new issue
Block a user