Documentação
Referência do Velk
O essencial pra construir e manter um site Velk. A skill oficial carrega esta mesma referência, completa, pra dentro da sua IA.
Começando
Requisitos
- PHP 8.2 ou superior
- MySQL 5.7+ ou MariaDB 10.2+ (o Velk usa colunas JSON nativas)
- Apache com .htaccess (incluso) — no nginx, escreva a regra de rewrite à mão
Instalação com IA (recomendada)
No fluxo de vibe coding você não baixa nada: a skill oficial cuida disso. Instale-a no Claude Code e peça a instalação — a IA baixa a release oficial, verifica o checksum, configura o .env e roda o instalador. O passo a passo está em Como usar.
Instale a skill elleven-digital/velk-skill em ~/.claude/skills/velk e recarregue as skills.
Instalação manual (sem IA)
Pra quem prefere programar direto: o Velk é distribuído como tarball versionado — não é um clone de repositório. Baixe a release, extraia na pasta do projeto, preencha o .env a partir do .env.example (banco e usuário admin inicial) e rode o instalador:
$ ./bin/velk install
cria o banco se faltar · roda migrations · cria o admin · copia .htaccess e robots.txt
$ ./bin/velk serve
Velk rodando em http://localhost:8080
Variáveis principais do .env: DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, INITIAL_USER_EMAIL, INITIAL_USER_PASSWORD, INITIAL_USER_NAME e APP_URL (opcional em local — usado pelo sitemap e por absolute_url()). SMTP é opcional: sem SMTP_HOST/USER/PASS/FROM, formulários salvam mas não enviam e-mail.
Estrutura de pastas
core/ engine — nunca edite
bin/velk CLI
migrations/ engine — aplicadas no install/upgrade
theme/ o SEU site: site.json, templates, assets
storage/ uploads e arquivos gerados
.env credenciais
.velk-version versão instalada (usada pelo upgrade)
site.json
theme/site.json é a fonte da verdade da estrutura: o painel administrativo é gerado dele. Chaves de topo:
| Chave | O que declara |
|---|---|
| site | name e language. A URL de produção não fica aqui — é APP_URL no .env, mantendo o tema portátil. |
| field_groups | Bundles de campos reutilizáveis, referenciados como {"group": "contact"}. |
| image_sizes | Tamanhos nomeados (width, height, crop) para image_url(). |
| pages | Páginas estruturais por chave (ex.: home): label, template, URL opcional e campos. |
| page_templates | Templates pro fluxo "+ Criar página" do admin — a página vive no banco, com URL /{slug}. |
| item_types | Tipos de conteúdo: label, slug, template, archive_template, taxonomies, templates alternativos e campos. |
| taxonomies | Taxonomias: label, slug, hierarchical, template e has_page (false tira a URL pública do termo). |
| options | Páginas de opções globais — só campos, sem slug nem listagem. |
| forms | Formulários públicos (campos, assunto do e-mail, mensagem de sucesso). |
| members | Área de membros do site (tipos, campos, rotas). |
| i18n | Multi-idioma: enabled e idioma default. |
Templates são vinculados por nome de arquivo, nunca por slug — renomeie slugs à vontade sem quebrar bindings. Depois de qualquer edição, rode ./bin/velk schema:validate.
Tipos de campo
Comuns a qualquer campo: name (obrigatório), label, help, required, placeholder. field('name') devolve o valor bruto — escalar nos simples, array em gallery/repeater/flexible.
| Tipo | Notas |
|---|---|
| text | Input de uma linha. |
| textarea | Texto longo sem formatação. |
| richtext | WYSIWYG (TipTap); salva HTML. Imprima sem escape, comentado /* trusted */. |
| image | Uma mídia, armazenada como id inteiro. Resolva com image_url(). |
| gallery | Lista ordenada de ids de mídia — passe cada um por image_url(). |
| select | Dropdown; options como {value: label}. |
| boolean | Checkbox. |
| number | Input numérico. |
| email · url · date | Variantes HTML5 do input. |
| repeater | Grupo de subcampos repetível; declare fields no campo. |
| flexible | Repeater onde cada linha escolhe um layout nomeado; a linha salva carrega _layout. |
| link | Composto {title, url, new_tab}; imprima com link_tag(). Aceita mailto:, âncoras e caminhos relativos. |
| item | Referência a outro item (combobox só com publicados). Declare item_type; resolva com item(field('x')). |
| group | Só organização: caixa recolhível no admin. Storage continua plano. |
| tabs | Só organização: abas no editor. Dois tabs não podem repetir nome de campo. |
Templates
Ordem de resolução:
- Página estrutural: template → fallback page.php
- Página criada no admin: file do template escolhido → page.php
- Item: template próprio do item (se houver) → template do tipo → single.php
- Arquivo de itens: archive_template → archive.php
- Arquivo de taxonomia: template da taxonomia → taxonomy.php
<?php with_context($item, function () {
get_header(); ?>
<article class="post">
<h1><?= e(the_title()) ?></h1>
<?= field('content') /* trusted */ ?>
</article>
<?php get_footer(); }); ?>
with_context() deixa field(), the_title() e afins funcionarem sem passar o registro adiante. Cada tipo de template recebe uma variável de contexto:
| Template | Variável | Conteúdo |
|---|---|---|
| page-*.php | $page | ->record é a página |
| single-*.php | $item | ->record é o item |
| archive-*.php | $archive | ->records é a lista de itens |
| taxonomy-*.php | $term | ->record é o termo, ->records os itens |
Helpers
Todos globais nos templates (definidos em core/helpers.php).
Saída e contexto
| Helper | Faz |
|---|---|
| e($value) | Escapa pra HTML. Use em toda saída — exceto richtext confiável. |
| dd(...$v) | Debug: despeja e encerra. |
| field($name, $default) | Campo do contexto atual (idioma já resolvido). |
| the_title() · the_slug() · the_url() | Básicos do registro atual. |
| with_context($ctx, $fn) | Executa o bloco com aquele registro como contexto. |
Conteúdo
| Helper | Faz |
|---|---|
| items($type, $args) | Lista itens — limit, offset, status (padrão published), term, order. |
| item($id) | Um item (ou null) — resolve campos item. |
| terms($taxonomy) | Termos de uma taxonomia. |
| the_terms($taxonomy) | Termos do item atual. |
URLs e mídia
| Helper | Faz |
|---|---|
| url($path) | Link de navegação com o base path do install (e idioma ativo). |
| absolute_url($path) | URL completa (scheme + host) — canonical, og:url, sitemap. |
| asset($path) · admin_url() · base_path() | Assets do tema, URL do admin, base do install. |
| image_url($value, $size) | Resolve id/URL/array numa URL; '' quando vazio — cheque antes do <img>. |
| image_alt($value, $fallback) | Texto alternativo (ciente de idioma). |
| media($id) | Modelo da mídia (width/height/mime). |
| link_tag($link, $text, $attrs) | <a> seguro pra campo link; nova aba ganha rel="noopener". |
Layout e diversos
| Helper | Faz |
|---|---|
| get_header() · get_footer() | Incluem theme/partials/header.php e footer.php. |
| partial($name, $data) | Inclui um parcial com dados. |
| option($path, $default) · options($key) | Opções globais por dot path (option('contact.phone')). |
| slugify($string) · config($key) · site($key) | Utilitários. |
URLs & rotas
- / → página home
- /{page-key} → demais páginas estruturais (ou a URL explícita declarada)
- /{type-slug} → arquivo · /{type-slug}/{item-slug} → item
- /{taxonomy-slug}/{term-slug} → arquivo do termo
- /admin → painel · /uploads/... → mídia · /theme/... → assets estáticos do tema (sem PHP)
O base path é detectado sozinho de SCRIPT_NAME, então o site funciona instalado na raiz ou em sub-diretório (site.com/velk/) sem configurar nada — desde que o tema use sempre os helpers de URL. Se um host exótico errar a detecção: 'base_path' => '/velk' em config/env.php.
Mídia
Biblioteca em /admin/media: upload (jpeg, png, gif, webp, svg, pdf — até 25 MB), texto alternativo, copiar URL e exclusão com limpeza dos tamanhos gerados. Nomes de arquivo são slugificados; colisões ganham sufixo -1, -2…
storage/uploads/{file}.{ext} originais
storage/uploads/thumb/{file}.{ext} gerado sob demanda (GD)
storage/uploads/hero/{file}.{ext} gerado sob demanda (GD)
Os tamanhos vêm de image_sizes no site.json — crop: true corta exato, false encaixa proporcional. No tema: image_url(field('photo'), 'thumb').
Opções globais
"options": {
"contact": {
"label": "Contact",
"fields": [
{ "name": "phone", "type": "text", "label": "Phone" }
]
}
}
Cada entrada vira um link na sidebar do admin e um formulário só de campos. Leia com option('contact.phone') — o dot path entra em grupos e linhas de repeater (option('contact.social.0.url')). Tudo de uma página sai com options('contact'). Armazenado como uma linha JSON em settings, com cache em processo: 50 chamadas, uma query.
Formulários
"forms": {
"contact": {
"label": "Contact",
"subject": "New contact: {{name}}",
"success_message": "Message sent.",
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true }
]
}
}
Tipos aceitos: text, email, tel, url, number, textarea, checkbox, hidden, select (com required e maxlength). O assunto aceita placeholders {{field}} preenchidos com a submissão.
O tema escreve o HTML do formulário:
- form_url('contact') no action (POST)
- csrf_field() + form_honeypot() dentro do form
- form_status('contact') pra ler o flash — status, mensagem, valores e erros
Ciclo: valida CSRF + honeypot + campos, salva em form_submissions, envia o e-mail aos destinatários definidos em /admin/forms/contact e redireciona de volta. E-mail que falha não perde a submissão.
Membros
Contas do site público — tabelas, autenticação e sessões separadas dos usuários do admin. Sem papéis pré-definidos: cada site declara os seus tipos e o tema decide o que cada um acessa.
"members": {
"enabled": true,
"types": {
"client": { "label": "Client",
"fields": [ { "name": "company", "type": "text" } ] }
},
"routes": { "login": "/login" }
}
| Helper | Faz |
|---|---|
| member_enabled() · member_check() | Recurso ligado? Alguém logado? |
| member() · member_field('x') · member_is('type') | Membro atual, campo dele, checagem de tipo. |
| member_require() · member_require_type('type') | Trava a página; redireciona quem não pode. |
| member_route('login') · member_status() | Rota resolvida e último flash de auth. |
Registro público vem desligado (public_registration: false) — por padrão só o admin cria contas. O tema constrói os formulários de login/registro/reset contra os endpoints POST do engine, sempre com csrf_field().
Multi-idioma
Três camadas independentes, todas nativas:
- Strings fixas do tema — embrulhe em t('Leia mais'); catálogo gerado por i18n:scan e traduzido em /admin/translations.
- URLs estruturais — slugs de páginas, tipos e taxonomias por idioma: /blog/post-x vira /en/news/post-x.
- Conteúdo por registro — título, slug e campos com colunas *_translations e abas por idioma no próprio editor.
"i18n": { "enabled": true, "default": "en" }
Os demais idiomas são cadastrados em /admin/languages, cada um com prefixo de URL. Nos templates, nada muda: field(), the_title() e option() já resolvem o idioma ativo com fallback pro padrão — escreva o template uma vez.
| Helper | Faz |
|---|---|
| lang() · lang_is('en') · available_languages() | Idioma atual e lista de idiomas. |
| url('/about', 'en') · lang_url('en') | URL em outro idioma; a página atual em outro idioma. |
| lang_switcher() | Dados prontos pra montar o seletor de idiomas. |
| the_html_lang() · the_hreflangs() · the_canonical() | Atributo do <html>, links alternate e canonical. |
URLs de idioma não-padrão são estritas — sem slug traduzido, sem URL naquele idioma (nada de conteúdo duplicado pro Google). O sitemap emite os hreflang só quando a tradução existe de verdade.
CLI
./bin/velk <comando>, da raiz do projeto. Quase tudo que o painel faz, a CLI faz — é o que deixa a IA operar o site.
Setup e schema
| Comando | Faz |
|---|---|
| install | Instalação completa: banco, migrations, admin, arquivos do projeto. |
| migrate · migrate:status | Aplica / lista migrations pendentes. |
| schema:validate | Valida o site.json — rode após toda edição. |
| page:sync | Cria/atualiza as linhas de páginas declaradas no schema. |
| serve [port] | Dev server (padrão :8080). |
| user:create | Novo usuário admin. |
| files:init | Copia .htaccess e robots.txt dos .example. |
| i18n:scan | Varre o tema atrás de t() e registra as strings. |
Conteúdo
| Comando | Faz |
|---|---|
| item:types · item:schema <type> | Lista tipos; mostra os campos de um tipo (leia antes de escrever). |
| item:list · item:get | Lista (com filtros) e lê um item como JSON. |
| item:create --json= · item:update --json= | Cria e atualiza (merge) por JSON. |
| item:publish · item:unpublish · item:delete --confirm | Muda status; deleta (exige --confirm). |
| page:list · page:schema · page:get · page:update | O mesmo, para páginas. |
Flags úteis
- --format=json — saída legível por máquina
- --dry-run — valida sem salvar
- --json-stdin — payload via stdin, sem inferno de aspas no shell
Deploy & upgrades
Deploy
- Hospedagem compartilhada sem SSH: suba os arquivos por FTP/painel — o auto-migrate completa a instalação no primeiro acesso.
- Com SSH (VPS, cloud, cPanel com terminal): a skill oficial cobre o fluxo completo — arquivos, banco, storage e robots.txt com a URL de produção.
- Sub-diretório: funciona sem configuração, porque os helpers de URL prefixam o base path sozinhos.
Em produção, preencha APP_URL no .env — o sitemap e o absolute_url() dependem dele.
Upgrades
O engine é uma dependência versionada, distribuída por tarball com checksum. O upgrade lê a versão em .velk-version, baixa a nova, verifica o sha256 e substitui apenas os caminhos do engine — theme/, storage/, .env, .htaccess e robots.txt ficam como estão. Peça pra IA: "atualiza o Velk desse projeto".
A referência completa vive na skill
Instale a skill oficial e a sua IA passa a consultar esta documentação inteira — schema, helpers, CLI e fluxos de deploy — sem sair do chat.