{% set type = block.type %} {% set props = block.props|default({}) %} {% set layout = block.layout|default({}) %} {% set designClasses = layout.visibility|default('all') != 'all' ? ' visibility-' ~ layout.visibility : '' %} {% for device, settings in layout.design|default({}) %} {% for key, value in settings %}{% set designClasses = designClasses ~ ' d-' ~ device ~ '-' ~ key ~ '-' ~ value %}{% endfor %} {% endfor %} {% for key, value in layout.effects|default({}) %}{% set designClasses = designClasses ~ ' fx-' ~ key ~ '-' ~ value %}{% endfor %} {% if type in ['section', 'row', 'column'] %} {% set tag = type == 'section' ? 'section' : 'div' %} <{{ tag }} id="{{ block.id }}" class="block-{{ type }} tone-{{ layout.tone|default('paper') }} width-{{ layout.width|default('normal') }}{{ designClasses }}{{ block.anchor|default('') ? ' has-anchor' : '' }}"> {% if block.anchor|default('') %}{% endif %} {% if type == 'section' %}
{% endif %} {% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %} {% if type == 'section' %}
{% endif %} {% elseif type == 'heading' %} {% if richHeadings|default(false) %}{{ props.text|raw }}{% else %}{{ props.text }}{% endif %} {% elseif type == 'text' %} {% if richText|default(false) %} {# Already filtered by RichText when it was stored, so printing it unescaped is safe. A div, not a p: the allowlist includes lists and paragraphs, which may not nest in one. #}
{{ props.text|raw }}
{% else %}

{{ props.text }}

{% endif %} {% elseif type == 'button' %} {% for device, settings in layout.button|default({}) %}{% for key, value in settings %}{% set designClasses = designClasses ~ ' b-' ~ device ~ '-' ~ key ~ '-' ~ value %}{% endfor %}{% endfor %} {{ props.text }} {% elseif type == 'image' %} {# Wie breit das Bild tatsaechlich erscheint. Steht im Block eine feste Breite - ein Logo mit 190px, ein Symbol mit 72px -, dann ist das die Antwort, und der Browser laedt die kleine Variante statt der grossen. Ohne feste Breite bleibt es bei der Annahme nach Spaltenbreite. #} {% set festeBreite = block.style.desktop.maxWidth|default('') %} {% set eager = eagerImage|default(null) == block.id %} {# „auto“ heißt: der Browser misst selbst nach, wie breit das Bild tatsächlich erscheint, und wählt genau dazu die Variante. Das geht nur bei verzögert geladenen Bildern - beim vorgezogenen Bild steht die Größe noch nicht fest. Die Angaben dahinter sind der Rückfall für Browser, die „auto“ noch nicht kennen (heute Safari). #} {% set annahme = '(max-width: 650px) 100vw, (max-width: 1024px) 50vw, 800px' %} {% set imageSizes = festeBreite ends with 'px' ? festeBreite : (eager or inHeader|default(false) ? annahme : 'auto, ' ~ annahme) %} {% set klick = props.click|default(props.href|default('') ? 'link' : 'keine') %} {% set imageLinked = klick == 'link' and props.href|default('') %} {% set grossesBild = props.sources|default([]) ? props.sources|last.src : props.src %}
{%- if klick == 'vergroessern' -%} {# Ohne Skript öffnet dieser Verweis schlicht das große Bild. Mit Skript fängt der Betrachter den Klick ab - die Seite bleibt also in jedem Fall benutzbar. #} {%- elseif imageLinked -%} {%- endif -%}{{ props.alt }}{% if klick == 'vergroessern' or imageLinked %}{% endif %}{% if props.caption|default('') %}
{{ props.caption }}
{% endif %}
{% elseif type == 'iconcard' %}
{% include 'blocks/icon.html.twig' with {name: props.icon} only %}

{% if props.href|default('') %}{{ props.title }}{% else %}{{ props.title }}{% endif %}

{% if props.text|default('') %}

{{ props.text }}

{% endif %}
{% elseif type == 'stat' %}

{{ props.value }}{{ props.label }}

{% elseif type == 'quote' %}
{% if props.rating|default(0) > 0 %}

Bewertung: {{ props.rating }} von 5

{% endif %}

{{ props.text }}

{{ props.author }}{% if props.role|default('') %} {{ props.role }}{% endif %}
{% elseif type == 'logos' %} {% elseif type == 'cta' %}

{{ props.title }}

{% if props.text|default('') %}

{{ props.text }}

{% endif %}
{{ props.buttonText }}
{% elseif type == 'contact' %} {% if not (props.phone|default('') or props.email|default('') or props.address|default('')) %} {% if editing|default(false) %}

Kontaktdaten: Telefon, E-Mail oder Adresse unter Inhalt eintragen.

{% endif %} {% else %}
{% if props.phone|default('') %}{% include 'blocks/icon.html.twig' with {name: 'phone'} only %}{{ props.phone }}{% endif %} {% if props.email|default('') %}{% include 'blocks/icon.html.twig' with {name: 'mail'} only %}{{ props.email }}{% endif %} {% if props.address|default('') %}{% include 'blocks/icon.html.twig' with {name: 'pin'} only %}{{ props.address }}{% endif %}
{% endif %} {% elseif type == 'divider' %}
{% elseif type == 'spacer' %} {% elseif type == 'menu' %} {% set links = navigation[props.slot]|default([]) %} {% if links is not empty %} {# The layout is a class, and the burger variant is a details element: a menu that needs a script to open would be a menu that does not open on a page without one. #} {% set menuLayout = props.layout|default('horizontal') %} {% set collapse = menuLayout == 'horizontal' ? props.collapse|default('tablet') : 'never' %} {% endif %} {% elseif type == 'partialref' %} {# A linked section: its blocks come from the partial, resolved by the controller. One change there reaches every page that links it, which is the difference to a copied section. #} {% set section = sections[props.partial]|default(null) %} {% if props.partial|default('') == '' %} {% if editing|default(false) %}

Verknüpfte Sektion: unter Inhalt die Adresse einer Sektion eintragen.

{% endif %} {% elseif section %}
{% for child in section.blocks %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif editing|default(false) %}

Die verknüpfte Sektion „{{ props.partial }}“ gibt es nicht mehr. Der Verweis bleibt gespeichert.

{% else %} {% endif %} {% elseif type == 'breadcrumb' %} {% if breadcrumbs|default([])|length > 1 %} {% endif %} {% elseif type == 'form' %} {% if props.form|default('') == '' %} {% if editing|default(false) %}

Formular: unter Inhalt die Adresse eines Formulars eintragen (siehe Verwaltung → Formulare).

{% endif %} {% else %}
{% include 'blocks/form.html.twig' with (forms[block.id]|default({form: null, sent: false, errors: {}, values: {}, optionImages: {}, blockId: block.id, page: {path: ''}, issued: 0, issuedSignature: ''})) only %}
{% endif %} {% elseif type == 'tabs' %} {# Without the script this is an anchor list followed by every panel: complete and readable. tabs.js turns it into a tab widget; it only changes presentation, never content. #}
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif type == 'tabitem' %}

{{ props.title }}

{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif type == 'accordion' %}
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif type == 'accordionitem' %}
{{ props.title }}
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif type == 'icon' %} {# A single icon. Decorative ones are hidden from assistive technology; one that means something carries its label as text for screen readers. #} {% set iconBody %}{% include 'blocks/icon.html.twig' with {name: props.icon} only %}{% if not props.decorative %}{{ props.label }}{% endif %}{% endset %} {% if props.href|default('') %} {% else %} {% endif %} {% elseif type == 'social' %} {# Die Profile kommen aus den Einstellungen, der Block sagt nur, wie sie aussehen. Eine Liste, weil es eine ist; ohne Beschriftung trägt jeder Link seinen Namen unsichtbar. #} {% set profiles = siteSettings.load().social|default({}) %} {% set chosen = props.networks|default([]) %} {% set shown = [] %} {% for key, name in constant('App\\Content\\SocialProfiles::NETWORKS') %} {% if profiles[key]|default('') and (chosen is empty or key in chosen) %}{% set shown = shown|merge([{key: key, name: name, url: profiles[key]}]) %}{% endif %} {% endfor %} {% if shown is not empty %} {% elseif editing|default(false) %}

Noch keine sozialen Profile eingetragen. Sie stehen unter Einstellungen → Unternehmensdaten → Soziale Profile.

{% else %} {% endif %} {% elseif type == 'progress' %} {# A progress bar as a real meter: the number is in the markup, not only in the width of a coloured div, so it is readable without seeing it. #}

{{ props.label }}{% if props.showValue %} {{ props.value }} %{% endif %}

{% elseif type == 'slider' %} {# A slider without a script: a scroll container with scroll snapping. It works with a finger, a trackpad, a mouse wheel and the keyboard, and it degrades to a scrollable strip on a browser that ignores snapping. No autoplay - a moving carousel is something people ask to stop, not something a page should start on its own. #}
= 3 %} data-autoplay="{{ props.autoplay }}"{% endif %} tabindex="0" role="group" aria-label="Zum Blättern seitwärts scrollen"> {% for child in block.children|default([]) %}
{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}
{% endfor %}
{% elseif type == 'embed' %} {% if props.url|default('') == '' %} {% if editing|default(false) %}

Externer Inhalt: unter Inhalt Adresse, Titel und Anbieter eintragen.

{% endif %} {% else %} {# Content from a third party. Until the group is granted, nothing of it is requested - no iframe, no image, no preconnect. That is the whole point of the consent question: the placeholder is not a nicety, it is what keeps the promise. In the editor it always stays a placeholder: the administration's policy forbids foreign frames, and a preview must not load anything from outside either. #} {% if not editing|default(false) and consentGate.granted(app.request, props.group) %}
{% else %}

{{ props.title }}

{{ props.hint ?: 'Dieser Inhalt wird von einem anderen Anbieter geladen. Dabei erfährt er deine IP-Adresse und welche Seite du ansiehst.' }}

Anbieter: {{ props.provider ?: props.url|split('/')[2] }}

{% if editing|default(false) %}

Wird erst auf der veröffentlichten Seite geladen, und dort erst nach Einwilligung.

{% else %}
{# The groups already granted stay granted; this button adds this one. #} {% for granted in consentGate.state(app.request).groups %}{% if granted.granted %}{% endif %}{% endfor %}
{% endif %}
{% endif %} {% endif %} {% elseif type == 'postlist' %}
{% set feed = postLists[block.id]|default([]) %} {% include 'blocks/post-list.html.twig' with {editing: editing|default(false), posts: feed.posts|default(feed), pages: feed.pages|default(1), page: feed.page|default(1), total: feed.total|default(null), layout: block.props.layout|default('karten'), showMeta: block.props.showMeta ?? true, showExcerpt: block.props.showExcerpt ?? true, showTerms: block.props.showTerms ?? true} only %}
{% elseif type == 'postnav' %}
{% include 'blocks/post-nav.html.twig' with {previous: neighbours.previous|default(null), next: neighbours.next|default(null), previousLabel: props.previousLabel|default('Vorheriger Beitrag'), nextLabel: props.nextLabel|default('Nächster Beitrag')} only %}
{% elseif type == 'legal' %} {# Impressum, Datenschutz und - sofern schon gefragt wurde - der Weg zurück zur Einwilligung. Als Baustein, damit die Zeile dort steht, wo sie hingehört, und die Farben ihres Bereichs erbt statt als heller Streifen unter einem dunklen Fuß zu kleben. #} {% set rechtliches = consentGate.legalLinks() %} {% set widerruflich = not editing|default(false) and consentGate.askedGroups is not empty and not consentGate.needsBanner(app.request) %} {% elseif type == 'gallery' %}
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
{% elseif kant_plugin_block(type) %} {# Ein Baustein eines aktiven Plugins. Die Vorlage bekommt nur, was der Kern geprüft hat: die Eigenschaften nach dem Schema des Manifests. Passen sie nicht, bleibt der Platz leer. #} {% set plugin = kant_plugin_block(type) %} {% set pluginProps = kant_plugin_props(block) %} {% if pluginProps is null %} {% if editing|default(false) %}

Baustein „{{ plugin.label }}“: die gespeicherten Eigenschaften passen nicht zum Plugin.

{% endif %} {% else %} {# Ein Container rendert seine Kinder hier, mit demselben Kontext wie jeder Kernbaustein, und reicht sie der Vorlage fertig gerendert als `children` - so kommt eine Plugin-Vorlage nie an Formulare, Beitragslisten oder die Navigation heran. #} {% set childrenHtml %}{% if plugin.container %}{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}{% endif %}{% endset %} {% include plugin.template with {block: block, props: pluginProps, designClasses: designClasses, editing: editing|default(false), plugin: plugin, children: childrenHtml, settings: kant_plugin_settings(plugin.plugin)} only %} {% endif %} {% else %} {# A block whose plugin is not available. Its stored content stays in the document; the core renders nothing from its unvalidated props. Visitors see no broken fragment, while the editor preview shows a placeholder so the block can still be found and moved. #} {% if editing|default(false) %}

Baustein „{{ type }}“ ist in dieser Installation nicht verfügbar. Der Inhalt bleibt gespeichert.

{% endif %} {% endif %}