249 lines
19 KiB
Twig
249 lines
19 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'settings' %}
|
||
{% block title %}Einstellungen{% endblock %}
|
||
{% block heading %}Unternehmen und Adressen{% endblock %}
|
||
{% block lead %}Aus diesen Angaben entstehen die strukturierten Daten der Startseite. Was hier steht, ist genau das, was Suchmaschinen lesen – es gibt keine zweite, unsichtbare Fassung.{% endblock %}
|
||
{% block scripts %}<script type="module" src="/assets/page-picker.js?v={{ kant_version() }}"></script>{% endblock %}
|
||
{% block content %}
|
||
|
||
{% if missingLegal is not empty %}
|
||
{# Not a hint, a warning: in Germany both pages are required, and they have to be reachable from
|
||
every page of the site. #}
|
||
<div class="card">
|
||
<div class="card-head"><h2>Pflichtseiten fehlen</h2><span class="muted">{{ missingLegal|join(' und ') }}</span></div>
|
||
<div class="card-body">
|
||
<p class="notice error" role="alert">Ohne {{ missingLegal|join(' und ') }} ist diese Website in Deutschland nicht rechtskonform – beide müssen von jeder Seite aus erreichbar sein. KantSites verlinkt sie automatisch im Fuß jeder Seite, sobald sie hier eingetragen sind.</p>
|
||
<p class="field-help">Der Knopf legt die fehlenden Seiten mit einem Gerüst an und verknüpft sie. Jede Pflichtangabe darin ist mit <code>NOCH AUSZUFÜLLEN</code> markiert – den Text musst du selbst schreiben oder von jemandem schreiben lassen, der dafür einstehen kann. Vorhandene Seiten werden nie überschrieben.</p>
|
||
<form action="{{ path('admin_legal_pages') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||
<button class="primary" type="submit">Fehlende Pflichtseiten anlegen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
<section class="stack-section" id="not-found" aria-labelledby="not-found-title">
|
||
<div class="section-head"><h2 id="not-found-title">404-Seite</h2><a class="text-button" href="/__404-vorschau__" target="_blank" rel="noopener">404-Vorschau ↗</a></div>
|
||
<div class="card card-body">
|
||
<p>Bei unbekannten Adressen erscheint diese Seite mit dem Standard-Header, Standard-Footer und Design deiner Website.</p>
|
||
{% if notFoundSelection %}
|
||
<p class="notice">{{ notFoundSelection.published ? 'Aktiv:' : 'Zugewiesener Entwurf:' }} <strong>{{ notFoundSelection.title }}</strong>{% if not notFoundSelection.published %} · Bis zur Veröffentlichung erscheint die Standard-404.{% endif %}</p>
|
||
<p><a class="secondary" href="{{ path('admin_editor', {id: notFoundSelection.id}) }}">404-Seite bearbeiten</a></p>
|
||
{% else %}
|
||
<p class="notice">Aktiv: Standard-404 im Website-Design.</p>
|
||
{% endif %}
|
||
{% if is_granted('ROLE_PUBLISHER') %}
|
||
<form action="{{ path('admin_not_found_save') }}" method="post" id="not-found-form">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||
<input type="hidden" name="expectedId" value="{{ settings.notFoundPageId }}">
|
||
{% include 'admin/content-picker.html.twig' with {id: 'not-found-picker', name: 'notFoundPageId', type: 'page', label: '404-Seite suchen', selected: notFoundSelection} only %}
|
||
<p class="field-help">Nur der veröffentlichte Stand wird angezeigt. Ohne Auswahl wird die Standard-404 verwendet.</p>
|
||
<button class="primary" type="submit">404-Zuordnung speichern</button>
|
||
</form>
|
||
{% if not notFoundSelection %}
|
||
<form action="{{ path('admin_not_found_save') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||
<input type="hidden" name="expectedId" value="{{ settings.notFoundPageId }}">
|
||
<input type="hidden" name="action" value="create">
|
||
<p class="field-help">Noch keine passende Seite? Lege eine fertige Vorlage an und bearbeite sie im Editor.</p>
|
||
<button class="secondary" type="submit">404-Vorlage anlegen</button>
|
||
</form>
|
||
{% endif %}
|
||
{% else %}<p class="field-help">Die Zuordnung kann von einem Benutzer mit Veröffentlichungsrechten geändert werden.</p>{% endif %}
|
||
</div>
|
||
</section>
|
||
<section class="stack-section">
|
||
<div class="section-head"><h2>Unternehmensdaten</h2><span class="muted">Grundlage für Organization und LocalBusiness</span></div>
|
||
<form action="{{ path('admin_settings_save') }}" method="post" class="card card-body">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="legalName">Name des Unternehmens</label>
|
||
<input id="legalName" name="legalName" maxlength="200" value="{{ settings.legalName }}">
|
||
</div>
|
||
<div>
|
||
<label for="phone">Telefon</label>
|
||
<input id="phone" name="phone" maxlength="40" value="{{ settings.phone }}">
|
||
</div>
|
||
<div>
|
||
<label for="email">E-Mail</label>
|
||
<input id="email" name="email" type="email" maxlength="120" value="{{ settings.email }}">
|
||
</div>
|
||
<div>
|
||
<label for="street">Straße und Hausnummer</label>
|
||
<input id="street" name="street" maxlength="120" value="{{ settings.street }}">
|
||
</div>
|
||
<div>
|
||
<label for="postalCode">Postleitzahl</label>
|
||
<input id="postalCode" name="postalCode" maxlength="16" value="{{ settings.postalCode }}">
|
||
</div>
|
||
<div>
|
||
<label for="city">Ort</label>
|
||
<input id="city" name="city" maxlength="120" value="{{ settings.city }}">
|
||
</div>
|
||
<div>
|
||
<label for="country">Land</label>
|
||
<input id="country" name="country" maxlength="2" pattern="[A-Za-z]{2}" value="{{ settings.country }}">
|
||
<p class="field-help">Zweibuchstabiger Code, zum Beispiel DE.</p>
|
||
</div>
|
||
</div>
|
||
<label for="homePath">Startseite</label>
|
||
<select id="homePath" name="homePath">
|
||
{% for page in choosableHome %}<option value="{{ page.path }}"{{ settings.homePath == page.path ? ' selected' : '' }}>{{ page.title }} (/{{ page.path }})</option>{% endfor %}
|
||
</select>
|
||
<p class="field-help">Diese Seite antwortet unter <code>/</code>; ihre eigene Adresse leitet dauerhaft dorthin. Nur veröffentlichte Seiten sind wählbar, und die Startseite lässt sich nicht umbenennen oder in den Papierkorb legen.</p>
|
||
<label for="description">Kurzbeschreibung</label>
|
||
<input id="description" name="description" maxlength="300" value="{{ settings.description }}">
|
||
<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.</p>
|
||
<label class="check-label" for="localBusiness"><input id="localBusiness" name="localBusiness" type="checkbox" value="1"{{ settings.localBusiness ? ' checked' : '' }}> Als lokales Unternehmen auszeichnen</label>
|
||
<p class="field-help">Braucht mindestens Straße und Ort. Sinnvoll bei einem Standort mit Kundenverkehr.</p>
|
||
<label class="check-label" for="llmsTxt"><input id="llmsTxt" name="llmsTxt" type="checkbox" value="1"{{ settings.llmsTxt ? ' checked' : '' }}> <code>llms.txt</code> ausliefern</label>
|
||
<p class="field-help">Eine maschinenlesbare Liste der veröffentlichten, indexierbaren Seiten. Optional. Kein Rankingfaktor und keine Voraussetzung für irgendeine Suchfunktion; ohne Aktivierung antwortet die Adresse mit 404.</p>
|
||
<label for="faviconMediaId">Symbol der Website</label>
|
||
<div class="favicon-field">
|
||
{% if settings.faviconMediaId %}<img class="favicon-preview" src="/favicon.png?v={{ settings.faviconMediaId|slice(0, 8) }}" alt="" width="32" height="32">{% endif %}
|
||
<select id="faviconMediaId" name="faviconMediaId">
|
||
<option value="">Mitgeliefertes Zeichen</option>
|
||
{% for image in images %}<option value="{{ image.id }}"{{ settings.faviconMediaId == image.id ? ' selected' : '' }}>{{ image.title }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<p class="field-help">Das Bild im Browser-Reiter und auf dem Startbildschirm. Aus dem gewählten Bild entsteht beim Speichern ein quadratischer Ausschnitt aus der Mitte in 32 und 180 Pixeln – ein Logo mit viel Rand wird darin klein, ein quadratisches Zeichen funktioniert am besten.</p>
|
||
<div class="section-head"><h2>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">
|
||
<div>
|
||
<label for="scrollTopPosition">Seite</label>
|
||
<select id="scrollTopPosition" name="scrollTopPosition">
|
||
<option value="rechts"{{ settings.scrollTopPosition == 'rechts' ? ' selected' : '' }}>Rechts unten</option>
|
||
<option value="links"{{ settings.scrollTopPosition == 'links' ? ' selected' : '' }}>Links unten</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopShape">Form</label>
|
||
<select id="scrollTopShape" name="scrollTopShape">
|
||
<option value="rund"{{ settings.scrollTopShape == 'rund' ? ' selected' : '' }}>Rund</option>
|
||
<option value="weich"{{ settings.scrollTopShape == 'weich' ? ' selected' : '' }}>Abgerundet</option>
|
||
<option value="eckig"{{ settings.scrollTopShape == 'eckig' ? ' selected' : '' }}>Eckig</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopSize">Größe</label>
|
||
<input id="scrollTopSize" name="scrollTopSize" type="number" min="36" max="72" value="{{ settings.scrollTopSize }}">
|
||
<p class="field-help">36 bis 72 Pixel. Unter 44 wird er auf dem Telefon schwer zu treffen.</p>
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopAfter">Erscheint nach</label>
|
||
<input id="scrollTopAfter" name="scrollTopAfter" type="number" min="0" max="4000" step="50" value="{{ settings.scrollTopAfter }}">
|
||
<p class="field-help">Pixel Scrollweg. 0 heißt: sofort.</p>
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopColor">Farbe</label>
|
||
<input id="scrollTopColor" name="scrollTopColor" value="{{ settings.scrollTopColor }}" placeholder="@accent" data-color data-palette="{{ palette|json_encode|e('html_attr') }}">
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopInk">Farbe des Pfeils</label>
|
||
<input id="scrollTopInk" name="scrollTopInk" value="{{ settings.scrollTopInk }}" placeholder="#ffffff" data-color data-palette="{{ palette|json_encode|e('html_attr') }}">
|
||
</div>
|
||
<div>
|
||
<label for="scrollTopLabel">Beschriftung für Vorlesesoftware</label>
|
||
<input id="scrollTopLabel" name="scrollTopLabel" maxlength="60" value="{{ settings.scrollTopLabel }}">
|
||
</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>
|
||
<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>
|
||
<input id="turnstileSiteKey" name="turnstileSiteKey" maxlength="100" value="{{ settings.turnstileSiteKey }}" placeholder="0x4AAAAAAA…">
|
||
<label for="turnstileSecret">Geheimer Schlüssel (Secret Key)</label>
|
||
<input id="turnstileSecret" name="turnstileSecret" maxlength="100" value="{{ settings.turnstileSecret }}" placeholder="0x4AAAAAAA…" autocomplete="off">
|
||
<p class="field-help">Antwortet Cloudflare einmal nicht, wird die Anfrage trotzdem angenommen: ein Ausfall dort darf keine Kundenanfrage verschlucken.</p>
|
||
<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>
|
||
<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="postMetaHeader"><input id="postMetaHeader" name="postMetaHeader" type="checkbox" value="1"{{ settings.postMetaHeader ? ' checked' : '' }}> Datum, Autor und Kategorie über einem Beitrag zeigen</label>
|
||
<label class="check-label" for="consentBanner"><input id="consentBanner" name="consentBanner" type="checkbox" value="1"{{ settings.consentBanner ? ' checked' : '' }}> Einwilligungshinweis anzeigen</label>
|
||
<p class="field-help">Ohne eingebettete Inhalte anderer Anbieter braucht diese Website keine Einwilligung – dann ist ein Banner nur im Weg und sollte aus bleiben. Eingebettete Inhalte laden ohnehin erst nach Zustimmung, unabhängig von diesem Schalter.</p>
|
||
<fieldset class="field-set">
|
||
<legend>Worüber gefragt wird</legend>
|
||
{% for value, label in consentGroups %}
|
||
<label class="check-label" for="group-{{ value }}"><input id="group-{{ value }}" name="consentGroups[]" type="checkbox" value="{{ value }}"{{ value in settings.consentGroups ? ' checked' : '' }}> {{ label }}</label>
|
||
<p class="field-help">{{ consentGroupHelp[value] }}</p>
|
||
{% endfor %}
|
||
</fieldset>
|
||
<label for="consentText">Text im Hinweis</label>
|
||
<textarea id="consentText" name="consentText" rows="3" maxlength="600">{{ settings.consentText }}</textarea>
|
||
<p class="field-help">Leer lassen für den Standardtext. Er soll sagen, was geladen wird und was ohne Zustimmung passiert – nicht zum Klicken drängen.</p>
|
||
<div class="fields">
|
||
<div>
|
||
<label for="privacyPath">Adresse der Datenschutzseite</label>
|
||
<input id="privacyPath" name="privacyPath" maxlength="400" value="{{ settings.privacyPath }}" placeholder="datenschutz">
|
||
<p class="field-help">Pflicht, sobald der Hinweis an ist, und die Seite muss existieren.</p>
|
||
</div>
|
||
<div>
|
||
<label for="imprintPath">Adresse des Impressums</label>
|
||
<input id="imprintPath" name="imprintPath" maxlength="400" value="{{ settings.imprintPath }}" placeholder="impressum">
|
||
</div>
|
||
</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>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="stack-section">
|
||
<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') }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="from">Alte Adresse</label>
|
||
<input id="from" name="from" maxlength="400" required placeholder="alte-seite">
|
||
</div>
|
||
<div>
|
||
<label for="to">Neue Adresse</label>
|
||
<input id="to" name="to" maxlength="400" placeholder="leistungen/webdesign">
|
||
<p class="field-help">Bei 410 leer lassen.</p>
|
||
</div>
|
||
<div>
|
||
<label for="status">Art</label>
|
||
<select id="status" name="status">
|
||
<option value="301">301 – dauerhaft verschoben</option>
|
||
<option value="410">410 – entfernt</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button class="primary" type="submit">Weiterleitung speichern</button>
|
||
</form>
|
||
|
||
{% if redirects.items is not empty %}
|
||
<table class="table redirect-table">
|
||
<caption class="visually-hidden">Gespeicherte Weiterleitungen</caption>
|
||
<thead><tr><th scope="col">Alte Adresse</th><th scope="col">Ziel</th><th scope="col">Art</th><th scope="col">Entfernen</th></tr></thead>
|
||
<tbody>
|
||
{% for entry in redirects.items %}
|
||
<tr>
|
||
<td><code>/{{ entry.from }}</code></td>
|
||
<td>{% if entry.to %}<code>/{{ entry.to }}</code>{% else %}<span class="muted">—</span>{% endif %}</td>
|
||
<td>{{ entry.status }}</td>
|
||
<td>
|
||
<form action="{{ path('admin_redirect_save') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||
<input type="hidden" name="remove" value="{{ entry.id }}">
|
||
<button class="text-button danger-text" type="submit" aria-label="Weiterleitung von /{{ entry.from }} entfernen">Entfernen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% include 'admin/pager.html.twig' with {result: redirects, route: 'admin_settings', label: 'Seiten der Weiterleitungen'} only %}
|
||
{% endif %}
|
||
</section>
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|