Skip to content

Layouts

Layouts are the foundation of your Nimbu theme. They provide the outer HTML structure (<html>, <head>, <body>), navigation, footer, and global scripts that wrap around your template content.

Purpose of Layouts

Layouts serve several critical functions:

  • HTML Structure: Provide the document structure and <head> meta tags
  • Global Elements: Include navigation, header, and footer across all pages
  • Asset Loading: Load CSS, JavaScript, and fonts
  • SEO: Define meta tags, Open Graph, and structured data
  • Consistency: Ensure design consistency across all pages

Basic Layout Structure

Here's a minimal layout (layouts/default.liquid):

liquid
<!DOCTYPE html>
<html lang="{{ locale }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ page.title }} - {{ site.name }}</title>

  {{ content_for_header }}
</head>
<body>
  {{ content_for_body }}
</body>
</html>

Required Elements

Every layout must include:

  1. {{ content_for_header }} - Nimbu-injected scripts, CSRF tokens, meta tags
  2. {{ content_for_body }} - Where templates render their content

Without these, your theme won't work properly.

Real-World Layout Example

Here's a production layout with all common features:

liquid
<!DOCTYPE html>
<html class="no-js" lang="{{ locale }}" itemscope itemtype="http://schema.org/WebPage">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  {%- capture page_title -%}
    {{ page.title }}{% unless page.title contains site.name %} · {{ site.name }}{% endunless %}
  {%- endcapture -%}

  <title>{{ page_title }}</title>

  <!-- SEO Meta Tags -->
  <meta name="description" content="{{ seo.description }}">
  <meta name="keywords" content="{{ seo.keywords }}">

  <!-- Open Graph -->
  {% if page.og_image.url %}
    {% assign og_image = page.og_image.url %}
  {% else %}
    {% assign og_image = 'og-default.jpg' | theme_image_url %}
  {% endif %}

  <meta property="og:title" content="{{ page_title }}">
  <meta property="og:type" content="website">
  <meta property="og:url" content="{{ url.current }}">
  <meta property="og:image" content="{{ og_image }}">
  <meta property="og:description" content="{{ seo.description }}">
  <meta property="og:site_name" content="{{ site.name }}">

  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="{{ page_title }}">
  <meta name="twitter:description" content="{{ seo.description }}">
  <meta name="twitter:image" content="{{ og_image }}">

  <!-- Favicon -->
  <link rel="icon" sizes="32x32" href="{{ 'favicon-32x32.png' | theme_image_url }}" type="image/png">
  <link rel="icon" sizes="16x16" href="{{ 'favicon-16x16.png' | theme_image_url }}" type="image/png">
  <link rel="apple-touch-icon" sizes="180x180" href="{{ 'apple-touch-icon.png' | theme_image_url }}">

  <!-- Stylesheets -->
  {{ "vendor.css" | stylesheet_tag }}
  {{ "style.css" | stylesheet_tag }}

  <!-- Nimbu Required -->
  {{ content_for_header }}
</head>

<body class="template-{{ template }}" id="top">
  <!-- Header & Navigation -->
  <header class="main-header">
    <div class="container">
      <a href="{% if locale_url_prefix %}{{ locale_url_prefix }}{% else %}/{% endif %}" class="logo">
        <img src="{{ 'logo.png' | theme_image_url }}" alt="{{ site.name }}">
      </a>

      <nav id="main-nav">
        {% nav main, class: "nav", link_class: "nav-link" %}
      </nav>

      <!-- Language Switcher -->
      {% assign translation_count = page.translations | size %}
      {% if translation_count > 1 %}
        <div class="language-switcher">
          {% for translation in page.translations %}
            <a href="{{ translation.url }}"
               class="{% if locale == translation.locale %}active{% endif %}">
              {{ translation.locale | upcase }}
            </a>
          {% endfor %}
        </div>
      {% endif %}
    </div>
  </header>

  <!-- Main Content Area -->
  <main id="main-content">
    {{ content_for_body }}
  </main>

  <!-- Footer -->
  <footer class="main-footer">
    <div class="container">
      <div class="footer-columns">
        <div class="footer-column">
          <h4>{% translate 'footer.about', default: 'About' %}</h4>
          <ul class="footer-menu">
            {% for item in menus.footer.items %}
              <li><a href="{{ item.target }}">{{ item.name }}</a></li>
            {% endfor %}
          </ul>
        </div>

        <div class="footer-column">
          <h4>{% translate 'footer.contact', default: 'Contact' %}</h4>
          <p>{% translate 'contact.email' %}</p>
          <p>{% translate 'contact.phone' %}</p>
        </div>
      </div>

      <div class="footer-bottom">
        <p>&copy; {{ 'now' | date: '%Y' }} {{ site.name }}</p>
      </div>
    </div>
  </footer>

  <!-- Scripts -->
  {{ "vendor.js" | javascript_tag }}
  {{ "app.js" | javascript_tag }}

  <!-- Optional: Additional scripts from templates -->
  {% if content_for_javascript %}
    {{ content_for_javascript }}
  {% endif %}

  <!-- Analytics -->
  {{ site.google_analytics_id | google_analytics_tag }}
</body>
</html>

Multiple Layouts

Create specialized layouts for different page types:

Minimal Layout (layouts/minimal.liquid)

For landing pages, login pages, or checkout:

liquid
<!DOCTYPE html>
<html lang="{{ locale }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ page.title }}</title>
  {{ "minimal.css" | stylesheet_tag }}
  {{ content_for_header }}
</head>
<body class="minimal-layout">
  <div class="centered-container">
    {{ content_for_body }}
  </div>
</body>
</html>

Optimized for printing:

liquid
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>{{ page.title }}</title>
  {{ "print.css" | stylesheet_tag }}
  {{ content_for_header }}
</head>
<body>
  <div class="print-header">
    <img src="{{ 'logo-print.png' | theme_image_url }}" alt="{{ site.name }}">
    <p>{{ 'now' | localized_date: 'long' }}</p>
  </div>

  {{ content_for_body }}

  <div class="print-footer">
    <p>{{ site.name }} - {{ url.current }}</p>
  </div>
</body>
</html>

Switching Layouts

Change layouts from within templates using the {% layout %} tag:

liquid
{%- layout 'minimal' -%}

<div class="login-form">
  <h1>Sign In</h1>
  <!-- Login form content -->
</div>

Common Layout Patterns

liquid
<header class="main-header sticky">
  <div class="container">
    <a href="/" class="logo">
      <img class="normal" src="{{ 'logo.png' | theme_image_url }}" alt="{{ site.name }}">
      <img class="scroll" src="{{ 'logo-dark.png' | theme_image_url }}" alt="{{ site.name }}">
    </a>
    <nav>{% nav main %}</nav>
  </div>
</header>

<style>
.main-header.sticky {
  position: fixed;
  top: 0;
  width: 100%;
  z-index: 1000;
  transition: all 0.3s ease;
}

.main-header.sticky.scrolled {
  background: white;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>

Mobile Navigation

liquid
<header class="main-header">
  <div class="container">
    <a href="/" class="logo">{{ site.name }}</a>

    <!-- Mobile Menu Toggle -->
    <button class="menu-toggle" aria-label="Toggle menu">
      <span></span>
      <span></span>
      <span></span>
    </button>

    <!-- Navigation -->
    <nav class="main-nav">
      {% nav main, class: "nav" %}
    </nav>
  </div>
</header>

<script>
document.querySelector('.menu-toggle').addEventListener('click', function() {
  document.querySelector('.main-nav').classList.toggle('active');
  this.classList.toggle('active');
});
</script>
liquid
<div class="breadcrumbs" aria-label="Breadcrumb">
  {% breadcrumbs %}
</div>

Or custom breadcrumbs:

liquid
<nav class="breadcrumbs">
  <a href="/">Home</a>
  {% if template == 'product' %}
    <span> / </span>
    <a href="/shop">Shop</a>
    <span> / </span>
    <span>{{ product.name }}</span>
  {% elsif template == 'page' %}
    <span> / </span>
    <span>{{ page.title }}</span>
  {% endif %}
</nav>

Template-Specific Content

Inject content from templates back into layouts using capture:

In your template:

liquid
{% capture content_for_javascript %}
  <script src="https://maps.googleapis.com/maps/api/js"></script>
  <script>
    initMap({{ location | to_geopoint | json }});
  </script>
{% endcapture %}

In your layout:

liquid
{% if content_for_javascript %}
  {{ content_for_javascript }}
{% endif %}

Performance Optimization

Conditional Asset Loading

liquid
<!-- Load only on product pages -->
{% if template == 'product' %}
  {{ "product-zoom.js" | javascript_tag }}
  {{ "product-gallery.css" | stylesheet_tag }}
{% endif %}

<!-- Load only when cart has items -->
{% if cart.items.size > 0 %}
  {{ "cart.js" | javascript_tag }}
{% endif %}

Async/Defer Scripts

liquid
{{ "vendor.js" | javascript_tag }}
{{ "app.js" | javascript_tag }}

<!-- Third-party scripts with defer -->
<script defer src="https://cdn.example.com/widget.js"></script>

Preconnect to External Resources

liquid
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com">

Accessibility Best Practices

Skip Navigation

liquid
<a href="#main-content" class="skip-link">Skip to main content</a>

<header>...</header>

<main id="main-content" tabindex="-1">
  {{ content_for_body }}
</main>

ARIA Landmarks

liquid
<header role="banner">
  <nav role="navigation" aria-label="Main navigation">
    {% nav main %}
  </nav>
</header>

<main role="main">
  {{ content_for_body }}
</main>

<footer role="contentinfo">
  ...
</footer>

Language Declaration

liquid
<html lang="{{ locale }}" {% if locale == 'ar' %}dir="rtl"{% endif %}>

Debugging Layouts

Display Current Variables

liquid
<!-- Only in development -->
{% if params.debug == 'true' %}
  <div class="debug-info" style="background: #f0f0f0; padding: 20px; margin: 20px;">
    <h3>Debug Info</h3>
    <p><strong>Template:</strong> {{ template }}</p>
    <p><strong>Locale:</strong> {{ locale }}</p>
    <p><strong>Path:</strong> {{ path }}</p>
    <p><strong>Customer:</strong> {{ customer.email | default: 'Not logged in' }}</p>
    <p><strong>Cart Items:</strong> {{ cart.items.size | default: 0 }}</p>
  </div>
{% endif %}

Best Practices

  1. Keep layouts DRY: Use snippets for repeated sections like header and footer
  2. Conditional loading: Only load scripts/styles when needed
  3. Semantic HTML: Use proper HTML5 elements (<header>, <nav>, <main>, <footer>)
  4. Mobile-first: Design for mobile, enhance for desktop
  5. Accessibility: Include skip links, ARIA labels, and proper landmarks
  6. Performance: Minimize HTTP requests, use CDN for assets, defer non-critical scripts
  7. SEO: Include proper meta tags, Open Graph, and structured data

Common Pitfalls

Missing required elements

liquid
<!-- Don't forget these! -->
{{ content_for_header }}
{{ content_for_body }}

Hardcoded URLs

liquid
<!-- Bad -->
<a href="/en/about">About</a>

<!-- Good -->
<a href="{{ locale_url_prefix }}/about">About</a>

Loading too many assets

liquid
<!-- Bad: Loading everything everywhere -->
{{ "feature-x.js" | javascript_tag }}
{{ "feature-y.js" | javascript_tag }}
{{ "feature-z.js" | javascript_tag }}

<!-- Good: Conditional loading -->
{% if template == 'feature-page' %}
  {{ "feature-x.js" | javascript_tag }}
{% endif %}

Next Steps

Layouts provide the foundation—now learn how templates fill in the content.