Guide 04 · Developer docs

Input types, layouts, components and images

The platform's central abstraction, in one line: an input type defines what an editor can enter, a layout area binds one input type to a named slot, a layout groups areas and points at a template, a record or component holds the content for each area, and the template reads it back. Read this book straight after the template languages.

L1–L3
Unit 09 · Patterns, live preview and upkeep

Pattern: heading levels and load priority

Two values a parent decides and its children use: the heading level, kept in the template context so that nested components produce a usable outline, and the load priority, passed to each component so that the first images load eagerly and the rest lazily.

Who this is for. Developers writing page and component templates that nest. After this page your components print their headings at the right level wherever an author places them, and your pages load the images at the top first.

Heading levels

A component cannot tell how deep it sits. A card that prints its title as <h2> is right at the top of a page and wrong inside a section that already has an <h2>; a hero that always prints <h1> fills a page built from several heroes with first-level headings. Screen readers and search engines then get an outline with no structure.

Keep the current level in one place that every template in the render can read and change: the template context, $_templateContext (Twig _templateContext). A value stored with its set(key, value) is returned by get(key) anywhere in the same page render, including the templates of nested components. An ordinary template variable is not shared that way, which is why the level lives in the context.

The rule

  1. The page shell sets the level to 1 once, before any component renders.
  2. A component reads the level and, if it has a heading, prints it at that level.
  3. Before rendering its children, a component that printed a heading raises the level by one, so that the children's headings sit under it.
  4. After its children, it puts the level back.
Template code
{* page shell, once, straight after <body> *}
{$_templateContext->set('heading-level', 1)}
{# page shell, once, straight after <body> #}
{% do _templateContext.set('heading-level', 1) %}
Template code
{* any component that prints a heading and renders children *}
{$headingLevel = $_templateContext->get('heading-level')}
{if !empty($component->heading)}
  <h{$headingLevel} class="component__heading">{$component->heading}</h{$headingLevel}>
  {$_templateContext->set('heading-level', $headingLevel + 1)}
{/if}
{foreach $component->getAreaCollectionElements('content') as $element}
  {$element->getComponent()->getHtml()}
{/foreach}
{$_templateContext->set('heading-level', $headingLevel + (($headingLevel eq 1) ? 1 : 0))}
{# any component that prints a heading and renders children #}
{% set level = _templateContext.get('heading-level') %}
{% if component.heading is not empty %}
  <h{{ level }} class="component__heading">{{ component.heading }}</h{{ level }}>
  {% do _templateContext.set('heading-level', level + 1) %}
{% endif %}
{% for element in component.getAreaCollectionElements('content') %}
  {{ element.getComponent().getHtml() }}
{% endfor %}
{% do _templateContext.set('heading-level', level + (level == 1 ? 1 : 0)) %}

Why the level does not go back to 1

The last line adds one when the level it found was 1: $headingLevel + (($headingLevel eq 1) ? 1 : 0). For a component entered at level 2 or deeper, that puts the level back exactly as it was. For a component entered at level 1, the first top-level component on the page, it leaves the level at 2. The next top-level component then prints its heading as <h2>. Only the first heading on the page can be an <h1>, and every later section starts at the second level.

That suits a page whose first component is its hero and carries the page's one <h1>. If your page shell prints the <h1> itself, from the page's name, seed the context with 2 instead and make the last line a plain restore, set('heading-level', $headingLevel), so that every top-level component starts at the second level.

Restore on every path

A component that raises the level and never restores it pushes every later heading on the page one level down. Put the restore after the children and outside any {if}, so that it runs whether or not the component had a heading.

Load priority

Images near the top of a page should load at once; the rest can wait until the reader scrolls. The HTML loading attribute says which, eager or lazy. A component cannot tell whether it is near the top, but the loop that renders the page's components can, so the parent decides and passes the value down.

The parent counts its elements and passes loadPriority to each component through getHtml(). The values are the attribute's own, so the child hands the variable straight to the image's loading attribute with no translation. Smarty's @iteration and Twig's loop.index both count from 1. The threshold of two is a choice: use the number of components that fit in the first screen on your layouts, or @first / loop.first for the first one only.

Template code
{* page template: the first two sections load eagerly *}
{foreach $page->getAreaCollectionElements('pageSections') as $element}
  {$loadPriority = ($element@iteration > 2) ? 'lazy' : 'eager'}
  {$element->getComponent()->getHtml(['loadPriority' => $loadPriority])}
{/foreach}
{# page template: the first two sections load eagerly #}
{% for element in page.getAreaCollectionElements('pageSections') %}
  {{ element.getComponent().getHtml({'loadPriority': loop.index > 2 ? 'lazy' : 'eager'}) }}
{% endfor %}
Template code
{* component template: lazy when no parent passed a value *}
{$loadPriority = $loadPriority|default:'lazy'}
{$component->getAreaImageOrPlaceholder('image', 414, 304)->getResized(414, 304, 'fit')->getHtml(['loading' => $loadPriority])}
{# component template: lazy when no parent passed a value #}
{{ component.getAreaImageOrPlaceholder('image', 414, 304).getResized(414, 304, 'fit').getHtml({'loading': loadPriority|default('lazy')}) }}

fetchpriority, derived from it

Browsers also read fetchpriority, which has its own values: high or auto. It is a second attribute, not the same value used twice. Derive it from the first: high when the image is eager, auto otherwise. An image include that collects an image's attributes in an array can do it for every image before rendering it:

Template code
{* image include: $attributes holds the image's HTML attributes *}
{if !isset($attributes.loading)}{$attributes.loading = 'lazy'}{/if}
{if !isset($attributes.fetchpriority) && $attributes.loading == 'eager'}{$attributes.fetchpriority = 'high'}{/if}
{# image include: attributes holds the image's HTML attributes #}
{% set attributes = {'loading': 'lazy'}|merge(attributes|default({})) %}
{% if attributes.fetchpriority is not defined and attributes.loading == 'eager' %}
  {% set attributes = attributes|merge({'fetchpriority': 'high'}) %}
{% endif %}

What getHtml() lets you pass

A component's template receives the page's variables, then the variables you pass to getHtml(), then component. Where two of these share a name, the first one wins:

  • A name the page already provides (page, _currentSite, _query, _globalComponents and the rest) keeps the page's value, so what you pass under that name is not used.
  • A name the page does not provide reaches the child. loadPriority is such a name, so it always arrives.
  • component is not one of the page's variables, so a component you pass wins over, and replaces, the component being rendered. Never pass a variable called component.

The loop that renders a collection, and the rest of what getHtml() does, are on The collection loop and rendering a component. Image attributes and responsive sources are on Picture tags and responsive sources.

Go deeper

Was this page helpful?

Your answer goes to the docs team.