Temas e CLI › Temas
Guia da agência: temas em Liquid na Denaryx
Este guia é para quem vai criar ou adaptar o tema de uma loja na Denaryx. Os temas são escritos em Liquid, com a estrutura de pastas, as tags, os filtros e os objetos que o ferramental Liquid já conhece (a extensão do editor, o verificador e o formatador funcionam). Aqui estão a estrutura, o que o tema recebe, as convenções da plataforma, as rotas em JSON, os limites e o caminho da prévia até a publicação.
1. Antes de começar
- Toda loja recebe código próprio. O desenho de cada página roda isolado, fora do processo que serve as outras lojas e o checkout, então qualquer loja pode copiar o código de um tema da plataforma ou receber o zip da agência, sem pedir liberação.
- Papel na equipe. A agência entra na loja pelo convite do dono, no papel Catálogo e vitrine. Esse papel edita o código, salva versões e manda links de prévia. Publicar um tema com código próprio é do dono e do gerente, porque o JavaScript do tema alcança o que o comprador faz na vitrine; o dono pode deixar o Catálogo e vitrine publicar também, na página Temas.
- Por onde começar. Há três caminhos: pelo CLI, no seu editor (
denaryx tema initcria a pasta com o Pilar, edenaryx tema push --novoa envia à loja); copiar o código de um tema da plataforma no painel ("Copiar o código", na página Temas; o Pilar é o tema de referência, com todas as convenções abaixo); ou enviar um zip com um tema feito do zero.
2. Estrutura do tema
As pastas são estas, sem subpastas:
| Pasta | O que aceita |
|---|---|
layout/ | .liquid. O theme.liquid é obrigatório. |
templates/ | .json (arranjo de seções) ou .liquid. Obrigatórios: index, product, collection, search, cart, page e 404. |
sections/ | .liquid (as seções, com {% schema %}) e .json (os grupos de seções, como o topo e o rodapé). |
blocks/ | .liquid: blocos do tema, que as seções aceitam pelo tipo. |
snippets/ | .liquid, chamados por {% render 'nome' %}. |
assets/ | CSS, JS, mjs, JSON, map, SVG, txt, imagens (png, jpg, jpeg, gif, webp, avif, ico), fontes (woff, woff2, ttf, otf), mp4 e webm. HTML nunca. |
config/ | settings_schema.json (os ajustes gerais) e settings_data.json (os valores padrão). |
locales/ | Os textos do tema por idioma, como pt-BR.default.json, lidos pelo filtro t. |
Os nomes de arquivo usam letras, números, ponto, hífen e sublinhado.
Obrigatório no layout: {{ content_for_header }} dentro do <head> e {{ content_for_layout }} no <body>. Sem eles o tema não é publicado.
Liquid em assets/ não existe. Para CSS que depende dos ajustes, use {% style %} numa seção ou snippet, {% stylesheet %} na seção, ou variáveis de CSS definidas no layout (o Pilar faz assim, em snippets/variaveis.liquid).
Zip. O tema pode vir num zip de até 50 MB (50 MB descompactado, 2.000 arquivos). Uma pasta única no topo é ignorada, assim como __MACOSX, .git e .DS_Store. O que estiver fora da estrutura fica de fora, e o painel mostra a lista. O nome do tema vem do theme_name do bloco theme_info do settings_schema.json, se houver.
3. Código e conteúdo
Na Denaryx, o código do tema e o conteúdo da loja ficam separados:
- os arquivos do tema (inclusive os
templates/*.json, os grupos desections/*.jsone oconfig/settings_data.json) são o padrão do tema; - o que o lojista monta no editor visual (as seções de cada página, os textos, as imagens, os ajustes gerais) é guardado à parte, por tema da biblioteca, e vale no lugar do padrão.
Por isso, mudar um templates/index.json não muda uma página que o lojista já arrumou no editor visual: o editor de arquivos avisa quando isso acontece e leva ao editor visual. Um envio de código também nunca apaga o conteúdo da loja. Uma seção nova aparece para o lojista acrescentar no editor visual, com os campos do {% schema %} dela.
Modelos da página (templates alternativos). O produto, a categoria e a página institucional podem usar outro desenho: o "modelo da página", escolhido no formulário de cada um no painel. O modelo lancamento de um produto usa o templates/product.lancamento (.json ou .liquid); o de uma categoria, o collection.<modelo>; o de uma página, o page.<modelo>. O nome do modelo usa letras minúsculas, números, hífen e sublinhado (até 40). O modelo vem de dois lugares:
- do código, quando o tema traz o arquivo (
templates/product.lancamento.json); - do conteúdo da loja, quando o lojista o cria no editor visual ("Novo modelo", na página do produto, da lista ou institucional), como uma cópia da página base. Esse não tem arquivo no tema.
O formulário mostra os modelos de todos os temas da biblioteca, marcando os que o tema publicado não tem: o lojista já pode ligar os produtos ao modelo que a agência está fazendo. Enquanto o tema publicado não tiver o modelo, a página usa a padrão (product), sem erro, e o "Antes de publicar" lista quantos produtos, categorias e páginas ficariam assim. No Liquid, product.template_suffix, collection.template_suffix e page.template_suffix dão o modelo (nil na padrão), e template.suffix, o da página aberta. Na prévia, ?view=<modelo> no endereço desenha a página em outro modelo, para ver um modelo que nenhum produto usa ainda.
4. Seções, blocos e ajustes
O {% schema %} das seções e dos blocos segue o formato do mercado: name, settings, blocks, presets, max_blocks, limit, enabled_on/disabled_on. Blocos dentro de blocos são desenhados até 8 níveis.
Blocos dentro de blocos. O bloco do tema (blocks/*.liquid) aceita outros blocos pelo blocks do schema dele: { "type": "@theme" } aceita todos os blocos do tema, e um bloco com name e settings é só dele (e só o tema o desenha, percorrendo block.blocks). Ele desenha os filhos com {% content_for 'blocks' %}. No editor visual, a loja monta a árvore: cada item mostra os de dentro recuados, com subir, descer, esconder, tirar e acrescentar, até o 8º nível (os blocos da seção são o 1º). Os blocos que a seção define no próprio schema não têm filhos.
- Presets com filhos: o
blocksde um preset (da seção ou do bloco) é a lista do mercado, e cada item pode ter o seublocks, também em lista. O preset do bloco do tema vira uma opção ao acrescentá-lo ("Cartão com texto"); sem preset, o bloco entra pelo nome, com os valores padrão. O que o dono não aceita fica de fora. - Limites: até 50 blocos dentro de cada bloco (o bloco do tema não tem
max_blocks), e olimitdos blocos que o schema define vale dentro de cada dono. - O bloco fixo (
{% content_for 'block', type: ..., id: ... %}) também pode ter filhos, quando é um bloco do tema que os aceita. Bloco fixo dentro de bloco ainda não entra no editor visual. - No conteúdo guardado: o filho que o código deixou de aceitar sai na leitura, e o "Antes de publicar" o lista no impacto no conteúdo, com o caminho ("Página inicial › Cartões › Cartão › Texto").
Tipos de ajuste aceitos: text, textarea, richtext, inline_richtext, image_picker, url, collection, product, product_list, select, radio, number, range, checkbox, color, video_url, link_list, font_picker, text_alignment, e os informativos header e paragraph.
Ao desenhar, cada ajuste já chega como objeto: a imagem com as larguras guardadas (use image_url e image_tag), a coleção, o produto, a lista de produtos na ordem escolhida (sem os que saíram da loja), o texto formatado em HTML, o menu, o vídeo e a fonte (family e fallback_families).
Cada seção é desenhada isolada: a que falha some da página na loja (e fica registrada) e mostra o erro na prévia; as outras seguem.
5. Convenções da plataforma
O tema próprio funciona com qualquer nome de ajuste. Alguns nomes, porém, ligam o tema ao resto da plataforma. Os temas da plataforma usam todos; o tema próprio ganha cada comportamento ao usar a chave correspondente nos ajustes gerais (config/settings_schema.json):
| Chave | Tipo | O que a plataforma faz com ela |
|---|---|---|
logo | image_picker | É o logo da loja: vai para o Open Graph, os dados estruturados, o checkout e os e-mails. |
favicon | image_picker | É o ícone da aba: a plataforma põe o <link rel="icon"> no content_for_header. |
corFundo, corTexto, corDestaque | color | O editor confere o contraste do texto e do destaque sobre o fundo, e o checkout e as páginas abertas pelos e-mails usam essas cores. |
corTopo, corRodape | color | Também entram na aparência do checkout, como no Pilar. |
cantos | select (retos, suaves, arredondados) | Os cantos dos botões e das caixas do checkout. |
largura | select (normal, larga) | A largura do conteúdo das páginas da plataforma. |
fonteTitulos, fonteTexto | font_picker | A plataforma serve a fonte (no próprio host da loja) e põe o @font-face no content_for_header; o checkout usa as mesmas fontes. |
Sem essas chaves, o checkout e as páginas dos e-mails ficam no visual padrão da plataforma. O verificador avisa a chave que falta (missing_platform_settings) e a que tem outro tipo (platform_setting_wrong_type), e confere o contraste das cores padrão (default_contrast).
Outras convenções:
- O
<head>é da plataforma. Ocontent_for_headertraz a descrição da página, o canônico, orobots, o Open Graph, os dados estruturados (loja, produto, trilha), o carregador da plataforma (com os eventos e o aviso de cookies) e o CSS e o JavaScript das seções usadas. O tema escreve só o<title>(compage_title) e não deve repetir descrição, canônico nem Open Graph: em dobro, eles confundem os buscadores. - A imagem da prévia nas redes (
og:image) sai em toda página. No produto, as fotos dele; na marca, o logo dela; nas outras, a imagem de compartilhamento que o lojista envia em Painel › Buscadores (o ideal é 1200 x 630), o logo do tema ou, sem nenhum dos dois, uma imagem que a plataforma gera com o nome da loja. O logo menor que 200 x 200 pixels fica de fora, porque o Facebook não o usa. Para conferir, cole o endereço no Depurador de compartilhamento do Facebook e clique em "Extrair novamente". - Endereços. As páginas da loja são
/,/produtos,/produtos/<slug>,/categorias/<caminho>,/marcas/<slug>,/busca?q=,/carrinhoe/paginas/<slug>, e a página de envio da receita,/pedidos/<número>/receita?t=<token>, aberta pelo link do e-mail (ver "Página de envio da receita"). A área do cliente responde em/conta,/conta/entrare/conta/sair(ver "Área do cliente"). Useroutese asurldos objetos em vez de escrever os caminhos. - Preços em centavos. Os objetos trazem os preços em centavos, como o filtro
moneyespera (em reais). - Listas. A paginação usa o parâmetro
paginae a ordem, o parâmetroordem(com os valores decollection.sort_options). Os filtros vêm prontos emcollection.filters, comurl_to_addeurl_to_remove. - Medição e consentimento. A plataforma mede e pede o consentimento. O tema publica eventos próprios com
Denaryx.analytics.publish(nome, dados)e consulta o consentimento emDenaryx.customerPrivacy. O link "Preferências de cookies" é qualquer elemento comdata-denaryx-preferencias-de-cookies(ou um script do tema que dispara o eventodenaryx:preferencias-de-cookiesnawindow), mostrado só quandoDenaryx.customerPrivacy.shouldShowBanner()é verdadeiro. Sem ele, o verificador avisa (missing_cookie_preferences_link). As cores do aviso vêm das variáveis--denaryx-aviso-fundo,-texto,-discreto,-borda,-botao,-botao-texto,-raio,-fonte,-fonte-titulose-largura. - Cookies ficam no endereço da loja. No endereço da plataforma (
<loja>.denaryx.store), todas as lojas dividem o mesmo domínio. Por isso, a plataforma tira o atributoDomainde todo cookie gravado na página (nodocument.cookiee nocookieStore.set), e o cookie fica só no endereço da loja; apagar com oDomaincontinua valendo. Isso vale a partir do{{ content_for_header }}: ponha os scripts do tema depois dele. O verificador avisa quando o código grava comDomain(cookie_with_domain). No domínio próprio da loja, nada muda. Mantenha os cookies do tema pequenos: quando um visitante chega com mais de 16 KB de cookies, a loja apaga os que não são da plataforma. - Checkout. O checkout e a página do pedido ficam numa origem própria e não rodam código do tema. O botão de finalizar leva a
/checkout(link ou{% form 'cart' %}comname="checkout"). - Área do cliente. A conta do comprador (entrar por um código mandado ao e-mail, os pedidos, os endereços, os dados) fica numa origem própria, como o checkout, e não roda código do tema; o tema só mostra o link e o estado (ver "Área do cliente"). O verificador avisa quando nenhum arquivo usa
routes.account_login_url(missing_account_link). Oorderexiste só na página de envio da receita. - Provador virtual de óculos. Na ótica que ligou o provador, a página da armação mostra o botão "Experimentar no rosto" pelo snippet
kit-provador, e o cartão leva à página com#provador(ver "Provador virtual"). O verificador avisa o tema de ótica que não chama o kit (missing_try_on).
6. Objetos
Os nomes são os do mercado. Os principais:
- Globais:
shop,request(compage_typeedesign_mode, verdadeiro na prévia),routes,localization,settings,customer,cart,collections,all_products,pages,linklists,canonical_url,page_title,seo_title,page_description,page_image,handle,template,current_page. - Da página:
product(na página de produto),collection(categoria, marca ou todos os produtos),searchepage. - No desenho:
section,block,form(dentro de{% form %}) epaginate(dentro de{% paginate %}). - Só da Denaryx, ao lado dos nomes do mercado:
product.brand,product.specifications(os atributos da ficha),product.condition,product.mpn,product.category(comancestors),collection.children,collection.parente o provador virtual (product.try_on,product.has_try_oneshop.try_on; ver "Provador virtual"). - Item composto e propriedades: a linha do carrinho (
line_item) temkey(a chave da linha, que identifica a linha: a mesma variante com complementos ou propriedades diferentes são duas linhas),properties(as propriedades escritas pelo comprador; as que começam com_são ocultas e o tema não deve mostrar),item_components(o nome do mercado) ecomponents(os complementos escolhidos na montagem do produto, como a lente e os tratamentos da armação, cada um uma linha comgroup={ code, name }eparent_key) ebundle_price(a linha e os complementos, vezes a quantidade). Ofinal_line_priceda linha é só o dela, e ocart.total_pricesoma tudo. No produto:product.assembly(a montagem, comoptionalegroups[]e os produtos de cada grupo),product.requires_assembly(verdadeiro quando o produto só vai montado: a montagem não é opcional e tem grupo obrigatório; vale também nas listas, ondeproduct.assemblynão vem),product.available_standalone,product.inventory_trackedeproduct.requires_prescription. - Receita oftalmológica: a linha com lente de grau (a lente sozinha, ou como complemento da armação) tem
line_item.requires_prescription(verdadeiro),line_item.prescription_status(pending: sem receita, para mandar depois da compra;provided: os valores digitados ou o arquivo da receita;declared: a lente de contato por grau com a declaração do comprador, ver adiante;nonena linha que não pede receita) eline_item.prescription(os valores digitados, com os nomes da API Admin:type,righteleftcomsphere,cylinder,axiseadd,pd,segment_height,prism,issued_at,doctor,note;nilsem eles). Só quem tem o carrinho vê a receita dele. Os temas da plataforma mostram "Receita informada" ou "Receita: envie depois da compra" embaixo do conjunto. Os campos estão em Receitas. - Política de receitas da loja:
shop.prescriptions(policy:later_allowedourequired_before_checkout;later_allowed, verdadeiro quando o comprador pode mandar a receita depois da compra;typed_required, verdadeiro quando a loja exige os valores digitados e só a foto não basta;instructions, o texto da loja para o comprador, que o tema escapa;acceptemax_file_size, para o campo de arquivo). É com ele que o configurador de lentes mostra ou esconde o "mando depois" e a opção de enviar só o arquivo. - Página de envio da receita (template
prescription):order(id,name=#1042,order_number,customer.first_name,cancelled,prescriptions_pendingeline_items, só os itens que pedem receita, cada um comid,title,components, a lista dos complementos já escritos, eprescription={ status, provided_at }, compending,providedouchecked) eprescription_request(link_valid,link_expired,link_expires_at,instructions, o texto da loja, que o tema escapa comescape,typed_required,due_dateemAAAA-MM-DDedue_date_labelemDD/MM/AAAA,posted_successfully,acceptemax_file_size). Com o link inválido ou vencido,orderénil. Do comprador, só o primeiro nome: nenhum dado de contato, documento ou endereço. - Menus:
linklists['main-menu']é a árvore das categorias elinklists.footer, as páginas institucionais.
O editor de arquivos completa os nomes dos objetos, das tags e dos filtros enquanto se digita.
Tags: section, sections, content_for ('blocks' e 'block'), render, layout, form, paginate, style, stylesheet, javascript, schema e as do Liquid. {% include %} não existe (use {% render %}), e {% render %} só alcança snippets/.
Formulários: {% form 'cart' %}, {% form 'product' %}, {% form 'newsletter' %}, {% form 'customer' %} (o cadastro de e-mail, como no mercado) e {% form 'prescription', line_item %}.
Área do cliente
O lojista liga a área do cliente em Configurações › Conta do cliente; ela nasce desligada. O comprador entra com um código de 6 números mandado ao e-mail, sem senha, e a conta fica numa origem própria, a do checkout, com o desenho da plataforma (o logo, as cores e as fontes do tema): os pedidos, os endereços, o e-mail e o CPF nunca chegam ao HTML do tema. O tema só mostra o link e o estado:
shop.customer_accounts_enabled: verdadeiro com a área ligada. Desligada, as rotas da conta respondem 404, e o tema não deve mostrar o link.customer: o comprador que entrou na conta, comfirst_nameename(os dois com o primeiro nome);nilsem sessão, na prévia do editor e com a área desligada. Não tem e-mail, pedidos nem endereços. O nome é o que o comprador escreveu: saia comescape.routes.account_url(/conta, os pedidos),routes.account_login_url(/conta/entrar),routes.account_register_url(o mesmo de entrar: o primeiro código cria a conta) eroutes.account_logout_url(/conta/sair). São rotas da plataforma no endereço da loja, que levam à origem da conta. A de entrar aceita?voltar=com um caminho da loja, para o comprador voltar à página de onde saiu depois do código.
O cabeçalho dos seis temas da plataforma faz assim:
{% if shop.customer_accounts_enabled %}
{% if customer %}
<a href="{{ routes.account_url }}">Olá, {{ customer.first_name | escape }}</a>
{% else %}
<a href="{{ routes.account_login_url }}?voltar={{ request.path | url_encode }}">Entrar</a>
{% endif %}
{% endif %}O customer vem de um cookie da loja que só guarda o primeiro nome e a validade da sessão, e a página com ele não fica em cache. O script do tema não lê esse cookie (é HttpOnly), e mudar o cookie à mão só muda o próprio cabeçalho: a sessão de verdade fica na origem da conta.
Página de envio da receita
O comprador que comprou lente de grau e ficou de mandar a receita depois recebe o link no e-mail do pagamento e nos lembretes, e o vê na página do pedido. O link abre o template prescription do tema, ou, se o tema não tem um, a página da plataforma, dentro do layout do tema (o cabeçalho e o rodapé da loja ficam). Os seis temas da plataforma têm a página, com o conteúdo no snippet kit-receita-do-pedido (copie-o e chame {% render 'kit-receita-do-pedido', botao: '<classes do botão>', titulo: '<classes do título>' %} numa seção do template). O tema que quer o próprio desenho cria templates/prescription.json (ou .liquid) com os objetos acima e um formulário por item pendente:
{% for line_item in order.line_items %}
{% if line_item.prescription.status == 'pending' %}
{% form 'prescription', line_item %}
{% if form.errors.size > 0 %}{{ form.errors | join: ' ' | escape }}{% endif %}
<select name="prescription[type]">...</select>
<input name="prescription[right][sphere]" inputmode="decimal">
...
<input type="file" name="prescription_file" accept="{{ prescription_request.accept }}">
<button>Enviar a receita</button>
{% endform %}
{% endif %}
{% endfor %}- O formulário volta por POST para a própria página (o endereço leva o token do link) com o
line_item_idescondido. Os campos são os do/cart/add.js(prescription[type],prescription[right][sphere],prescription[pd][binocular]...; ver Receitas) e o arquivo vai no próprio formulário, emprescription_file(JPEG, PNG, WebP ou PDF de até 10 MB). Valores, arquivo ou os dois; a loja pode exigir os valores (typed_required). - Com erro, a página volta desenhada com
form.errors(eform.errors.messages, por campo:right.sphere) só no formulário do item enviado, com o status 422. Deu certo, a página recarrega comprescription_request.posted_successfullye o item emprovided. - A receita que já chegou não volta pelo link (a loja troca no painel), e o pedido cancelado não recebe mais.
- A página fica fora dos buscadores e da medição, e não manda o Referer: não ponha nela pixels ou scripts de terceiros, porque o endereço leva o token.
Configurador de lentes
A armação que tem uma montagem (product.assembly) vende com a lente e os tratamentos escolhidos pelo comprador, e a lente de grau pede a receita. Os seis temas da plataforma fazem isso dentro do próprio {% form 'product' %}, com os mesmos três arquivos, que vêm em qualquer um deles ("Copiar o código" ou o denaryx tema init, que traz o Pilar inteiro): o snippet kit-configurador-de-montagem, o snippet kit-campos-da-receita e o script assets/kit-configurador.js, que o snippet carrega. O jeito mais curto de levar o configurador ao seu tema é copiar os três e:
- chamar
{% render 'kit-configurador-de-montagem', product: product, botao: '<classes do botão principal>', botao_secundario: '<classes do secundário>', botao_link: '<classes do botão de texto>' %}dentro do formulário de compra, antes do botão de comprar, quandoproduct.assemblyexiste; - no envio do formulário, perguntar ao configurador (o elemento
[data-configurador]guarda o objeto em.configurador):conferir()antes de enviar (falso quando falta um passo, que ele abre),aberto()para saber se o comprador está montando,formulario(secoes)para oFormDatado formulário inteiro (comsections[]) eadicionado()depois do envio. Fechado, mande sóidequantity, como sem montagem; - esconder no CSS
.comprar[data-escondido-pelo-configurador](o botão de comprar aparece no resumo) e desenhar as classes do configurador (configurador-*,passo,opcao-da-montagem,valor,receita-*,tabela-da-receita); os textos vêm dos seus locales (configurador.*ereceita.*, os mesmos dos temas da plataforma).
O que esses arquivos fazem, para quem prefere escrever o seu:
- Os grupos da montagem (
product.assembly.groups[], comcode,name,required,multipleeproducts[], cada produto comvariants[],price,availableeprescription_range) viram camposcomponents[<code>][id](a variante escolhida) oucomponents[<code>][id][]no grupo de várias escolhas. Sem JavaScript, um<select>por grupo (ou caixas de marcar) basta; o script do kit esconde esses controles e desenha os passos (o tipo de lente, a receita, o material, os tratamentos e o resumo), escrevendo as escolhas de volta nos mesmos campos. - A receita vai nos campos
prescription[...]do snippetkit-campos-da-receita(os mesmos da página de envio do Pilar), comprescription[later]=1para "mando depois" (só quandoshop.prescriptions.later_allowed). A foto ou o PDF é enviado antes porPOST /denaryx/arquivos(precisa de JavaScript) e oiddevolvido vai emprescription_file; sem JavaScript, o comprador digita ou manda depois. - A faixa de grau (
variant.prescription_range:sphere_min,sphere_max,cylinder_max,add_min,add_max, ounil) só filtra: o configurador desabilita, com "não atende ao seu grau", a variante cuja faixa não cobre os dois meridianos (esférico e esférico + cilíndrico) de cada olho digitado, o cilíndrico ou a adição. A plataforma não indica lente (Decreto 24.492/1934, art. 13): nada no tema deve dizer "recomendada para o seu grau". A API confere de novo ao adicionar e recusa com 422 a lente fora da faixa. - O envio é o próprio formulário, como
multipart/form-data, paraPOST /cart/add.js(comsections[]para a gaveta): a resposta é a linha, comcomponents[],prescription_statuseprescription. O botão da barra presa ao pé da tela fica de fora na armação que exige a montagem (product.requires_assembly): ela mandaria a armação sem a lente. Pelo mesmo motivo, a compra rápida (o "Adicionar" do cartão de produto, de uma sugestão ou de um kit) leva à página do produto quandoproduct.requires_assemblyouproduct.requires_prescriptioné verdadeiro: a API recusaria a armação sem a lente e a lente sem a receita. - A montagem opcional (
product.assembly.optional): o comprador pode levar só a armação, e o envio semcomponentsvale. Com algum complemento escolhido, os gruposrequiredvalem (o tratamento não vai sem a lente). O configurador começa fechado, com o botão "Montar com"; aberto, o botão "Comprar sem" volta para só a armação, e o envio leva sóidequantity. Sem JavaScript, o<select>do grupo obrigatório ganha a opção vazia "Comprar sem". - No carrinho, mostre os complementos e a situação da receita embaixo do nome da linha (o snippet
kit-complementos-da-linha): o verificador avisa comcart_without_componentsquando nenhum arquivo lêline_item.item_components.
Lente de contato
O produto com product.prescription_type == 'contact_lens' é a lente de contato: a variante é a caixa (a curva base, a quantidade de lentes), vendida sozinha, sem montagem. O comprador digita o grau de cada olho e escolhe quantas caixas vão para cada um. Os seis temas da plataforma fazem isso no snippet kit-lente-de-contato, dentro do {% form 'product' %}, no lugar da quantidade; o snippet carrega o script assets/kit-lente-de-contato.js (o "mesmo grau nos dois olhos", a curva base e o diâmetro da caixa escolhida e o envio da foto), que guarda no elemento [data-lente-de-contato] o mesmo objeto do configurador (conferir(), aberto(), formulario(secoes) e adicionado()): o envio do tema pode procurar [data-configurador], [data-lente-de-contato] e tratar os dois do mesmo jeito. Copie o snippet e o script, ou escreva o seu com os campos abaixo:
- As caixas de cada olho vão em
eyes[right]eeyes[left]({ "right": 2, "left": 1 }em JSON); a quantidade da linha é a soma, e oquantityque vier não conta. O olho com zero caixas fica sem lente. A linha de lente de contato semeyesé recusada com 422 ecode: "eyes_required", eeyesem outra linha também. - O grau é obrigatório na compra (é ele que escolhe a caixa):
prescription[type]=contact_lenseprescription[right][sphere],[cylinder]e[axis](na tórica),[add](na multifocal), e, opcionais,[base_curve],[diameter]eprescription[lens_brand](a lente receitada). Não há "mando depois" nem DP. Sem o grau, 422 comcode: "prescription_required". A foto ou o PDF da receita é opcional, porprescription_file, como no configurador. - A caixa tem a faixa em
variant.prescription_range, combase_curveediameter(em mm). Mostre-os ao comprador; o snippet do kit só mostra as colunas de cilindro e de adição quando alguma variante tem cilindro ou adição na faixa. A receita de óculos não serve para a lente de contato (e a de contato não serve para os óculos): a API recusa. - No carrinho,
line_item.eyes({ right, left }, ounil) traz as caixas de cada olho; o snippetkit-complementos-da-linhamostra "Caixas: OD 2 · OE 1". A quantidade da linha não muda pelo/cart/change.js(a API recusa comcode: "eyes_required"): mostre só o remover, e o comprador escolhe de novo na página do produto.
A lente de contato por grau (product.contact_lens_mode == 'by_power'; nas outras, by_prescription) é a esférica pronta: cada variante é a caixa de um grau (variant.title é o grau, como -2,00), com estoque próprio, e a faixa da variante é o próprio grau. O snippet kit-lente-de-contato troca o grau digitado por um <select> por olho, e o tema não desenha as opções nem o select name="id" desse produto. Os campos:
- O grau de cada olho vai em
power[right]epower[left], o id da variante ({ "right": "<id>", "left": "<id>" }em JSON), com as caixas emeyes[right]eeyes[left]. A loja faz as linhas: o mesmo grau nos dois olhos é uma linha com as caixas dos dois; graus diferentes são duas linhas, cada uma na variante do seu grau, com o outro olho em zero (line_item.eyes{ right: 2, left: 0 }e{ right: 0, left: 1 }). A resposta é{ items: [...] }, como noitems[]. O olho com caixas e sem grau é recusado com 422. Mostre todos os graus, com o esgotado (variant.availablefalso) desabilitado: o comprador vê que a loja trabalha com o grau. - No lugar da receita, a declaração do comprador de que usa a receita do médico:
prescription[declaration]=1(uma caixa de marcar obrigatória), com o texto deshop.prescriptions.declaration, sem mudar (é o texto que o pedido guarda). Sem ela, 409 comcode: "prescription_required". A foto ou o PDF da receita continua opcional, emprescription_file, e vai só na primeira linha. Não mandeprescription[type]nem o grau digitado. - No carrinho,
line_item.prescription_statusédeclaredna linha com a declaração; o snippetkit-complementos-da-linhamostra "Receita: você declarou que usa a do seu médico".
Provador virtual
O provador virtual de óculos mostra a armação no rosto do comprador, pela câmera ou por uma foto escolhida no aparelho. Tudo acontece no navegador do comprador: a imagem, a foto e os pontos do rosto não saem do aparelho, e o programa que reconhece o rosto vem da própria loja, sem chamada a terceiros. O lojista liga o provador em Configurações › Provador virtual e cadastra os arquivos de cada armação no painel (o bloco "Provador virtual" do produto, ou um zip em lote em Produtos › Provador virtual) ou pela API Admin. O arquivo é a frente em PNG com fundo transparente (o 2,5D, com o lado opcional) ou um GLB (o 3D), sempre com a largura da armação em mm, que dá a escala. A cor que tem arquivo próprio troca no provador; as outras usam o do produto. Dentro da janela, o comprador também experimenta as outras armações da mesma categoria (a troca de modelo), sem fechar.
O jeito mais curto. Os seis temas da plataforma fazem tudo com dois arquivos, iguais em todos: o snippet kit-provador e o script assets/kit-provador.js, que o snippet carrega. Copie os dois de um tema da plataforma ("Copiar o código" ou o denaryx tema init) e:
- Na página do produto, chame o snippet no bloco de compra, depois do
{% form 'product' %}(fora dele: nada da janela vai no/cart/add), no mesmo elemento que contém o formulário:
liquid {% render 'kit-provador', product: product, botao: 'botao botao-cheio', botao_secundario: 'botao botao-contorno', botao_fechar: 'botao-icone' %}
botao são as classes do "Ligar a câmera" (o botão principal); botao_secundario, as do "Experimentar no rosto", do "Usar uma foto" e do "Salvar a foto"; botao_fechar, as do X da janela. O snippet usa um {% render 'icone', nome: 'fechar' %} no X: troque pelo ícone do seu tema. Ele só desenha alguma coisa com o provador ligado e um arquivo na armação, então pode ficar em todo produto.
- No cartão do produto, o link que abre o provador direto, só no produto que tem arquivo:
liquid {% if product.has_try_on %} <a href="{{ product.url }}#provador">{{ 'provador.no_cartao' | t }}<span class="so-leitor">: {{ product.title | escape }}</span></a> {% endif %}
A página do produto aberta com #provador abre a janela sozinha; ao fechar, o #provador sai do endereço. O "Ver esta armação" da troca de modelo leva à outra armação com #provador ou, com a câmera ligada, com #provador-camera: a janela abre e a câmera liga sozinha se o navegador já tem a permissão (sem ela, a janela fica no aviso).
- Os textos ficam nos seus locales, em
provador.*(experimentar,no_cartao,titulo,fechar,dica,ligar_camera,usar_foto,salvar_foto,cores,modelos,esta_armacao,ver_armacao,imagem,carregando,sem_rosto,sem_rosto_na_foto,sem_camera,erroe os deserve:estreita,na_medida,larga,pela_iris,pela_dpede_frente). Copie o blocoprovadordopt-BR.default.jsonde um tema da plataforma; o verificador aponta o texto que falta (missing_translation). Os textos do "serve no seu rosto" comparam medidas e não recomendam nada: a ótica não pode indicar lente (Decreto 24.492/1934, art. 13), e a frase continua factual. - O CSS da janela, com as classes do kit (
provador,provador-abrir,provador-janela,provador-caixa,provador-topo,provador-aviso,provador-aviso-da-loja,provador-dica,provador-palco,provador-medidas,provador-serve,provador-serve-fonte,provador-cores,provador-cor,provador-outra,provador-outra-texto,provador-outra-nome,provador-outra-preco,provador-ver,provador-modelos,provador-modelo,provador-modelo-nome,provador-mensagem,provador-acoes,provador-escolher-fotoeprovador-no-cartao). Copie o bloco do provador do CSS de um tema da plataforma e troque as variáveis pelas suas. Três pontos que o CSS precisa manter:- Sem JavaScript, o provador não funciona: esconda
.provadore mostre-o com a marcacom-jsno<html>, posta por um script no começo do<head>(document.documentElement.classList.add('com-js')). Assim o botão já nasce no lugar e a página não pula. - A tela cheia do celular: quando a armação aparece, o script marca o
<dialog>comdata-tela-cheiana tela estreita ou de toque. O CSS faz a janela ocupar a tela (100dvh, a página de trás sem rolar, com:root:has(.provador-janela[data-tela-cheia])), põe o canvas comobject-fit: covereobject-position: var(--provador-foco)(o script calcula o foco nos olhos) e respeita as áreas seguras (env(safe-area-inset-*)). Na tela cheia, os controles se ordenam peloorder: as ações em cima, a faixa da outra armação (.provador-outra) e o "serve no seu rosto" embaixo, depois as cores e, por último, as miniaturas das outras armações (.provador-modelos), só com a foto. - O canvas da câmera é espelhado pelo script, como um espelho; não espelhe de novo no CSS.
- Sem JavaScript, o provador não funciona: esconda
Os objetos, para quem prefere escrever o seu:
shop.try_on:enabled(o provador ligado na loja),notice(o aviso da plataforma, o mesmo em toda loja, que o tema mostra sem mudar antes de abrir a câmera) estore_notice(o texto que a loja acrescenta embaixo, vazio sem ele; saia comescape).product.try_on, na página do produto e no carrinho:available(há arquivo, do produto ou de alguma variante, na loja que ligou o provador),type(2dou3d),width_mm,adjustments(vertical_offset_mm,tilt_degrees,size_factor),assets(frontesidecomurl,widtheheight;modelcomurletriangles),hingeecolor(a altura da dobradiça e a cor do aro, para a haste genérica do 2,5D) evariants, o mapa do id da variante para o arquivo dela, já resolvido (o dela ou o do produto). Os campos de fora vêm do arquivo do produto ou, sem ele, da primeira variante que tem um. Nas listas,availableé falso.similarsão as armações parecidas, só na página do produto com arquivo: até 8 outros produtos à venda da categoria principal (e das subcategorias), com arquivo, dos mais novos para os mais velhos (sem categoria, os da loja). Cada um temid,title,handle,url(a página dele na variante experimentada),variant_id,variant_title,priceecompare_at_price(em centavos),featured_image(a foto da variante ou a capa;nullsem foto) e os campos do arquivo da variante (type,width_mm,adjustments,assets,hingeecolor). A variante é a primeira à venda que tem arquivo.product.has_try_on: o produto tem provador; vale também nas listas, para o link do cartão.
O que o snippet e o script fazem (os ganchos que o kit-provador.js procura, se você escrever a sua marcação):
- O elemento
[data-provador]com o JSON<script type="application/json" data-provador-dados>(produto, oproduct.handle;armacao, oproduct.try_on | jsoninteiro; etextos, os do locale), o botão[data-abrir-provador]e o<dialog data-provador-janela>. Dentro da janela:[data-provador-aviso],[data-provador-palco](com o<canvas data-provador-canvas>e o<video data-provador-video muted playsinline>, que fica na página sem aparecer, porque o Safari do iPhone não entrega os quadros de um vídeo fora dela),[data-provador-medidas]com[data-provador-serve-texto]e[data-provador-serve-fonte],[data-provador-mensagem],[data-ligar-camera], o campo de arquivo[data-usar-foto],[data-salvar-foto],[data-fechar-provador]e as cores ([data-provador-cores], um botão[data-provador-cor="<id da variante>"]por variante com arquivo, só quando há mais de um arquivo diferente entre elas). A troca de modelo, quandoproduct.try_on.similarnão está vazio: as miniaturas[data-provador-modelos], com um botão[data-provador-modelo=""](a armação da página) e um[data-provador-modelo="<id do produto>"]por parecida, comdata-nome,data-preco(o preço já formatado) edata-endereco(ourl); e a faixa[data-provador-outra], com[data-provador-outra-nome],[data-provador-outra-preco]e o link[data-provador-ver]. A troca de modelo não mexe no formulário de compra; com outra armação no rosto, o script esconde as cores. - A variante: a armação é a da variante escolhida no formulário de compra (
form [name="id"]) do mesmo bloco do snippet. Trocar a cor na janela troca o[name="id"]do formulário e dispara ochange: o tema que já segue esse evento (o preço, as fotos, o endereço) acompanha. - A escala: o runtime mede pela íris. Se o configurador de lentes estiver no mesmo bloco, a DP que o comprador digitou no campo
prescription[pd][binocular]vale no lugar da íris, sem aparecer na janela. A janela não tem campo de DP. - O runtime não é do tema: ao abrir a janela, o script pede
/denaryx/provador/atual(no endereço da loja) e importa o runtime da versão que vier, com cache de um ano. Não copie o runtime paraassets/nem o carregue de outro endereço: ele muda com a plataforma. - Os eventos: o script dispara
denaryx:provadornodocument, e o carregador da plataforma publicatry_on_opened,try_on_modeetry_on_photo_savednodataLayere nas ferramentas de medição, com o item da variante e nada do rosto. O clique em "Ver esta armação" vira oproduct_clicked(oselect_itemdo GA4) da lista "Provador virtual", com a variante experimentada. O tema próprio que usa o script ganha os eventos sem escrever código; ver "Os eventos do provador virtual" no guia do dataLayer.
Imagens com CORS. O runtime lê a frente e o GLB com CORS, porque o WebGL recusa a imagem sem o cabeçalho. Se o tema mostrar a frente da armação numa <img> (uma miniatura, como a das outras armações sem foto do produto), ponha crossorigin="anonymous" nela: sem isso, o navegador guarda uma cópia sem CORS e a reaproveita, e o provador não abre ("O provador não abriu neste navegador").
Privacidade. O script não envia nada: só lê a versão do runtime e os arquivos da armação, sem localStorage, e os eventos de medição levam só a variante e o modo. Não acrescente ao provador um envio da foto ou das medidas, nem um pixel que leia o canvas: a imagem do rosto é dado sensível (LGPD, art. 5º, II), e a política de privacidade da plataforma diz ao comprador que nada sai do aparelho.
O verificador avisa o tema de ótica que não chama o kit (missing_try_on): o tema que já usa product.try_on, product.has_try_on, shop.try_on ou o link #provador, ou que usa a receita oftalmológica, sem nenhum arquivo que carregue o kit-provador.js.
7. Rotas em JSON
Todas no próprio endereço da loja, para o JavaScript do tema:
| Rota | O que faz |
|---|---|
GET /cart.js (ou /cart.json) | O carrinho, no formato do {{ cart | json }}, com os preços em centavos. |
POST /cart/add.js | { id, quantity, properties, components } ou { items: [...] }. O id é o da variante; properties é um objeto (properties[Gravação] no formulário) e components são os complementos da montagem: [{ id, group }] em JSON, ou components[<grupo>][id] no formulário (components[<grupo>][id][] no grupo de várias escolhas). O complemento sozinho é recusado com 422 e code: "component_only". files são os ids dos arquivos enviados pelo comprador para a linha (até 3): ["<id>"] em JSON, files[] no formulário. A mesma variante com arquivos diferentes são duas linhas. A receita da lente de grau vai em prescription (os campos da API Admin: prescription[type], prescription[right][sphere], prescription[pd]... no formulário, com vírgula ou ponto; os campos em branco ficam de fora) e o arquivo dela em prescription_file (o id do envio, como em files); prescription[later]=1 é "mando depois". A loja que exige a receita antes de comprar recusa a linha sem ela com 422 e code: "prescription_required"; a lente que não atende à faixa de grau da variante, com 422 e a mensagem; o campo fora da faixa, com 422 e a mensagem do campo. A linha com receita é sempre uma linha nova. |
POST /denaryx/arquivos | Envia um arquivo do comprador (a foto ou o PDF da receita, uma arte para personalizar), no campo file de um multipart/form-data, e responde { id, name, size }. Aceita JPEG, PNG, WebP e PDF de até 10 MB, conferidos pelo conteúdo; HEIC (a foto do iPhone) ainda não: use accept="image/jpeg,image/png,image/webp,application/pdf" no campo, que o Safari do iPhone converte a foto para JPEG. A foto é gravada sem os metadados (EXIF). O arquivo fica guardado por 7 dias até entrar num pedido; mande o id em files no /cart/add.js. Até 5 arquivos por carrinho e 20 envios por hora. |
DELETE /denaryx/arquivos/<id> | Remove o arquivo que ainda não está numa linha (o comprador trocou a foto). O da linha sai com ela. |
POST /cart/change.js | { id, quantity } (o id é o key da linha, ou o id da variante quando só uma linha a tem) ou { line, quantity } (a linha conta de 1). Zero tira a linha, com os complementos. |
POST /cart/update.js | { updates: [...] } (na ordem das linhas) ou { updates: { <key ou id>: quantidade } }. |
POST /cart/clear.js | Esvazia o carrinho. |
GET /search/suggest.json?q= | A busca preditiva: produtos, coleções, páginas e termos (resources[type], resources[limit], resources[limit_scope], resources[options][unavailable_products]). Aqui os preços vêm em reais, num texto com duas casas. |
POST /email/inscrever | O cadastro de e-mail (contact[email]), com Accept: application/json para a resposta em JSON. |
GET <página>?sections=a,b | O HTML das seções pedidas da página, em JSON (até 20). |
GET <página>?section_id=a | O HTML de uma seção. |
As rotas do carrinho aceitam também o parâmetro sections, para o tema trocar a gaveta do carrinho sem recarregar. Os mesmos caminhos sem o .js funcionam como formulário, sem JavaScript, e levam ao /carrinho. Os erros seguem o formato do mercado: { status, message, description }. Pedidos de outro site são recusados (403).
8. Limites
| O quê | Limite |
|---|---|
| Arquivos no tema | 2.000 |
| Arquivo de código (Liquid, JSON) | 256 KB |
Arquivo de assets/ | 10 MB |
| Tema inteiro | 50 MB |
| Arquivos de tema por loja (somando cada conteúdo uma vez) | 1 GB |
| Tempo de desenho de uma página (seções e layout somados) | 2 s |
| HTML de uma página | 4 MB |
{% render %} dentro de {% render %} | 8 níveis |
| Blocos dentro de blocos | 8 níveis |
| Temas na biblioteca da loja | 20 |
| Links de prévia ativos | 20 |
| Arquivo enviado pelo comprador | 10 MB |
| Arquivos enviados por carrinho, ainda sem pedido | 5 |
| Arquivos por linha do carrinho | 3 |
Passar do tempo, do tamanho do HTML ou da memória do motor derruba a página inteira, e não só a seção: a página mostra o erro na prévia.
O limite de tempo é conferido entre um trecho do template e o seguinte. Uma operação só que demora (um filtro sobre uma lista ou um texto enorme, como um where_exp sobre milhões de itens) não é interrompida no meio. Se ela prender o desenho por mais de 3 s, ou passar da memória reservada ao desenho, a página é interrompida: o comprador recebe uma página curta de erro, e o caso entra em "Em produção", no editor de arquivos. As outras páginas da loja, e as outras lojas, seguem atendendo. Na prévia, a resposta traz o cabeçalho Server-Timing com o tempo total, a leitura da loja e do tema, o desenho e as 20 seções mais lentas: abra o DevTools, aba Rede, e veja "Timing" da página.
9. Editar, ver a prévia e publicar
- Editar. Na página Temas, "Editar código" abre o editor de arquivos do painel: a árvore, as abas, o histórico de cada arquivo (com a diferença e "Restaurar"), as versões do código e a prévia ao lado. Ctrl+S salva. Duas pessoas no mesmo arquivo não se atropelam: quem salva depois vê a diferença e escolhe.
- Salvar não publica. Salvar muda o tema em trabalho; a loja continua como está até alguém publicar. Salvar com erro pode; publicar com erro não.
- Erros e avisos. A cada gravação, o código passa pelo verificador de temas: JSON inválido, erro de sintaxe do Liquid,
{% schema %}inválido, layout semcontent_for_headeroucontent_for_layout, template obrigatório faltando, snippet ou seção que não existe, e os avisos (tradução que falta, snippet que ninguém usa, o que o<head>da plataforma já escreve). Os problemas aparecem na linha, na árvore e no painel de problemas, cada um com o código da regra, que leva à explicação. O CLI roda o mesmo verificador sem rede:denaryx tema check. - Prévia ao lado. A prévia do editor recarrega a cada gravação, sem perder a rolagem.
- Link de prévia para o cliente. Na página Temas, "Compartilhar prévia" (ou
denaryx tema share) cria um link com nome e validade (1, 7 ou 30 dias). O endereço aparece uma vez, na hora de criar: guarde-o. Quem abre o link não precisa de conta. A loja aparece com o tema da prévia, com uma faixa embaixo (com "Sair da prévia"), fora dos buscadores e sem medição; o carrinho e o checkout são os de verdade. Na lista "Links de prévia", cada link mostra o último acesso, e "Revogar" o tira do ar na hora. O link é de um destes tipos:- congelado: mostra para sempre o código e o conteúdo daquele momento ("versão de 01/10 14h05", na faixa). É o que o cliente aprova, e o que vai ao ar se ninguém mudar nada depois;
- acompanhando o trabalho: mostra o tema em trabalho e se atualiza sozinho a cada gravação, enquanto a aba está visível.
- Versões. "Salvar versão", com uma nota, guarda o código do momento. Restaurar uma versão troca só o código; o conteúdo da loja fica.
- Antes de publicar. "Publicar", na página Temas ou nos editores, abre a página "Antes de publicar", com quatro partes:
- os problemas do código: com erro, o tema não vai ao ar;
- o que muda no conteúdo: as seções, os itens e os ajustes que a loja montou no editor visual e que o código não lê mais (porque a seção saiu, o ajuste mudou de nome, o valor guardado não vale mais). Eles saem da vitrine ao publicar. Antes de renomear uma seção ou um ajuste, confira aqui o que a loja perde;
- os modelos que faltam: os modelos da página que produtos, categorias e páginas usam e o tema não tem; com ele no ar, esses ficam com a página padrão;
- o ensaio das páginas: a página de exemplo de cada template (o primeiro produto, a primeira categoria, a busca, o carrinho, a primeira página institucional e a página não encontrada) desenhada como na prévia, com os erros de cada uma e as seções mais lentas. Os modelos da página entram com o exemplo do template base deles. Um erro no ensaio não impede publicar, mas na loja a parte que falhou sai em branco. O mesmo ensaio roda pelo CLI, no terminal ou no CI:
denaryx tema check --paginas.
- Publicar agora ou agendar. Quem publica (dono, gerente ou, se o dono deixar, o Catálogo e vitrine) publica na hora, com uma nota que fica nas publicações, ou agenda para um dia e uma hora (de 5 minutos a 60 dias à frente). O agendamento guarda o tema como está naquele momento: o que mudar depois não entra. Na hora, o tema vai ao ar em nome de quem agendou, se essa pessoa ainda puder publicar; senão, a página Temas mostra por que não foi. A vitrine mostra a publicação agendada em até um minuto. Há uma publicação agendada por loja; o tema agendado não sai da biblioteca até o agendamento ser cancelado. Voltar a uma publicação anterior é um clique, na página Temas.
- Erros em produção. Os erros que as páginas da loja têm com o código publicado de um tema próprio (uma seção que falha, um snippet que não existe) chegam ao editor de arquivos, no painel "Em produção", juntados a cada minuto, com a contagem, a última página e se o arquivo já mudou desde então. Ficam 30 dias depois da última vez, até 200 por loja.
10. Exportar
"Exportar", na página Temas, baixa o zip com o código do tema (sem o conteúdo da loja). O tema que nasceu da cópia de um tema da plataforma leva um LICENCA.txt: o código copiado vale para lojas na Denaryx. Pelo CLI, denaryx tema pull baixa os arquivos para a pasta do projeto, e denaryx tema package gera o zip da pasta.
11. Pelo CLI e pelo Git
O CLI de temas leva o mesmo caminho para o seu editor e para o Git: denaryx login entra pela conta de quem está na equipe da loja (com o papel Catálogo e vitrine, sem pedir um token ao dono), denaryx tema init começa pelo Pilar, tema check confere sem rede, tema dev mostra a loja de verdade num endereço local e a atualiza a cada arquivo salvo, cada pessoa no seu tema de desenvolvimento (Desenvolver com o tema dev), tema push e tema pull sincronizam sem atropelar quem edita no painel, tema share cria o link para o cliente e tema publish publica com o "Antes de publicar". No repositório fica só o código: o conteúdo que o lojista monta no editor visual continua na loja e nunca vira commit. Para enviar a cada push e conferir cada pull request, há dois caminhos: ligar o tema a uma branch pelo App da Denaryx no GitHub, sem CI e sem token, com uma prévia na loja para cada pull request (O tema no GitHub), ou rodar o CLI no seu CI (o CLI no CI e no GitHub).