Documentation
Velk reference
The essentials to build and maintain a Velk site. The official skill loads this same reference, in full, into your AI.
- site.json
- field()
- image_url()
- option()
- bin/velk
- t()
Getting started
Requirements
- PHP 8.2 or higher
- MySQL 5.7+ or MariaDB 10.2+ (Velk uses native JSON columns)
- Apache with
.htaccess(included). On nginx, write the rewrite rule by hand
Install with AI (recommended)
In the vibe coding flow you download nothing: the official skill handles it. Install it in Claude Code and ask for the install — the AI downloads the official release, verifies the checksum, configures the .env and runs the installer. The step-by-step is in .env and runs the installer. The step-by-step is in How to use.
Install Velk in this folder and create my admin user.
Manual install (without AI)
For those who prefer to code directly: Velk ships as a versioned tarball — not a repository clone. Download the release, extract it into the project folder, fill in the .env a partir do .env.example (database and initial admin user) and run the installer:
Main variables of the .env: DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, INITIAL_USER_EMAIL, INITIAL_USER_PASSWORD, INITIAL_USER_NAME and APP_URL (optional locally, used by the sitemap and by absolute_url()). SMTP is optional: without SMTP_HOST/USER/PASS/FROM, forms still save but send no email.
Folder structure
core/ engine, never edit
bin/velk CLI
migrations/ engine, applied on install/upgrade
theme/ YOUR site: site.json, templates, assets
storage/ uploads and generated files
.env credentials
.velk-version installed version (used by the upgrade)
theme/ (plus theme/functions.php for load-time PHP). The upgrade replaces core/, bin/ e migrations/ entirely: edits in there evaporate. site.json
theme/site.json theme/site.json is the source of truth for structure: the admin panel is generated from it. Top-level keys:
| Key | What it declares |
|---|---|
| site | name and language. The production URL is not here — it is APP_URL in .env, keeping the theme portable. |
| field_groups | Reusable field bundles, referenced as {"group": "contact"}. |
| image_sizes | Named sizes (width, height, crop) for image_url(). |
| pages | Structural pages by key (e.g. home): label, template, optional URL and fields. |
| page_templates | Templates for the admin "+ Create page" flow — the page lives in the DB, with URL /{slug}. |
| item_types | Content types: label, slug, template, archive_template, taxonomies, alternate templates and fields. |
| taxonomies | Taxonomies: label, slug, hierarchical, template and has_page (false removes the public term URL). |
| options | Global options pages — fields only, no slug or listing. |
| forms | Public forms (fields, email subject, success message). |
| members | Site members area (types, fields, routes). |
| i18n | Multi-language: enabled and default language. |
meta_title, meta_description e og_image automatically. Do not redeclare these fields. Templates are bound by file name, never by slug — rename slugs freely without breaking bindings. After any edit, run ./bin/velk schema:validate. ./bin/velk schema:validate.
Field types
Common to every field: name (required), label, help, required, placeholder. field('name') returns the raw value: a scalar for simple fields, an array for gallery/repeater/flexible.
| Type | Notes |
|---|---|
| text | Single-line input. |
| textarea | Long text without formatting. |
| richtext | WYSIWYG (TipTap); saves HTML. Print unescaped, commented /* trusted */. |
| image | One media item, stored as an integer id. Resolve with image_url(). |
| gallery | Ordered list of media ids — pass each through image_url(). |
| select | Dropdown; options as {value: label}. |
| boolean | Checkbox. |
| number | Numeric input. |
| email · url · date | HTML5 input variants. |
| repeater | Repeating group of subfields; declare fields on the field. |
| flexible | Repeater where each row picks a named layout; the saved row carries _layout. |
| link | Composite {title, url, new_tab}; print with link_tag(). Accepts mailto:, anchors and relative paths. |
| item | Reference to another item (combobox, published only). Declare item_type; resolve with item(field('x')). |
| group | Organization only: collapsible box in the admin. Storage stays flat. |
| tabs | Organization only: tabs in the editor. Two tabs cannot reuse a field name. |
Templates
Resolution order:
- Structural page: template → fallback page.php
page.php - Admin-created page: file of the chosen template → page.php
page.php - Item: the item's own template (if any) → the type's template → single.php
single.php - Item archive: archive_template → archive.php
archive.php - Taxonomy archive: the taxonomy's template → taxonomy.php
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() with_context() lets field(), the_title() and friends work without passing the record around. Each template type gets one context variable:
| Template | Variable | Content |
|---|---|---|
| page-*.php | $page | ->record is the page |
| single-*.php | $item | ->record is the item |
| archive-*.php | $archive | ->records is the list of items |
| taxonomy-*.php | $term | ->record is the term, ->records the items |
Helpers
All global in templates (defined in core/helpers.php). core/helpers.php).
Output and context
| Helper | Does |
|---|---|
| e($value) | Escapes for HTML. Use on all output — except trusted richtext. |
| dd(...$v) | Debug: dumps and exits. |
| field($name, $default) | Field from the current context (language already resolved). |
| the_title() · the_slug() · the_url() | Basics of the current record. |
| with_context($ctx, $fn) | Runs the block with that record as context. |
Content
| Helper | Does |
|---|---|
| items($type, $args) | Lists items — limit, offset, status (default published), term, order. |
| item($id) | One item (or null) — resolves item fields. |
| terms($taxonomy) | Terms of a taxonomy. |
| the_terms($taxonomy) | Terms of the current item. |
URLs and media
| Helper | Does |
|---|---|
| url($path) | Navigation link with the install base path (and active language). |
| absolute_url($path) | Full URL (scheme + host) — canonical, og:url, sitemap. |
| asset($path) · admin_url() · base_path() | Theme assets, admin URL, install base. |
| image_url($value, $size) | Resolves id/URL/array to a URL; '' when empty — check before the <img>. |
| image_alt($value, $fallback) | Alt text (language-aware). |
| media($id) | The media model (width/height/mime). |
| link_tag($link, $text, $attrs) | Safe <a> for a link field; a new tab gets rel="noopener". |
Layout and misc
| Helper | Does |
|---|---|
| get_header() · get_footer() | Include theme/partials/header.php and footer.php. |
| partial($name, $data) | Includes a partial with data. |
| option($path, $default) · options($key) | Global options by dot path (option('contact.phone')). |
| slugify($string) · config($key) · site($key) | Utilities. |
URLs & routes
/→ home page/{page-key}/{page-key} → other structural pages (or the explicit URL declared)/{type-slug}→ archive ·/{type-slug}/{item-slug}→ item/{taxonomy-slug}/{term-slug}→ term archive/admin→ panel ·/uploads/...→ media ·/theme/...→ static theme assets (no PHP)
The base path is auto-detected from SCRIPT_NAME, so the site works installed at the root or in a subdirectory (site.com/velk/) with no configuration — as long as the theme always uses the URL helpers. If an exotic host gets detection wrong: 'base_path' => '/velk' in config/env.php. 'base_path' => '/velk' em config/env.php.
/blog by hand in a template: the day the site moves folder, it breaks. url('/blog') does not break. Media
Biblioteca em /admin/mediaLibrary at /admin/media: upload (jpeg, png, gif, webp, svg, pdf — up to 25 MB), alt text, copy URL and deletion that cleans up generated sizes. File names are slugified; collisions get a -1, -2… suffix.
storage/uploads/{file}.{ext} originais
storage/uploads/thumb/{file}.{ext} gerado sob demanda (GD)
storage/uploads/hero/{file}.{ext} gerado sob demanda (GD)
The sizes come from image_sizes in site.json: crop: true cuts exactly, false fits proportionally. In the theme: image_url(field('photo'), 'thumb').
Global options
"options": {
"contact": {
"label": "Contact",
"fields": [
{ "name": "phone", "type": "text", "label": "Phone" }
]
}
}
Each entry becomes a sidebar link in the admin and a fields-only form. Read it with option('contact.phone'): the dot path reaches into groups and repeater rows (option('contact.social.0.url')). Everything from one page comes out with options('contact'). Stored as a single JSON row in settings, cached in-process: 50 calls, one query.
Forms
"forms": {
"contact": {
"label": "Contact",
"subject": "New contact: {{name}}",
"success_message": "Message sent.",
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true }
]
}
}
Accepted types: text, email, tel, url, number, textarea, checkbox, hidden, select (with required and maxlength). The subject accepts {{field}} placeholders filled from the submission. {{field}} filled in with the submission.
The theme writes the form HTML:
form_url('contact')no action (POST)csrf_field()+form_honeypot()inside the formform_status('contact')to read the flash: status, message, values and errors
The cycle: validates CSRF + honeypot + fields, saves to form_submissions, emails the recipients set in /admin/forms/contact and redirects back. A failed email never loses the submission.
Members
Accounts for the public site — tables, authentication and sessions separate from admin users. No predefined roles: each site declares its own types and the theme decides what each can access.
"members": {
"enabled": true,
"types": {
"client": { "label": "Client",
"fields": [ { "name": "company", "type": "text" } ] }
},
"routes": { "login": "/login" }
}
| Helper | Does |
|---|---|
| member_enabled() · member_check() | Feature on? Anyone logged in? |
| member() · member_field('x') · member_is('type') | Current member, their field, type check. |
| member_require() · member_require_type('type') | Gates the page; redirects those who cannot. |
| member_route('login') · member_status() | Resolved route and last auth flash. |
Public registration is off (public_registration: false) — by default only admins create accounts. The theme builds the login/register/reset forms against the engine's POST endpoints, always with csrf_field(). csrf_field().
Multi-language
Three independent layers, all native:
- Fixed theme strings embrulhe em
t('Read more')wrap in t('Read more'); catalog built by i18n:scan and translated at /admin/translations. - Structural URLs: slugs of pages, types and taxonomies per language: /blog/post-x becomes /en/news/post-x.
- Per-record content title, slug and fields with *_translations columns and language tabs in the editor itself.
"i18n": { "enabled": true, "default": "en" }
The other languages are registered at /admin/languages, each with a URL prefix. In the templates nothing changes: field(), the_title() e option() already resolve the active language with a fallback to the default, so you write the template once.
| Helper | Does |
|---|---|
| lang() · lang_is('en') · available_languages() | Current language and language list. |
| url('/about', 'en') · lang_url('en') | URL in another language; the current page in another language. |
| lang_switcher() | Ready-made data to build the language selector. |
| the_html_lang() · the_hreflangs() · the_canonical() | The <html> attribute, alternate links and canonical. |
Non-default language URLs are strict — no translated slug, no URL in that language (no duplicate content for Google). The sitemap emits hreflang only when the translation really exists.
CLI
./bin/velk <comando>./bin/velk <command>, from the project root. Almost everything the panel does, the CLI does — that is what lets the AI run the site.
Setup and schema
| Command | Does |
|---|---|
| install | Full install: database, migrations, admin, project files. |
| migrate · migrate:status | Applies / lists pending migrations. |
| schema:validate | Validates the site.json — run after every edit. |
| page:sync | Creates/updates the page rows declared in the schema. |
| serve [port] | Dev server (default :8080). |
| user:create | New admin user. |
| files:init | Copies .htaccess and robots.txt from the .example files. |
| i18n:scan | Scans the theme for t() and registers the strings. |
Content
| Command | Does |
|---|---|
| item:types · item:schema <type> | Lists types; shows a type's fields (read before writing). |
| item:list · item:get | Lists (with filters) and reads an item as JSON. |
| item:create --json= · item:update --json= | Creates and updates (merge) by JSON. |
| item:publish · item:unpublish · item:delete --confirm | Changes status; deletes (requires --confirm). |
| page:list · page:schema · page:get · page:update | The same, for pages. |
Useful flags
--format=jsonmachine-readable output--dry-runvalidate without saving--json-stdin--json-stdin — payload via stdin, no shell quoting hell
Deploy & upgrades
Deploy
- Shared hosting without SSH: upload the files over FTP/panel — auto-migrate completes the install on the first request.
- With SSH (VPS, cloud, cPanel with terminal): the official skill covers the whole flow — files, database, storage and robots.txt with the production URL.
- Subdirectory: works with no configuration, because the URL helpers prefix the base path themselves.
In production, fill in APP_URL in the .env: o sitemap e o absolute_url() depend on it.
Upgrades
The engine is a versioned dependency, shipped as a tarball with a checksum. The upgrade reads the version in .velk-version, downloads the new one, verifies the sha256 and replaces only the engine paths: theme/, storage/, .env, .htaccess and robots.txt stay as they are. Ask the AI:
Update Velk in this project to the latest version.
The full reference lives in the skill
Install the official skill and your AI starts consulting this whole reference (schema, helpers, CLI and deploy flows) without leaving the chat.