K KodePressdocumentation
Laravel 11 · MySQL 8 Specification Built 2026-10-10

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.

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.

{{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:

  1. Per-page override wins. If pages.{kind}_mode = 'none', no part is rendered. If it is 'custom' and pages.{kind}_part_id points at a published part, use it. Specificity 100.
  2. Otherwise collect every published part of this kind whose conditions match the page, excluding any whose exclude matches.
  3. Score each match by its mode specificity. Highest wins.
  4. Tie-break on position ascending, then on the lowest id — deterministic, never random.
  5. If nothing matched, use the part with is_default = 1.
  6. 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.