Templates, Layouts, and the Rendering Pipeline Beginner 12 min read

theme.liquid Deep Dive

Understand the master layout file that wraps every page in your Shopify theme, including required placeholders, section groups, and best practices.

Every page in your Shopify store passes through theme.liquid. It’s the master layout file that wraps all your content, providing the HTML structure, head elements, and global sections that appear on every page. Understanding this file is essential for building well-structured themes.

What is theme.liquid?

The theme.liquid file lives in your layout/ directory and serves as the outer shell for every page. Think of it as the HTML skeleton that holds everything together:

┌─────────────────────────────────────┐
│ theme.liquid │
│ ┌───────────────────────────────┐ │
│ │ Header Section Group │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ │ │
│ │ {{ content_for_layout }} │ │
│ │ (Template content goes │ │
│ │ here) │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Footer Section Group │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘

Basic Structure

Here’s a minimal theme.liquid file:

<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }}
</head>
<body>
{{ content_for_layout }}
{{ 'theme.js' | asset_url | script_tag }}
</body>
</html>

Required Placeholders

Two placeholders are absolutely required in every layout file:

{{ content_for_header }}

This placeholder outputs essential code that Shopify needs to function:

  • Analytics and tracking scripts
  • Shopify’s internal JavaScript
  • App scripts and stylesheets
  • Dynamic checkout buttons
  • Payment provider scripts
<head>
<!-- Your meta tags and title -->
{{ content_for_header }}
<!-- Your stylesheets -->
</head>

Important: Always place content_for_header in the <head> section. Putting it elsewhere can break checkout, apps, and analytics.

{{ content_for_layout }}

This placeholder outputs the actual page content from your templates:

<body>
<header>...</header>
<main id="main-content">
{{ content_for_layout }}
</main>
<footer>...</footer>
</body>

For a product page, content_for_layout outputs the rendered product.json template. For a collection page, it outputs collection.json, and so on.

Section Groups

Section groups let merchants add, remove, and reorder sections in specific areas of your layout. The most common are header and footer groups:

<body>
{% sections 'header-group' %}
<main id="main-content">
{{ content_for_layout }}
</main>
{% sections 'footer-group' %}
</body>

Section groups are defined in JSON files in the sections/ directory:

sections/header-group.json
{
"type": "header-group",
"name": "Header Group",
"sections": {
"announcement-bar": {
"type": "announcement-bar"
},
"header": {
"type": "header"
}
},
"order": ["announcement-bar", "header"]
}

Merchants can then customize these sections and their order in the theme editor.

The Head Section

A well-structured <head> includes several important elements:

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{%- comment -%} Title with fallback {%- endcomment -%}
<title>
{{ page_title }}
{%- if current_tags %} &ndash; tagged "{{ current_tags | join: ', ' }}"{% endif -%}
{%- if current_page != 1 %} &ndash; Page {{ current_page }}{% endif -%}
{%- unless page_title contains shop.name %} &ndash; {{ shop.name }}{% endunless -%}
</title>
{%- if page_description -%}
<meta name="description" content="{{ page_description | escape }}">
{%- endif -%}
{%- comment -%} Canonical URL {%- endcomment -%}
<link rel="canonical" href="{{ canonical_url }}">
{%- comment -%} Favicon {%- endcomment -%}
<link rel="icon" type="image/png" href="{{ 'favicon.png' | asset_url }}">
{%- comment -%} Preconnect to Shopify CDN {%- endcomment -%}
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{%- comment -%} Required Shopify content {%- endcomment -%}
{{ content_for_header }}
{%- comment -%} Theme stylesheets {%- endcomment -%}
{{ 'base.css' | asset_url | stylesheet_tag }}
{%- comment -%} Theme settings as CSS variables {%- endcomment -%}
{% render 'css-variables' %}
</head>

Loading Fonts

If you’re using custom fonts, preload them for better performance:

{%- comment -%} Preload critical fonts {%- endcomment -%}
<link
rel="preload"
as="font"
href="{{ 'custom-font.woff2' | asset_url }}"
type="font/woff2"
crossorigin
>
{%- comment -%} Or use Shopify's font picker {%- endcomment -%}
{% style %}
{{ settings.type_body_font | font_face: font_display: 'swap' }}
{{ settings.type_header_font | font_face: font_display: 'swap' }}
{% endstyle %}

CSS Variables from Theme Settings

A common pattern is rendering theme settings as CSS custom properties:

{# snippets/css-variables.liquid #}
{% style %}
:root {
--color-primary: {{ settings.color_primary }};
--color-secondary: {{ settings.color_secondary }};
--color-background: {{ settings.color_background }};
--color-text: {{ settings.color_text }};
--font-body: {{ settings.type_body_font.family }}, {{ settings.type_body_font.fallback_families }};
--font-heading: {{ settings.type_header_font.family }}, {{ settings.type_header_font.fallback_families }};
--spacing-unit: {{ settings.spacing_unit }}px;
--border-radius: {{ settings.border_radius }}px;
}
{% endstyle %}

Then in your CSS:

body {
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-background);
}
h1,
h2,
h3 {
font-family: var(--font-heading);
color: var(--color-primary);
}

JavaScript Loading

Load JavaScript at the end of the body for better performance:

<body>
{% sections 'header-group' %}
<main>
{{ content_for_layout }}
</main>
{% sections 'footer-group' %}
{%- comment -%} Scripts at end of body {%- endcomment -%}
<script src="{{ 'vendor.js' | asset_url }}" defer></script>
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{%- comment -%} Pass Liquid data to JavaScript {%- endcomment -%}
<script>
window.theme = {
moneyFormat: {{ shop.money_format | json }},
cartCount: {{ cart.item_count }},
customerId: {{ customer.id | default: 'null' }}
};
</script>
</body>

Include a skip link for keyboard users:

<body>
<a href="#main-content" class="skip-link">
Skip to content
</a>
{% sections 'header-group' %}
<main id="main-content" tabindex="-1">
{{ content_for_layout }}
</main>
{% sections 'footer-group' %}
</body>

With CSS:

.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 1rem;
background: var(--color-primary);
color: white;
z-index: 9999;
}
.skip-link:focus {
top: 0;
}

Alternate Layouts

You can create additional layout files for special pages:

layout/
├── theme.liquid # Default layout
├── password.liquid # Password page layout
└── gift_card.liquid # Gift card layout

Password Layout

For stores with password protection enabled:

{# layout/password.liquid #}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ shop.name }}</title>
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }}
</head>
<body class="password-page">
{{ content_for_layout }}
</body>
</html>

Gift Card Layout

Gift cards have special requirements:

{# layout/gift_card.liquid #}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ 'gift_cards.issued.title' | t }} | {{ shop.name }}</title>
{{ content_for_header }}
{{ 'gift-card.css' | asset_url | stylesheet_tag }}
</head>
<body class="gift-card-page">
{{ content_for_layout }}
{%- comment -%} QR code script for gift cards {%- endcomment -%}
<script src="{{ 'qrcode.js' | shopify_asset_url }}" defer></script>
</body>
</html>

Detecting the Current Page

Use the request object to apply page-specific logic in your layout:

<body
class="template-{{ request.page_type }}
{%- if customer %} customer-logged-in{% endif -%}
{%- if request.design_mode %} design-mode{% endif -%}"
data-template="{{ template.name }}"
>

Or load page-specific styles:

{%- case request.page_type -%}
{%- when 'product' -%}
{{ 'product.css' | asset_url | stylesheet_tag }}
{%- when 'collection' -%}
{{ 'collection.css' | asset_url | stylesheet_tag }}
{%- endcase -%}

Complete Example

Here’s a production-ready theme.liquid:

<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>
{{ page_title }}
{%- unless page_title contains shop.name %} | {{ shop.name }}{% endunless -%}
</title>
{%- if page_description -%}
<meta name="description" content="{{ page_description | escape }}">
{%- endif -%}
<link rel="canonical" href="{{ canonical_url }}">
{%- if settings.favicon -%}
<link rel="icon" type="image/png" href="{{ settings.favicon | image_url: width: 32 }}">
{%- endif -%}
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{%- comment -%} Detect JS support {%- endcomment -%}
<script>document.documentElement.classList.replace('no-js', 'js');</script>
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }}
{% render 'css-variables' %}
</head>
<body class="template-{{ request.page_type }}">
<a href="#main-content" class="skip-link visually-hidden-focusable">
{{ 'general.accessibility.skip_to_content' | t }}
</a>
{% sections 'header-group' %}
<main id="main-content" role="main" tabindex="-1">
{{ content_for_layout }}
</main>
{% sections 'footer-group' %}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
<script>
window.shopUrl = {{ request.origin | json }};
window.routes = {
cart: '{{ routes.cart_url }}',
cartAdd: '{{ routes.cart_add_url }}',
cartChange: '{{ routes.cart_change_url }}'
};
</script>
</body>
</html>

Key Takeaways

  1. theme.liquid wraps every page in your store
  2. content_for_header is required in <head> for Shopify functionality
  3. content_for_layout outputs the template content
  4. Section groups enable header/footer customization
  5. CSS variables from theme settings enable dynamic theming
  6. Alternate layouts handle special pages like password and gift cards
  7. Performance matters: preconnect, defer scripts, optimize font loading

What’s Next?

Now that you understand the layout layer, the next lesson covers JSON Templates and how they define which sections appear on each page type.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...