Files
KantSites/templates/page.html.twig
T
kantarciandClaude Opus 5 a434e0cefc
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
Serve every font from this server and surface open tasks in 1.14.0
Fonts were still reachable from Google in two places. The editor and area
previews were handed `googleFonts` and no `localFonts`, so the screen used
most of the day fetched Lato from fonts.googleapis.com on every open; the
public page fell back to the same link whenever the files were missing.
Neither path remains: the template has no Google branch, `googleFontsUrl()`
is gone, and the policies name no foreign font host - `font-src 'self'`
publicly, `style-src 'self'; font-src 'self'` in administration. Without
local files a site now shows the theme's fallback and says so.

Preloading took the first two weights per family. The Kant-IT theme names
Lato 400, 700 and 900 and sets its headings on 800/900, so the one weight
that carried them loaded after the stylesheet and `font-display:swap`
swapped the heading in plain sight. Every weight of the theme is preloaded
now, latin subset, and the links sit before the stylesheet: measured cold,
all three files finish at 168 ms against a first paint at 292 ms.

Open tasks no longer hide on the screen that owns them. A strip above every
management screen names what needs a button and carries it: a new version
leads to System, missing fonts to Design. Neither asks the network - the
version comes from the daily update-check task, the fonts from a look in
the folder.

System and update was unusable on a phone: `.stack` is a grid whose
automatic column grows to its widest content, and one long database path
pushed the page sideways. It gets `minmax(0, 1fr)`, the backup table stacks
below 620px, and the prose is 45% shorter with the fine print folded away.

The device chips drew the current device with an outline, which lies outside
the box, ran under its neighbour and was clipped by the sidebar - it read as
a cut-off red frame. Used devices are filled in `#9d4e0d` now (5.6:1), the
current one carries a border inside the chip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:16:16 +02:00

110 lines
8.9 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>
{# Ohne Klasse spielt die Website Bewegung immer ab; mit ihr gilt die Browservorgabe. #}
<html lang="{{ page.locale }}"{{ siteSettings.load().motion == 'respect' ? ' class="motion-respect"' : '' }}>
<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 }}">
{% if canonical|default(null) %}<link rel="canonical" href="{{ canonical }}">{% endif %}
{% 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' }}">
{% if canonical|default(null) %}<meta property="og:url" content="{{ canonical }}">{% endif %}
{# 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 localFonts|default([]) %}
{# Jeder Schnitt, den das Theme nennt, vorab - und zwar alle. Fehlt einer, laedt er erst,
wenn das Stylesheet geparst ist, und die Ueberschrift springt sichtbar von der
Ersatzschrift auf die richtige. Schriften kommen ausschliesslich vom eigenen Server;
ein Stylesheet von fonts.googleapis.com wird nie verlinkt. #}
{% for font in localFonts %}<link rel="preload" href="{{ font }}" as="font" type="font/woff2" crossorigin>{% endfor %}
{% endif %}
{% if styleBundle|default(null) %}
<link rel="stylesheet" href="{{ styleBundle }}">
{% else %}
<link rel="stylesheet" href="/assets/site.css?v={{ kant_version() }}">
<link rel="stylesheet" href="/assets/buttons.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 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 type="module" src="/assets/slider.js?v={{ kant_version() }}"></script>{% endif %}
{% if needsMotion|default(false) %}<script type="module" src="/assets/motion.js?v={{ kant_version() }}"></script>{% endif %}
{% if needsScrolled|default(false) %}<script type="module" src="/assets/scrolled.js?v={{ kant_version() }}"></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' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}">
{% 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 %}
{% 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>