Documentación

Referencia de Velk

Lo esencial para construir y mantener un sitio Velk. La skill oficial carga esta misma referencia, completa, dentro de tu IA.

Empezando

Requisitos

  • PHP 8.2 o superior
  • MySQL 5.7+ o MariaDB 10.2+ (Velk usa columnas JSON nativas)
  • Apache con .htaccess (incluido) — en nginx, escribe la regla de rewrite a mano

Instalación con IA (recomendada)

En el flujo de vibe coding no descargas nada: la skill oficial se encarga. Instálala en Claude Code y pide la instalación — la IA descarga la release oficial, verifica el checksum, configura el .env y ejecuta el instalador. El paso a paso está en Cómo usar.

Instala la skill elleven-digital/velk-skill en ~/.claude/skills/velk y recarga las skills.

Instalación manual (sin IA)

Para quien prefiere programar directo: Velk se distribuye como un tarball versionado — no es un clon de repositorio. Descarga la release, extráela en la carpeta del proyecto, rellena el .env a partir del .env.example (base de datos y usuario admin inicial) y ejecuta el instalador:

Descargar la última versión

terminal

$ ./bin/velk install

crea la base de datos si falta · corre migraciones · crea el admin · copia .htaccess y robots.txt

$ ./bin/velk serve

Velk corriendo en http://localhost:8080

Variables principales del .env: DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, INITIAL_USER_EMAIL, INITIAL_USER_PASSWORD, INITIAL_USER_NAME y APP_URL (opcional en local — usada por el sitemap y por absolute_url()). SMTP es opcional: sin SMTP_HOST/USER/PASS/FROM, los formularios guardan pero no envían email.

Estructura de carpetas

my-site/
core/           engine — nunca editar
bin/velk        CLI
migrations/     engine — aplicadas en la instalación/actualización
theme/          TU sitio: site.json, plantillas, assets
storage/        uploads y archivos generados
.env            credenciales
.velk-version   versión instalada (usada por el upgrade)
Regla de oro: todo lo que es tuyo vive en theme/ (más theme/functions.php para PHP de carga). La actualización reemplaza core/, bin/ y migrations/ enteros — las ediciones ahí se evaporan.

site.json

theme/site.json es la fuente de la verdad de la estructura: el panel administrativo se genera de él. Claves de nivel superior:

ClaveLo que declara
sitename y language. La URL de producción no va aquí — es APP_URL en el .env, manteniendo el tema portátil.
field_groupsBundles de campos reutilizables, referenciados como {"group": "contact"}.
image_sizesTamaños con nombre (width, height, crop) para image_url().
pagesPáginas estructurales por clave (ej.: home): label, template, URL opcional y campos.
page_templatesPlantillas para el flujo "+ Crear página" del admin — la página vive en la base de datos, con URL /{slug}.
item_typesTipos de contenido: label, slug, template, archive_template, taxonomies, plantillas alternativas y campos.
taxonomiesTaxonomías: label, slug, hierarchical, template y has_page (false quita la URL pública del término).
optionsPáginas de opciones globales — solo campos, sin slug ni listado.
formsFormularios públicos (campos, asunto del email, mensaje de éxito).
membersÁrea de miembros del sitio (tipos, campos, rutas).
i18nMulti-idioma: enabled e idioma default.
El SEO viene gratis: todo tipo con página recibe meta_title, meta_description y og_image automáticamente — no redeclares estos campos.

Las plantillas se vinculan por nombre de archivo, nunca por slug — renombra slugs a voluntad sin romper los bindings. Tras cualquier edición, ejecuta ./bin/velk schema:validate.

Tipos de campo

Comunes a cualquier campo: name (obligatorio), label, help, required, placeholder. field('name') devuelve el valor crudo — escalar en los simples, array en gallery/repeater/flexible.

TipoNotas
textInput de una línea.
textareaTexto largo sin formato.
richtextWYSIWYG (TipTap); guarda HTML. Imprímelo sin escapar, comentado /* trusted */.
imageUn medio, almacenado como id entero. Resuélvelo con image_url().
galleryLista ordenada de ids de medios — pasa cada uno por image_url().
selectDesplegable; options como {value: label}.
booleanCheckbox.
numberInput numérico.
email · url · dateVariantes HTML5 del input.
repeaterGrupo de subcampos repetible; declara fields en el campo.
flexibleRepeater donde cada fila elige un layout con nombre; la fila guardada lleva _layout.
linkCompuesto {title, url, new_tab}; imprime con link_tag(). Acepta mailto:, anclas y rutas relativas.
itemReferencia a otro item (combobox solo con publicados). Declara item_type; resuélvelo con item(field('x')).
groupSolo organización: caja plegable en el admin. El almacenamiento sigue plano.
tabsSolo organización: pestañas en el editor. Dos tabs no pueden repetir nombre de campo.

Plantillas

Orden de resolución:

  1. Página estructural: template → fallback page.php
  2. Página creada en el admin: file de la plantilla elegida → page.php
  3. Item: la plantilla propia del item (si hay) → la plantilla del tipo → single.php
  4. Archivo de items: archive_template → archive.php
  5. Archivo de taxonomía: la plantilla de la taxonomía → 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() permite que field(), the_title() y compañía funcionen sin pasar el registro alrededor. Cada tipo de plantilla recibe una variable de contexto:

PlantillaVariableContenido
page-*.php$page->record es la página
single-*.php$item->record es el item
archive-*.php$archive->records es la lista de items
taxonomy-*.php$term->record es el término, ->records los items

Helpers

Todos globales en las plantillas (definidos en core/helpers.php).

Salida y contexto

HelperHace
e($value)Escapa para HTML. Úsalo en toda salida — excepto richtext de confianza.
dd(...$v)Debug: vuelca y termina.
field($name, $default)Campo del contexto actual (idioma ya resuelto).
the_title() · the_slug() · the_url()Básicos del registro actual.
with_context($ctx, $fn)Ejecuta el bloque con ese registro como contexto.

Contenido

HelperHace
items($type, $args)Lista items — limit, offset, status (por defecto published), term, order.
item($id)Un item (o null) — resuelve campos item.
terms($taxonomy)Términos de una taxonomía.
the_terms($taxonomy)Términos del item actual.

URLs y medios

HelperHace
url($path)Enlace de navegación con el base path de la instalación (y el idioma activo).
absolute_url($path)URL completa (scheme + host) — canonical, og:url, sitemap.
asset($path) · admin_url() · base_path()Assets del tema, URL del admin, base de la instalación.
image_url($value, $size)Resuelve id/URL/array en una URL; '' cuando está vacío — revisa antes del <img>.
image_alt($value, $fallback)Texto alternativo (consciente del idioma).
media($id)El modelo del medio (width/height/mime).
link_tag($link, $text, $attrs)<a> seguro para un campo link; una nueva pestaña recibe rel="noopener".

Layout y varios

HelperHace
get_header() · get_footer()Incluyen theme/partials/header.php y footer.php.
partial($name, $data)Incluye un parcial con datos.
option($path, $default) · options($key)Opciones globales por dot path (option('contact.phone')).
slugify($string) · config($key) · site($key)Utilidades.

URLs y rutas

  • / → página de inicio
  • /{page-key} → demás páginas estructurales (o la URL explícita declarada)
  • /{type-slug} → archivo · /{type-slug}/{item-slug} → item
  • /{taxonomy-slug}/{term-slug} → archivo del término
  • /admin → panel · /uploads/... → medios · /theme/... → assets estáticos del tema (sin PHP)

El base path se detecta solo desde SCRIPT_NAME, así que el sitio funciona instalado en la raíz o en un subdirectorio (site.com/velk/) sin configurar nada — siempre que el tema use siempre los helpers de URL. Si un host exótico falla la detección: 'base_path' => '/velk' en config/env.php.

Nunca escribas /blog a mano en una plantilla — el día que el sitio cambie de carpeta, se rompe. url('/blog') no se rompe.

Medios

Biblioteca en /admin/media: subida (jpeg, png, gif, webp, svg, pdf — hasta 25 MB), texto alternativo, copiar URL y borrado que limpia los tamaños generados. Los nombres de archivo se slugifican; las colisiones reciben sufijo -1, -2…

storage/uploads
storage/uploads/{file}.{ext}          originales
storage/uploads/thumb/{file}.{ext}    generado bajo demanda (GD)
storage/uploads/hero/{file}.{ext}     generado bajo demanda (GD)

Los tamaños vienen de image_sizes en el site.json — crop: true recorta exacto, false encaja proporcional. En el tema: image_url(field('photo'), 'thumb').

Opciones globales

site.json — options
"options": {
  "contact": {
    "label": "Contact",
    "fields": [
      { "name": "phone", "type": "text", "label": "Phone" }
    ]
  }
}

Cada entrada se convierte en un enlace en la barra lateral del admin y en un formulario solo de campos. Léelo con option('contact.phone') — el dot path entra en grupos y filas de repeater (option('contact.social.0.url')). Toda una página sale con options('contact'). Almacenado como una fila JSON en settings, con caché en proceso: 50 llamadas, una query.

Formularios

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 aceptados: text, email, tel, url, number, textarea, checkbox, hidden, select (con required y maxlength). El asunto acepta placeholders {{field}} rellenados con el envío.

El tema escribe el HTML del formulario:

  • form_url('contact') en el action (POST)
  • csrf_field() + form_honeypot() dentro del form
  • form_status('contact') para leer el flash — status, mensaje, valores y errores

Ciclo: valida CSRF + honeypot + campos, guarda en form_submissions, envía el email a los destinatarios definidos en /admin/forms/contact y redirige de vuelta. Un email fallido no pierde el envío.

Miembros

Cuentas del sitio público — tablas, autenticación y sesiones separadas de los usuarios del admin. Sin roles predefinidos: cada sitio declara sus tipos y el tema decide a qué accede cada uno.

site.json — members
"members": {
  "enabled": true,
  "types": {
    "client": { "label": "Client",
      "fields": [ { "name": "company", "type": "text" } ] }
  },
  "routes": { "login": "/login" }
}
HelperHace
member_enabled() · member_check()¿Función activa? ¿Alguien con sesión iniciada?
member() · member_field('x') · member_is('type')Miembro actual, su campo, comprobación de tipo.
member_require() · member_require_type('type')Bloquea la página; redirige a quien no puede.
member_route('login') · member_status()Ruta resuelta y último flash de auth.

El registro público viene desactivado (public_registration: false) — por defecto solo el admin crea cuentas. El tema construye los formularios de login/registro/reset contra los endpoints POST del engine, siempre con csrf_field().

Multi-idioma

Tres capas independientes, todas nativas:

  • Cadenas fijas del tema — envuelve en t('Leer más'); catálogo generado por i18n:scan y traducido en /admin/translations.
  • URLs estructurales — slugs de páginas, tipos y taxonomías por idioma: /blog/post-x se convierte en /en/news/post-x.
  • Contenido por registro — título, slug y campos con columnas *_translations y pestañas por idioma en el propio editor.
site.json — i18n
"i18n": { "enabled": true, "default": "en" }

Los demás idiomas se registran en /admin/languages, cada uno con prefijo de URL. En las plantillas nada cambia: field(), the_title() y option() ya resuelven el idioma activo con fallback al predeterminado — escribe la plantilla una vez.

HelperHace
lang() · lang_is('en') · available_languages()Idioma actual y lista de idiomas.
url('/about', 'en') · lang_url('en')URL en otro idioma; la página actual en otro idioma.
lang_switcher()Datos listos para montar el selector de idiomas.
the_html_lang() · the_hreflangs() · the_canonical()El atributo del <html>, enlaces alternate y canonical.

Las URLs de idioma no predeterminado son estrictas — sin slug traducido, sin URL en ese idioma (nada de contenido duplicado para Google). El sitemap emite los hreflang solo cuando la traducción existe de verdad.

CLI

./bin/velk <comando>, desde la raíz del proyecto. Casi todo lo que hace el panel, lo hace la CLI — es lo que permite que la IA opere el sitio.

Setup y esquema

ComandoHace
installInstalación completa: base de datos, migraciones, admin, archivos del proyecto.
migrate · migrate:statusAplica / lista las migraciones pendientes.
schema:validateValida el site.json — ejecútalo tras cada edición.
page:syncCrea/actualiza las filas de páginas declaradas en el esquema.
serve [port]Servidor de desarrollo (por defecto :8080).
user:createNuevo usuario admin.
files:initCopia .htaccess y robots.txt desde los .example.
i18n:scanRecorre el tema en busca de t() y registra las cadenas.

Contenido

ComandoHace
item:types · item:schema <type>Lista tipos; muestra los campos de un tipo (lee antes de escribir).
item:list · item:getLista (con filtros) y lee un item como JSON.
item:create --json= · item:update --json=Crea y actualiza (merge) por JSON.
item:publish · item:unpublish · item:delete --confirmCambia el estado; elimina (requiere --confirm).
page:list · page:schema · page:get · page:updateLo mismo, para páginas.

Flags útiles

  • --format=json — salida legible por máquina
  • --dry-run — valida sin guardar
  • --json-stdin — payload por stdin, sin infierno de comillas en el shell
Auto-migrate: un sitio instalado aplica las migraciones pendientes por sí solo en la primera petición web tras una actualización (las peticiones concurrentes se serializan con un lock de MySQL). Un hosting sin SSH termina la actualización solo subiendo los archivos. Desactiva con AUTO_MIGRATE=false.

Deploy y upgrades

Deploy

  • Hosting compartido sin SSH: sube los archivos por FTP/panel — el auto-migrate completa la instalación en la primera petición.
  • Con SSH (VPS, cloud, cPanel con terminal): la skill oficial cubre el flujo completo — archivos, base de datos, storage y robots.txt con la URL de producción.
  • Subdirectorio: funciona sin configuración, porque los helpers de URL prefijan el base path solos.

En producción, rellena APP_URL en el .env — el sitemap y absolute_url() dependen de él.

Upgrades

El engine es una dependencia versionada, distribuida como tarball con checksum. El upgrade lee la versión en .velk-version, descarga la nueva, verifica el sha256 y reemplaza solo las rutas del engine — theme/, storage/, .env, .htaccess y robots.txt quedan como están. Pídele a la IA: "actualiza Velk en este proyecto".

La referencia completa vive en la skill

Instala la skill oficial y tu IA pasa a consultar toda esta documentación — esquema, helpers, CLI y flujos de deploy — sin salir del chat.