Das Feld "Vorschaubild beim Teilen" gab es seit dem SEO-Paket, ausgegeben wurde es nie: keine mit KantSites gebaute Seite hatte ein og:image. Es wird jetzt aufgeloest - erst das gesetzte Vorschaubild, sonst das Beitragsbild - als absolute Adresse bis 1600 px, dazu og:site_name, og:locale, og:image:alt und twitter:card (klein ohne Bild, gross mit). Dasselbe Bild steht im strukturierten Datensatz eines Beitrags; Archive haben jetzt ebenfalls Open-Graph-Angaben. Eigene robots.txt-Zeilen: 200 statt 30, und Leerzeilen bleiben als Trenner erhalten - Regeln je Bot (welche KI-Crawler lesen duerfen) brauchen Bloecke, nicht einen Klumpen. Behoben: die Symbolerzeugung suchte feste Breiten statt der Varianten, die das Bild wirklich hat - ein 192 px grosses Zeichen, also der haeufigste Fall, scheiterte damit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
12 KiB
Twig
167 lines
12 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 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">
|
||
<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>
|
||
<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 %}
|