{% set type = block.type %} {% set props = block.props|default({}) %} {% set layout = block.layout|default({}) %} {% set designClasses = '' %} {% 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 }}"> {% 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' %} {{ 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 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 props.href|default('') -%} {%- endif -%}{{ props.alt }}{% if klick == 'vergroessern' or props.href|default('') %}{% 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 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 %}
{% 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. #} {% 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 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' %}
{% include 'blocks/form.html.twig' with (forms[block.id]|default({form: null, sent: false, errors: {}, values: {}, blockId: block.id, page: {path: ''}, issued: 0, issuedSignature: ''})) only %}
{% 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 == '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' %} {# 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 %} {% 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 %}
{% 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 %}