Honne
Back to Theme Editor

Sistema de Temas — Honne

Motor de temas LiquidJS para páginas públicas de usuários. Temas são compostos por templates Liquid, seções, snippets, assets (CSS/JS) e um arquivo de configuração (schema.json) que define as settings editáveis pelo usuário no editor visual.


⬇ Download do Tema Base

Baixar Tema Base (ZIP)

O ZIP contém a estrutura completa do tema base com todos os arquivos Liquid, CSS, JS e configuração. Use como ponto de partida para criar temas customizados.


Índice


Arquitetura

honne/
└── themes/
    └── base/                     ← Tema padrão (fallback)
        ├── layout/
        │   └── theme.liquid      ← HTML shell (head, CSS, footer)
        ├── templates/
        │   ├── profile.liquid    ← Padrão (links + social + player)
        │   ├── portfolio.liquid  ← Currículo/portfólio
        │   └── business.liquid   ← Perfil empresarial
        ├── sections/             ← Componentes reutilizáveis
        │   ├── header.liquid
        │   ├── links.liquid
        │   ├── social.liquid
        │   ├── player.liquid
        │   ├── featured-video.liquid
        │   ├── countdown.liquid
        │   ├── footer.liquid
        │   ├── language-selector.liquid
        │   ├── business-header.liquid
        │   ├── business-hours.liquid
        │   ├── business-location.liquid
        │   ├── business-testimonials.liquid
        │   ├── business-contact.liquid
        │   ├── portfolio-header.liquid
        │   ├── portfolio-skills.liquid
        │   ├── portfolio-projects.liquid
        │   └── portfolio-hire.liquid
        ├── snippets/             ← Partials pequenos
        │   └── link-button.liquid
        ├── assets/               ← Estáticos públicos
        │   ├── theme.css
        │   └── theme.js
        └── config/
            ├── schema.json       ← Define settings + grupos (UI do editor)
            └── settings.json     ← Valores padrão de todas as settings

Estrutura de Diretórios

meu-tema/
├── layout/
│   └── theme.liquid        ← Obrigatório. HTML shell com {{ content }}
├── templates/
│   ├── profile.liquid      ← Obrigatório. Template principal
│   ├── portfolio.liquid    ← Opcional. fallback: profile.liquid
│   └── business.liquid     ← Opcional. fallback: profile.liquid
├── sections/               ← Opcional. Componentes usados via {% render %}
│   └── *.liquid
├── snippets/               ← Opcional. Partials usados via {% render %}
│   └── *.liquid
├── assets/                 ← Opcional. Servidos em /theme-assets/base/*
│   ├── theme.css
│   └── theme.js
└── config/
    ├── schema.json          ← Opcional. Define settings editáveis
    └── settings.json        ← Opcional. Valores padrão

Fluxo de Render

Request /:username
  ↓
Profile Controller (apps/web/src/controllers/profile.ts)
  ├── Busca user no DB (username)
  ├── Busca links ativos (linkQueries.listLinks)
  ├── Carrega settings padrão (themes/base/config/settings.json)
  ├── Merge com user.theme (JSONB da tabela users)
  │   { ...baseSettings, ...userTheme }
  └── Chama renderUserPage(rawUser, mergedSettings)
        ↓
Theme Engine (packages/theme-engine/src/index.ts)
  ├── normalizeUser(rawUser)
  │   ├── Filtra links (enabled, scheduled_at, expires_at)
  │   ├── Ordena links por position
  │   ├── Enriquece socials com _icon_slug (mapeia twitter→x)
  │   └── Retorna NormalizedUser
  ├── parseEmbedUrl(player_url) → player (ou null)
  ├── parseEmbedUrl(featured_video_url) → featured_video (ou null)
  ├── Se featured_video_autoplay → adiciona ?autoplay=1 na embedUrl
  ├── Monta contexto Liquid:
  │   { user, links, player, featured_video, theme, settings, socials }
  ├── Renderiza template conforme profile_mode:
  │   "profile"   → templates/profile.liquid
  │   "portfolio" → templates/portfolio.liquid
  │   "business"  → templates/business.liquid
  ├── Render interno: {{ content }} dentro de layout/theme.liquid
  │   ↓
  │   layout/theme.liquid
  │     ├── Meta tags (OG, theme-color, viewport)
  │     ├── Google Fonts (font_family da setting)
  │     ├── Tailwind CDN (config dinâmica com cores do tema)
  │     ├── CSS inline com variáveis CSS (:root { --bg, --text, ... })
  │     ├── CSS custom ({{ settings.custom_css }})
  │     ├── <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
  │     ├── {{ content }} → template renderizado
  │     └── <script src="{{ 'theme.js' | asset_url }}"></script>
  ├── Cache LRU (template parsed + HTML output)
  └── Retorna HTML final

Variáveis do Contexto Liquid

Todas as variáveis abaixo estão disponíveis em qualquer template, section ou snippet do tema.

Objeto user

Variável Tipo Descrição
{{ user.name }} string Nome do perfil
{{ user.username }} string Username (identificador único)
{{ user.bio }} string Biografia
{{ user.avatarUrl }} string URL do avatar
{{ user._id }} string ID interno do usuário
{{ user.links }} array Links filtrados + ordenados (mesmo que {{ links }})
{{ user.socials }} array Redes sociais (mesmo que {{ socials }})
{{ user.theme.mode }} string "dark" ou "light"
{{ user.theme.accent }} string Cor de destaque (ex: "#ffffff")
{{ user.theme.font }} string Fonte preferida (ex: "Inter")
{{ user.settings.publicProfile }} boolean Perfil público?
{{ user.settings.discoverable }} boolean Descobertas?
{{ user.settings.allowIndexing }} boolean Indexação por buscadores?
{{ user.language }} string Idioma do usuário (ex: "pt-BR")

Objeto settings

Contém o merge de settings.json + user.theme (JSONB do banco). Valores do usuário sobrescrevem os defaults.

Variável Tipo Default Descrição
{{ settings.primary_color }} string #8b5cf6 Cor primária (botões, destaques)
{{ settings.background_color }} string #09090b Cor de fundo
{{ settings.card_background }} string #1a1a2e Fundo dos cards/botões
{{ settings.text_color }} string #fafafa Cor do texto
{{ settings.muted_color }} string #a1a1aa Cor secundária/muted
{{ settings.font_family }} string Inter Fonte principal (Google Fonts)
{{ settings.button_radius }} number 16 Raio das bordas dos botões
{{ settings.button_shadow }} boolean true Sombra nos botões
{{ settings.show_socials }} boolean true Mostrar redes sociais
{{ settings.social_position }} string bottom "top" ou "bottom"
{{ settings.social_style }} string icon "icon", "label" ou "both"
{{ settings.show_footer }} boolean true Mostrar "Powered by Honne"
{{ settings.hero_title }} string "" Título custom (vazio = nome do user)
{{ settings.hero_subtitle }} string "" Subtítulo do header
{{ settings.logo_image }} string "" URL da logo/avatar custom
{{ settings.show_player }} boolean false Mostrar player de música
{{ settings.player_url }} string "" URL Spotify/SoundCloud
{{ settings.player_position }} string top "top", "before_links", "after_links", "bottom"
{{ settings.show_featured_video }} boolean false Mostrar vídeo em destaque
{{ settings.featured_video_url }} string "" URL YouTube/Vimeo
{{ settings.featured_video_position }} string top "top", "before_links", "after_links", "bottom"
{{ settings.featured_video_autoplay }} boolean false Autoplay do vídeo
{{ settings.show_countdown }} boolean false Mostrar contador regressivo
{{ settings.countdown_date }} string "" Data alvo (ISO: 2027-12-25T00:00:00)
{{ settings.countdown_message }} string "Lançamento em" Texto antes do timer
{{ settings.countdown_expired_message }} string "Já disponível!" Texto após expirar
{{ settings.profile_mode }} string "profile" "profile", "portfolio" ou "business"
{{ settings.business_name }} string "" Nome da empresa
{{ settings.business_address }} string "" Endereço da empresa
{{ settings.business_phone }} string "" Telefone
{{ settings.business_whatsapp }} string "" WhatsApp (5511999999999)
{{ settings.business_hours }} string "" Horários (multilinha)
{{ settings.show_testimonials }} boolean false Mostrar depoimentos
{{ settings.business_testimonials }} string "" Depoimentos (formato Nome|estrelas|texto)
{{ settings.portfolio_title }} string "" Cargo no portfólio
{{ settings.skills }} string "" Habilidades (separadas por vírgula)
{{ settings.hire_me_text }} string "Contrate-me" Texto do botão de contratação
{{ settings.hire_me_url }} string "" URL do botão de contratação
{{ settings.card_bg }} string #1a1a2e Fundo do cartão digital
{{ settings.card_text }} string #fafafa Texto do cartão digital
{{ settings.custom_css }} string "" CSS custom injetado no <style>
{{ settings.publicProfile }} boolean true (injetado pelo engine)

Objeto theme

Alias para settings — contém os mesmos valores. Útil para diferenciar settings de tema de outras settings.

{{ theme.primary_color }}   → mesmo que {{ settings.primary_color }}
{{ theme.show_player }}     → mesmo que {{ settings.show_player }}

Objeto player / featured_video

Presentes apenas quando a URL correspondente está preenchida no settings.

Variável Tipo Descrição
{{ player.provider }} string "spotify" ou "soundcloud"
{{ player.id }} string ID do conteúdo (track ID, etc.)
{{ player.embedUrl }} string URL do embed iframe
{{ player.type }} string "music"
{{ featured_video.provider }} string "youtube", "vimeo", "twitch"
{{ featured_video.id }} string ID do vídeo
{{ featured_video.embedUrl }} string URL do embed iframe
{{ featured_video.type }} string "video"

Se a URL não estiver preenchida, a variável é null/false — pode ser usada em condicionais:

{% if player %}
  <iframe src="{{ player.embedUrl }}"></iframe>
{% endif %}

Objeto links

Array de links do usuário, já filtrados (apenas enabled + dentro do agendamento) e ordenados por position.

Cada link tem os campos:

Campo Tipo Descrição
link.id string ID único do link
link.title string Título do link
link.url string URL de destino
link.type string Tipo: "link", "button", "video", "music", "image", "text", "divider", "case"
link.position number Posição de ordenação
link.enabled boolean Ativo? (sempre true após filtro)
link.cta_text string Texto do CTA (botão)
link.cta_color string Cor do CTA (hex)
link.thumbnail string URL da thumbnail (case)
link.embed_url string URL de embed (preenchido pelo engine)
link.embed_provider string Provider do embed: "youtube", "vimeo", "spotify", etc.
link.embed_id string ID do embed
link.embed_type string Tipo do embed: "video", "music", "social"
link.content object Dados específicos do tipo (ex: { description, tags } para case)
link.scheduled_at string Data de agendamento (ISO)
link.expires_at string Data de expiração (ISO)

Objeto socials

Array de redes sociais do usuário.

Campo Tipo Descrição
social.platform string Nome da plataforma: "instagram", "x", "youtube", "linkedin", "github", etc.
social.url string URL do perfil
social._icon_slug string Slug do simple-icons (já mapeado: twitter → x)

Objeto translation / locales / locale

(Funcionalidade futura de internacionalização)


Filtros Custom

Filtro Descrição Exemplo
asset_url Gera URL pública pra asset do tema {{ 'theme.css' | asset_url }} → /theme-assets/base/theme.css
format_date Formata data (futuro) {{ "2027-12-25" | format_date }}
pluralize Pluralização (futuro) {{ 3 | pluralize: "item", "itens" }}

Filtro asset_url

Segurança: sanitiza o path (remove caracteres especiais, bloqueia ..).

<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
<script src="{{ 'theme.js' | asset_url }}"></script>
<img src="{{ 'images/logo.svg' | asset_url }}" alt="Logo">

Tags Custom

Tag {% render %}

Carrega uma section ou snippet do tema.

{% render 'header' %}              ← sections/header.liquid
{% render 'link-button',           ← snippets/link-button.liquid
  title: link.title,
  url: link.url,
  type: link.type %}

A tag procura primeiro em sections/, depois em snippets/.

Named Arguments

Você pode passar variáveis nomeadas para a section/snippet:

{% render 'card',
  title: project.name,
  description: project.bio,
  image: project.thumbnail %}

Escopo de Variáveis

Dentro de uma section/snippet renderizada, as seguintes variáveis estão automaticamente disponíveis sem precisar passar como argumento:

Variável Origem
user Objeto do usuário
links Array de links
socials Array de redes sociais
settings Settings do tema
theme Alias para settings
player Player de música (se configurado)
featured_video Vídeo em destaque (se configurado)

Além disso, qualquer argumento nomeado passado no {% render %} está disponível como variável local.

{% render 'meu-componente', cor: 'azul', tamanho: 'grande' %}

Dentro de meu-componente.liquid:

{{ cor }}      → "azul"
{{ tamanho }}  → "grande"

Sistema de Settings

Definição (schema.json)

O arquivo config/schema.json define todas as settings que o usuário pode editar no editor visual.

Estrutura:

{
  "groups": [
    { "id": "hero", "label": "Hero", "icon": "person", "fields": ["logo_image", "hero_title", "hero_subtitle"] }
  ],
  "settings": [
    { "id": "hero_title", "label": "Título", "type": "text", "default": "", "placeholder": "Ex: Criador de Conteúdo", "info": "Texto principal acima dos links" }
  ]
}

Tipos de Setting

Tipo UI Exemplo schema
color Seletor de cor { "id": "primary_color", "type": "color", "default": "#8b5cf6" }
font Select de fontes { "id": "font_family", "type": "font", "default": "Inter" }
toggle Switch on/off { "id": "show_socials", "type": "toggle", "default": true }
text Input de texto { "id": "hero_title", "type": "text", "default": "", "placeholder": "..." }
textarea Área de texto { "id": "custom_css", "type": "textarea", "default": "" }
range Slider (min-max) { "id": "button_radius", "type": "range", "min": 0, "max": 40, "default": 16 }
select Dropdown { "id": "social_position", "type": "select", "default": "bottom", "options": [{"value": "top", "label": "Topo"}, {"value": "bottom", "label": "Rodapé"}] }
datetime DateTime picker { "id": "countdown_date", "type": "datetime", "default": "" }
image_picker URL de imagem { "id": "logo_image", "type": "image_picker", "default": "" }

Grupos (Tabs no Editor)

Cada grupo define uma seção colapsável no editor visual.

Campo Descrição
id Identificador único do grupo
label Nome exibido no editor
icon Nome do ícone Material Symbols (ex: "person", "palette", "music_note")
fields Array de IDs das settings que pertencem a este grupo

Grupos do tema base:

Grupo Ícone Campos
Hero person logo_image, hero_title, hero_subtitle
Colors palette primary_color, background_color, card_background, text_color, muted_color, card_bg, card_text
Typography text_fields font_family, button_radius, button_shadow
Social share show_socials, social_position, social_style
Footer bottom_panel show_footer
Media music_note show_player, player_url, player_position, show_featured_video, featured_video_url, featured_video_position, featured_video_autoplay
Countdown timer show_countdown, countdown_date, countdown_message, countdown_expired_message
Business business business_name, business_address, business_phone, business_whatsapp, business_hours, show_testimonials, business_testimonials
Portfolio work portfolio_title, skills, hire_me_text, hire_me_url
Advanced tune profile_mode, custom_css

Merge Base + Usuário

  1. Base: themes/base/config/settings.json (valores padrão do tema)
  2. Usuário: users.theme (coluna JSONB no PostgreSQL)
  3. Resultado: { ...baseSettings, ...userTheme }

O usuário pode sobrescrever qualquer setting. Valores não definidos pelo usuário mantêm o default.


Templates

profile.liquid

Template padrão. Fluxo de render:

social (top)     ← se social_position == 'top'
header           ← avatar + nome + bio
player (top)     ← se player_position == 'top'
featured-video (top)  ← se featured_video_position == 'top'
countdown        ← se show_countdown e countdown_date
player (before_links) ← se player_position == 'before_links'
featured-video (before_links)
links            ← lista de links do usuário
player (after_links)  ← se player_position == 'after_links'
featured-video (after_links)
social (!top)    ← se social_position != 'top'
player (bottom)  ← se player_position == 'bottom'
featured-video (bottom)
footer

portfolio.liquid

social (top)
portfolio-header   ← título + avatar + cargo
portfolio-skills   ← lista de habilidades
portfolio-projects ← links do tipo 'case'
portfolio-hire     ← botão "Contrate-me"
social (!top)
footer

business.liquid

social (top)
business-header    ← nome da empresa + avatar
player/video (top)
business-hours     ← horários
business-location  ← endereço + Google Maps
countdown
player/video (before_links)
links
player/video (after_links)
business-testimonials ← depoimentos
business-contact   ← WhatsApp + telefone
social (!top)
player/video (bottom)
footer

Sections

header.liquid / portfolio-header.liquid / business-header.liquid

Renderizam o topo da página:

{% if settings.logo_image %}
  <img src="{{ settings.logo_image }}">
{% elsif user.avatarUrl %}
  <img src="{{ user.avatarUrl }}">
{% endif %}

<h1>{{ hero_title }}</h1>  ← translation.title > settings.hero_title > user.name
<p>{{ settings.hero_subtitle }}</p>
<p>{{ user.bio }}</p>

links.liquid

Itera sobre {{ links }} e renderiza cada um com o snippet link-button:

{% for link in links %}
  {% render 'link-button',
    title: link.title,
    url: link.url,
    type: link.type,
    click_id: link.id,
    username: user.username,
    cta_text: link.cta_text,
    cta_color: link.cta_color,
    thumbnail: link.thumbnail,
    embed_url: link.embed_url,
    embed_provider: link.embed_provider,
    embed_id: link.embed_id %}
{% endfor %}

social.liquid

Renderiza ícones de redes sociais. Usa simple-icons CDN para os ícones (com fallback inline SVG para LinkedIn).

Comportamento conforme settings.social_style:

social_style Mostra
icon Apenas ícone (layout horizontal)
label Apenas texto (layout vertical)
both Ícone + texto (layout vertical)
<img src="https://cdn.simpleicons.org/{{ social._icon_slug }}/a1a1aa">

Para LinkedIn (não disponível no simple-icons), renderiza SVG inline.

player.liquid

Renderiza iframe do player de música (Spotify ou SoundCloud).

{% if settings.show_player and player %}
  <iframe src="{{ player.embedUrl }}"
    height="{% if player.provider == 'spotify' and player.type == 'track' %}80{% else %}166{% endif %}">
  </iframe>
{% endif %}

featured-video.liquid

Renderiza iframe do vídeo em destaque (YouTube, Vimeo, Twitch).

{% if settings.show_featured_video and featured_video %}
  <div style="padding-bottom: 56.25%;">
    <iframe src="{{ featured_video.embedUrl }}" allowfullscreen></iframe>
  </div>
{% endif %}

countdown.liquid

Renderiza contador regressivo com data dinâmica via JavaScript.

<section data-countdown="{{ settings.countdown_date }}"
         data-message="{{ settings.countdown_message }}"
         data-expired="{{ settings.countdown_expired_message }}">
  <div class="countdown-timer"></div>
</section>

O CSS e JS do contador estão em assets/theme.css e assets/theme.js.

footer.liquid

{% if settings.show_footer %}
  <footer>Powered by <strong>Honne</strong></footer>
{% endif %}

Business Sections

Section Função Settings necessárias
business-header.liquid Header empresarial business_name, logo_image
business-hours.liquid Horários business_hours
business-location.liquid Endereço + Maps business_address
business-testimonials.liquid Depoimentos show_testimonials, business_testimonials
business-contact.liquid WhatsApp + Telefone business_whatsapp, business_phone

Portfolio Sections

Section Função Settings necessárias
portfolio-header.liquid Header portfólio portfolio_title, logo_image
portfolio-skills.liquid Habilidades skills (separadas por vírgula)
portfolio-projects.liquid Projetos (links type case) links com type: "case"
portfolio-hire.liquid Botão contratar hire_me_url, hire_me_text

language-selector.liquid

Seletor de idioma (futuro). Exibido apenas se locales.size > 1.

<select onchange="switchLanguage(this.value)">
  {% for l in locales %}
    <option value="{{ l.code }}">{{ l.code | upcase }}</option>
  {% endfor %}
</select>

Snippets

link-button.liquid

Snippet principal que renderiza cada tipo de link. Recebe os seguintes argumentos:

Argumento Tipo Descrição
title string Título do link
url string URL de destino
type string Tipo: link, button, video, music, image, text, divider, case
click_id string ID do link (para tracking)
username string Username (para tracking)
cta_text string Texto do CTA
cta_color string Cor do CTA
thumbnail string URL da thumbnail
embed_url string URL de embed (se houver)
embed_provider string Provider do embed

Tipos de Link

type Renderização
divider Linha horizontal separadora
text Bloco de texto simples
image Imagem clicável (com legenda opcional)
video + embed_url Iframe embed (YouTube/Vimeo/Twitch) + link "Assistir no {provider}"
video sem embed Link normal com thumbnail
music + embed_url Iframe embed (Spotify/SoundCloud) + link "Ouvir no {provider}"
music sem embed Player <audio> nativo
case Card de projeto (thumbnail + título + CTA)
link / button / default Botão clicável (com CTA opcional + tracking /click/:username/:id)

Tracking de cliques: Quando click_id e username estão presentes, o link aponta para /click/:username/:id que registra o clique e redireciona.

CTA customizado:


Embeds (Vídeo / Música)

O sistema detecta automaticamente URLs de serviços de streaming e gera iframes embed.

Providers Suportados

Provider Tipo Formato do Embed
YouTube video https://www.youtube.com/embed/{id}
Vimeo video https://player.vimeo.com/video/{id}
Twitch video https://player.twitch.tv/?video={id} ou ?channel={channel}
Spotify music https://open.spotify.com/embed/{type}/{id}
SoundCloud music https://w.soundcloud.com/player/?url={url}
TikTok social https://www.tiktok.com/embed/v2/{id}
Instagram social https://www.instagram.com/p/{id}/embed

Como Funciona

  1. No backend (packages/core/src/embed.ts): parseEmbedUrl(url) analisa a URL e retorna { provider, id, embedUrl, type } ou null se não for reconhecida.

  2. No engine (packages/theme-engine/src/index.ts): Para cada link, se a URL for reconhecida como embed, os campos embed_url, embed_provider, embed_id, embed_type são adicionados ao objeto do link.

  3. No template (link-button.liquid): Se embed_url existe e o tipo é video ou music, renderiza o iframe embed em vez do botão padrão.

Exemplo

{% if link.type == 'video' and link.embed_url %}
  <iframe src="{{ link.embed_url }}"></iframe>
{% endif %}

Ícones Sociais (simple-icons)

As redes sociais usam o simple-icons CDN para renderizar os ícones.

URL padrão:

https://cdn.simpleicons.org/{slug}/a1a1aa

Onde {slug} é o slug da plataforma no simple-icons e a1a1aa é a cor do ícone (hex sem #).

Mapeamento de Nomes

Nome no DB Slug no CDN
instagram instagram
youtube youtube
tiktok tiktok
x x
twitter → mapeado para x
linkedin → não existe no simple-icons (usa SVG inline)
whatsapp whatsapp
telegram telegram
facebook facebook
discord discord
github github
bluesky bluesky
mastodon mastodon
custom (qualquer slug) usado diretamente no CDN

LinkedIn

LinkedIn não tem ícone no simple-icons. O tema base inclui um SVG inline do logo do LinkedIn no social.liquid.

Custom Platform

O editor de redes sociais tem uma opção "Outra…" onde o usuário pode digitar qualquer slug do simple-icons (ex: patreon, onlyfans, nintendoswitch, medium, devto).


Assets do Tema

Arquivos em assets/ são servidos estaticamente via:

GET /theme-assets/base/:file

Arquivos do tema base:

Uso nos templates:

<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
<script src="{{ 'theme.js' | asset_url }}"></script>

MIME types suportados: .css, .js, .png, .jpg, .svg, .woff2


Campos do Link (formato do banco)

Cada link armazenado no banco tem a seguinte estrutura:

{
  id: string;              // UUID
  userId: string;          // FK do usuário
  title: string;           // Título
  url: string;             // URL de destino
  type: string;            // "link" | "button" | "video" | "music" | "image" | "text" | "divider" | "case"
  position: number;        // Ordenação (0-based)
  enabled: boolean;        // Ativo?
  ctaText: string | null;  // Texto do CTA
  ctaColor: string | null; // Cor do CTA (hex)
  thumbnail: string | null;// URL da thumbnail
  content: object | null;  // Dados específicos do tipo:
                            //   case: { description, tags: string[] }
  scheduledAt: Date | null;// Agendamento (início)
  expiresAt: Date | null;  // Agendamento (fim)
  createdAt: Date;
  updatedAt: Date;
}

Editor Visual (Admin)

O editor de temas (GET /admin/theme) permite:

  1. Settings visuais — Formulário gerado dinamicamente do schema.json, agrupado por groups
  2. Preview ao vivo — Iframe com mockup iPhone (375x650) ou desktop, sandboxed
  3. Drag & drop dos grupos para reordenar
  4. Auto-save — Salva automaticamente 2s após a última alteração
  5. Atalho: Ctrl+S para salvar

Edição de Arquivos

Editam o tema base em disco (themes/base/), compartilhado por todo usuário sem tema instalado — só platform admin, não é ação por conta.

Rota Método Função
/admin/theme/files GET Lista arquivos do tema (platform admin)
/admin/theme/files POST Salva conteúdo de um arquivo (platform admin)
/admin/theme/upload POST Upload de ZIP, substitui o tema base (platform admin)
/admin/theme/download GET Baixa o tema base em ZIP (platform admin)
/admin/theme/restore POST Restaura o tema base pro usuário logado (qualquer usuário)
/admin/theme/publish POST Publica (limpa cache) (qualquer usuário)

Preview

O preview é carregado em um iframe com sandbox="allow-scripts allow-same-origin allow-popups allow-forms". Atualiza automaticamente ao salvar settings.


Cache

3 camadas de cache LRU:

Cache Max TTL Conteúdo
fileContentCache 100 30min Conteúdo bruto dos arquivos do tema
parsedTemplateCache 500 30min Templates parseados (AST LiquidJS)
previewCache 200 5min HTML do preview (por usuário)

Clear cache em:


Criar um Tema Novo

  1. Baixe o tema base como ponto de partida (botão no topo desta página)
  2. Estrutura mínima:
    meu-tema/
    ├── layout/theme.liquid    ← HTML shell
    ├── templates/profile.liquid ← Template principal
    └── config/
        ├── schema.json        ← Settings editáveis
        └── settings.json      ← Valores padrão
    
  3. Customize os templates usando as variáveis Liquid documetandas acima
  4. Adicione sections em sections/ e use {% render 'nome' %}
  5. Adicione assets em assets/ e use {{ 'arquivo' | asset_url }}
  6. Defina as settings no schema.json com groups e settings
  7. Teste localmente fazendo upload do ZIP no editor de temas (/admin/theme/upload)

Regras do Sandbox

Boas Práticas