Ihr Malerservice Drews (Gernsbach) as its own installation: seed script with theme, header, footer, eight pages, 31 posts in two categories, contact form, consent-gated map and videos, redirects; the wording lives in a generated content file. Reset script, launch entry on port 8093, Cormorant Garamond served locally with its OFL text. Two CMS fixes found while building: wide and full sections lost against the theme's .kant .shell max-width, and the scroll-top link sat outside every landmark (axe "region"). Lighthouse on six pages: accessibility, best practices, SEO and agentic browsing 100, performance 98-100; axe zero violations on nine pages at 1440 and 390 px. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
114 lines
9.4 KiB
Twig
114 lines
9.4 KiB
Twig
<!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' : '' }}{{ kant_header_surface(areas.header) ? ' has-own-surface' : '' }}">
|
||
{% 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 %}
|
||
{% 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 %}
|
||
{# Der Knopf steht hinter der Einwilligungsleiste: Solange sie offen ist, setzt er sich per
|
||
Ankerpositionierung auf ihre Oberkante, und ein Anker muss im Dokument davor stehen. #}
|
||
{% 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. Der nav-Rahmen
|
||
gibt ihm eine Landmarke: ein Verweis außerhalb jeder Landmarke ist für Vorlesesoftware
|
||
Inhalt ohne Ort, und axe meldet ihn als Verstoß gegen „region“. #}
|
||
<nav aria-label="{{ oben.scrollTopLabel|default('Nach oben') }}"><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></nav>
|
||
{% endif %}
|
||
</body>
|
||
</html>
|