08 — Header and footer (template parts)#
Headers and footers are rows in template_parts, edited with the page editor in the part context,
with their own drafts, versions and publish step. A site may have several named headers and footers
and assign them by condition.
8.1 Kinds#
kind |
What it is | Position |
|---|---|---|
header |
the main site header, optionally containing a top bar and a sticky variant | above the page |
footer |
the site footer, including the bottom bar | below the page |
topbar |
a standalone top bar, when it should vary independently of the header | above the header |
announcement |
a dismissible announcement strip | very top, above everything |
In practice most sites use one header with its top-bar zone filled in, and one footer. The
separate topbar and announcement kinds exist so a strip can be swapped or scheduled without
republishing the header.
8.2 Header structure#
A header tree has up to three zones, each a normal section list:
template_parts.content = {
"version": 1,
"zones": {
"topbar": { "enabled": true, "sections": [ ... ] },
"main": { "enabled": true, "sections": [ ... ] },
"sticky": { "enabled": false, "sections": [ ... ] } // empty = reuse main, shrunk
}
}
A footer uses the same shape with zones main and bottom. For a page, content.sections is used
directly; the zones wrapper applies only to parts. The validator accepts either shape and the
renderer dispatches on which key is present.
topbar— small strip: phone, email, social icons, language switcher, a short text.main— logo, menu slot, search, CTA button, account link.sticky— what is shown once the user scrolls, whensettings.stickyis on. Left empty, themainzone is reused with the shrink settings applied, which is what most users want.
The editor shows the three zones as collapsible groups in the outline, each with its own
Add section.
8.3 Header presets#
Chosen when a header is created, and switchable later from Design -> Header -> Change preset. A
preset is a templates row with kind = header and seeds the tree; after that the user edits it
freely. Switching presets warns that the current layout will be replaced and offers to save the
current one as a template first.
| Preset | Layout |
|---|---|
logo-left |
logo left, menu centre-right, CTA right |
centered-logo |
logo centred on row 1, menu centred on row 2 |
two-row |
top bar with contact info, main row with logo and menu |
transparent |
overlays the first section (hero), turns solid on scroll |
hamburger |
logo and a hamburger only, menu in a drawer at every width |
sidebar |
vertical header fixed to the left edge, page content offset |
transparent and sidebar need cooperation from the page layout; both are implemented with body
classes emitted by the renderer (kp-header-transparent, kp-header-sidebar) and handled by the
theme CSS, not by per-page markup changes.
8.4 Header blocks and behaviour#
Blocks available in the part context: logo, menu_slot, search, language_switcher,
social_icons, button, text, image, html (Admin), announcement_bar, heading,
divider, spacer, back_to_top (footer), newsletter (footer), post_list (footer),
form (footer).
The logo block holds three variants — default, dark (for a transparent header over a dark hero)
and mobile — plus a height per device and a link (home by default).
template_parts.settings for a header:
| Setting | Values | Default |
|---|---|---|
sticky |
off, always, after-scroll | after-scroll |
sticky_offset |
px before it sticks | 120 |
shrink_on_scroll |
bool + target height | false |
bg_change_on_scroll |
bool + token | false |
hide_on_scroll_down |
bool (show again on scroll up) | false |
height |
px per device | auto |
shadow |
none, sm, md, lg | sm |
border_bottom |
none, hairline, token colour | hairline |
container |
container, wide, full | container |
mobile.mode |
drawer, fullscreen, dropdown | drawer |
mobile.breakpoint |
px | 1024 |
mobile.drawer_side |
left, right | right |
mobile.show_search |
bool | true |
mobile.tree |
optional separate mobile zone tree | null (reuse desktop) |
All scroll behaviour is one small Alpine component reading data-kp-header attributes emitted by the
renderer. There is no per-site JavaScript and nothing is compiled per site.
mobile.tree deserves a note: the default is to transform the desktop tree (menu slot becomes a
drawer, extra blocks stack or hide). A user who wants a genuinely different mobile header enables
Separate mobile design, which copies the current tree as a starting point. Two trees mean two
things to maintain, so the UI says so when enabling it.
8.5 Footer structure#
mainzone: 1 to 6 columns, anypart-context blocks. Typical columns: logo + about text, amenu_slot, a secondmenu_slot, anewsletterform, address text + map image,social_icons, payment icons (animageoricon_list), latest posts (post_list), app-store buttons.bottomzone: copyright text supporting{{year}}(and{{site}},{{page}}), policy links, alanguage_switcher, aback_to_top.- Style settings: background colour / image / gradient (tokens first), text colour, top divider shape (none, angle, curve, zigzag — rendered as an inline SVG), padding, column gap, and column layout per breakpoint (e.g. 4 columns desktop, 2 tablet, 1 mobile).
{{year}} is expanded at render time, not at publish time, so the cached HTML would otherwise
freeze on 31 December. The renderer therefore keeps the token in the cached file and substitutes it
on serve for this one case, and the cache is additionally keyed with the current year. A test
asserts the footer shows the right year after a simulated year change.
8.6 Assignment: which part does a page get?#
Multiple published parts per kind are allowed. template_parts.conditions holds rules:
{
"mode": "all",
"include": { "types": ["post"], "page_ids": [], "category_ids": [3], "paths": ["blog/*"] },
"exclude": { "page_ids": [1] }
}
mode is one of:
mode |
Meaning | Specificity |
|---|---|---|
all |
every page | 10 |
type |
pages of the listed types (page, post, landing) |
20 |
category |
posts in the listed categories | 30 |
path |
paths matching the listed glob patterns | 40 |
pages |
the listed page ids | 50 |
Resolution algorithm (PartResolver)#
For a given page and kind:
- Per-page override wins. If
pages.{kind}_mode = 'none', no part is rendered. If it is'custom'andpages.{kind}_part_idpoints at a published part, use it. Specificity 100. - Otherwise collect every published part of this kind whose conditions match the page, excluding
any whose
excludematches. - Score each match by its
modespecificity. Highest wins. - Tie-break on
positionascending, then on the lowestid— deterministic, never random. - If nothing matched, use the part with
is_default = 1. - If there is no default (only possible if the owner deleted it), render nothing and log a warning;
the Dashboard shows
No default header set. The public page still renders.
The admin UI makes this visible rather than magic: each part lists Applies to: all pages except
Home (12 pages) with a link to the matched list, and the page editor shows Header: Shop header
(by path rule) with a dropdown to override.
Why specificity instead of priority numbers#
Hand-managed priority integers are the classic source of "why is the wrong header showing?". Fixed specificity per rule type means the behaviour is explainable in one sentence: the more specific rule wins, and a per-page choice beats every rule.
8.7 Drafts, preview and versions#
| Capability | Behaviour |
|---|---|
| Draft | template_parts.draft_content; autosaved exactly like a page |
| Preview on any page | Preview on: picker chooses any published page; the preview route renders that page with this part's draft, signed-token protected |
| Publish | copies draft to content, creates a template_part_versions row, sets status = published — and clears the whole page cache |
| Version restore | loads an old version into the draft; publishing it creates a new version |
| Unpublish | a part that is not published is never resolved; unpublishing the default is blocked with a message |
8.8 Theme tokens, and the limits of freedom#
Colour and font fields in part editing show theme tokens. A raw colour is available only under
Advanced, which only Admin and Designer can open. That keeps a header consistent with the rest of
the site by default, while leaving an escape hatch for someone who knows what they are doing.
8.9 Cache implications#
Any publish of a template_part, any change to a menu it contains, and any theme token change
clears all cached pages (see
13-caching-and-performance.md). This is
accepted: these changes are rare and the rebuild is lazy, one page at a time, as visitors arrive,
with an optional cron warm-up of the most-visited paths.
8.10 Permissions#
| Action | Admin | Editor | Writer | Designer |
|---|---|---|---|---|
| Edit / publish a header or footer | yes | no | no | yes |
| Create a new named part | yes | no | no | yes |
| Change assignment conditions | yes | no | no | yes |
| Per-page header/footer override | yes | yes | no | yes |
| Use the HTML block inside a part | yes | no | no | no |
| Open the Advanced tab (raw CSS/colour) | yes | no | no | yes |
An Editor can say "this landing page has no header" without being able to redesign the header.
8.11 Tests for this document#
| Test | Asserts |
|---|---|
default_header_renders_on_every_page |
the seeded default appears on a fresh page |
path_rule_beats_type_rule |
specificity ordering |
page_override_beats_every_rule |
including over a pages rule |
header_mode_none_renders_no_header |
and the page still returns 200 |
deleting_default_is_blocked |
with a translated message |
part_publish_clears_all_pages |
two cached pages are both gone |
draft_preview_uses_draft_not_published |
and requires a valid signed token |
restore_part_version |
restores into the draft, not into production |
footer_year_token_is_render_time |
the year changes after a simulated clock move |
sticky_settings_emit_data_attributes |
behaviour is data-driven, no per-site JS |
editor_cannot_publish_header |
403 via TemplatePartPolicy |
KodePress documentation · generated from the Markdown sources by
tools/build-docs-site.py · internal preview, not indexed.