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:

Baixar a última versão

terminal

$ ./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

my-site/
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)
Regra de ouro: tudo que é seu mora em theme/ (mais theme/functions.php pra PHP de carga). O upgrade troca core/, bin/ e migrations/ inteiros — edições ali evaporam.

site.json

theme/site.json é a fonte da verdade da estrutura: o painel administrativo é gerado dele. Chaves de topo:

ChaveO que declara
sitename e language. A URL de produção não fica aqui — é APP_URL no .env, mantendo o tema portátil.
field_groupsBundles de campos reutilizáveis, referenciados como {"group": "contact"}.
image_sizesTamanhos nomeados (width, height, crop) para image_url().
pagesPáginas estruturais por chave (ex.: home): label, template, URL opcional e campos.
page_templatesTemplates pro fluxo "+ Criar página" do admin — a página vive no banco, com URL /{slug}.
item_typesTipos de conteúdo: label, slug, template, archive_template, taxonomies, templates alternativos e campos.
taxonomiesTaxonomias: label, slug, hierarchical, template e has_page (false tira a URL pública do termo).
optionsPáginas de opções globais — só campos, sem slug nem listagem.
formsFormulários públicos (campos, assunto do e-mail, mensagem de sucesso).
membersÁrea de membros do site (tipos, campos, rotas).
i18nMulti-idioma: enabled e idioma default.
SEO vem de graça: todo tipo com página ganha meta_title, meta_description e og_image automaticamente — não redeclare esses campos.

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.

TipoNotas
textInput de uma linha.
textareaTexto longo sem formatação.
richtextWYSIWYG (TipTap); salva HTML. Imprima sem escape, comentado /* trusted */.
imageUma mídia, armazenada como id inteiro. Resolva com image_url().
galleryLista ordenada de ids de mídia — passe cada um por image_url().
selectDropdown; options como {value: label}.
booleanCheckbox.
numberInput numérico.
email · url · dateVariantes HTML5 do input.
repeaterGrupo de subcampos repetível; declare fields no campo.
flexibleRepeater onde cada linha escolhe um layout nomeado; a linha salva carrega _layout.
linkComposto {title, url, new_tab}; imprima com link_tag(). Aceita mailto:, âncoras e caminhos relativos.
itemReferência a outro item (combobox só com publicados). Declare item_type; resolva com item(field('x')).
groupSó organização: caixa recolhível no admin. Storage continua plano.
tabsSó organização: abas no editor. Dois tabs não podem repetir nome de campo.

Templates

Ordem de resolução:

  1. Página estrutural: template → fallback page.php
  2. Página criada no admin: file do template escolhido → page.php
  3. Item: template próprio do item (se houver) → template do tipo → single.php
  4. Arquivo de itens: archive_template → archive.php
  5. Arquivo de taxonomia: template da taxonomia → taxonomy.php
theme/templates/single-post.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:

TemplateVariávelConteú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

HelperFaz
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

HelperFaz
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

HelperFaz
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

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

Nunca escreva /blog na mão num template — no dia que o site mudar de pasta, quebra. url('/blog') não quebra.

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

site.json — options
"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

site.json — forms
"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.

site.json — members
"members": {
  "enabled": true,
  "types": {
    "client": { "label": "Client",
      "fields": [ { "name": "company", "type": "text" } ] }
  },
  "routes": { "login": "/login" }
}
HelperFaz
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.
site.json — i18n
"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.

HelperFaz
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

ComandoFaz
installInstalação completa: banco, migrations, admin, arquivos do projeto.
migrate · migrate:statusAplica / lista migrations pendentes.
schema:validateValida o site.json — rode após toda edição.
page:syncCria/atualiza as linhas de páginas declaradas no schema.
serve [port]Dev server (padrão :8080).
user:createNovo usuário admin.
files:initCopia .htaccess e robots.txt dos .example.
i18n:scanVarre o tema atrás de t() e registra as strings.

Conteúdo

ComandoFaz
item:types · item:schema <type>Lista tipos; mostra os campos de um tipo (leia antes de escrever).
item:list · item:getLista (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 --confirmMuda status; deleta (exige --confirm).
page:list · page:schema · page:get · page:updateO 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
Auto-migrate: um site instalado aplica migrations pendentes sozinho no primeiro acesso web após um upgrade (requisições concorrentes são serializadas com lock no MySQL). Hospedagem sem SSH termina o upgrade só subindo os arquivos. Desligue com AUTO_MIGRATE=false.

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.