Add complete Acai development documentation

- CLAUDE.md: expanded with critical rules, hook syntax, DB conventions, web-base endpoint
- docs/modular-system.md: modules, general sections, global vars, multiv2
- docs/builder-fields.md: all field types, c-if/c-for/c-class, c-form, built-in components
- docs/twig-filters.md: get, hook, module, queryDB, imagec, translate, raw, etc.
- docs/hooks-and-api.md: PHP hooks, CmsApi CRUD, table schemas, field formats
- docs/css-js-conventions.md: Tailwind, BEM, CSS vars, Vue 3 integration, CmsApi JS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jordan
2026-03-09 18:53:10 +00:00
parent 8f76455c96
commit 963174b4c4
6 changed files with 645 additions and 67 deletions

View File

@@ -6,7 +6,7 @@ This is an Acai CMS website project. Follow these instructions when working with
- The site runs in Docker, typically at **http://localhost:8080**
- You can make HTTP requests to test pages, APIs, or form submissions
- If you need to inspect the live site, use browser tools or HTTP requests to localhost:8080
- If you need to inspect the live site, use browser tools (Playwright MCP) or HTTP requests to localhost:8080
## Project Structure
@@ -15,10 +15,12 @@ This is an Acai CMS website project. Follow these instructions when working with
├── template/estandar/
│ ├── modulos/ # Builder modules (visual components)
│ │ └── <module-id>/
│ │ ├── index-base.tpl # Twig template (source)
│ │ ├── index.tpl # Compiled template (auto-generated, do NOT edit)
│ │ ├── index-base.tpl # Twig template (source — EDIT THIS)
│ │ ├── style.css # Module styles
│ │ └── script.js # Module JavaScript
│ │ ├── index.tpl # Compiled (auto-generated, do NOT edit)
│ │ ├── index-twig.tpl # Compiled (auto-generated, do NOT edit)
│ │ └── builder.json # Compiled builder vars (auto-generated, do NOT edit)
│ ├── css/ # Global CSS
│ └── js/ # Global JavaScript
├── hooks/ # PHP hooks (server-side logic)
@@ -38,17 +40,28 @@ This is an Acai CMS website project. Follow these instructions when working with
## Key Concepts
### Modules (`template/estandar/modulos/`)
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig), CSS, and JS. Modules are placed on pages via the drag-and-drop builder.
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig + Acai attributes), CSS, and JS. Modules are placed on pages via the drag-and-drop builder. The editable file is always `index-base.tpl`.
- Include other modules: `<module_id :param1="value1"></module_id>`
- Each module instance gets a unique `section_id` variable for anchors/scoping
- Use `interno` variable to detect CMS editor mode vs public view
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
### General Sections
Reusable layout blocks (header, footer, sidebars) that appear across multiple pages. They use the same Twig engine as modules but serve a structural purpose.
Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules.
- Upload fields return arrays: `thisrecord.image[0].urlPath`
- Foreign keys use `_num` suffix: `category_num`
See [docs/modular-system.md](docs/modular-system.md) for details.
### Hooks (`hooks/`)
PHP files that execute server-side logic at specific points: before/after page load, on form submission, on API calls, scheduled tasks, etc. Hooks extend the CMS behavior without modifying the core.
PHP files that execute server-side logic. Triggered by:
- Twig filter: `'hooks/module_id/' | hook({param: value})`
- HTML tag: `<hook result="var" endpoint="/hooks/module_id/" :param="value"></hook>`
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
- Form action: via `c-form` attribute
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
@@ -57,25 +70,41 @@ See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
When the site is running in Docker, you can connect to the database:
- **Host:** `127.0.0.1`
- **Port:** Check `.docker/.env` or `docker-compose.yml` for the mapped port (usually 3307+)
- **Port:** Check `.docker/docker-compose.yml` for the mapped port (usually 3307+)
- **Credentials:** Read from `.docker/.env`:
- `DB_USERNAME`
- `DB_PASSWORD`
- `DB_DATABASE`
You can also exec into the container:
```bash
docker exec -it dw-<project-name>-db mysql -u root -p<password> <database>
```
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
## Acai Core (web-base)
The project workspace contains only the **customization layer** (modules, hooks, schemas, uploads). The CMS core (routing, rendering engine, admin panel, APIs) lives in a separate directory called **web-base** that is mounted as a Docker volume.
If you need to understand core behavior (Twig filters, CmsApi methods, routing, etc.), the web-base path can be obtained from the plugin settings. Do NOT modify web-base files — they are shared across all projects.
The web-base path can be obtained via: `GET http://localhost:9090/api/web-base-path`
Do NOT modify web-base files — they are shared across all projects.
## Critical Rules
1. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
2. Use Twig **filters** (with `|`), never Twig functions
3. Table names without `cms_` prefix everywhere
4. Primary key is `num`, never `id`
5. Upload fields are arrays — access with `[0].urlPath`
6. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
7. Twig concatenation uses `~` operator: `'value=' ~ variable`
8. `enlace` (link) fields already include slashes
## Documentation
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, Twig syntax, builder vars, custom filters
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, database operations
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, c-form, c-if/c-for/c-class, data-field-type
- [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.)
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, database operations
- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS patterns, Tailwind, BEM, Vue 3 integration

View File

@@ -1,32 +1,213 @@
# Builder Fields & Attributes
# Builder Fields & Acai Attributes
## Field Types (`data-field-type`)
The builder uses `data-field-type` attributes to define editable areas in module templates.
The builder uses `data-field-type` attributes on HTML elements to define editable areas.
<!-- TODO: Document available field types (text, image, richtext, link, repeater, etc.) -->
| Type | Description | Returns |
|------|-------------|---------|
| `textfield` | Single line text | String |
| `headfield` | Heading text (generates `_tag` variable for semantic tag: h1-h6) | String |
| `textbox` | Multi-line text | String |
| `wysiwyg` | Rich text editor (HTML output) | HTML string |
| `link` | URL field (already includes slashes) | String |
| `upload` | Single image/file | Array: `[0].urlPath`, `[0].info1` (alt), `[0].info2-4` |
| `uploadMulti` | Multiple images | Iterable: `item.urlPath` |
| `list` | Dropdown (fixed options or from table) | String or foreign key num |
| `multiv2` | Repeatable group of fields | Array of objects |
### headfield Example
```html
<{{ title_tag | default('h2') }} data-field-type="headfield" class="text-3xl font-bold">
Section Title
</{{ title_tag | default('h2') }}>
```
### upload Example
```html
<!-- Single image -->
<img data-field-type="upload"
src="{{ image[0].urlPath }}"
alt="{{ image[0].info1 }}"
class="w-full rounded" />
<!-- Multiple images -->
<div c-for="photo in gallery">
<img src="{{ photo.urlPath }}" alt="{{ photo.info1 }}" />
</div>
```
### list Example
```html
<!-- Fixed options -->
<select data-field-type="list" data-list-options="option1,option2,option3">
<option>option1</option>
</select>
<!-- From table -->
<select data-field-type="list" data-list-table="categories" data-list-field="name">
<option>Category</option>
</select>
```
## Conditional Attributes
## Acai Attributes
### `c-if`
### `c-if` — Conditional Rendering
<!-- TODO: Document c-if syntax and usage for conditional rendering -->
```html
<!-- Boolean check -->
<div c-if="showBanner">Banner content</div>
### `c-for`
<!-- Equality check (uses = not ==) -->
<div c-if="layout = 'grid'">Grid layout</div>
<!-- TODO: Document c-for syntax for repeating elements -->
<!-- Variable exists / is not empty -->
<div c-if="subtitle">{{ subtitle }}</div>
```
### `c-class`
### `c-else`
<!-- TODO: Document c-class for conditional CSS classes -->
Must immediately follow the `c-if` element:
```html
<div c-if="image">
<img src="{{ image[0].urlPath }}" />
</div>
<div c-else>
<p>No image available</p>
</div>
```
### `c-for` — Iteration
```html
<!-- Over array/multiv2 -->
<div c-for="item in record.features">
<h3>{{ item.title }}</h3>
</div>
<!-- Over database table -->
<div c-for="product in products" c-where="active = 1" c-order="orden ASC" c-limit="6">
<h3>{{ product.nombre }}</h3>
</div>
```
Available inside loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`
### `c-class` — Dynamic CSS Classes
```html
<div c-class="{ 'bg-blue-500': isActive, 'text-white': isActive, 'hidden': !showElement }">
Content
</div>
```
### `c-hidden` — Hidden Elements
Element is not rendered but can declare builder variables:
```html
<div c-hidden="true">
<input data-field-type="textfield" value="default config value" />
</div>
```
### `c-required` — Conditional Required Fields
```html
<input type="text" name="company" c-required="userType = 'business'" />
```
## Forms (`c-form`)
<!-- TODO: Document c-form usage for builder-integrated forms -->
Complete form handling with automatic validation, storage, and email sending.
```html
<form c-form
tableName="'contacto'"
mailRecord="['correos', 'CONTACTO']"
sendTo="'admin@domain.com'"
sendToClient="'email'"
captcha="true"
honeypot="true"
messageOK="'Mensaje enviado correctamente'"
messageKO="'Error al enviar el mensaje'"
redirect="'/gracias/'"
attachFiles="true"
showImages="true"
>
<input type="text" name="nombre" required placeholder="Nombre" />
<input type="email" name="email" required placeholder="Email" />
<textarea name="mensaje" required placeholder="Mensaje"></textarea>
<captcha/>
<button type="submit">Enviar</button>
</form>
```
### c-form Attributes
| Attribute | Description |
|-----------|-------------|
| `tableName="'table'"` | Store submissions in database table |
| `mailRecord="['correos', 'ID']"` | Email template from `correos` table |
| `sendTo="'email@domain.com'"` | Recipient email(s), comma-separated |
| `sendToClient="'fieldname'"` | Field containing client's email for auto-reply |
| `captcha="true"` | Enable Google reCAPTCHA |
| `honeypot="true"` | Anti-spam hidden field |
| `messageOK="'text'"` | Success message |
| `messageKO="'text'"` | Error message |
| `redirect="'/path/'"` | Redirect after successful submit |
| `attachFiles="true"` | Attach uploaded files to email |
| `showImages="true"` | Show image thumbnails in email |
## Builder Variables Access
## Built-in Components
<!-- TODO: Document how to access and use builder variables in templates -->
### Carousel (`c-tns-wrapper`)
```html
<div class="c-tns-wrapper"
data-responsive='{"0":1,"768":2,"1024":3}'
data-speed="400"
data-nav="true"
data-autoplay-timeout="3000">
<div c-for="slide in record.slides">
<img src="{{ slide.image[0].urlPath }}" />
</div>
</div>
```
### Lightbox
```html
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
<img src="{{ image[0].urlPath | imagec(400) }}" />
</a>
```
### Breadcrumb
```html
<breadCrumb/>
```
### Animate On Scroll (AOS)
```html
<div data-aos="fade-up" data-aos-delay="200">
Animated content
</div>
```
### Lazy Loading
```html
<img class="lazyload" data-src="{{ image[0].urlPath }}" />
<!-- or -->
<img data-lazy="true" src="{{ image[0].urlPath }}" />
```

101
docs/css-js-conventions.md Normal file
View File

@@ -0,0 +1,101 @@
# CSS & JavaScript Conventions
## CSS
### Tailwind First
Use Tailwind CSS as the primary styling method. Only use custom CSS when Tailwind is insufficient.
```html
<!-- Good: Tailwind classes -->
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
</div>
<!-- Custom CSS only when needed (animations, complex selectors, etc.) -->
```
### BEM for Custom CSS
When custom CSS is needed, scope everything under a root class using BEM naming:
```css
/* Root class in kebab-case */
.hero-section { }
.hero-section__title { }
.hero-section__image { }
.hero-section--dark { }
```
Never use global classes without a module prefix.
### CSS Variables
Acai provides theme variables:
```css
var(--main-color) /* Primary brand color */
var(--main-color-light) /* Lighter variant */
var(--main-color-dark) /* Darker variant */
```
### Utility Classes (Built-in)
| Class | Description |
|-------|-------------|
| `transition3s` | 0.3s smooth transition |
| `click-a-child` | Makes parent clickable via child `<a>` tag |
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Text truncation with ellipsis |
| `filter-white` | CSS filter to make images/icons white |
| `lazyload` | Lazy loading (use with `data-src`) |
## JavaScript
### Module Scripts (`script.js`)
Keep JavaScript scoped to the module. Use `section_id` for targeting:
```js
// Scope to this module instance
const section = document.getElementById('{{ section_id }}');
if (section) {
const buttons = section.querySelectorAll('.btn');
// ...
}
```
### CmsApi (Client-Side)
```js
// Call a hook
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
console.log(response);
});
```
### Vue 3 Integration
For complex interactivity, use Vue 3 via CDN with Composition API:
```html
<div id="app-{{ section_id }}">
<p>${ message }</p>
<button @click="increment">${ count }</button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
delimiters: ['${', '}'], // Avoid conflict with Twig {{ }}
setup() {
const message = ref('Hello');
const count = ref(0);
const increment = () => count.value++;
return { message, count, increment };
}
}).mount('#app-{{ section_id }}');
</script>
```
**Important:** Use `'${'` and `'}'` as Vue delimiters to avoid conflicts with Twig's `{{ }}` syntax.

View File

@@ -2,43 +2,141 @@
## Hooks
Hooks are PHP files in the `hooks/` directory that execute server-side logic at specific lifecycle points.
Hooks are PHP files in the `hooks/` directory that execute server-side logic. They can also live inside a module at `template/estandar/modulos/<module-id>/hook.php`.
### Hook Types
### How to Call Hooks
<!-- TODO: Document hook types (before_page, after_page, on_form, on_api, cron, etc.) -->
**From Twig:**
```twig
{{ 'hooks/module_id/' | hook({param1: 'value1', param2: variable}) }}
```
### Hook File Naming
**From HTML (with result):**
```html
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
<p>{{ myVar }}</p>
```
<!-- TODO: Document naming conventions and how the CMS discovers hooks -->
**From JavaScript:**
```js
CmsApi.hook('/hooks/module_id/', { param1: 'value1' }, function(response) {
console.log(response);
});
```
### Hook Examples
**From c-form:**
Hooks are automatically triggered on form submission when configured.
<!-- TODO: Add practical examples of common hooks -->
### Hook Parameters
Parameters are received as PHP variables:
```php
<?php
// Called with: 'hooks/my_hook/' | hook({category: 'electronics', limit: 10})
// Available as:
$category; // 'electronics'
$limit; // 10
```
### Hook Return Values
Hooks can `echo` or `return` values. When called from Twig or `<hook>` tag, the output is captured into the result variable.
## CmsApi
## CmsApi (PHP)
The `CmsApi` class provides methods to interact with the CMS from hooks.
Server-side API for database operations. Available in all hooks.
<!-- TODO: Document main CmsApi methods (getPage, getRecord, createRecord, updateRecord, etc.) -->
### Read Records
```php
// Get all records
$products = CmsApi::get('productos');
// With WHERE condition
$active = CmsApi::get('productos', ['active' => 1]);
// With order and limit
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
// With operators
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]);
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]);
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]);
```
### Insert Records
```php
$newRecord = CmsApi::insert('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
'mensaje' => 'Hello',
]);
```
### Update Records
```php
CmsApi::update('productos',
['precio' => 29.99, 'activo' => 1], // fields to update
['num' => 42] // where condition
);
```
### Delete Records
```php
CmsApi::delete('productos', ['num' => 42]);
```
### Important Rules
- Table names **without** `cms_` prefix
- Primary key is always `num`, never `id`
- Upload fields are handled separately (not via insert/update)
- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
## CmsApi (JavaScript — Client-Side)
```js
// Hook call
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
// response is the hook output
});
// Record operations (if exposed via hooks)
CmsApi.get('tableName', { where: conditions }, function(records) {
// records array
});
```
## CocoDB
`CocoDB` is the database abstraction layer for Acai.
Low-level database abstraction layer. Use when CmsApi is too high-level.
<!-- TODO: Document CocoDB usage (query, insert, update, delete, transactions) -->
<!-- TODO: Document CocoDB methods (query, insert, update, delete, transactions) -->
## Database Operations
## Table Schemas
### Direct Queries
Table schemas are stored as JSON in `cms/data/schema/`. Each file defines:
- Field names and types
- Validation rules
- Relationships (foreign keys)
- Display configuration
<!-- TODO: Document how to run raw SQL queries from hooks -->
### Field Format Types
### Table Schemas
Table schemas are stored as JSON files in `cms/data/schema/`. Each file defines the fields, types, and configuration of a CMS table.
<!-- TODO: Document schema format and how to create/modify tables -->
| Type | PHP Format | Notes |
|------|-----------|-------|
| Text | String | Plain text |
| Date/time | `YYYY-MM-DD HH:mm:ss` | MySQL datetime format |
| Checkbox | `1` or `0` | Boolean as integer |
| WYSIWYG | HTML string | Rich text with Tailwind classes |
| List | String or num | Foreign key if linked to table |
| Multivalores | JSON string | Serialized array |
| Upload | — | Handled separately, never in insert/update |

View File

@@ -4,51 +4,102 @@
Modules are the visual building blocks of Acai websites. Each module lives in `template/estandar/modulos/<module-id>/`.
### File structure
### File Structure
```
<module-id>/
├── index-base.tpl # Source Twig template (EDIT THIS)
├── index-base.tpl # Source template (EDIT THIS)
├── index.tpl # Compiled output (auto-generated, do NOT edit)
├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
├── style.css # Module-scoped styles
└── script.js # Module JavaScript
```
### Twig Templates
### Template Syntax
<!-- TODO: Document Twig syntax, variable access, builder vars -->
Templates use a hybrid of **Twig** and **Acai attributes**. The source file is always `index-base.tpl`.
### Custom Twig Filters
```html
<section class="hero-section" id="{{ section_id }}">
<div class="container mx-auto px-4">
<h2 data-field-type="headfield" class="text-3xl font-bold">
Title here
</h2>
<p data-field-type="textbox" class="text-lg text-gray-600">
Description text
</p>
<img data-field-type="upload" src="placeholder.jpg" class="w-full rounded-lg" />
<a data-field-type="link" href="#" class="btn">Call to action</a>
</div>
</section>
```
<!-- TODO: Document custom filters available in Acai (e.g., |translate, |slugify, etc.) -->
### Including Modules from Other Modules
### Builder Variables
```html
<module_id :param1="value1" :param2="'string value'"></module_id>
```
<!-- TODO: Document how builder vars work, how to access them in templates -->
Parameters are received as variables inside the included module.
### Calling Modules from Other Modules
### Global Variables
<!-- TODO: Document how to include/call modules from other modules or general sections -->
| Variable | Description |
|----------|-------------|
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
| `interno` | `true` when viewing in CMS editor, `false` on public site |
| `server.HTTP_HOST` | Current domain |
| `loop.index` | 1-based iteration index (inside `c-for`) |
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
## General Sections
General sections are reusable layout blocks (headers, footers, sidebars) shared across pages.
General sections are database-backed templates used for record views, headers, footers, and reusable layouts. They use the same template engine as modules.
<!-- TODO: Document how general sections differ from modules, where they live, how to create/edit them -->
### Key Differences from Modules
- Access record data via the `thisrecord` variable
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
- Saved via `save_general_section()` (not `save_module()`)
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
### Example: Record Template
```html
<article class="product-card">
<img src="{{ thisrecord.imagen[0].urlPath }}"
alt="{{ thisrecord.imagen[0].info1 }}"
class="w-full h-64 object-cover" />
<h3 class="text-xl font-semibold">{{ thisrecord.nombre }}</h3>
<p class="text-gray-600">{{ thisrecord.descripcion | raw }}</p>
<span class="text-2xl font-bold">{{ thisrecord.precio }}€</span>
</article>
```
### Variable Assignment
Use `<set>` tag to create variables from queries:
```html
<set :categories="'categorias' | get()"></set>
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
```
## CSS & JavaScript
## Repeatable Content (multiv2)
### Module Styles (`style.css`)
The `multiv2` builder field type creates repeatable groups of fields:
<!-- TODO: Document scoping rules, CSS conventions -->
```html
<div c-for="item in record.items">
<h3 data-field-type="textfield">{{ item.title }}</h3>
<p data-field-type="textbox">{{ item.description }}</p>
<img data-field-type="upload" src="{{ item.image }}" />
</div>
```
### Module Scripts (`script.js`)
<!-- TODO: Document JS conventions, lifecycle, event handling -->
## Builder Field Types
<!-- TODO: Document data-field-type usage, c-form, c-if, c-for, c-class attributes -->
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.

118
docs/twig-filters.md Normal file
View File

@@ -0,0 +1,118 @@
# Twig Filters Reference
Acai uses Twig **filters** (with `|` pipe syntax). Never use Twig functions — only filters are supported.
## Database Queries
### `get` — Query Table
```twig
{# All records #}
{% set products = 'productos' | get() %}
{# With WHERE #}
{% set active = 'productos' | get({activo: 1}) %}
{# With WHERE + ORDER + LIMIT #}
{% set latest = 'noticias' | get({publicado: 1}, 'fecha DESC', 6) %}
{# Single record (first result) #}
{% set product = 'productos' | get({num: 42}) %}
{{ product[0].nombre }}
```
### `queryDB` — Raw SQL Query
```twig
{% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %}
```
Note: Raw SQL uses the full table name WITH `cms_` prefix.
## Module & Hook Execution
### `hook` — Execute PHP Hook
```twig
{# Call hook and output result #}
{{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }}
{# Capture into variable #}
{% set result = 'hooks/module_id/' | hook({action: 'getData'}) %}
```
### `module` — Render Another Module
```twig
{{ 'other_module_id' | module({param1: value1}) }}
```
## Text & Content
### `translate` — Translation
```twig
{{ 'Hello' | translate }}
{{ variable | translate }}
```
### `raw` — Render HTML Without Escaping
```twig
{{ record.description | raw }}
```
### `truncate` — Text Truncation
```twig
{{ record.description | truncate(150) }}
```
### `json_decode` — Parse JSON String
```twig
{% set data = jsonString | json_decode %}
{{ data.key }}
```
## Images
### `imagec` — Image Optimization/Resize
```twig
{# Resize to width #}
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
{# In srcset #}
<img src="{{ record.image[0].urlPath | imagec(800) }}"
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
{{ record.image[0].urlPath | imagec(800) }} 800w" />
```
## Operators & Syntax
### Concatenation
Twig uses `~` for string concatenation (not `.` or `+`):
```twig
{{ 'Hello ' ~ name ~ '!' }}
{% set url = '/products/' ~ product.slug ~ '/' %}
```
### Ternary / Default
```twig
{{ title | default('Default Title') }}
{{ isActive ? 'active' : 'inactive' }}
```
### Comparisons in Twig
```twig
{% if items | length > 0 %}
{% if type == 'premium' %}
{% if name is not empty %}
```
Note: In `c-if` attributes, use `=` (single equals) for equality. In Twig `{% if %}` blocks, use `==` (double equals).