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
- Estrutura de Diretórios
- Fluxo de Render
- Variáveis do Contexto Liquid
- Filtros Custom
- Tags Custom
- Sistema de Settings
- Templates
- Sections
- Snippets
- Embeds (Vídeo / Música)
- ícones Sociais (simple-icons)
- Assets do Tema
- Campos do Link (formato do banco)
- Editor Visual (Admin)
- Cache
- Criar um Tema Novo
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)
{{ translation.title }}— Título traduzido{{ translation.subtitle }}— Subtítulo traduzido{{ translation.bio }}— Bio traduzida{{ locales }}— Array de idiomas disponíveis{{ locale }}— Código do idioma ativo ("pt","en", etc.)
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
- Base:
themes/base/config/settings.json(valores padrão do tema) - Usuário:
users.theme(coluna JSONB no PostgreSQL) - 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:
- Se
cta_colorestá presente: fundo = cor do CTA, texto = branco - Se não: fundo =
var(--card), texto =var(--text) - Se
cta_textestá presente: mostra o texto do CTA em vez do título - Se
settings.button_shadow: adiciona box-shadow
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} |
| social | https://www.instagram.com/p/{id}/embed |
Como Funciona
No backend (
packages/core/src/embed.ts):parseEmbedUrl(url)analisa a URL e retorna{ provider, id, embedUrl, type }ounullse não for reconhecida.No engine (
packages/theme-engine/src/index.ts): Para cada link, se a URL for reconhecida como embed, os camposembed_url,embed_provider,embed_id,embed_typesão adicionados ao objeto do link.No template (
link-button.liquid): Seembed_urlexiste e o tipo évideooumusic, 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 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:
theme.css— Animações (fade-in, scale), estilos do countdown, lightboxtheme.js— Countdown timer, link tracking client-side
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:
- Settings visuais — Formulário gerado dinamicamente do
schema.json, agrupado porgroups - Preview ao vivo — Iframe com mockup iPhone (375x650) ou desktop, sandboxed
- Drag & drop dos grupos para reordenar
- Auto-save — Salva automaticamente 2s após a última alteração
- Atalho:
Ctrl+Spara 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:
- Publicar tema
- Salvar settings
- Upload de novo tema
- Restaurar tema padrão
Criar um Tema Novo
- Baixe o tema base como ponto de partida (botão no topo desta página)
- 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 - Customize os templates usando as variáveis Liquid documetandas acima
- Adicione sections em
sections/e use{% render 'nome' %} - Adicione assets em
assets/e use{{ 'arquivo' | asset_url }} - Defina as settings no
schema.jsoncomgroupsesettings - Teste localmente fazendo upload do ZIP no editor de temas (
/admin/theme/upload)
Regras do Sandbox
- LiquidJS sem acesso a
fs,require,process,rede ownPropertyOnly: true— não acessa prototype chain- Path traversal sanitization em todos os file paths
- Rate limit de render: 100 renders/min por usuário
Boas Práticas
- Use variáveis CSS (
var(--bg),var(--text),var(--primary),var(--card),var(--muted),var(--radius)) em vez de valores fixos — elas são definidas nolayout/theme.liquidconforme as settings do usuário - Adapte o design para mobile-first (largura máxima de 640px)
- Use
loading="lazy"em imagens - Teste todos os tipos de link (link, button, video, music, image, text, divider, case)
- Mantenha compatibilidade com o sistema de tracking (
/click/:username/:id) - Para animações, use as classes CSS do
assets/theme.css(.animate-fade-in) - O tema base usa Tailwind CSS via CDN — você pode usar classes utilitárias nos seus templates