Liquid Snippets by ALSEL
🏗️ テーマ基盤中級

theme.liquid(テーマのメインレイアウト)

すべてのページに共通する HTML ドキュメント構造を定義するメインレイアウトファイル。フォント・カラー・スペーシング・ボタンスタイルなどをテーマ設定から CSS 変数に変換し、グローバルスタイルを一元管理する。

用途
Online Store 2.0 テーマの基盤として、ストアのあらゆるページに適用される共通レイアウト。カスタマイザー設定の値を CSS 変数として <head> に埋め込むことで、セクション・スニペット内での動的スタイリングを可能にする。
設置場所
layout/theme.liquid(固定パス)として新規作成またはカスタマイズ。`{{ content_for_header }}` は Shopify 必須タグのため削除厳禁。templates/ 内のすべてのテンプレートがこのレイアウトを自動適用する。
注意点
font_modify フィルターは太字・イタリックなど複数バリエーション生成するため、設定でシステムフォント選択時(system? = true)と Web フォント選択時で処理を分ける必要がある。CSS 変数として RGB 値を格納(`{{ settings.colors_text.red }}, {{ settings.colors_text.green }}, {{ settings.colors_text.blue }}`)するため、CSS での使用時は `rgb(var(--color-base-text))` の形式で呼び出す。body_scale・heading_scale は 100 で除算または乗算する前に必ず数値型に統一し、計算結果がマイナスや極端に大きい値にならないよう at_most・at_least で上下限をチェックする。
タグ:layoutcss-variabletheme-settingfontcolor

コード

242 行 / liquid
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="theme-color" content="">
    <link rel="canonical" href="{{ canonical_url }}">
    <link rel="preconnect" href="https://cdn.shopify.com" crossorigin>

    {%- if settings.favicon != blank -%}
      <link rel="icon" type="image/png" href="{{ settings.favicon | image_url: width: 32, height: 32 }}">
    {%- endif -%}

    {%- unless settings.type_header_font.system? and settings.type_body_font.system? -%}
      <link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
    {%- endunless -%}

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

    {% if page_description %}
      <meta name="description" content="{{ page_description | escape }}">
    {% endif %}

    {{ content_for_header }}

    {%- liquid
      assign body_font_bold = settings.type_body_font | font_modify: 'weight', 'bold'
      assign body_font_italic = settings.type_body_font | font_modify: 'style', 'italic'
      assign body_font_bold_italic = body_font_bold | font_modify: 'style', 'italic'
    %}

    {% style %}
      {{ settings.type_body_font | font_face: font_display: 'swap' }}
      {{ body_font_bold | font_face: font_display: 'swap' }}
      {{ body_font_italic | font_face: font_display: 'swap' }}
      {{ body_font_bold_italic | font_face: font_display: 'swap' }}
      {{ settings.type_header_font | font_face: font_display: 'swap' }}

      :root {
        --font-body-family: {{ settings.type_body_font.family }}, {{ settings.type_body_font.fallback_families }};
        --font-body-style: {{ settings.type_body_font.style }};
        --font-body-weight: {{ settings.type_body_font.weight }};
        --font-body-weight-bold: {{ settings.type_body_font.weight | plus: 300 | at_most: 1000 }};

        --font-heading-family: {{ settings.type_header_font.family }}, {{ settings.type_header_font.fallback_families }};
        --font-heading-style: {{ settings.type_header_font.style }};
        --font-heading-weight: {{ settings.type_header_font.weight }};

        --font-body-scale: {{ settings.body_scale | divided_by: 100.0 }};
        --font-heading-scale: {{ settings.heading_scale | times: 1.0 | divided_by: settings.body_scale }};

        --color-base-text: {{ settings.colors_text.red }}, {{ settings.colors_text.green }}, {{ settings.colors_text.blue }};
        --color-shadow: {{ settings.colors_text.red }}, {{ settings.colors_text.green }}, {{ settings.colors_text.blue }};
        --color-base-background-1: {{ settings.colors_background_1.red }}, {{ settings.colors_background_1.green }}, {{ settings.colors_background_1.blue }};
        --color-base-background-2: {{ settings.colors_background_2.red }}, {{ settings.colors_background_2.green }}, {{ settings.colors_background_2.blue }};
        --color-base-solid-button-labels: {{ settings.colors_solid_button_labels.red }}, {{ settings.colors_solid_button_labels.green }}, {{ settings.colors_solid_button_labels.blue }};
        --color-base-outline-button-labels: {{ settings.colors_outline_button_labels.red }}, {{ settings.colors_outline_button_labels.green }}, {{ settings.colors_outline_button_labels.blue }};
        --color-base-accent-1: {{ settings.colors_accent_1.red }}, {{ settings.colors_accent_1.green }}, {{ settings.colors_accent_1.blue }};
        --color-base-accent-2: {{ settings.colors_accent_2.red }}, {{ settings.colors_accent_2.green }}, {{ settings.colors_accent_2.blue }};
        --payment-terms-background-color: {{ settings.colors_background_1 }};

        --gradient-base-background-1: {% if settings.gradient_background_1 != blank %}{{ settings.gradient_background_1 }}{% else %}{{ settings.colors_background_1 }}{% endif %};
        --gradient-base-background-2: {% if settings.gradient_background_2 != blank %}{{ settings.gradient_background_2 }}{% else %}{{ settings.colors_background_2 }}{% endif %};
        --gradient-base-accent-1: {% if settings.gradient_accent_1 != blank %}{{ settings.gradient_accent_1 }}{% else %}{{ settings.colors_accent_1 }}{% endif %};
        --gradient-base-accent-2: {% if settings.gradient_accent_2 != blank %}{{ settings.gradient_accent_2 }}{% else %}{{ settings.colors_accent_2 }}{% endif %};

        --page-width: {{ settings.page_width | divided_by: 10 }}rem;
        --page-width-margin: {% if settings.page_width == '1600' %}2{% else %}0{% endif %}rem;

        --badge-corner-radius: {{ settings.badge_corner_radius | divided_by: 10.0 }}rem;

        --spacing-sections-desktop: {{ settings.spacing_sections }}px;
        --spacing-sections-mobile: {% if settings.spacing_sections < 24 %}{{ settings.spacing_sections }}{% else %}{{ settings.spacing_sections | times: 0.7 | round | at_least: 20 }}{% endif %}px;

        --buttons-radius: {{ settings.buttons_radius }}px;
        --buttons-radius-outset: {% if settings.buttons_radius > 0 %}{{ settings.buttons_radius | plus: settings.buttons_border_thickness }}{% else %}0{% endif %}px;
        --buttons-border-width: {% if settings.buttons_border_opacity > 0 %}{{ settings.buttons_border_thickness }}{% else %}0{% endif %}px;
        --buttons-border-opacity: {{ settings.buttons_border_opacity | divided_by: 100.0 }};
        --buttons-shadow-opacity: {{ settings.buttons_shadow_opacity | divided_by: 100.0 }};
        --buttons-shadow-visible: {% if settings.buttons_shadow_opacity > 0 %}1{% else %}0{% endif %};
        --buttons-shadow-horizontal-offset: {{ settings.buttons_shadow_horizontal_offset }}px;
        --buttons-shadow-vertical-offset: {{ settings.buttons_shadow_vertical_offset }}px;
        --buttons-shadow-blur-radius: {{ settings.buttons_shadow_blur }}px;
        --buttons-border-offset: {% if settings.buttons_radius > 0 or settings.buttons_shadow_opacity > 0 %}0.3{% else %}0{% endif %}px;

        --inputs-radius: {{ settings.inputs_radius }}px;
        --inputs-border-width: {{ settings.inputs_border_thickness }}px;
        --inputs-border-opacity: {{ settings.inputs_border_opacity | divided_by: 100.0 }};
        --inputs-shadow-opacity: {{ settings.inputs_shadow_opacity | divided_by: 100.0 }};
        --inputs-shadow-horizontal-offset: {{ settings.inputs_shadow_horizontal_offset }}px;
        --inputs-margin-offset: {% if settings.inputs_shadow_vertical_offset != 0 and settings.inputs_shadow_opacity > 0 %}{{ settings.inputs_shadow_vertical_offset | abs }}{% else %}0{% endif %}px;
        --inputs-shadow-vertical-offset: {{ settings.inputs_shadow_vertical_offset }}px;
        --inputs-shadow-blur-radius: {{ settings.inputs_shadow_blur }}px;
        --inputs-radius-outset: {% if settings.inputs_radius > 0 %}{{ settings.inputs_radius | plus: settings.inputs_border_thickness }}{% else %}0{% endif %}px;
      }

      *,
      *::before,
      *::after {
        box-sizing: inherit;
      }

      html {
        box-sizing: border-box;
        font-size: calc(var(--font-body-scale) * 62.5%);
        height: 100%;
      }

      body {
        display: grid;
        grid-template-rows: auto auto 1fr auto;
        grid-template-columns: 100%;
        min-height: 100%;
        margin: 0;
        font-size: 1.5rem;
        letter-spacing: 0.06rem;
        line-height: calc(1 + 0.8 / var(--font-body-scale));
        font-family: var(--font-body-family);
        font-style: var(--font-body-style);
        font-weight: var(--font-body-weight);
      }

      .redirect {
        margin: 0 auto;
        padding-left: 1.5rem;
        padding-right: 1.5rem;
        text-align: center;
      }

      @media screen and (min-width: 750px) {
        body {
          font-size: 1.6rem;
        }
      }
    {% endstyle %}

    {{ 'base.css' | asset_url | stylesheet_tag }}

    {%- unless settings.type_body_font.system? -%}
      <link rel="preload" as="font" href="{{ settings.type_body_font | font_url }}" type="font/woff2" crossorigin>
    {%- endunless -%}
    {%- unless settings.type_header_font.system? -%}
      <link rel="preload" as="font" href="{{ settings.type_header_font | font_url }}" type="font/woff2" crossorigin>
    {%- endunless -%}

    {% comment %}
      Should redirect if all conditions are met:
      - The template is defined (will be undefined for app proxies like /apps|a|community|tools/*)
      - The current template is not the login page, unless multipass login is enabled
    {% endcomment %}
    {%- assign should_redirect = template != blank and template.name != 'login' or settings.multipass_login -%}

    {%- if settings.storefront_hostname != blank and should_redirect -%}
    <script>
      function getCookie(name) {
        name = name + '=';
        var decodedCookie = decodeURIComponent(document.cookie);
        var cookies = decodedCookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
          var cookie = cookies[i].trim();
          if (cookie.indexOf(name) == 0) {
            return cookie.substring(name.length, cookie.length);
          }
        }
      }

      function deleteCookie(name) {
        document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
      }

      if (!window.Shopify.designMode) {
        var currentHostname = window.location.hostname;
        var storefrontHostname = "{{ settings.storefront_hostname }}";

        if (currentHostname !== storefrontHostname) {
          var redirectUrl = window.location.href.replace(currentHostname, storefrontHostname);

          var customRedirectsStr = "{{ settings.custom_redirects | newline_to_br | strip_newlines | replace: ' ', '' }}";
          if (customRedirectsStr) {
            var customRedirects = customRedirectsStr.split('<br/>');
  
            for (var cri = 0; cri < customRedirects.length; cri += 1) {
              var redirect = customRedirects[cri].split('>');
              var fromPath = redirect[0];
              var toPath = redirect[1];
  
              if (fromPath && toPath && redirectUrl.startsWith('https://' + storefrontHostname + fromPath)) {
                redirectUrl = redirectUrl.replace(storefrontHostname + fromPath, storefrontHostname + toPath);
                break;
              }
            }
          }

          redirectUrl = new URL(redirectUrl);

          {% comment %} 
            Handle discount code logic (e.g. xxx.myshopify.com/discount/freeshipping?redirect=/products)
            - Add discount code to as param to the edirect URL if the `discount_code` cookie exists
            - Cookie is deleted after to prevent it from being automatically added to other routes
          {% endcomment %}
          var discountCode = getCookie('discount_code');
          if (discountCode) {
            redirectUrl.searchParams.append("discount", discountCode);
            deleteCookie("discount_code");
          }

          window.storefrontRedirectUrl = redirectUrl;
          window.location.replace(redirectUrl);
        }
      }
    </script>
    {%- endif -%}
  </head>
  <body>
    {{ content_for_layout }}

    {%- if should_redirect -%}
      <div class="redirect">
        {%- if settings.storefront_hostname != blank -%}
        <h1>Redirecting...</h1>
        <p>
          <a id="redirect-link" href="https://{{ settings.storefront_hostname }}/">Click here</a>
          if you are not automically redirected.
        </p>
        <script>
          if (!window.Shopify.designMode) {
            var redirectLink = document.getElementById("redirect-link");
            redirectLink.href = window.storefrontRedirectUrl;
          }
        </script>
        {%- else -%}
        <h1>Redirect URL not set</h1>
        <p>Configure the redirects in "Theme settings > Storefront".</p>
        {%- endif -%}
      </div>
    {%- endif -%}
  </body>
</html>

出典・ライセンス

License:
MIT

このコードは instantcommerce 著作の MIT ライセンスソースです。 原本の著作権は instantcommerce が保有します。日本語訳は ALSEL によるものです。

関連項目

🏗️ テーマ基盤中級

セクションスキーマのカスタムブロック定義

セクションの schema ブロック内でカスタムブロックタイプを定義する方法を示すサンプル。@theme と _private というプレフィックスで異なるブロック種別を宣言する。

📁 theme-tools·MIT·7
🏗️ テーマ基盤初級

セクション呼び出しのフォーマット

Liquid の section タグを複数行に記述する際の正しいフォーマット例。タグ名を改行で分割しても正しく認識される書き方を示す。

📁 theme-tools·MIT·7
🏗️ テーマ基盤初級

レイアウトテンプレートの指定

Liquid の `layout` タグを使い、テーマのレイアウトファイルを指定する構文。複数のレイアウト候補と whitespace 制御(ダッシュ記号)の使い方を示す。

📁 theme-tools·MIT·8
🏗️ テーマ基盤上級

Liquid フォーマッター互換のセクション表記

Prettier Liquid プラグインのテストケースで、セクションタグの様々な書式を検証する。異なる間隔・改行・ホワイトスペーストリミングの組み合わせでもセクション名が崩れないことを確認する。

📁 theme-tools·MIT·9
🏗️ テーマ基盤初級

基本的なHTMLレイアウト

Prettier Liquid プラグインのテスト用に作られた最小限の HTML テンプレート。DOCTYPE から body タグまでの基本的なドキュメント構造を示す。

📁 theme-tools·MIT·10
🏗️ テーマ基盤上級

layout タグのフォーマット

Liquid の layout タグの書式をコード品質ツール(Prettier)でフォーマットするテストケース。スペースや改行位置の違いを正規化し、レイアウト名が壊れないことを検証する。

📁 theme-tools·MIT·10