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 init cria a pasta com o Pilar, e denaryx tema push --novo a 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:

PastaO 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 de sections/*.json e o config/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 blocks de um preset (da seção ou do bloco) é a lista do mercado, e cada item pode ter o seu blocks, 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 o limit dos 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):

ChaveTipoO que a plataforma faz com ela
logoimage_pickerÉ o logo da loja: vai para o Open Graph, os dados estruturados, o checkout e os e-mails.
faviconimage_pickerÉ o ícone da aba: a plataforma põe o <link rel="icon"> no content_for_header.
corFundo, corTexto, corDestaquecolorO 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, corRodapecolorTambém entram na aparência do checkout, como no Pilar.
cantosselect (retos, suaves, arredondados)Os cantos dos botões e das caixas do checkout.
larguraselect (normal, larga)A largura do conteúdo das páginas da plataforma.
fonteTitulos, fonteTextofont_pickerA 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. O content_for_header traz a descrição da página, o canônico, o robots, 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> (com page_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=, /carrinho e /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/entrar e /conta/sair (ver "Área do cliente"). Use routes e as url dos objetos em vez de escrever os caminhos.
  • Preços em centavos. Os objetos trazem os preços em centavos, como o filtro money espera (em reais).
  • Listas. A paginação usa o parâmetro pagina e a ordem, o parâmetro ordem (com os valores de collection.sort_options). Os filtros vêm prontos em collection.filters, com url_to_add e url_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 em Denaryx.customerPrivacy. O link "Preferências de cookies" é qualquer elemento com data-denaryx-preferencias-de-cookies (ou um script do tema que dispara o evento denaryx:preferencias-de-cookies na window), mostrado só quando Denaryx.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-titulos e -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 atributo Domain de todo cookie gravado na página (no document.cookie e no cookieStore.set), e o cookie fica só no endereço da loja; apagar com o Domain continua valendo. Isso vale a partir do {{ content_for_header }}: ponha os scripts do tema depois dele. O verificador avisa quando o código grava com Domain (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' %} com name="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). O order existe 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 (com page_type e design_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), search e page.
  • No desenho: section, block, form (dentro de {% form %}) e paginate (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 (com ancestors), collection.children, collection.parent e o provador virtual (product.try_on, product.has_try_on e shop.try_on; ver "Provador virtual").
  • Item composto e propriedades: a linha do carrinho (line_item) tem key (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) e components (os complementos escolhidos na montagem do produto, como a lente e os tratamentos da armação, cada um uma linha com group = { code, name } e parent_key) e bundle_price (a linha e os complementos, vezes a quantidade). O final_line_price da linha é só o dela, e o cart.total_price soma tudo. No produto: product.assembly (a montagem, com optional e groups[] 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, onde product.assembly não vem), product.available_standalone, product.inventory_tracked e product.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; none na linha que não pede receita) e line_item.prescription (os valores digitados, com os nomes da API Admin: type, right e left com sphere, cylinder, axis e add, pd, segment_height, prism, issued_at, doctor, note; nil sem 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_allowed ou required_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; accept e max_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_pending e line_items, só os itens que pedem receita, cada um com id, title, components, a lista dos complementos já escritos, e prescription = { status, provided_at }, com pending, provided ou checked) e prescription_request (link_valid, link_expired, link_expires_at, instructions, o texto da loja, que o tema escapa com escape, typed_required, due_date em AAAA-MM-DD e due_date_label em DD/MM/AAAA, posted_successfully, accept e max_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 e linklists.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, com first_name e name (os dois com o primeiro nome); nil sem 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 com escape.
  • 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) e routes.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_id escondido. 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, em prescription_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 (e form.errors.messages, por campo: right.sphere) só no formulário do item enviado, com o status 422. Deu certo, a página recarrega com prescription_request.posted_successfully e o item em provided.
  • 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, quando product.assembly existe;
  • 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 o FormData do formulário inteiro (com sections[]) e adicionado() depois do envio. Fechado, mande só id e quantity, 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.* e receita.*, os mesmos dos temas da plataforma).

O que esses arquivos fazem, para quem prefere escrever o seu:

  • Os grupos da montagem (product.assembly.groups[], com code, name, required, multiple e products[], cada produto com variants[], price, available e prescription_range) viram campos components[<code>][id] (a variante escolhida) ou components[<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 snippet kit-campos-da-receita (os mesmos da página de envio do Pilar), com prescription[later]=1 para "mando depois" (só quando shop.prescriptions.later_allowed). A foto ou o PDF é enviado antes por POST /denaryx/arquivos (precisa de JavaScript) e o id devolvido vai em prescription_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, ou nil) 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, para POST /cart/add.js (com sections[] para a gaveta): a resposta é a linha, com components[], prescription_status e prescription. 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 quando product.requires_assembly ou product.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 sem components vale. Com algum complemento escolhido, os grupos required valem (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ó id e quantity. 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 com cart_without_components quando 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] e eyes[left] ({ "right": 2, "left": 1 } em JSON); a quantidade da linha é a soma, e o quantity que vier não conta. O olho com zero caixas fica sem lente. A linha de lente de contato sem eyes é recusada com 422 e code: "eyes_required", e eyes em outra linha também.
  • O grau é obrigatório na compra (é ele que escolhe a caixa): prescription[type]=contact_lens e prescription[right][sphere], [cylinder] e [axis] (na tórica), [add] (na multifocal), e, opcionais, [base_curve], [diameter] e prescription[lens_brand] (a lente receitada). Não há "mando depois" nem DP. Sem o grau, 422 com code: "prescription_required". A foto ou o PDF da receita é opcional, por prescription_file, como no configurador.
  • A caixa tem a faixa em variant.prescription_range, com base_curve e diameter (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 }, ou nil) traz as caixas de cada olho; o snippet kit-complementos-da-linha mostra "Caixas: OD 2 · OE 1". A quantidade da linha não muda pelo /cart/change.js (a API recusa com code: "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] e power[left], o id da variante ({ "right": "<id>", "left": "<id>" } em JSON), com as caixas em eyes[right] e eyes[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 no items[]. O olho com caixas e sem grau é recusado com 422. Mostre todos os graus, com o esgotado (variant.available falso) 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 de shop.prescriptions.declaration, sem mudar (é o texto que o pedido guarda). Sem ela, 409 com code: "prescription_required". A foto ou o PDF da receita continua opcional, em prescription_file, e vai só na primeira linha. Não mande prescription[type] nem o grau digitado.
  • No carrinho, line_item.prescription_status é declared na linha com a declaração; o snippet kit-complementos-da-linha mostra "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:

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

  1. 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).

  1. 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, erro e os de serve: estreita, na_medida, larga, pela_iris, pela_dp e de_frente). Copie o bloco provador do pt-BR.default.json de 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.
  2. 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-foto e provador-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 .provador e mostre-o com a marca com-js no <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> com data-tela-cheia na 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 com object-fit: cover e object-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 pelo order: 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.

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) e store_notice (o texto que a loja acrescenta embaixo, vazio sem ele; saia com escape).
  • 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 (2d ou 3d), width_mm, adjustments (vertical_offset_mm, tilt_degrees, size_factor), assets (front e side com url, width e height; model com url e triangles), hinge e color (a altura da dobradiça e a cor do aro, para a haste genérica do 2,5D) e variants, 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. similar sã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 tem id, title, handle, url (a página dele na variante experimentada), variant_id, variant_title, price e compare_at_price (em centavos), featured_image (a foto da variante ou a capa; null sem foto) e os campos do arquivo da variante (type, width_mm, adjustments, assets, hinge e color). 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, o product.handle; armacao, o product.try_on | json inteiro; e textos, 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, quando product.try_on.similar nã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, com data-nome, data-preco (o preço já formatado) e data-endereco (o url); 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 o change: 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 para assets/ nem o carregue de outro endereço: ele muda com a plataforma.
  • Os eventos: o script dispara denaryx:provador no document, e o carregador da plataforma publica try_on_opened, try_on_mode e try_on_photo_saved no dataLayer e nas ferramentas de medição, com o item da variante e nada do rosto. O clique em "Ver esta armação" vira o product_clicked (o select_item do 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:

RotaO 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/arquivosEnvia 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.jsEsvazia 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/inscreverO cadastro de e-mail (contact[email]), com Accept: application/json para a resposta em JSON.
GET <página>?sections=a,bO HTML das seções pedidas da página, em JSON (até 20).
GET <página>?section_id=aO 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 tema2.000
Arquivo de código (Liquid, JSON)256 KB
Arquivo de assets/10 MB
Tema inteiro50 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ágina4 MB
{% render %} dentro de {% render %}8 níveis
Blocos dentro de blocos8 níveis
Temas na biblioteca da loja20
Links de prévia ativos20
Arquivo enviado pelo comprador10 MB
Arquivos enviados por carrinho, ainda sem pedido5
Arquivos por linha do carrinho3

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

  1. 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.
  2. 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.
  3. 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 sem content_for_header ou content_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.
  4. Prévia ao lado. A prévia do editor recarrega a cada gravação, sem perder a rolagem.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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).