✨ UI部品中級
FAQ アコーディオン(スキーマ対応)
質問と回答をアコーディオン形式で開閉表示するセクション。管理画面から質問・回答をブロック形式で追加でき、Google 検索結果の FAQ リッチリザルト表示に対応する JSON-LD スキーマを自動生成する。
用途
ストアの FAQ ページで、よくある質問を整理して表示したいとき。開閉アニメーションで UX を向上させ、検索エンジンにも構造化データで正確に認識させる。
設置場所
sections/faq.liquid に配置し、テーマカスタマイザーで FAQ セクションをページに追加する。管理画面の「ブロック」から質問・回答をいくつでも追加でき、各質問の初期状態(展開/折りたたみ)を選択できる。
注意点
JavaScript ファイル(faq.js)がアセットに存在することが前提で、このセクションはそのファイルを参照している。クライアント側で開閉アニメーションが正常に動作するには JavaScript が有効である必要があり、無効な環境ではすべての回答がデフォルトで表示される。リッチスキーマ生成時に回答は `strip_html` で HTML タグが削除されるため、構造化データでは純テキストのみが Google に送信される点を理解した上で、HTML 形式の複雑な回答(リストや画像など)が必要な場合は管理画面でスキーマの有効/無効を切り替えて対応する。
コード
352 行 / liquid{%- comment -%}
Section published at https://sections.design/blogs/shopify/faq-rich-snippets-section
Get the latest version: https://github.com/mirceapiturca/Sections/tree/master/FAQ
{%- endcomment -%}
{%- comment -%} ---------------- THE CSS ---------------- {%- endcomment -%}
{%- assign id = '#shopify-section-' | append: section.id -%}
{% style %}
{{ id }} {
background: {{ section.settings.background_color }};
--panel-bg: {{ section.settings.panel_color }};
--border-color: {{ section.settings.border_color }};
--question-color: {{ section.settings.q_color }};
--answer-color: {{ section.settings.a_color }};
{%- assign min = section.settings.q_size_small -%}
{%- assign max = section.settings.q_size_large -%}
{%- assign min_rem = min | append: 'rem' -%}
{%- assign max_rem = max | append: 'rem' -%}
--title-font-size: clamp({{ min_rem }}, calc({{ min_rem }} + ({{ max }} - {{ min }}) * ((100vw - 25rem) / (64 - 25))), {{ max_rem }});
}
{% endstyle %}
<style>
.flex { display: flex }
.items-center { align-items: center }
.justify-between { justify-content: space-between }
.w-full { width: 100% }
.text-left { text-align: left }
.m-0 { margin: 0 }
.p-0 { padding: 0 }
.p-4 { padding: 1rem }
.overflow-hidden { overflow: hidden }
.cursor-pointer { cursor: pointer }
{{ id }} .faq-container {
max-width: {{ section.settings.max_width }};
margin: {{ section.settings.margin_top }}rem auto {{ section.settings.margin_bottom }}rem
}
{{ id }} .faq-title {
border-bottom: 1px solid var(--border-color);
font-size: var(--title-font-size);
color: var(--question-color);
}
{{ id }} .faq-panel {
will-change: height;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-bg);
color: var(--answer-color);
}
.faq-button {
font: inherit;
background: transparent;
border: 0;
}
.faq-icon {
width: clamp(12px, 0.65em, 20px);
height: clamp(12px, 0.65em, 20px);
min-width: clamp(12px, 0.65em, 20px);
margin-left: 1rem;
}
.faq-icon-minus {
transition: transform 240ms cubic-bezier(0.4, 0.0, 0.2, 1);
transform-origin: 50% 50%;
}
.faq-button[aria-expanded="true"] .faq-icon-minus {
transform: rotate(90deg);
}
.faq-panel * {
color: inherit;
}
.faq-panel[data-is-animating] {
display: block!important;
}
</style>
{%- comment -%} ---------------- THE MARKUP ---------------- {%- endcomment -%}
<div class="faq-container">
{%- for block in section.blocks -%}
{%- if block.settings.title != blank and block.settings.content != blank -%}
{%- if block.settings.checkbox_expanded == true -%}
{%- assign expanded = 'true' -%}
{%- assign hidden = '' -%}
{%- else -%}
{%- assign expanded = 'false' -%}
{%- assign hidden = 'hidden' -%}
{%- endif -%}
<h2 class="faq-title m-0 p-0" data-faq-trigger="{{ block.id }}" {{ block.shopify_attributes }}>
<button class="faq-button flex items-center justify-between w-full text-left m-0 p-4 cursor-pointer" data-faq-button="{{ block.id }}" aria-expanded="{{ expanded }}">
<span>{{ block.settings.title }}</span>
<svg class="faq-icon" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path class="faq-icon-minus" fill="currentColor" d="M8 0v14H6V0z"></path>
<path fill="currentColor" d="M0 6h14v2H0z"></path>
</svg>
</button>
</h2>
<div class="faq-panel rte overflow-hidden custom-bg" data-faq-panel="{{ block.id }}" {{ hidden }}>
<div class="faq-wrap p-4">{{ block.settings.content }}</div>
</div>
{%- endif -%}
{%- endfor -%}
</div>
{%- comment -%} -------------- THE RICH SCHEMA ------------- {%- endcomment -%}
{%- if section.settings.enable_rich_schema -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{%- for block in section.blocks -%}
{%- if block.settings.title != blank and block.settings.content != blank -%}
{
"@type": "Question",
"name": {{ block.settings.title | json }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ block.settings.content | strip_html | json }}
}
}{%- unless forloop.last -%},{%- endunless -%}
{%- endif -%}
{%- endfor -%}
]
}
</script>
{%- endif -%}
{%- comment -%} ---------------- THE CONFIG ---------------- {%- endcomment -%}
<script type="application/json" data-faq-config="{{ section.id }}" ></script>
{%- comment -%} ---------------- THE SETTINGS ---------------- {%- endcomment -%}
{% schema %}
{
"name": "FAQ",
"class": "sd-faq",
"tag": "article",
"settings": [
{
"type": "header",
"content": "Rich schema"
},
{
"type": "checkbox",
"id": "enable_rich_schema",
"default": true,
"label": "Enable FAQ rich schema?"
},
{
"type": "header",
"content": "Dimensions"
},
{
"type": "text",
"id": "max_width",
"label": "Max width",
"default": "64rem"
},
{
"type": "range",
"id": "margin_top",
"min": 0,
"max": 10,
"step": 0.1,
"unit": "rem",
"label": "Margin top",
"default": 1
},
{
"type": "range",
"id": "margin_bottom",
"min": 0,
"max": 10,
"step": 0.1,
"unit": "rem",
"label": "Margin bottom",
"default": 1
},
{
"type": "header",
"content": "Colors"
},
{
"type": "color",
"id": "background_color",
"label": "Background color",
"default": "#ffffff"
},
{
"type": "color",
"id": "border_color",
"label": "Border color",
"default": "#eeeeee"
},
{
"type": "color",
"id": "panel_color",
"label": "Panel color",
"default": "#fdfdfd"
},
{
"type": "header",
"content": "Question"
},
{
"type": "range",
"id": "q_size_small",
"min": 1,
"max": 2,
"step": 0.1,
"unit": "rem",
"label": "Small devices font size",
"default": 1
},
{
"type": "range",
"id": "q_size_large",
"min": 1,
"max": 3,
"step": 0.1,
"unit": "rem",
"label": "Large devices font size",
"default": 1.4
},
{
"type": "color",
"id": "q_color",
"label": "Question text color"
},
{
"type": "header",
"content": "Answer"
},
{
"type": "color",
"id": "a_color",
"label": "Answer text color"
}
],
"blocks": [
{
"type": "faq",
"name": "FAQ",
"settings": [
{
"type": "checkbox",
"id": "checkbox_expanded",
"default": false,
"label": "Expanded?"
},
{
"type": "text",
"id": "title",
"label": "FAQ title",
"default": "FAQ title"
},
{
"type": "richtext",
"id": "content",
"label": "FAQ content",
"default": "<p>FAQ content</p>"
}
]
}
],
"presets": [
{
"name": "FAQ"
}
]
}
{% endschema %}
{%- comment -%} ------------------ THE JS ----------------- {%- endcomment -%}
<script src="{{ 'faq.js' | asset_url }}" defer></script>
{%- comment -%} ---------------- THE NO-JS ---------------- {%- endcomment -%}
<noscript>
<style>
#shopify-section-{{ section.id }} [hidden] { display: block }
.faq-icon { display: none }
</style>
</noscript>
{%- comment -%} ---------------- THE EDITOR ------------------ {%- endcomment -%}
{%- if request.design_mode -%}
<script>
(function FAQThemeEditor(SectionsDesign) {
'use strict';
document.addEventListener('shopify:section:load', sectionLoad);
document.addEventListener('shopify:block:select', blockToggle);
document.addEventListener('shopify:block:deselect', blockToggle);
function sectionLoad(evt) {
var sectionId = evt.detail.sectionId;
var section = SectionsDesign.faq[sectionId];
if (!section) return;
SectionsDesign.faq[sectionId] = section.init(sectionId);
}
function blockToggle(evt) {
var section = SectionsDesign.faq[evt.detail.sectionId];
if (!section) return;
var block = section.blocks[evt.detail.blockId];
if (!block) return;
evt.type === 'shopify:block:select' ? block.select() : block.deselect();
}
})(window.SectionsDesign = window.SectionsDesign || {});
</script>
{%- endif -%}
出典・ライセンス
- Repository:
- https://github.com/mirceapiturca/Sections
- License:
- MIT
このコードは mirceapiturca 著作の MIT ライセンスソースです。 原本の著作権は mirceapiturca が保有します。日本語訳は ALSEL によるものです。
関連項目
✨ UI部品初級
成功チェックマークアイコン
緑色の円形背景にチェックマークを描いたSVGアイコン。フォームやメッセージの完了状態を視覚的に表現する。
📁 shopify-headless-theme·MIT·6 行
✨ UI部品初級
エラーアイコン
エラー状態を示す円形アイコン。SVG で描画されたアラート記号を含む UI 部品で、accessibility 対応により視覚障害ユーザーには非表示。
📁 theme-tools·MIT·8 行
✨ UI部品中級
カスタム要素のラッパーコンポーネント
child-element というカスタム HTML 要素をラップし、スロットに子要素を挿入するスニペット。Web Components パターンで再利用可能なコンポーネント構造を実現する。
📁 theme-tools·MIT·10 行
✨ UI部品初級
静的コンテンツ
管理画面で入力したテキストコンテンツを静的に描画するブロック。セクション内で複数回使い分けられる汎用コンテナとして機能する。
📁 theme-tools·MIT·11 行
✨ UI部品初級
テキスト
セクションに追加できるシンプルなテキストブロック。管理画面からドラッグ&ドロップで配置でき、固定テキスト「hello world」を表示する基本的なブロック実装。
📁 theme-tools·MIT·15 行
✨ UI部品初級
セクション共通ヘッダー(タイトル・説明文)
セクション内で繰り返し使うタイトルと説明文をまとめて描画するスニペット。テーマカスタマイザーから設定したテキストとフォントサイズを条件付きで出力する。
📁 ks-bootshop·MIT·24 行