Skip to content

Objetos Liquid ​

Contexto Liquid: contrato de dados do Storefront V3 ​

Dados comerciais (shop, catalog, content, page, category, product) são fornecidos pela aplicação em tempo de renderização. Eles não são arquivos nem configuração do tema. theme e section são metadados de renderização. Um objeto específico, como category ou product, pode não existir fora de seu contexto de página; teste sua presença quando um component for compartilhado.

Toda página recebe o mesmo contexto global:

Objetos comuns ​

js
shop = {
  id,
  name,
  seo: { description, keywords },
  customerPolicy: { individual: boolean, company: boolean },
  logo: null | {
    url,
    width,
    height,
    alt
  },
  favicon: null | {
    url,
    type
  }
}

catalog = {
  categories: category[]
}

content = {
  pages: [{ id, title, slug }]
}

theme = {
  id,
  revision,
  assets: {
    css,
    js
  }
}

section = {
  id,
  type
}

shop.customerPolicy informa os tipos de comprador habilitados no painel: individual para CPF e company para CNPJ. São permissões comerciais, não settings visuais. Os temas oficiais limitam as opções do cadastro/perfil e identificam o campo de documento do checkout conforme essas flags. Com ambos desabilitados, não oferecem finalização de compra. O servidor revalida cadastro, perfil e novos pedidos sob transação, inclusive visitantes e o tipo da conta autenticada. Apenas ocultar campos no Liquid não substitui essa validação. Replays de pedidos já concluídos permanecem consultáveis. Temas já publicados recebem a nova apresentação somente ao publicar uma nova revisão; a proteção no backend vale imediatamente.

SEO no HTML ​

shop.seo = { description, keywords } contém a descrição e as palavras-chave cadastradas no painel administrativo. São fatos comerciais, sem configuração paralela no tema. O renderer entrega page.seo com title, description, keywords, image, robots e canonical. Título e descrição específicos de produto, categoria ou página institucional têm precedência; valores vazios usam os da loja. Produtos também usam suas palavras-chave cadastradas quando disponíveis.

A plataforma gera no <head> o título, meta description, meta keywords, Open Graph (og:title, og:description, og:site_name, og:locale, og:type, og:url, og:image, og:image:alt) e Twitter Cards (twitter:card, twitter:title, twitter:description, twitter:image). A imagem da página usa a imagem principal do produto ou a capa da categoria; nas demais páginas, usa a primeira imagem do conteúdo principal e, na ausência dela, a logo da loja. As URLs de imagem são absolutas no HTML público. Campos vazios são omitidos e os valores são escapados. Tags gerenciadas pelo renderer são substituídas sem duplicatas, inclusive nas revisões já instaladas; o tema continua controlando livremente a estrutura e a aparência do corpo da página.

A URL canônica pública usa a origem validada da requisição e seu caminho, preservando page/perPage válidos e removendo rastreamento. Não redireciona entre domínios da mesma loja. Busca, carrinho, checkout, confirmação, conta e prévias recebem noindex,nofollow e não publicam canonical ou og:url. O cadastro de metatags/scripts arbitrários do legado não é executado por esse contrato. As tags básicas são geradas no HTML inicial, sem depender de JavaScript.

catalog.categories e content.pages são dados disponíveis ao tema. A maneira como esses dados aparecem na interface é inteiramente responsabilidade dos arquivos Liquid e CSS. Não existe sistema obrigatório de menu ou navegação no Theme V3, nem é preciso criar registros de menu, navigation.json, menu.json ou configurações de header e footer.

No servidor de desenvolvimento, uma origem STOREFRONT_DEVELOPMENT_ORIGIN configurada permite que Visitar loja abra a revisão publicada da loja do painel nessa origem. A entrada __storefront_host seleciona um domínio público validado e um cookie mantém a seleção nas páginas seguintes, com nova validação a cada requisição. Os caminhos Liquid continuam normais (/produto, /categoria etc.), usando storefront_url; não acrescente parâmetros ou domínios locais ao tema. Essa seleção é exclusiva do servidor local e do painel de testes (admin-teste.lojavirtual.com.br) e é ignorada na produção.

shop.logo é a identidade global da loja, compartilhada com o legado por loja.fk_id_imagem_logomarca → imagem.id_imagem. Ela não é um asset do tema, não deve ser copiada para a revisão nem substituída por uma imagem gerada pela IA. O tema controla somente sua apresentação (posição, tamanho, margens e layout) e deve usar shop.name como fallback quando shop.logo for null.

shop.favicon é a identidade global equivalente para a aba do navegador, compartilhada com o legado por loja.fk_id_imagem_favicon → imagem.id_imagem. A plataforma injeta o <link rel="icon"> correspondente no <head> de toda página, inclusive em revisões já publicadas, substituindo ícones declarados pelo tema. O tema não deve embutir um favicon próprio nem copiá-lo para a revisão. O lojista envia o arquivo (PNG, ICO, JPEG ou WebP, até 1 MB) no cartão “Favicon da loja” do editor de temas; a troca fica pendente na prévia e entra na loja somente ao Publicar, na mesma operação atômica da revisão e da logo. O editor também pode gerar o favicon a partir da logo. Se a edição por IA falhar, o servidor recorta a própria logo localmente e informa esse resultado no editor; nenhuma das duas opções publica o favicon automaticamente. Na preparação inicial, se a loja ainda não tiver favicon, a IA gera um símbolo quadrado próprio em paralelo à logo, sem aguardar seus bytes. O resultado usa o mesmo vínculo global do favicon, com validação e variações do armazenamento legado. Um favicon já cadastrado ou enviado durante a geração sempre prevalece.

O manifesto pode declarar um contrato opcional e explícito de cores de marca:

json
{
  "branding": {
    "colors": {
      "primary": "--color-primary",
      "primaryHover": "--color-primary-hover",
      "primaryContrast": "--color-primary-contrast",
      "accent": "--color-accent"
    }
  }
}

Cada chave semântica autoriza somente a variável CSS indicada. A aplicação da paleta modifica assets/theme.css no rascunho atual, incluindo alterações já presentes nesse arquivo; não publica a revisão e não procura cores arbitrárias por todo o CSS. A paleta sugerida é extraída localmente e de forma determinística com sharp, sem IA. A troca de logo no editor permanece no rascunho em memória junto com as alterações de layout e cores, até Publicar. A prévia usa os bytes validados, sem gravar imagem no cadastro ou no storage legado. Na publicação, a plataforma grava a nova imagem e ativa sua referência global junto com a revisão do tema, em uma operação atômica. Conflito na revisão ou na logo atual recusa ambas. Trocar/restaurar um tema mantém a logo global atual: a logo não integra bundles, ZIPs ou o histórico visual. Descartar a troca pendente preserva a logo publicada.

Revisões anteriores que não declaram branding.colors continuam renderizáveis, mas recusam a aplicação automática com uma mensagem funcional; restaurar ou atualizar para uma revisão oficial habilita o contrato. O editor também pode extrair a paleta da logo global já cadastrada, sem exigir um novo upload. A URL pública de shop.logo usa a variação média do armazenamento legado para reduzir o peso nos headers, enquanto a original permanece preservada.

Um tema pode usar todas as categorias no header, selecionar apenas algumas, colocar as páginas no footer ou ignorar ambos os conjuntos. Por exemplo, um pedido como "coloque Quem Somos no menu" pode ser atendido alterando components/header.liquid para percorrer ou selecionar itens de content.pages. Da mesma forma, todas as páginas institucionais podem ir para o rodapé apenas alterando components/footer.liquid.

Categoria ​

js
category = {
  id,
  parentId,
  name,
  slug,
  url,
  description,
  featured,
  position,
  image,
  banner,
  children: category[]
}

category.image é a imagem de capa que representa a categoria em vitrines, cards e menus. Quando o tema usar uma imagem de categoria, priorize category.image; recorra a uma foto de produto ou asset editorial somente se a capa estiver ausente. A capa é carregada independentemente da presença de banner. category.banner é o banner exibido no topo da página da própria categoria. Ambos seguem o formato image descrito na seção de produto, são cadastrados pelo lojista no painel de categorias e podem ser null. Um não substitui o outro: sem capa, o tema decide a alternativa (inicial, foto de um produto da seleção ou imagem editorial do bundle). O banner costuma ser largo e a capa quadrada; para o banner prefira banner.originalUrl, pois url é a variação média. Os dois estão disponíveis em catalog.categories (inclusive em children) e no objeto category da página de categoria. Em product.categories, image e banner são sempre null: localize a categoria pelo id em catalog.categories quando precisar da imagem. Os produtos de page.products na Home carregam suas categorias ativas da mesma loja. Assim, o Aura pode usar uma foto de produto vinculado quando a categoria ainda não tem capa, sem novas consultas por card e sem confundir banner com capa.

category.url é o caminho interno canônico produzido pela plataforma, sem o prefixo da prévia. Use {{ category.url | storefront_url }} em qualquer link de categoria, inclusive menus, vitrines e categorias relacionadas ao produto. Não concatene slug no Liquid nem aplique encoding novamente. slug preserva o dado legado e pode ser null ou vazio; não é o destino de navegação.

Slugs utilizáveis e únicos entre categorias ativas da mesma loja mantêm a URL amigável com encoding de segmento. Ausência, ambiguidade pela collation do banco, caminhos inseguros, slugs com /, ? ou # e identificadores que a rota interpreta como números usam /categoria/{id}. Isso evita depender da interpretação de delimitadores codificados em proxies. Espaços, acentos e % continuam usando encoding normal. O resolvedor mantém o isolamento por loja e não escolhe uma categoria arbitrária quando o slug é ambíguo. Breadcrumbs e paginação já recebem URLs prontas, incluindo o prefixo da prévia quando aplicável.

Na árvore de catalog.categories, children contém as subcategorias. No contexto da página de categoria, o objeto raiz category acrescenta:

js
category.products = product[]
category.pagination = {
  page,
  perPage,
  total,
  totalPages,
  hasPreviousPage,
  hasNextPage,
  previousUrl,
  nextUrl
}

Esses campos são saída do servidor. page vem da URL e assume 1; perPage vem do parâmetro válido da requisição ou da constante DEFAULT_CATEGORY_PER_PAGE em src/modules/storefront/product-selection.js, atualmente 24 e com máximo 100; total é a contagem da mesma seleção autorizada; os demais campos são derivados desses valores. Entradas inválidas retornam 400 e não são salvas no tema. Página além do fim retorna 404; categoria vazia na primeira página retorna totalPages: 0. Os links são relativos e preservam um perPage não padrão.

No preview navegável, filtros e paginação usam GET dentro do mesmo namespace temporário e mantêm o rascunho ao trocar de página.

Produto ​

js
product = {
  id,
  name,
  slug,
  url,
  shortDescription,
  description,
  descriptionIsHtml,
  brand,
  model,
  specifications,
  price,
  compareAtPrice,
  priceRange,
  hasVariablePrices,
  hasVariants,
  variantsLoaded,
  requiresSelection,
  salesState,
  visible,
  priceVisible,
  purchasable,
  inStock,
  stock,
  lowStock,
  minimumQuantity,
  maximumQuantity,
  mainImage,
  images,
  video,
  categories,
  optionGroups,
  variants,
};

product.url é o caminho interno canônico do produto, sem o prefixo da prévia. Use {{ product.url | storefront_url }} em cards, vitrines, carrinho (item.product.url) e demais links de produto. Não concatene slug no Liquid nem aplique encoding novamente. Quando o slug do cadastro está ausente, contém /, ?, # ou é formado só por dígitos (que a rota interpreta como ID), a plataforma usa /produto/{id}, que abre o mesmo produto da loja. Nesses casos, product.slug também recebe o ID, pois é sempre o identificador usado em product.url; ele nunca é null para produtos da vitrine.

product.specifications é uma lista de { label, value } com as informações preenchidas no cadastro: marca, modelo, garantia e entrega. Os títulos respeitam as personalizações do painel; títulos vazios ou placeholders históricos usam o nome padrão. Valores vazios são omitidos. São textos escapados pelo Liquid, não HTML nem opções de variante. Os temas oficiais exibem a lista junto à descrição curta do produto. O campo de entrega é informativo e não substitui a cotação por CEP.

Quando descriptionIsHtml for verdadeiro, description já foi sanitizada pela aplicação e pode usar saída sem escape no Liquid. Quando for falso, o tema deve manter a saída escapada.

Valores monetários são { amount, currency }, com amount em centavos, ou null quando o preço não pode ser exibido. Use o filtro Liquid money, por exemplo {{ product.price | money }}, e respeite priceVisible e purchasable. price é o menor preço do primeiro conjunto não vazio nesta ordem: itens com estoque físico, itens compráveis por venda sem estoque e itens válidos esgotados. A quantidade mínima permanece uma condição separada de purchasable. Filtros e ordenação usam a mesma prioridade. priceRange = { minimum, maximum } descreve esse conjunto e hasVariablePrices autoriza o texto “A partir de”. Em uma combinação, price já é o preço unitário final (preço-base + ajuste, somados antes do arredondamento): Liquid e navegador nunca devem somá-lo. compareAtPrice é null em produtos com variações porque o legado não fornece preço anterior confiável por combinação.

hasVariants considera tanto os indicadores quanto os relacionamentos comerciais carregados. No contrato V3 os estados são inseparáveis: hasVariants: true sempre implica requiresSelection: true, e hasVariants: false sempre implica requiresSelection: false. Produto variável usa exclusivamente variantes como unidades compráveis; produto simples usa exclusivamente o pai. A antiga flg_subproduto_obrigatorio não é exposta nem decide esse contrato. variantsLoaded distingue o resumo (false, com optionGroups e variants vazios) do detalhe completo (true). Resumos de home, categoria e dataSources já usam combinações reais para preço e disponibilidade sem enviar todas as combinações a cada card.

inStock significa saldo físico positivo pertinente; purchasable também considera estado comercial, venda sem estoque e quantidade mínima. Saldos negativos persistidos pelos fluxos legados significam produto esgotado e são apresentados como stock: 0. Nas combinações, loja_link_subproduto.num_link_estoque = NULL também representa saldo zero: a variante permanece válida, com inStock: false, e só permite compra se a venda sem estoque estiver habilitada e as demais regras comerciais forem atendidas. Essa interpretação não se aplica ao estoque obrigatório do produto simples nem a campos ausentes ou formatos ilegíveis, que continuam interrompendo a normalização em vez de virarem mínimo 1 ou estoque zero. maximumQuantity é o estoque pertinente quando a venda sem estoque não é permitida, e null quando essa camada não deriva um limite. Antes da escolha em todo produto variável, stock e maximumQuantity também são null: isso significa limite ainda não determinado, não estoque ilimitado. A variante retornada pela resolução contém o limite específico. Esses campos são informativos e não reservam estoque; uma futura operação de carrinho deverá revalidá-los no servidor.

Neste MVP, a política central combina o estado individual flg_estado_produto com loja.flg_habilita_precos_loja, loja.flg_ocultar_precos, loja.flg_mostrar_preco_login e loja.flg_preco_apos_login. Quando a loja exige login, somente uma sessão de comprador válida da própria loja autoriza preço: price, compareAtPrice e preços de variações ficam null para visitantes na home, categoria, detalhe e vitrines. Uma vitrine monetária anônima recebe status: 'restricted' sem consultar produtos; uma vitrine não monetária pode listar os produtos com preços ausentes. Após o login, a mesma política libera preços, filtros monetários, variantes, carrinho e checkout. Ocultação global e restrições individuais continuam valendo. O backend resolve a sessão antes de calcular preços, com isolamento por loja e cache privado; flags do navegador e o DTO viewer não autorizam valores. Prévia/editor não usam a sessão do comprador e permanecem restritos. Não há preço personalizado por cliente. Falha de banco continua sendo erro; não é convertida em vitrine vazia ou restrita.

Depois que a IA gera uma proposta para a Home, o laboratório conserva em memória no navegador a lista de arquivos retornada. Trocas entre os contextos suportados e páginas da categoria enviam esses mesmos arquivos ao endpoint autenticado de reapresentação; o servidor reaplica as permissões do editor, limites de tamanho e validação completa sobre uma única fotografia do tema de desenvolvimento. Produtos, preços e identidade continuam sendo carregados no servidor, e o rascunho não é publicado nem gravado no tema padrão.

O rascunho navegável aceita até 30 arquivos. Arquivos textuais mantêm o limite individual de 1 MiB, imagens WebP geradas podem ter até 1,5 MiB depois da decodificação, e a requisição serializada completa fica limitada a 8 MiB em UTF-8. O Base64, a estrutura JSON, os caminhos e os caracteres escapados contam no limite total. A proposta da IA passa pela mesma medição antes de ser oferecida como rascunho navegável; excesso retorna HTTP 413 sem registrar o conteúdo dos arquivos.

js
image = {
  id,
  url,
  thumbUrl,
  originalUrl,
  name,
  type,
  width,
  height,
};

optionGroup = {
  id,
  name,
  options: [{ id, name, image }],
};

variant = {
  id,
  optionIds,
  price,
  compareAtPrice,
  stock,
  lowStock,
  active,
  inStock,
  purchasable,
  minimumQuantity,
  maximumQuantity,
};

product.video é null ou { url, provider: 'youtube', id, embedUrl, thumbnailUrl }, derivado do link do YouTube cadastrado no produto pelo painel (loja_produto.desc_link_video_produto). embedUrl aponta para o player youtube-nocookie.com e thumbnailUrl para a miniatura oficial do vídeo. Templates Liquid não podem conter <iframe>: os temas oficiais apresentam o vídeo como uma miniatura na galeria (data-gallery-video) e o runtime da plataforma insere o player no container data-gallery-media quando o comprador escolhe essa miniatura, devolvendo a foto ao escolher outra imagem ou opção. Links que não são do YouTube resultam em null e não interrompem a página.

mainImage pode ser null. Na listagem da home, categoria e vitrines, images, optionGroups e variants são arrays vazios e variantsLoaded é falso; o resumo comercial calculado no servidor permanece disponível nos campos do produto. Os dados completos aparecem apenas no contexto de produto. Opções inativas e combinações incoerentes nunca são escolhas comerciais válidas, e a ausência de uma combinação é diferente de uma combinação existente com inStock: false.

Alerta de estoque baixo — Está Acabando ​

product.lowStock e variant.lowStock são sinais booleanos calculados nativamente pelo Node, sem ativação ou consulta a aplicativo. A presença do aviso nos arquivos Liquid define se o tema apresenta esse recurso. O limite vem de loja_produto.num_estoque_minimo para produto simples e de loja_link_subproduto.num_link_estoque_critico para a combinação selecionada. Esses booleanos preservam o limite comercial cadastrado. Não dependem do antigo aplicativo “Está Acabando” do admin_loja. O tema pode usar esse sinal pronto ou definir seu próprio limite de apresentação comparando o saldo real disponível.

Para um produto simples, por exemplo, a IA pode exibir o aviso somente entre uma e cinco unidades, sem configuração adicional no admin:

liquid
{% if product.requiresSelection == false and product.visible and product.priceVisible and product.purchasable and product.stock != nil and product.stock > 0 and product.stock <= 5 %}
  <p>Está acabando! Últimas unidades.</p>
{% endif %}

O número 5, o texto e o CSS pertencem ao tema. stock é o saldo físico, não o limite de compra. Não apresente escassez para estoque zero com venda por encomenda, preço oculto, compra indisponível ou produto variável sem seleção. Essa condição muda somente a apresentação, nunca a reserva ou as validações de estoque.

O alerta exige limite positivo, saldo real maior que zero e menor ou igual ao limite, produto visível, compra permitida e preço público. Saldo insuficiente para o mínimo de compra sem venda por encomenda não gera urgência. Limite ausente, zero ou negativo desativa o aviso; formato inválido é registrado e omite o aviso sem interromper a compra. Venda sem estoque com saldo zero também não gera aviso. Com saldo positivo, a regra continua indicando somente o estoque físico baixo.

Produto variável mantém product.lowStock = false antes da seleção; cada variante usa exclusivamente seu próprio limite, sem herdar o do pai. Produtos simples também fornecem o sinal nos resumos de home, categoria e vitrines; cada tema decide se o utiliza. Não inferir escassez agregando estoques de variantes nos cards. Nenhum saldo, maximumQuantity, validação do carrinho ou checkout é substituído pelo limite crítico. Isso difere explicitamente do V2, que podia apresentar o limite crítico no lugar de um saldo real maior. O aviso não reserva estoque nem faz atualização contínua de saldo enquanto a página permanece aberta.

O tema escolhe texto, posição e CSS. Use um elemento opcional dentro de [data-product-detail], mantendo-o presente e oculto quando o sinal for falso:

liquid
<p data-product-low-stock role="status" aria-live="polite"{% unless product.lowStock %} hidden{% endunless %}>Últimas unidades</p>

Em cada [data-product-variant], declare data-low-stock="{% if variant.lowStock %}true{% else %}false{% endif %}". O runtime compartilhado atualiza apenas hidden, preservando o texto do tema; seleção incompleta, inválida ou esgotada oculta o aviso. Para adotar um limite visual próprio em produtos variáveis, calcule o booleano em Liquid usando variant.stock > 0 and variant.stock <= 5, também exigindo variant.purchasable e product.priceVisible, e use o resultado em data-low-stock de cada opção [data-product-variant]. Preserve os demais atributos funcionais. Mantenha [data-product-low-stock] inicialmente oculto até uma combinação ser selecionada; o runtime usa o sinal daquela combinação, sem somar estoques. Aura e Default exibem o aviso próximo à compra; outros temas podem adotar o mesmo hook sem copiar lógica funcional. Revisões publicadas recebem a apresentação somente em uma nova revisão explícita.

Página e contextos ​

Home

js
page = {
  type: 'home',
  products: product[]
}

Além de page, a home recebe o contexto global (shop, catalog e content).

Categoria recebe o contexto global, page = { type: 'category' } e o objeto raiz category, incluindo category.products e category.pagination.

Categoria e busca recebem também listing, a fonte compartilhada de produtos, total, ordenação, facets reais, filtros ativos com URLs de remoção e paginação que preserva a query. category.products e category.pagination são aliases de compatibilidade. Opções de variante possuem value opaco gerado pelo servidor; Liquid somente o devolve em option=<valor>. Filtros usam formulários GET e não devem recalcular regras comerciais.

Nos temas oficiais (Aura, Lumen, Brisa, Fluxo, Nova, Cobalto, Pulso e Vértice), components/listing_results.liquid compõe uma barra com Filtrar, total e ordenação. O details.listing-filters começa sem open em todas as larguras, inclusive sem JavaScript. Seu summary abre um painel ancorado na barra, com grupos de filtros e rolagem limitada à altura da janela. No celular, a ordenação ocupa outra linha e os grupos ficam em uma coluna. Não é um modal: o foco não fica preso no painel. O JavaScript visual fecha ao clicar ou mover o foco para fora; Escape fecha e devolve o foco ao acionador. Sem JavaScript, abrir/fechar e enviar os formulários continuam nativos; Escape e fechamento externo são melhorias do JavaScript.

Os filtros ativos ficam fora do painel recolhido, com remoção individual e Limpar filtros. O acionador mostra sua quantidade. Ao ordenar, preserve filtros, termo de busca e tamanho da página; ao filtrar, preserve ordenação, termo e tamanho da página. Ambos omitem page para reiniciar a paginação. Use as URLs prontas de remoção, limpeza e paginação, incluindo o namespace da prévia. listing_filters.liquid só apresenta preço quando autorizado pelos dados, marcas e variantes disponíveis, além do controle existente de disponibilidade.

Essa apresentação é uma escolha dos bundles oficiais, não uma estrutura visual obrigatória do V3. Temas externos podem usar outras composições com o mesmo contrato comercial. Não há novo setting, endpoint ou hook no runtime compartilhado. Lojas já publicadas recebem essas mudanças apenas por uma nova revisão explícita.

Busca usa o template semântico search e recebe page = { type: 'search' }, search = { term } e listing. A rota pública é /busca?q=...; termo vazio não carrega o catálogo e páginas de busca usam noindex,follow.

O laboratório e o editor oferecem Busca como contexto de prévia. O formulário navega por GET dentro da sessão temporária e preserva os mesmos arquivos ainda não publicados usados nos demais contextos.

Produto recebe o contexto global, page = { type: 'product' } e o objeto raiz product, incluindo galeria (images), optionGroups e variants.

Página institucional recebe o contexto global e:

js
page = {
  type: 'static',
  id,
  title,
  slug,
  content,
  contentIsHtml,
  seo: {
    title,
    description,
  },
};

O conteúdo editorial marcado pela aplicação como HTML é previamente sanitizado. Somente use saída sem escape para page.content quando page.contentIsHtml for verdadeiro; os demais valores devem permanecer escapados.

Temas V3 · Liquid estrutura, CSS desenha, JSON compõe.