Migration 20260912000500 legt eine Tabelle menu an: ein Menü je Platz (header, footer) und Sprache, Elementbaum als JSON, Version für dieselbe Konflikterkennung wie bei Seiten. Ein Eintrag verweist auf eine Seite, auf ein Sprungziel innerhalb einer Seite oder auf eine Adresse (HTTPS, lokaler Pfad, mailto:, tel:). Seitenverweise speichern die Seiten-ID, nie den Pfad - ein Eintrag folgt damit einer umbenannten Seite. Beim Rendern fallen Einträge zu gelöschten oder unveröffentlichten Seiten heraus, ohne den gespeicherten Baum zu ändern und ohne die übrigen Einträge zu verlieren. Die Verwaltung nutzt eine nummerierte Zeilenliste statt Drag-and-drop und funktioniert damit vollständig ohne JavaScript; Untermenüs in der Ausgabe über details/summary. tests/bootstrap.php verwirft var/cache/test je Lauf. Ohne Debug prüft Symfony weder den kompilierten Container noch die Vorlagen auf Aktualität, was zweimal ein falsches Testergebnis erzeugt hat. Geprüft: 79 PHP-Tests / 285 Assertions unter PHP 8.2.12 und 8.4.22, 11 JavaScript-Modelltests, Lint/Format/Container, Migration zuerst gegen eine Datenbankkopie, wiederholtes Seeding. Keine Browser- oder Lighthouse-Läufe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
5.6 KiB
Twig
85 lines
5.6 KiB
Twig
{% extends 'admin/base.html.twig' %}
|
|
{% block title %}Deine Seiten{% endblock %}
|
|
{% block body %}
|
|
<header class="admin-header">
|
|
<a class="brand" href="{{ path('admin_pages') }}"><span class="brand-icon" aria-hidden="true">k.</span>kant<span>cms</span></a>
|
|
<div class="header-actions"><a href="/">Website ansehen ↗</a><span class="user-name">{{ app.user.userIdentifier }}</span><form action="{{ path('admin_logout') }}" method="post"><input type="hidden" name="_csrf_token" value="{{ csrf_token('logout') }}"><button class="subtle" type="submit">Abmelden</button></form></div>
|
|
</header>
|
|
<main id="admin-main" class="pages-main" tabindex="-1">
|
|
<p class="eyebrow">DEINE WEBSITE</p><h1>Gute Inhalte beginnen hier.</h1>
|
|
<p class="muted">Wähle eine Seite. Gestalte deinen Entwurf. Veröffentliche, wenn alles passt.</p>
|
|
<p class="admin-links"><a class="secondary" href="{{ path('admin_media') }}">Medienverwaltung</a> <a class="secondary" href="{{ path('admin_menus') }}">Menüs</a></p>
|
|
|
|
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
|
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
|
{% endfor %}{% endfor %}
|
|
|
|
{% if is_granted('ROLE_EDITOR') %}
|
|
<details class="create-panel"{% if app.flashes is not empty %} open{% endif %}>
|
|
<summary>Neue Seite oder neuen Beitrag anlegen</summary>
|
|
<form action="{{ path('admin_create') }}" method="post" class="create-form">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('pages') }}">
|
|
<div class="create-grid">
|
|
<div>
|
|
<label for="new-title">Titel</label>
|
|
<input id="new-title" name="title" maxlength="160" required autocomplete="off">
|
|
<p class="field-help">Wird als Überschrift und als Seitentitel übernommen.</p>
|
|
</div>
|
|
<div>
|
|
<label for="new-slug">Adresse</label>
|
|
<input id="new-slug" name="slug" maxlength="80" pattern="[a-z0-9]+(-[a-z0-9]+)*" autocomplete="off" aria-describedby="new-slug-help">
|
|
<p class="field-help" id="new-slug-help">Kleinbuchstaben, Ziffern und Bindestriche, zum Beispiel <code>ueber-uns</code>. Leer lassen, um sie aus dem Titel zu bilden.</p>
|
|
</div>
|
|
<div>
|
|
<label for="new-type">Art</label>
|
|
<select id="new-type" name="type">
|
|
<option value="page">Seite</option>
|
|
<option value="post">Beitrag</option>
|
|
</select>
|
|
<p class="field-help">Beiträge sind für Blogartikel gedacht und haben keine Unterseiten.</p>
|
|
</div>
|
|
<div>
|
|
<label for="new-parent">Übergeordnete Seite</label>
|
|
<select id="new-parent" name="parent_id">
|
|
<option value="">— keine, direkt unter der Startseite —</option>
|
|
{% for parent in parents %}<option value="{{ parent.id }}">{{ parent.title }} (/{{ parent.path }})</option>{% endfor %}
|
|
</select>
|
|
<p class="field-help">Bestimmt die Adresse, zum Beispiel <code>/leistungen/webdesign</code>.</p>
|
|
</div>
|
|
</div>
|
|
<button class="primary" type="submit">Anlegen und bearbeiten</button>
|
|
</form>
|
|
</details>
|
|
{% endif %}
|
|
|
|
<div class="section-heading"><h2>Seiten <span class="count">{{ pages|length }}</span></h2><span class="muted">Entwürfe bleiben bis zur Veröffentlichung privat.</span></div>
|
|
<div class="page-list">
|
|
{% for page in pages %}
|
|
<article class="page-card" style="margin-left:{{ page.depth * 28 }}px">
|
|
<div class="page-icon" aria-hidden="true">{{ page.type == 'post' ? '✎' : '▤' }}</div>
|
|
<div class="page-info">
|
|
<h3><a href="{{ path('admin_editor', {id: page.id}) }}">{{ page.title }}</a></h3>
|
|
<p>/{{ page.path == 'home' ? '' : page.path }} <span>· {{ page.locale|upper }} · {{ page.type == 'post' ? 'Beitrag' : 'Seite' }}</span></p>
|
|
</div>
|
|
<span class="badge {{ page.published_revision_id == page.draft_revision_id ? 'published' : '' }}">{{ page.published_revision_id is null ? 'Nicht veröffentlicht' : (page.published_revision_id == page.draft_revision_id ? 'Veröffentlicht' : 'Entwurf geändert') }}</span>
|
|
<a class="secondary" href="{{ path('admin_editor', {id: page.id}) }}" aria-label="{{ page.title }} bearbeiten">Bearbeiten <span aria-hidden="true">→</span></a>
|
|
{% if is_granted('ROLE_ADMIN') %}
|
|
<details class="delete-panel">
|
|
<summary aria-label="{{ page.title }} löschen">Löschen</summary>
|
|
<form action="{{ path('admin_delete', {id: page.id}) }}" method="post">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('pages') }}">
|
|
<input type="hidden" name="expectedVersion" value="{{ page.version }}">
|
|
<p>Löscht „{{ page.title }}“ mit allen gespeicherten Ständen. Das lässt sich nicht rückgängig machen.</p>
|
|
<button class="secondary danger-text" type="submit">Endgültig löschen</button>
|
|
</form>
|
|
</details>
|
|
{% endif %}
|
|
</article>
|
|
{% else %}
|
|
<div class="empty-state"><h3>Noch keine Seiten vorhanden.</h3><p>Lege oben deine erste Seite an.</p></div>
|
|
{% endfor %}
|
|
</div>
|
|
<p class="footnote">Kant CMS · Visueller Editor, erster Prototyp</p>
|
|
</main>
|
|
{% endblock %}
|