Nimbu Developer Docs
Key Concepts

Layouts

Understand how layouts provide the HTML structure and global elements for your Nimbu theme

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

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

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

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

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

{%- layout 'minimal' -%}

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

Common Layout Patterns

<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

<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>
<div class="breadcrumbs" aria-label="Breadcrumb">
  {% breadcrumbs %}
</div>

Or custom breadcrumbs:

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

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

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

Performance Optimization

Conditional Asset Loading

<!-- 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

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

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

Accessibility Best Practices

Skip Navigation

<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

<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

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

Debugging Layouts

Display Current Variables

<!-- 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

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

โŒ Hardcoded URLs

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

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

โŒ Loading too many assets

<!-- 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.

On this page