Files
KantSites/templates/page.html.twig
T
kantarciandClaude Opus 5 d6d476177a
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Fassung 1.4.3: Update ohne Git, Verwaltung auf dem Telefon, eigene Symbole
Vier Wuensche und ein Fund beim Nachmessen.

- Farbwaehler im Designbereich. Das Textfeld bleibt massgeblich, damit
  "@akzent" ein Verweis auf die Palette bleibt und nicht beim ersten
  Klick zu einem festen Hexwert erstarrt (ADR 066).
- Die Verwaltung auf dem Telefon: das Menue klappt unter 760 px zu und
  nennt den Bereich, in dem man steht; Ziele 44 px, kein Skript.
- Eigene SVG-Symbole statt Unicode-Zeichen - ein Telefon ersetzt die
  sonst durch Emoji in fremdem Stil. Zwei Eintraege trugen dabei
  denselben Pfad; ein Test findet beides (ADR 067).
- Update aus dem Releasearchiv: der Weg fuer Installationen ohne Git.
  Zwei Schritte, weil ein Prozess, der sich selbst ausgetauscht hat,
  mit dem alten Code weiterarbeitet. var/, .env, hochgeladene
  Schriften und das erzeugte Buendel bleiben unberuehrt; public/ wird
  dazu Datei fuer Datei zusammengefuehrt. Geprueft ohne Netz gegen
  eine Releaseseite im Speicher (ADR 068).

Beim Nachmessen der echten Startseite statt einer Testseite: das
groesste Element ueber der Falz ist dort kein <img>, sondern der
Hintergrund des ersten Abschnitts - und der steht nur im Stylesheet.
Drei Vorabverweise mit denselben Geraetegrenzen wie der Compiler holen
genau die Variante, die die Regel auch nimmt. LCP 1,3 s -> 1,1 s.
Dabei gefunden: dasselbe Bild wurde je Geraeteregel neu abgefragt -
Startseite 19 -> 15 Abfragen (ADR 069).

Geprueft: 428 Tests auf PHP 8.2 und 8.4, Editor-, Browser- und
Linkpruefung, Lighthouse auf drei echten Seiten (alles ausser der
Leistung der Startseite 100).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:01:32 +02:00

115 lines
9.1 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="{{ page.locale }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# Der Titel fuer die Suche, sonst der Titel der Seite. #}
{% set seoTitle = page.seo_title|default('') ?: page.document.title %}
<title>{{ seoTitle }}</title>
<meta name="description" content="{{ page.document.description }}">
<link rel="canonical" href="{{ canonical }}">
{% if page.robots|default('index') != 'index' %}<meta name="robots" content="noindex, follow">{% endif %}
<meta property="og:title" content="{{ seoTitle }}">
<meta property="og:description" content="{{ page.document.description }}">
<meta property="og:type" content="{{ page.type|default('page') == 'post' ? 'article' : 'website' }}">
<meta property="og:url" content="{{ canonical }}">
{# In der Vorschau des Editors gibt es den Websitenamen nicht - dort fehlt das Feld einfach. #}
{% if page.website_name|default('') %}<meta property="og:site_name" content="{{ page.website_name }}">{% endif %}
<meta property="og:locale" content="{{ page.locale == 'de' ? 'de_DE' : page.locale }}">
{% if socialImage|default(null) %}
<meta property="og:image" content="{{ socialImage.url }}">
{% if socialImage.alt %}<meta property="og:image:alt" content="{{ socialImage.alt }}">{% endif %}
<meta name="twitter:card" content="summary_large_image">
{% else %}
{# Ohne Bild ist die große Karte eine leere Fläche; die kleine zeigt Titel und Text. #}
<meta name="twitter:card" content="summary">
{% endif %}
{% include 'favicon.html.twig' %}
{# Ein Bündel statt vier Dateien: jede einzelne hält das Zeichnen der Seite auf. Fehlt es
(kein Schreibrecht im Ordner), werden die Quellen einzeln geladen - langsamer, aber heil. #}
{# Das grosse Bild ueber der Falz, wenn es der Hintergrund des ersten Abschnitts ist: Es
steht sonst nur im Stylesheet und wird erst gefunden, wenn das geparst ist. Genau eine der
drei Abfragen trifft zu - und zwar auf dieselbe Variante, die auch die Regel nimmt. #}
{% for bild in heroBackground|default([]) %}<link rel="preload" as="image" href="{{ bild.src }}" media="{{ bild.media }}" fetchpriority="high">
{% endfor %}
{% if styleBundle|default(null) %}
<link rel="stylesheet" href="{{ styleBundle }}">
{% else %}
<link rel="stylesheet" href="/assets/site.css?v={{ kant_version() }}">
{% if localFonts|default([]) %}<link rel="stylesheet" href="/assets/fonts/fonts.css?v={{ kant_version() }}">{% endif %}
{% if page.document.schemaVersion >= 2 %}<link rel="stylesheet" href="/assets/design.css?v={{ kant_version() }}">{% endif %}
{% if page.document.schemaVersion >= 3 %}<link rel="stylesheet" href="/assets/enhancements.css?v={{ kant_version() }}">{% endif %}
{% endif %}
{% if localFonts|default([]) %}
{# Die wichtigsten Schnitte vorab, damit die erste Zeile in der richtigen Schrift steht. #}
{% for font in localFonts %}<link rel="preload" href="{{ font }}" as="font" type="font/woff2" crossorigin>{% endfor %}
{% elseif googleFonts|default(null) %}<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="{{ googleFonts }}">{% endif %}
{% if hasPosts|default(false) %} <link rel="alternate" type="application/rss+xml" title="Beiträge" href="/feed.xml">
{% endif %}
{% if needsTabs|default(false) %}<script src="/assets/tabs.js?v={{ kant_version() }}" defer></script>{% endif %}
{% if needsSlider|default(false) %}<script src="/assets/slider.js?v={{ kant_version() }}" defer></script>{% endif %}
{% if needsZoom|default(false) %}<script src="/assets/lightbox.js?v={{ kant_version() }}" defer></script>{% endif %}
{# Own scripts of this page, as a file. Never rendered in the editor preview: the preview must
show the layout, not run code that could change the page under the editor's hands. #}
{% if pageScript|default(null) and not editing|default(false) %}<script src="{{ pageScript }}" defer></script>{% endif %}
{# A data block, not a script: CSP script-src does not apply to application/ld+json. #}
{% for block in structuredData|default([]) %}<script type="application/ld+json">{{ block|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE'))|raw }}</script>
{% endfor %}
{% if pageStyles|default('') %}
{# In the editor the preview inherits the administration's policy, which forbids inline
styles. The stylesheet therefore travels as inert template text and the editor applies
it as a constructed stylesheet – same result, no policy violation and a clean console. #}
{% if editing|default(false) %}<template id="kant-styles">{{ pageStyles|raw }}</template>
{% else %}<style>{{ pageStyles|raw }}</style>{% endif %}
{% endif %}
</head>
{% set consentOpen = not editing|default(false) and consentGate.needsBanner(app.request) %}
<body class="kant{{ consentOpen ? ' consent-open' : '' }}">
<span id="seitenanfang" tabindex="-1"></span>
<a class="skip-link" href="#main">Zum Inhalt springen</a>
{# Header and footer are block documents like any page, so they are built from elements
instead of being written into this template. #}
{% if areas.header|default(null) %}
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}">
{% for block in areas.header.blocks %}{% include 'blocks/block.html.twig' with {block: block, editing: editing|default(false), postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: areas.header.schemaVersion|default(1) >= 12, richHeadings: areas.header.schemaVersion|default(1) >= 17, eagerImage: eagerImage|default(null), inHeader: true} only %}{% endfor %}
</header>
{% endif %}
<main id="main" tabindex="-1">
{% if postMeta is defined and postMeta is not null and siteSettings.load().postMetaHeader %}
<p class="post-header shell">
{% if page.published_at %}<time datetime="{{ page.published_at|date('Y-m-d') }}">{{ page.published_at|date('d.m.Y') }}</time>{% endif %}
{% if page.author %}<span>von {{ page.author }}</span>{% endif %}
{% for term in postMeta.category %}<a href="{{ term.href }}">{{ term.name }}</a>{% endfor %}
{% for term in postMeta.tag %}<a class="post-tag" href="{{ term.href }}">#{{ term.name }}</a>{% endfor %}
</p>
{% endif %}
{% for block in page.document.blocks %}{% include 'blocks/block.html.twig' with {block: block, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: page.document.schemaVersion|default(1) >= 12, richHeadings: page.document.schemaVersion|default(1) >= 17, neighbours: neighbours|default({}), eagerImage: eagerImage|default(null)} only %}{% endfor %}
</main>
{% if areas.footer|default(null) %}
<footer class="site-footer">
{% 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. #}
<a class="scroll-top{{ oben.scrollTopPosition|default('rechts') == 'links' ? ' scroll-top-links' : '' }}" href="#seitenanfang" aria-label="{{ oben.scrollTopLabel|default('Nach oben') }}"><span aria-hidden="true">↑</span></a>
{% 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 %}
</body>
</html>