Pular para o conteúdo

Exemplos

Cada exemplo abaixo é autocontido: schema, Liquid, SCSS e JavaScript juntos. Adapte o que precisar - os contratos gerais estão em Schema e em Arquivos e subarquivos.


O ponto de partida para qualquer seção. Campos de settings controlam aparência; campos de content controlam o que é exibido.

{
"title": "Destaque da Home",
"description": "Seção com título e cor editáveis.",
"component_config": {
"settings": {
"background_color": {
"type": "string",
"title": "Cor de fundo",
"default": "#F5F5F5"
}
},
"content": {
"title": {
"type": "string",
"title": "Título",
"default": "Bem-vindo"
},
"body_text": {
"type": "string",
"title": "Texto",
"format": "markdown"
}
}
},
"defaultContent": {
"settings": { "background_color": "#F5F5F5" },
"content": {
"title": "Bem-vindo à nossa loja",
"body_text": "Explore nossa coleção completa."
}
}
}
{% assign settings = component_configs.settings %}
{% assign content = component_configs.content %}
<section class="destaque-home" style="--destaque-bg: {{ settings.background_color }};">
<div class="container">
{% if content.title != blank %}
<h2 class="destaque-home__title">{{ content.title }}</h2>
{% endif %}
{% if content.body_text != blank %}
<div class="destaque-home__text">{{ content.body_text }}</div>
{% endif %}
</div>
</section>
.destaque-home {
background: var(--destaque-bg, #f5f5f5);
padding: 4.8rem 0;
&__title {
font-size: 3.2rem;
margin-bottom: 1.6rem;
}
&__text {
font-size: 1.6rem;
line-height: 1.6;
}
}
const DestaqueHome = {
init() {},
};
export default DestaqueHome;

Mostra como combinar upload de imagem com texto e como renderizar a imagem corretamente com lazyload e proporção.

{
"title": "Banner com texto",
"description": "Imagem à esquerda e texto à direita.",
"component_config": {
"settings": {
"background_color": {
"type": "string",
"title": "Cor de fundo",
"default": "#FFFFFF"
}
},
"content": {
"banner_image_url": {
"type": "string",
"title": "Imagem"
},
"title": {
"type": "string",
"title": "Título"
},
"body_text": {
"type": "string",
"title": "Texto",
"format": "markdown"
},
"cta_label": {
"type": "string",
"title": "Texto do botão"
},
"cta_url": {
"type": "string",
"title": "Link do botão"
}
}
},
"defaultContent": {
"content": {
"title": "Nossa história",
"body_text": "Conte aqui um pouco sobre a marca ou a campanha.",
"cta_label": "Saiba mais",
"cta_url": "#"
}
}
}
{% assign settings = component_configs.settings %}
{% assign content = component_configs.content %}
<section class="banner-texto" style="background-color: {{ settings.background_color }};">
<div class="container banner-texto__grid">
{% if content.banner_image_url != blank %}
<figure class="image -horizontal banner-texto__imagem">
<img
class="lazy"
loading="lazy"
data-src="{{ content.banner_image_url }}"
alt="{{ content.title }}"
/>
</figure>
{% endif %}
<div class="banner-texto__conteudo">
{% if content.title != blank %}
<h2 class="banner-texto__title">{{ content.title }}</h2>
{% endif %}
{% if content.body_text != blank %}
<div class="banner-texto__text">{{ content.body_text }}</div>
{% endif %}
{% if content.cta_label != blank and content.cta_url != blank %}
<a href="{{ content.cta_url }}" class="button-default">{{ content.cta_label }}</a>
{% endif %}
</div>
</div>
</section>
.banner-texto {
padding: 4.8rem 0;
&__grid {
display: grid;
gap: 3.2rem;
@include media('md') {
grid-template-columns: 1fr 1fr;
align-items: center;
}
}
&__title {
font-size: 3.2rem;
margin-bottom: 1.6rem;
}
&__text {
font-size: 1.6rem;
line-height: 1.6;
margin-bottom: 2.4rem;
}
}
const BannerTexto = {
init() {},
};
export default BannerTexto;

O lojista pode adicionar, remover e reordenar itens. Cada item tem imagem, título e link.

{
"title": "Cards de categorias",
"description": "Coleção de cards com imagem e link.",
"component_config": {
"settings": {
"columns": {
"type": "string",
"title": "Colunas",
"oneOf": [
{ "const": "3", "title": "3 colunas" },
{ "const": "4", "title": "4 colunas" }
],
"default": "4"
}
},
"content": {
"items": [
{
"cover_image_url": { "type": "string", "title": "Imagem" },
"title": { "type": "string", "title": "Título" },
"link_url": { "type": "string", "title": "Link" }
}
]
}
},
"defaultContent": {
"settings": { "columns": "4" },
"content": {
"items": [
{ "title": "Categoria 1", "link_url": "#" },
{ "title": "Categoria 2", "link_url": "#" },
{ "title": "Categoria 3", "link_url": "#" },
{ "title": "Categoria 4", "link_url": "#" }
]
}
}
}
{% assign settings = component_configs.settings %}
{% assign content = component_configs.content %}
<section class="cards-categorias" data-columns="{{ settings.columns }}">
<div class="container">
<ul class="cards-categorias__lista">
{% for item in content.items %}
{% if item.title != blank %}
<li class="cards-categorias__item">
<a href="{{ item.link_url | default: '#' }}" class="cards-categorias__link">
{% if item.cover_image_url != blank %}
<figure class="image -square cards-categorias__imagem">
<img
class="lazy"
loading="lazy"
data-src="{{ item.cover_image_url }}"
alt="{{ item.title }}"
/>
</figure>
{% endif %}
<span class="cards-categorias__nome">{{ item.title }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</section>
.cards-categorias {
padding: 4.8rem 0;
&__lista {
display: grid;
gap: 1.6rem;
grid-template-columns: repeat(2, 1fr);
list-style: none;
padding: 0;
@include media('md') {
grid-template-columns: repeat(var(--columns, 4), 1fr);
}
}
&__link {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
&__nome {
font-size: 1.4rem;
text-align: center;
}
}
const CardsCategorias = {
init() {
const section = document.querySelector('.cards-categorias');
if (!section) return;
const columns = section.dataset.columns;
section.style.setProperty('--columns', columns);
},
};
export default CardsCategorias;

Quando o layout mobile diverge do desktop - colunas, tamanhos ou imagens diferentes - use settings_mobile e content_mobile. O Liquid faz fallback para o desktop quando o campo mobile não está preenchido.

{
"title": "Destaque responsivo",
"description": "Layout diferente por breakpoint.",
"component_config": {
"settings": {
"background_color": {
"type": "string",
"title": "Cor de fundo",
"default": "#F5F5F5"
}
},
"content": {
"banner_image_url": { "type": "string", "title": "Imagem (desktop)" },
"title": { "type": "string", "title": "Título" }
},
"settings_mobile": {
"background_color": {
"type": "string",
"title": "Cor de fundo (mobile)",
"default": "#FFFFFF"
}
},
"content_mobile": {
"banner_image_url": { "type": "string", "title": "Imagem (mobile)" }
}
},
"defaultContent": {
"settings": { "background_color": "#F5F5F5" },
"settings_mobile": { "background_color": "#FFFFFF" },
"content": { "title": "Novidade" }
}
}
{% assign settings = component_configs.settings_mobile | default: component_configs.settings %}
{% assign content = component_configs.content_mobile | default: component_configs.content %}
{% comment %}Para campos individuais, use o desktop como fallback explícito:{% endcomment %}
{% assign imagem = component_configs.content_mobile.banner_image_url
| default: component_configs.content.banner_image_url %}
<section class="destaque-responsivo" style="background-color: {{ settings.background_color }};">
<div class="container">
{% if imagem != blank %}
<figure class="image -horizontal destaque-responsivo__imagem">
<img class="lazy" loading="lazy" data-src="{{ imagem }}" alt="{{ component_configs.content.title }}" />
</figure>
{% endif %}
{% if component_configs.content.title != blank %}
<h2>{{ component_configs.content.title }}</h2>
{% endif %}
</div>
</section>

Cada ocorrência desta seção tem sua própria coleção de produtos, identificada por uma tag gerada automaticamente.

{
"title": "Vitrine de produtos",
"description": "Produtos associados a uma tag própria.",
"requires_external_content": {
"type": "static",
"static_tag_name": "home-produtos",
"instructions": {
"title": "Associe produtos à vitrine",
"steps": [
"1. Use a tag sugerida nesta seção",
"2. Acesse [Tags]({{ADMIN_URL}}/tags) e [Produtos]({{ADMIN_URL}}/produtos)",
"3. Vincule a tag aos produtos desejados"
]
}
},
"component_config": {
"settings": {},
"content": {
"title": { "type": "string", "title": "Título da vitrine" }
}
},
"defaultContent": {
"content": { "title": "Produtos em destaque" }
}
}
{% assign content = component_configs.content %}
{% if content.tag_name != blank %}
{% load_products tag: content.tag_name per_page: 10 %}
{% if products.size > 0 %}
<section class="vitrine-produtos">
<div class="container">
{% if content.title != blank %}
<h2 class="vitrine-produtos__titulo">{{ content.title }}</h2>
{% endif %}
<div class="vitrine-produtos__lista">
{% for product in products %}
{% render 'partials/components/product_block',
product: product,
mobile: mobile,
component_configs: product_block_config
%}
{% endfor %}
</div>
</div>
</section>
{% endif %}
{% endif %}
.vitrine-produtos {
padding: 4.8rem 0;
&__titulo {
font-size: 3.2rem;
margin-bottom: 3.2rem;
text-align: center;
}
&__lista {
display: grid;
gap: 1.6rem;
grid-template-columns: repeat(2, 1fr);
@include media('md') {
grid-template-columns: repeat(4, 1fr);
}
}
}

Cada aba representa uma coleção diferente. A tag é gerada automaticamente a partir do título de cada aba.

{
"title": "Abas de produtos",
"description": "Uma coleção por aba.",
"requires_external_content": {
"type": "dynamic",
"prefix": "home-tab-",
"instructions": {
"title": "Associe produtos às abas",
"steps": [
"1. Confira a tag sugerida em cada aba",
"2. Acesse [Tags]({{ADMIN_URL}}/tags) e [Produtos]({{ADMIN_URL}}/produtos)",
"3. Vincule a tag aos produtos de cada aba"
]
}
},
"component_config": {
"settings": {},
"content": {
"items": [
{ "title": { "type": "string", "title": "Título da aba" } }
]
}
},
"defaultContent": {
"content": {
"items": [
{ "title": "Novidades" },
{ "title": "Mais vendidos" }
]
}
}
}
{% assign content = component_configs.content %}
{% assign tabs_markup = '' %}
{% assign panels_markup = '' %}
{% assign has_active_tab = false %}
{% for item in content.items %}
{% if item.title != blank and item.tag_name != blank %}
{% load_products tag: item.tag_name per_page: 8 %}
{% if products.size > 0 %}
{% assign active_class = '' %}
{% unless has_active_tab %}
{% assign active_class = ' -active' %}
{% assign has_active_tab = true %}
{% endunless %}
{% capture tab %}
<button
type="button"
class="abas-produtos__tab{{ active_class }}"
data-tab="{{ forloop.index }}"
>{{ item.title }}</button>
{% endcapture %}
{% assign tabs_markup = tabs_markup | append: tab %}
{% capture panel %}
<div
class="abas-produtos__painel{{ active_class }}"
data-painel="{{ forloop.index }}"
>
<div class="abas-produtos__lista">
{% for product in products %}
{% render 'partials/components/product_block',
product: product,
mobile: mobile,
component_configs: product_block_config
%}
{% endfor %}
</div>
</div>
{% endcapture %}
{% assign panels_markup = panels_markup | append: panel %}
{% endif %}
{% endif %}
{% endfor %}
{% if has_active_tab %}
<section class="abas-produtos">
<div class="container">
<nav class="abas-produtos__nav">{{ tabs_markup }}</nav>
{{ panels_markup }}
</div>
</section>
{% endif %}
const AbasProdutos = {
init() {
const tabs = document.querySelectorAll('.abas-produtos__tab');
const panels = document.querySelectorAll('.abas-produtos__painel');
if (!tabs.length) return;
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const index = tab.dataset.tab;
tabs.forEach(t => t.classList.remove('-active'));
panels.forEach(p => p.classList.remove('-active'));
tab.classList.add('-active');
document.querySelector(`.abas-produtos__painel[data-painel="${index}"]`)
?.classList.add('-active');
});
});
},
};
export default AbasProdutos;

Formulário com captação de e-mail. Exige cadastro prévio no Painel Administrativo antes de funcionar - veja Formulários.

{
"title": "Newsletter",
"description": "Captação de e-mails.",
"requires_external_content": {
"type": "manual",
"instructions": {
"title": "Cadastro do formulário",
"steps": [
"1. Acesse [Forms]({{ADMIN_URL}}/config/mensagens-e-avisos/forms)",
"2. Crie um formulário com assunto `newsletter`",
"3. Informe o e-mail do destinatário e salve antes de publicar"
]
}
},
"component_config": {
"settings": {
"background_color": {
"type": "string",
"title": "Cor de fundo",
"default": "#03060B"
},
"text_color": {
"type": "string",
"title": "Cor do texto",
"default": "#FFFFFF"
}
},
"content": {
"title": { "type": "string", "title": "Título" },
"description": { "type": "string", "title": "Descrição" },
"placeholder": { "type": "string", "title": "Placeholder do campo de e-mail" },
"cta_label": { "type": "string", "title": "Texto do botão" }
}
},
"defaultContent": {
"settings": { "background_color": "#03060B", "text_color": "#FFFFFF" },
"content": {
"title": "Fique por dentro",
"description": "Receba novidades e ofertas exclusivas no seu e-mail.",
"placeholder": "Seu e-mail",
"cta_label": "Quero receber"
}
}
}
{% assign settings = component_configs.settings %}
{% assign content = component_configs.content %}
<section
class="newsletter"
style="--newsletter-bg: {{ settings.background_color }}; --newsletter-color: {{ settings.text_color }};"
>
<div class="container newsletter__inner">
<div class="newsletter__texto">
{% if content.title != blank %}
<h2 class="newsletter__titulo">{{ content.title }}</h2>
{% endif %}
{% if content.description != blank %}
<p class="newsletter__descricao">{{ content.description }}</p>
{% endif %}
</div>
<form data-webform="newsletter" class="newsletter__form">
<fieldset>
<input type="text" name="vnda" style="display:none;" tabindex="-1" autocomplete="off" />
<input type="hidden" name="key" value="{{ current_shop.subdomain }}-newsletter" />
<input type="hidden" name="reply_to" value="" />
<div class="newsletter__campo">
<input
type="email"
name="email"
class="input"
placeholder="{{ content.placeholder | default: 'Seu e-mail' }}"
required
/>
<button type="button" class="action button-default">
{{ content.cta_label | default: 'Enviar' }}
</button>
</div>
<div class="messages" data-msg-retorno>
<p class="msg-received" data-msg>Processando…</p>
<p class="msg-received" data-msg-success>Cadastro realizado com sucesso!</p>
<p class="msg-received" data-msg-error>Erro ao enviar. Tente novamente.</p>
</div>
</fieldset>
</form>
</div>
</section>

Mantenha reply_to vazio no markup. O comportamento padrão do formulário preenche esse campo com o e-mail informado antes do envio.

.newsletter {
background: var(--newsletter-bg, #03060b);
color: var(--newsletter-color, #fff);
padding: 4.8rem 0;
&__inner {
display: flex;
flex-direction: column;
gap: 3.2rem;
align-items: center;
text-align: center;
@include media('md') {
flex-direction: row;
text-align: left;
}
}
&__texto {
flex: 1;
}
&__titulo {
font-size: 2.4rem;
margin-bottom: 0.8rem;
}
&__descricao {
font-size: 1.4rem;
opacity: 0.8;
}
&__form {
flex: 1;
width: 100%;
}
&__campo {
display: flex;
gap: 0.8rem;
.input {
flex: 1;
}
}
}
const Newsletter = {
init() {},
};
export default Newsletter;

Os exemplos de Botões e Ícones trazem a referência-base completa para esses contratos. Antes de publicar, compare-a à referência aprovada para a loja e preserve todos os símbolos e estados que ela utilizar.