---
title: "Step 2: Add a layout"
space: "Commera"
url: "https://docs.bwh.tech/commera/build-a-theme/add-a-layout"
updated: "2026-10-08"
---

In this step, you add the Acme layout. Every Acme page extends it, so the header, the footer and the stylesheets are in one file.

## What you'll build

- `components/theme_layout.html`, which extends Shop Base Theme's `components/base.html`
- A header with the store logo, a **Shop** link and a cart count
- A footer with the year and the store name

## 1. Create the layout file

Create `acme_theme/themes/acme/components/theme_layout.html`:

```jinja
{% extends "components/base.html" %}
{% from "components/macros/branding.html" import brand_logo with context %}

{% block head %}
	{# The shared toaster that base.html includes is styled with Tailwind utilities, so a plain-CSS theme still loads them. #}
	<link rel="stylesheet" href="/assets/commera/css/tailwind.css">
	<link rel="stylesheet" href="{{ shop_theme_asset_url('css/acme.css') }}">
{% endblock %}

{% block body_class %}theme-acme{% endblock %}

{% block chrome_top %}
	{% block site_header %}
	<header class="acme-header">
		<a class="acme-brand" href="/{{ frappe.lang or 'en' }}">{{ brand_logo(css_class="acme-logo") }}</a>
		<nav class="acme-nav">
			<a href="/{{ frappe.lang or 'en' }}/products">{{ _("Shop") }}</a>
			<a href="/{{ frappe.lang or 'en' }}/cart">
				{{ _("Cart") }} (<span x-text="$store.cart.items.length">0</span>)
			</a>
		</nav>
	</header>
	{% endblock %}
{% endblock %}

{% block chrome_bottom %}
	{% block site_footer %}
	<footer class="acme-footer">
		<p>&copy; {{ frappe.utils.nowdate()[:4] }} {{ app_name }}</p>
	</footer>
	{% endblock %}
{% endblock %}
```

`components/base.html` is not in your theme. Commera finds it in the parent theme, Shop Base Theme. The same is true for `components/macros/branding.html`, which has the `brand_logo()` macro.

The cart count reads `$store.cart`. Shop Base Theme loads that Alpine store on every page, so your layout does not load it.

## 2. Fill the right blocks

`components/base.html` has these blocks. Fill the blocks that your theme needs, and leave the others alone.

| Block | Where it is | Fill it with |
| --- | --- | --- |
| `seo` | In `<head>` | Leave it. A page sets the `seo` context variable instead. |
| `json_ld` | In `<head>` | Leave it. A page sets the `json_ld` context variable instead. |
| `analytics_head` | In `<head>` | Leave it. It has the merchant's analytics tags. |
| `alpine_plugins` | In `<head>`, before Alpine starts | `<script>` tags for extra Alpine plugins |
| `head` | End of `<head>` | Your stylesheets, fonts and preload tags |
| `body_class` | The `class` of `<body>` | A class for your theme, such as `theme-acme` |
| `body_attributes` | Inside the `<body>` tag | Extra attributes. Start with a space: ` id="bg"`. |
| `chrome_top` | Before `<main>` | The header |
| `body` | Inside `<main>` | The page content. Each page fills this block. |
| `uncontained_body` | Inside `<main>`, after `body` | Content that a page adds after its main content |
| `chrome_bottom` | After `<main>` | The footer |
| `analytics_events` | End of `<body>` | Leave it. It sends the storefront's analytics events. |
| `script` | After `</body>` | Your scripts |

:::caution[Keep the SEO and analytics blocks]

If you empty `seo` or `json_ld`, every page loses its meta tags, canonical URL, `hreflang` links and structured data. Search engines see no error. Leave both blocks as they are.

:::

### Name the header and footer blocks

Name the layout file `components/theme_layout.html`. Put the header in a block named `site_header` and the footer in a block named `site_footer`, as the Acme layout does. The names `header` and `footer` also work. The `/commera` dashboard renders this file to preview the header and the footer, and it empties the block that it does not show. With a different file name or different block names, the preview shows the wrong part of the page.

## 3. Import macros with context

```jinja
{% from "components/macros/branding.html" import brand_logo with context %}
```

:::caution[Add with context to every macro import]

Without `with context`, a macro cannot see the page's variables, such as `app_name`. Jinja does not stop with an error. It prints the text `{{ app_name }}` on the page. Add `with context` when the macro reads a page variable, such as `app_name`, `app_logo` or a value from a controller. Jinja globals, such as `frappe`, `_()` and the Commera helpers, work without it.

:::

## 4. Load Tailwind for the toast

Shop Base Theme adds a toast, the "Item added to your bag" message, to every page. The toast uses Tailwind CSS classes. Acme is plain CSS, so the layout loads Commera's Tailwind file before its own CSS:

```jinja
<link rel="stylesheet" href="/assets/commera/css/tailwind.css">
<link rel="stylesheet" href="{{ shop_theme_asset_url('css/acme.css') }}">
```

Without it, the toast shows as a large unstyled shape under the footer.

Tailwind resets the size and weight of headings. Set `h1`, `h2` and the other headings in your own CSS. Shop Default Theme loads the same file. Load it in a Tailwind theme too, because your own Tailwind build contains only the classes that your templates use.

## Next steps

- [Step 3: Add pages](/commera/build-a-theme/add-pages)
- [Templates](/commera/theme-reference/templates): every context variable and Jinja helper
