24 Commits
Author SHA1 Message Date
Jordan DiazandClaude Fable 5.1 05d4be1af0 chore(seguridad): prevención de secretos en un repo público
gitleaks / gitleaks (push) Canceled after 0s
- .gitignore: .claude/settings.local.json (ahí van las claves), .env, *.pem, *.key
- .githooks/pre-push: bloquea el push si lo que sube contiene claves (gitleaks
  si está instalado; si no, grep de los formatos más comunes). Activar con
  git config core.hooksPath .githooks
- .gitea/workflows/gitleaks.yml: escaneo en cada push y pull request
- CLAUDE.md: regla 14, el repo es público y las claves nunca se versionan

Motivo: en 2026-03 se subió una clave de API en .claude/settings.json. Ya está
revocada y el historial reescrito para quitarla.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPL66q9F33YZxbLPUT7KW8
2026-09-11 15:20:12 +00:00
Jordan Diaz ac6c8e1fbd mas 2026-04-01 15:25:06 +00:00
Jordan Diaz 34db92d5f7 ajustes mios 2026-04-01 15:11:28 +00:00
Jordan Diaz bfb6f8df5b mas 2026-04-01 14:49:19 +00:00
Jordan Diaz db7d85133a mejora mia 2026-04-01 14:39:07 +00:00
Jordan Diaz 730237d1b9 mas cambios codex 2026-04-01 14:14:45 +00:00
Jordan Diaz 586679d84e de codex 2026-04-01 14:08:23 +00:00
Jordan Diaz d00697ca36 ajustes de codex 2026-04-01 13:37:45 +00:00
Jordan Diaz 9526443b4a ajustes codex 2026-04-01 13:31:36 +00:00
Jordan Diaz c51c5222d5 Mas ajustes de codex 2026-04-01 12:57:24 +00:00
Jordan Diaz 555d2cb3c1 Ajusets desde codex 2026-04-01 12:49:26 +00:00
Jordan Diaz 50a0bb9549 fix: update compile and git handlers for permission normalization 2026-04-01 10:42:58 +00:00
Jordan Diaz a90f3e1ef2 fix: update compile and git handlers for permission normalization 2026-04-01 10:39:04 +00:00
Jordan ec98ea6038 Update CLAUDE.md 2026-03-31 13:23:16 +01:00
Jordan 383f4bbda0 Update CLAUDE.md 2026-03-31 13:22:50 +01:00
Jordan 6d3d904aa6 Update builder-fields.md 2026-03-29 12:42:30 +01:00
Jordan 8f38d9ea7d fdsfds 2026-03-28 22:54:31 +00:00
Jordan 14c69ed0d2 otro 2026-03-28 22:45:06 +00:00
Jordan dee931f100 Update CLAUDE.md 2026-03-28 22:12:30 +00:00
Jordan adfbc1ded6 Update mcp-tools-reference.md 2026-03-28 21:54:26 +00:00
Jordan a0a14921cf ajustes 2026-03-28 13:38:38 +00:00
Jordan fe0db50d12 Update settings.json 2026-03-27 18:26:13 +00:00
Jordan 490fe6104a Update settings.json 2026-03-27 18:18:42 +00:00
Jordan 84690ce1d3 Update settings.json 2026-03-27 17:52:43 +00:00
10 changed files with 550 additions and 176 deletions
+27
View File
@@ -0,0 +1,27 @@
# Escaneo de secretos en cada push y pull request. Este repo es PUBLICO.
# Falla si gitleaks encuentra una clave en los commits nuevos.
name: gitleaks
on:
push:
pull_request:
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Instalar gitleaks
run: |
set -e
VERSION=8.21.2
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tgz
tar -xzf /tmp/gitleaks.tgz -C /tmp gitleaks
sudo install -m 755 /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
- name: Escanear el historial
run: gitleaks git --no-banner --redact --verbose .
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
# Hook pre-push: bloquea el push si lo que se va a subir contiene secretos.
#
# Activar una vez por clon: git config core.hooksPath .githooks
#
# Este repo es PUBLICO: un secreto que llega al remoto queda expuesto aunque se
# borre en el commit siguiente (sigue en el historial). En 2026-03 se filtro asi
# una clave de API pegada en .claude/settings.json.
#
# Si gitleaks esta instalado (https://github.com/gitleaks/gitleaks) se usa el;
# si no, se aplica un escaneo minimo con grep sobre los formatos mas comunes.
set -u
remote="$1"
z40=0000000000000000000000000000000000000000
fallo=0
while read -r local_ref local_sha remote_ref remote_sha; do
[ "$local_sha" = "$z40" ] && continue # borrado de rama: nada que escanear
if [ "$remote_sha" = "$z40" ]; then
rango="$local_sha" # rama nueva: todo lo alcanzable
else
rango="$remote_sha..$local_sha"
fi
if command -v gitleaks >/dev/null 2>&1; then
if ! gitleaks git --no-banner --redact --log-opts="$rango" . ; then
fallo=1
fi
else
# Escaneo minimo: lineas AÑADIDAS en el rango con formatos de clave conocidos.
hits=$(git log -p --no-color "$rango" -- . ':!*.min.*' ':!*.lock' \
| grep -E '^\+' \
| grep -E 'AIza[0-9A-Za-z_-]{35}|sk_(live|test)_[0-9A-Za-z]{20,}|ghp_[0-9A-Za-z]{30,}|xox[bp]-[0-9A-Za-z-]{10,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY|(ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|OPENAI_API_KEY|API_KEY|SECRET|PASSWORD|TOKEN)["'"'"']?[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{8,}' \
| grep -vE '\$\{?[A-Z_]+\}?|\*\*\*|EJEMPLO|EXAMPLE|placeholder' )
if [ -n "$hits" ]; then
echo "pre-push: posibles secretos en lo que vas a subir ($rango):" >&2
printf '%s\n' "$hits" | cut -c1-120 | sed 's/^/ /' >&2
echo "pre-push: instala gitleaks para un escaneo completo. Si es un falso positivo, --no-verify." >&2
fallo=1
fi
fi
done
if [ "$fallo" -ne 0 ]; then
echo "pre-push: PUSH BLOQUEADO. Quita el secreto del commit (git commit --amend o rebase) y rota la clave si ya la has pegado en algun sitio." >&2
exit 1
fi
exit 0
+14
View File
@@ -0,0 +1,14 @@
# Configuracion LOCAL de Claude Code: aqui van las claves (env con ANTHROPIC_*,
# tokens de proveedores...). NUNCA en .claude/settings.json, que es la plantilla
# versionada y este repo es PUBLICO. En 2026-03 se filtro una clave por ahi.
.claude/settings.local.json
# Secretos y entorno
.env
.env.*
*.pem
*.key
# Ruido de sistema
.DS_Store
Thumbs.db
+41 -12
View File
@@ -48,6 +48,16 @@ Visual components that the site builder uses. Each module is a self-contained un
See [docs/modular-system.md](docs/modular-system.md) for detailed rules. See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
### Pages
Every record with an `enlace` field is a page. Pages are either **Builder** (modular) or **Standard**:
- **Builder**: `controlador` = `cms/lib/plugins/builder_saas/controlador.php` — content via modules
- **Standard**: `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php` — content in record fields
**Critical**: Never change `enlace` or `controlador` of existing pages unless explicitly asked.
See [docs/pages-and-records.md](docs/pages-and-records.md) for full details.
### General Sections ### General Sections
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. 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.
@@ -63,6 +73,20 @@ PHP files that execute server-side logic. Triggered by:
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)` - JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
- Form action: via `c-form` attribute - Form action: via `c-form` attribute
There are two valid hook locations:
- Global hooks in `hooks/hooks.<hook-id>.php` for reusable/shared server-side logic
- Module-specific hooks in `template/estandar/modulos/<module-id>/hook.php` for logic owned by a single module
How to reference them:
- Global hook `hooks/hooks.calcular_precio.php` -> endpoint `/hooks/calcular_precio/`
- Module hook `template/estandar/modulos/hero_banner/hook.php` -> endpoint `/hooks/hero_banner/`
- Module hook `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` -> endpoint `/hooks/buscadorapartados_hjd8s/`
Rule of thumb:
- If the logic is only used by one module, prefer that module's `hook.php`
- If the logic will be reused by several modules/pages, create a global hook in `hooks/`
- Return arrays from hooks; do not use `echo json_encode(...)` or `exit`
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage. See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
## Database Access ## Database Access
@@ -93,16 +117,19 @@ Do NOT modify web-base files — they are shared across all projects.
## Critical Rules ## Critical Rules
1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action. 1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action.
2. **NEVER use `mkdir` to create directories.** Instead, use the `Write` tool to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file. 2. **NEVER use `mkdir` to create directories.** Instead, use `acai-write` to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file.
3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated 3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
3. **After editing any `index-base.tpl`, ALWAYS call the `compile_module` MCP tool** to compile the module/section. This is mandatory — without compilation, changes won't take effect in the CMS. 4. Editing or creating any `index-base.tpl` through `acai-write` or `acai-line-replace` triggers automatic compilation. `compile_module` is only for manual recovery when you need to force a recompile without changing the file.
4. Use Twig **filters** (with `|`), never Twig functions 5. `script.js` and `style.css` are static files — do NOT use Twig syntax inside them. Pass dynamic values from `index-base.tpl` via `data-*` attributes.
5. Table names without `cms_` prefix everywhere 6. Use Twig **filters** (with `|`), never Twig functions
6. Primary key is `num`, never `id` 7. Table names without `cms_` prefix everywhere
7. Upload fields are arrays — access with `[0].urlPath` 8. Primary key is `num`, never `id`
8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed 9. Upload fields are arrays — access with `[0].urlPath`
9. Twig concatenation uses `~` operator: `'value=' ~ variable` 10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
10. `enlace` (link) fields already include slashes 11. Twig concatenation uses `~` operator: `'value=' ~ variable`
12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
14. **This repository is PUBLIC. NEVER commit secrets** (API keys, `ANTHROPIC_*` tokens, passwords). Local Claude Code config with credentials goes in `.claude/settings.local.json` (git-ignored), never in `.claude/settings.json`. Enable the secret-scanning hook once per clone: `git config core.hooksPath .githooks`. A leaked key stays in git history even after deleting it: rotate it immediately.
## MCP Tools ## MCP Tools
@@ -111,9 +138,9 @@ This project has MCP tools for managing modules, records, media, and more. **Bef
See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows. See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows.
Key workflows: Key workflows:
- **Create module**: `create_module` → `add_module_to_record` → `set_module_config_vars` → images - **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → create files with `acai-write` / refine with `acai-line-replace` → automatic compile on `index-base.tpl` → `add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields. Use `compile_module` only if you need a manual recompile without editing the file.
- **Edit module**: read vars → edit `index-base.tpl` → `compile_module` - **Edit module**: `acai-view` → `acai-line-replace` (or `acai-write` for full rewrites) → automatic compile on `index-base.tpl`
- **Add images**: `get_module_config_vars` → `upload_record_image` (fieldName from builder.json relations) - **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image`
- **Generate images**: `generate_image` → `upload_record_image` with returned URL - **Generate images**: `generate_image` → `upload_record_image` with returned URL
## Documentation ## Documentation
@@ -127,4 +154,6 @@ Key workflows:
- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms) - [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms)
- [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components) - [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components)
- [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.) - [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.)
- [docs/pages-and-records.md](docs/pages-and-records.md) — Page types (Builder vs Standard), sections, visibility, critical rules
- [docs/module-creation-guide.md](docs/module-creation-guide.md) — Module creation workflow, style reference, field types
- [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) — MCP tools reference, available tools, workflows - [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) — MCP tools reference, available tools, workflows
+16 -1
View File
@@ -4,6 +4,13 @@
El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas). El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas).
Reglas obligatorias:
- Todo elemento editable con `data-field-type` debe incluir también `data-field-label`
- Si falta `data-field-label`, el builder puede generar variables temporales o incorrectas y el módulo queda mal configurado
- Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto"
Evita también en `index-base.tpl` las clases Tailwind con valores arbitrarios como `text-[44px]`, `font-['Cinzel']` o `leading-[1.1]`. En este stack pueden romper el parseo/compilación del template. Muévelas a `style.css`.
| Label | Variable | | Label | Variable |
|-------|----------| |-------|----------|
| Categoría Noticia | `categoranoticia` | | Categoría Noticia | `categoranoticia` |
@@ -445,7 +452,15 @@ Antes de modificar cualquier módulo, usar `get_module_config_vars` para conocer
- **sectionId**: el `section_id` de la instancia del módulo (ej: `6c6d8`) - **sectionId**: el `section_id` de la instancia del módulo (ej: `6c6d8`)
### Escribir variables de un módulo ### Escribir variables de un módulo
Usar `set_module_config_vars` con los mismos tableName, recordNum y sectionId. La respuesta incluye `configVars` con el `recordNum` del registro `builder_custom` creado/actualizado. Usar `set_module_config_vars` con los mismos tableName, recordNum y sectionId. Pasar todos los valores como strings.
La respuesta incluye `configVars` con el `recordNum` del registro `builder_custom` creado/actualizado y `uploadFields` para imágenes.
**Tipos de almacenamiento (manejado automáticamente):**
- `headfield`, `textfield`, `link`, `textbox`, `wysiwyg`, `upload` → se guardan en tabla `builder_custom`
- `list`, `checkbox`, `colorpicker` → se guardan directamente en el JSON config-vars (no en builder_custom)
No necesitas preocuparte por esto — `set_module_config_vars` lo maneja internamente. Solo pasa los valores como strings.
### Subir imágenes a un módulo ### Subir imágenes a un módulo
El nombre del campo de imagen viene de `builder.json` → `vars.NOMBRE.relations.builder_custom` (ej: `"image1"`). NO es el nombre de la variable (ej: NO `"imagenes"`). El nombre del campo de imagen viene de `builder.json` → `vars.NOMBRE.relations.builder_custom` (ej: `"image1"`). NO es el nombre de la variable (ej: NO `"imagenes"`).
+39 -3
View File
@@ -72,12 +72,29 @@ Patrón para colores configurables por el usuario:
JavaScript scopeado al módulo usando `section_id`: JavaScript scopeado al módulo usando `section_id`:
This is the default and expected place for module JavaScript.
Do NOT embed `<script>` tags directly inside `index-base.tpl` for normal module behavior.
Keep `index-base.tpl` for HTML/Twig markup and put interactive logic in `script.js`.
`script.js` is a static file: do NOT use Twig syntax inside it.
Do NOT write `{{ section_id }}`, `{{ variable }}`, `{% if ... %}`, or builder attributes inside `script.js`.
If JavaScript needs dynamic values, pass them from `index-base.tpl` through `data-*` attributes:
```html
<section
class="buscador-apartados"
data-section-id="{{ section_id }}"
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/">
</section>
```
```js ```js
const section = document.getElementById('{{ section_id }}'); document.querySelectorAll('.buscador-apartados').forEach((section) => {
if (section) { const sectionId = section.dataset.sectionId;
const hookEndpoint = section.dataset.hookEndpoint;
const buttons = section.querySelectorAll('.btn'); const buttons = section.querySelectorAll('.btn');
// ... // ...
} });
``` ```
### CmsApi (Client-Side) ### CmsApi (Client-Side)
@@ -88,6 +105,25 @@ CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(respon
}); });
``` ```
If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
```js
// Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
console.log(response);
});
```
Do not try to build this endpoint with Twig inside `script.js`. Put the final endpoint in a `data-hook-endpoint` attribute in `index-base.tpl` if needed.
### Module Styles (`style.css`)
`style.css` is also a static file.
Do NOT use Twig syntax or builder attributes inside it.
Do NOT write selectors or values that depend on `{{ section_id }}`.
Scope styles with the module root class instead of dynamic Twig ids.
### Cuándo usar Vue 3 ### Cuándo usar Vue 3
Usar Vue 3 CDN cuando la lógica requiera: Usar Vue 3 CDN cuando la lógica requiera:
+112 -139
View File
@@ -2,7 +2,39 @@
## Hooks ## Hooks
Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos/<module-id>/hook.php`. Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
- Hooks globales en `hooks/hooks.<hook-id>.php`
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
### Tipos de hooks
**1. Hook global**
- Archivo: `hooks/hooks.<hook-id>.php`
- Endpoint: `/hooks/<hook-id>/`
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios
**2. Hook propio de módulo**
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
- Endpoint: `/hooks/<module-id>/`
- Úsalo cuando la lógica pertenece solo a ese módulo
Ejemplos:
- `hooks/hooks.buscar_barcos.php` -> `/hooks/buscar_barcos/`
- `template/estandar/modulos/hero_banner/hook.php` -> `/hooks/hero_banner/`
Regla práctica:
- Si el hook solo sirve a un módulo, créalo dentro del módulo
- Si varias piezas del proyecto lo van a consumir, créalo como hook global
## Reglas obligatorias para hooks
- Un hook debe devolver datos con `return [...]`
- No uses `echo json_encode(...)`
- No uses `exit`
- Para leer parámetros, usa `$_REQUEST[...]` o las variables ya inyectadas por el sistema
- En hooks, usa `CmsApi::get()` o `CocoDB::get()` como primera opción
- No uses `CocoDB::getInstance()` salvo necesidad real muy excepcional
- No escribas SQL manual con `prepare()/bind_param()` salvo que no exista forma razonable de resolverlo con `CmsApi` o `CocoDB`
### Estructura de un Hook ### Estructura de un Hook
@@ -34,6 +66,10 @@ No usar X-Hooks-Token en desarrollo local.
### Cómo Llamar Hooks ### Cómo Llamar Hooks
La referencia siempre se hace con el endpoint `/hooks/<id>/`:
- Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
- Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
**Desde HTML (recomendado para módulos):** **Desde HTML (recomendado para módulos):**
```html ```html
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook> <hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
@@ -53,6 +89,14 @@ CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
}); });
``` ```
**Ejemplo real para hook de módulo:**
```js
// Módulo: template/estandar/modulos/buscadorapartados_hjd8s/
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (data) => {
console.log(data);
});
```
**Desde otro Hook PHP:** **Desde otro Hook PHP:**
```php ```php
<?php <?php
@@ -71,37 +115,49 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks
### Read — `CmsApi::get()` ### Read — `CmsApi::get()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Todos los registros // Todos los registros
$products = CmsApi::get('productos'); $products = CmsApi::get("productos");
// Con condición WHERE // Con condición WHERE en string
$active = CmsApi::get('productos', ['active' => 1]); $active = CmsApi::get("productos", "active=1");
// Con orden y límite // Con orden y límite
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5); $latest = CmsApi::get("noticias", "", "fecha DESC", 5);
// Con condición string // Con condición string
$activos = CmsApi::get('productos', 'activo=1'); $activos = CmsApi::get("productos", "activo=1");
// Condición compleja como array // Condición compleja
$caros = CmsApi::get('productos', [ $caros = CmsApi::get("productos", "precio > 100");
["column" => "precio", "operator" => ">", "value" => 100]
]);
// Múltiples condiciones (AND) // Múltiples condiciones (AND)
$resultados = CmsApi::get('productos', [ $resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
["column" => "activo", "operator" => "=", "value" => 1],
["column" => "stock", "operator" => ">", "value" => 0]
]);
// Con operadores // Con operadores
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]); $expensive = CmsApi::get("productos", "precio >= 100");
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]); $search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]); $inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
// Con opciones // Con opciones
$datos = CmsApi::get('productos', '', '', '', [ $datos = CmsApi::get("productos", "", "", "", [
'translates' => true, 'translates' => true,
'uploads' => true, 'uploads' => true,
'relations' => true, 'relations' => true,
@@ -111,6 +167,8 @@ $datos = CmsApi::get('productos', '', '', '', [
### Insert — `CmsApi::insert()` ### Insert — `CmsApi::insert()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Un registro // Un registro
CmsApi::insert('contacto', [ CmsApi::insert('contacto', [
@@ -131,8 +189,19 @@ CmsApi::insert('productos',
); );
``` ```
#### Opciones de insert
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Update — `CmsApi::update()` ### Update — `CmsApi::update()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Con condición string // Con condición string
CmsApi::update('productos', ["precio" => 150], "num=1"); CmsApi::update('productos', ["precio" => 150], "num=1");
@@ -147,8 +216,19 @@ CmsApi::update('productos',
CmsApi::update('productos', ["activo" => 0], "precio < 50"); CmsApi::update('productos', ["activo" => 0], "precio < 50");
``` ```
#### Opciones de update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Delete — `CmsApi::delete()` ### Delete — `CmsApi::delete()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
CmsApi::delete('productos', "num=5"); CmsApi::delete('productos', "num=5");
@@ -187,134 +267,24 @@ CmsApi.get('tableName', { where: conditions }, function(records) {
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control. Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
Para búsquedas y lecturas habituales, prioriza:
1. `CmsApi::get()`
2. `CocoDB::get()`
3. SQL manual solo si de verdad no hay alternativa razonable
### `CocoDB::get($table, $where, $order, $limit, $options)` ### `CocoDB::get($table, $where, $order, $limit, $options)`
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
```php
// Básico
$records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10);
// Where con operadores avanzados
$records = CocoDB::get('productos', [
['column' => 'precio', 'value' => 100, 'operator' => '>='],
['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'],
]);
// Condiciones OR
$records = CocoDB::get('productos', [
['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'],
['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true],
]);
// NOT
$records = CocoDB::get('productos', [
['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true],
]);
// IS NULL
$records = CocoDB::get('productos', [
['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'],
]);
// Limit con offset
$records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]);
```
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
### `CocoDB::insertRecords($table, $records, $functions, $options)` ### `CocoDB::insertRecords($table, $records, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
```php
// Un registro
$count = CocoDB::insertRecords('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
]);
// Usar mysql_insert_id() para obtener el nuevo num
// Múltiples
$count = CocoDB::insertRecords('productos', [
['nombre' => 'Product A', 'precio' => 10],
['nombre' => 'Product B', 'precio' => 20],
]);
```
#### Opciones de insert/update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)` ### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
```php
CocoDB::updateRecords('productos',
['precio' => 29.99, 'activo' => 1],
['num' => 42]
);
// Con operador en where
CocoDB::updateRecords('productos',
['activo' => 0],
[['column' => 'stock', 'value' => 0, 'operator' => '<=']]
);
```
### `CocoDB::deleteRecords($table, $where, $options)` ### `CocoDB::deleteRecords($table, $where, $options)`
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
```php
CocoDB::deleteRecords('productos', ['num' => 42]);
CocoDB::deleteRecords('logs', [
['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<']
]);
```
### Parámetro `$functions`
Permite aplicar funciones MySQL a valores durante insert/update:
```php
CocoDB::insertRecords('logs', [
'mensaje' => 'Login exitoso',
'fecha' => '',
], [
'fecha' => 'NOW()',
]);
```
### Where Clause — Formatos
**Simple (key-value):**
```php
['campo' => 'valor'] // campo = 'valor'
```
**Avanzado (array de condiciones):**
```php
[
'column' => 'field_name',
'value' => 'match_value',
'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL
'or' => false, // OR en vez de AND
'not' => false, // Negar la condición
'raw_key' => false, // Saltar check de existencia de columna
]
```
--- ---
@@ -379,12 +349,15 @@ return [
<p>Total: ${{ precio.total }}</p> <p>Total: ${{ precio.total }}</p>
``` ```
En este ejemplo, el endpoint usa `calcular_precio` porque el archivo vive en:
`template/estandar/modulos/calcular_precio/hook.php`
### Hook con Operaciones de BD ### Hook con Operaciones de BD
```php ```php
<?php <?php
// hook.php del módulo "procesar_compra" // hook.php del módulo "procesar_compra"
$producto = CmsApi::get('productos', "num=$producto_id"); $producto = CmsApi::get("productos", "num=" . $producto_id);
if (empty($producto)) { if (empty($producto)) {
return ["success" => false, "message" => "Producto no encontrado"]; return ["success" => false, "message" => "Producto no encontrado"];
@@ -402,7 +375,7 @@ CmsApi::insert('ventas', [[
]], [], ['return_last_id' => true]); ]], [], ['return_last_id' => true]);
// Actualizar stock // Actualizar stock
$stock = CmsApi::get('stocks', "producto_num=$producto_id"); $stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
if (!empty($stock)) { if (!empty($stock)) {
CmsApi::update('stocks', CmsApi::update('stocks',
["cantidad" => $stock[0]['cantidad'] - $cantidad], ["cantidad" => $stock[0]['cantidad'] - $cantidad],
+48 -21
View File
@@ -4,11 +4,15 @@
| Tool | Categoría | Acción | | Tool | Categoría | Acción |
|------|-----------|--------| |------|-----------|--------|
| `create_module` | Módulos | Crea módulo nuevo (directorio + archivos + compila) | | `compile_module` | Módulos | Recompilación manual de rescate cuando necesitas forzar la compilación sin editar el archivo |
| `compile_module` | Módulos | Compila módulo tras editar index-base.tpl |
| `check_module` | Módulos | Preview de cómo renderiza un módulo | | `check_module` | Módulos | Preview de cómo renderiza un módulo |
| `check_module_usage` | Módulos | Qué páginas usan un módulo | | `check_module_usage` | Módulos | Qué páginas usan un módulo |
| `set_module_example_data` | Módulos | Datos de ejemplo para editor visual | | `acai-view` | Archivos | Lee un archivo del proyecto por tramos |
| `acai-glob` | Archivos | Busca rutas de archivos por patrón |
| `acai-grep` | Archivos | Busca texto en archivos del proyecto |
| `acai-line-replace` | Archivos | Reemplaza un bloque concreto en un archivo existente |
| `acai-write` | Archivos | Crea o reescribe un archivo completo. Antes de usarlo, lee la doc correspondiente según el tipo de archivo (`module-creation-guide`, `builder-fields`, `css-js-conventions`, `hooks-and-api`) |
| `acai-delete` | Archivos | Borra un archivo del proyecto |
| `list_page_modules` | Registros | Lista módulos de una página | | `list_page_modules` | Registros | Lista módulos de una página |
| `add_module_to_record` | Registros | Añade módulo a una página | | `add_module_to_record` | Registros | Añade módulo a una página |
| `remove_module_from_record` | Registros | Elimina módulo de una página | | `remove_module_from_record` | Registros | Elimina módulo de una página |
@@ -28,43 +32,67 @@
| `delete_record_upload` | Media | Borrar upload | | `delete_record_upload` | Media | Borrar upload |
| `reorder_record_uploads` | Media | Reordenar imágenes de un campo | | `reorder_record_uploads` | Media | Reordenar imágenes de un campo |
| `refresh_acai_token` | Auth | Renovar token JWT expirado | | `refresh_acai_token` | Auth | Renovar token JWT expirado |
| `orchestrate_task` | Orquestador | Guía paso a paso para tareas complejas | | `navigate_browser` | Navegación | Navegar el browser del frontend a una URL |
| `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md |
| `rollback_git` | Git | Recuperar cambios de git remoto | | `rollback_git` | Git | Recuperar cambios de git remoto |
## Flujos de trabajo ## Flujos de trabajo
### Crear un módulo nuevo desde cero ### Crear un módulo nuevo desde cero
1. `create_module` — Crea el directorio con index-base.tpl, style.css, script.js y compila 1. `acai-write` — Crea `index-base.tpl`, `style.css`, `script.js` y cualquier hook necesario con rutas relativas al proyecto
2. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`) 2. `acai-write` o `acai-line-replace` compilan automáticamente al tocar `index-base.tpl`
3. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada 3. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`)
4. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` devuelto por set_module_config_vars 4. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada. Devuelve:
5. Verificar con `check_module` o recargando la página - `configVars`: mapa de variables → recordNums
- `uploadFields`: mapa de variables upload → `{ fieldName, recordNum }` — **usa estos directamente** para subir imágenes sin necesidad de leer builder.json
- Para vars multi con uploads: `uploadFields["varName.subVarName"]` es un array con `[{ index, fieldName, recordNum }]`
5. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del `uploadFields` devuelto en el paso 4
6. Verificar con `check_module` o recargando la página
### Editar un módulo existente ### Editar un módulo existente
1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums) 1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums)
2. Editar `index-base.tpl` con la tool `Write` o `Edit` 2. `acai-view` — Leer solo el tramo de `index-base.tpl` que se va a modificar
3. `compile_module` — **OBLIGATORIO** tras cada edición de index-base.tpl 3. `acai-line-replace` — Editar el bloque concreto. Usa `acai-write` solo si el archivo es nuevo o el cambio es masivo
4. Si cambias variables: `set_module_config_vars` para actualizar valores 4. La compilación es automática tras cada edición de `index-base.tpl`
5. Si cambias variables: `set_module_config_vars` para actualizar valores
### Editar archivos del proyecto con bajo consumo de tokens
1. `acai-view` — Leer el archivo o un rango de líneas
2. `acai-glob` — Encontrar archivos relevantes por ruta cuando no conoces el path exacto
3. `acai-grep` — Buscar texto o atributos concretos dentro de archivos del proyecto
4. `acai-line-replace` — Reemplazar el bloque exacto en archivos existentes
5. `acai-write` — Crear archivos nuevos o reescribirlos por completo si es necesario
6. `acai-delete` — Borrar archivos solo cuando sea explícitamente necesario
Reglas:
- Usa siempre rutas relativas al proyecto
- No edites `index.tpl`, `index-twig.tpl` ni `builder.json` — son auto-generados
- Tras editar cualquier `index-base.tpl` con las file tools, la compilación se ejecuta automáticamente
### Añadir/modificar imágenes de un módulo ### Añadir/modificar imágenes de un módulo
**Tras `set_module_config_vars`** (método recomendado — sin pasos extra):
1. El response de `set_module_config_vars` incluye `uploadFields` con los `recordNum` y `fieldName` de cada variable upload
2. `upload_record_image` con `tableName: "builder_custom"`, `recordId` y `fieldName` del `uploadFields`
3. Para uploads dentro de vars multi: `uploadFields["records.imagen"]` devuelve array con `{ index, fieldName, recordNum }` por cada record
**Sin haber llamado a `set_module_config_vars`**:
1. `get_module_config_vars` — Obtener el `recordNum` de builder_custom 1. `get_module_config_vars` — Obtener el `recordNum` de builder_custom
2. Leer `builder.json` del módulo para encontrar el `fieldName` real (ej: `image1`, NO el nombre de la variable) 2. Leer `builder.json` del módulo para encontrar el `fieldName` real (ej: `image1`, NO el nombre de la variable)
3. `upload_record_image` con: 3. `upload_record_image` con:
- `tableName`: `"builder_custom"` (siempre sin cms_) - `tableName`: `"builder_custom"` (siempre sin cms_)
- `recordId`: el recordNum del paso 1 - `recordId`: el recordNum del paso 1
- `fieldName`: el campo de relations del builder.json (ej: `image1`) - `fieldName`: el campo de relations del builder.json (ej: `image1`)
- `imageUrl`: URL accesible desde Docker (ej: `http://localhost/cms/uploads/...`) - `imageUrl`: usa la URL recomendada por la tool que generó/subió la imagen. En Forge, si `generate_image` devuelve `uploadUrl` o `fullUrl`, priorízala frente a `dockerUrl`
4. `list_record_uploads` para verificar
5. `reorder_record_uploads` si necesitas cambiar el orden
### Generar imagen con IA ### Generar imagen con IA
1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...) 1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...)
2. La imagen se guarda en `cms/uploads/generated/` y devuelve `dockerUrl` 2. La imagen se guarda en `cms/uploads/generated/` y devuelve una URL local de preview (`dockerUrl`) y, cuando aplica, una URL recomendada para subida (`uploadUrl` / `fullUrl`)
3. Usar esa `dockerUrl` con `upload_record_image` para asignarla a un módulo 3. Para `upload_record_image`, usa la URL recomendada por la tool. En Forge, prioriza `uploadUrl` o `fullUrl` si están presentes
### Gestionar registros de una tabla ### Gestionar registros de una tabla
@@ -75,10 +103,9 @@
### Explorar el sitio ### Explorar el sitio
1. `orchestrate_task` con workflow `explore_site` — Guía para entender la estructura 1. `list_page_modules` — Ver qué módulos tiene cada página
2. `list_page_modules` — Ver qué módulos tiene cada página 2. `get_module_config_vars` — Ver los datos de cada módulo
3. `get_module_config_vars` — Ver los datos de cada módulo 3. `check_module` — Preview de cómo renderiza
4. `check_module` — Preview de cómo renderiza
## Reglas importantes para todas las tools ## Reglas importantes para todas las tools
+100
View File
@@ -0,0 +1,100 @@
# Module Creation Guide
## Style Reference
When creating new modules, you MUST match the visual style of the existing project. Follow these steps IN ORDER:
### Step 1: Check for `docs/project-styles.md`
- If the file exists → read it and use it as your style reference. DONE — skip to module creation.
- If the file does NOT exist → continue to Step 2.
### Step 2: Determine if exploration is needed
- Count modules in `template/estandar/modulos/` that have `builder.json` and do NOT start with `custom-`
- If 3+ qualifying modules exist → continue to Step 3
- If fewer than 3 → skip exploration, create the module based on the user's description. The style will be defined as modules are created.
### Step 3: Explore and GENERATE the style guide (MANDATORY)
- Read `index-base.tpl` and `style.css` of 3-4 representative modules (only those with `builder.json`, skip `custom-*`)
- **You MUST then call `save_project_styles`** with a markdown summary including:
- Primary/secondary/accent colors (hex values)
- Font families and sizes used
- Spacing scale (padding/margin patterns)
- Common Tailwind classes and custom CSS patterns
- Button styles, card styles, section layouts
- Any recurring design patterns (gradients, shadows, borders, etc.)
- This saves `docs/project-styles.md` which will be read by future module creation tasks — no re-exploration needed.
**After creating a module:** if `docs/project-styles.md` does not exist yet and there are now 3+ modules, call `save_project_styles`.
## Module Structure
Each module lives in `template/estandar/modulos/<moduleId>/` with:
- `index-base.tpl` — Twig template (source — EDIT THIS)
- `style.css` — Module styles
- `script.js` — Module JavaScript
- `builder.json` — Compiled builder vars (auto-generated, do NOT edit)
- `index.tpl` / `index-twig.tpl` — Compiled (auto-generated, do NOT edit)
## Creating a Module — Full Workflow
If the module needs JavaScript, you MUST read `docs/css-js-conventions.md` before writing `index-base.tpl` or `script.js`.
If the module needs server-side logic, dynamic data processing, form handling, or reusable backend behavior, you MUST read `docs/hooks-and-api.md` before creating `hook.php` or any global hook.
If the module will call hooks from Twig, also review `docs/twig-filters.md` for the `hook` filter syntax.
If `index-base.tpl` contains builder fields (`data-field-type`), you MUST review `docs/builder-fields.md` before writing the template.
Hard rules for module files:
- `index-base.tpl` is for HTML/Twig only
- `script.js` is for module JavaScript
- `style.css` is for module CSS
- `hook.php` is for server-side logic
- Do NOT embed `<script>` tags in `index-base.tpl`
- Do NOT put PHP logic in `index-base.tpl`
- `script.js` and `style.css` are static files: do NOT use Twig syntax inside them
- Do NOT use `{{ ... }}`, `{% ... %}`, `c-if`, `c-for`, or builder attributes inside `script.js` or `style.css`
- If JavaScript needs dynamic values such as `section_id` or a hook endpoint, expose them from `index-base.tpl` via `data-*` attributes
- Every editable builder field with `data-field-type` MUST also define `data-field-label`
- Avoid Tailwind arbitrary-value syntax such as `text-[...]`, `font-[...]`, `leading-[...]`, `bg-[...]` inside `index-base.tpl`; move those styles to `style.css`
1. **Read style reference** (steps above)
2. **`acai-write`** — Create the module files directly (`index-base.tpl`, `style.css`, `script.js`, optional `hook.php`) using project-relative paths and complete file contents.
- If the server-side logic belongs only to that module, create `template/estandar/modulos/<module-id>/hook.php`
- If the logic should be reused across modules/pages, create a global hook in `hooks/hooks.<hook-id>.php`
- Inside the module, reference its own hook with `/hooks/<module-id>/`
- Example: module folder `template/estandar/modulos/buscadorapartados_hjd8s/` -> hook endpoint `/hooks/buscadorapartados_hjd8s/`
3. **Automatic compile** — Writing `index-base.tpl` automatically creates the generated template placeholders and triggers compilation. `compile_module` is only a manual recovery tool if you need to force a recompile without changing the file.
4. **`add_module_to_record`** — Adds the module to a page. Response includes `sectionId` — use it directly in the next step.
5. **`set_module_config_vars`** — Fill variables with content. Response includes `uploadFields` with `{ fieldName, recordNum }` for each upload variable.
6. **Upload images** — Use `generate_image` then `upload_record_image` with the `recordNum` and `fieldName` from step 5's `uploadFields`. No need to read builder.json or call get_module_config_vars.
7. **`navigate_browser`** — Navigate to the page so the user can see the result.
## HTML Field Types
Use these `data-field-type` attributes in `index-base.tpl`:
| Attribute | Purpose | Example |
|-----------|---------|---------|
| `headfield` | Editable heading | `<h2 data-field-type="headfield">Title</h2>` |
| `textfield` | Short editable text | `<span data-field-type="textfield">Text</span>` |
| `wysiwyg` | Rich text editor | `<div data-field-type="wysiwyg">Content</div>` |
| `upload` | Image upload | `<img data-field-type="upload" src="...">` |
| `list` | Select dropdown | `<div data-field-type="list" data-options="opt1,opt2">` |
| `multiv2` | Repeater/records | `<div data-field-type="multiv2">...</div>` |
| `checkbox` | Toggle | `<div data-field-type="checkbox">` |
| `colorpicker` | Color picker | `<div data-field-type="colorpicker">` |
## MJML Modules
Modules with `MJMLModule: true` in their schema are email modules:
- Only appear when the page table is `mail_marketing`
- For `mail_marketing` tables, only MJML modules are shown
- Use MJML markup instead of standard HTML
## Key Rules
- Always use Tailwind CSS as primary styling
- Use `section_id` variable for unique anchors/scoping
- Use `interno` variable to detect CMS editor vs public view
- Include other modules with: `<module_id :param1="value1"></module_id>`
- Editing `index-base.tpl` with `acai-write` or `acai-line-replace` compiles automatically
- Twig uses filters (with `|`), never functions
- Twig concatenation uses `~`: `'value=' ~ variable`
+104
View File
@@ -0,0 +1,104 @@
# Pages & Records Guide
## Page Types
Every CMS record that has an `enlace` (URL) field is a **page**. Pages come in two types determined by the `controlador` field:
### Builder (Modular) Pages
- `controlador` = `cms/lib/plugins/builder_saas/controlador.php`
- Content is built from **modules** (drag & drop components)
- The `builder` field contains a JSON array of module instances
- Use MCP tools: `add_module_to_record`, `set_module_config_vars`, etc.
- The page template renders modules in order from the builder JSON
### Standard Pages
- `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php`
- Content lives directly in the record fields (`content`, `titulo_alternativo`, etc.)
- The `content` field is HTML (wysiwyg)
- Use `create_or_update_record` to edit content directly
- No modules involved
### How to determine page type
**Always check the `controlador` field** of the record:
- Contains `controlador.php` (without `_tabla`) → **Builder**
- Contains `controlador_tabla.php` → **Standard**
## Table Types (Sections)
Tables with pages are called **sections**. There are two section types defined by `menuType` in the schema:
### Category (`menuType = "category"`)
- **Hierarchical** — pages have parent/child relationships
- Fields: `parentNum`, `depth`, `globalOrder`, `lineage`, `siblingOrder`
- Example: `apartados` (main site pages)
- Uses `visible_en_el_menu` field for menu visibility
- Ordered by `globalOrder`
### Multi (`menuType = "multi"`)
- **Flat list** — no hierarchy
- Uses `dragSortOrder` for ordering
- Example: `blog`, `travesias`
- Typically uses `visible` field (not `visible_en_el_menu`)
## Critical Rules for Pages
### NEVER change the `enlace` field
Unless the user explicitly asks to change a page URL, **never modify the `enlace` field**. Changing it breaks existing links, SEO, and navigation. The enlace is set when the page is created and should remain stable.
### NEVER change the `controlador` field
The controlador defines whether the page is Builder or Standard. Changing it breaks the page rendering. Only set it during page creation.
### Visibility fields
- `apartados` and other category tables use: `visible_en_el_menu` (1 = visible, 0 = hidden)
- `blog`, `travesias` and other multi tables use: `visible` (1 = visible, 0 = hidden)
- Always check which field the table has before toggling visibility
### Name/Title fields
- Some tables use `name` (e.g. `apartados`)
- Others use `title` (e.g. `blog`, `travesias`)
- Check the schema to know which one to use
## Working with Builder Pages
### Adding content to a new Builder page
1. List available modules: `list_available_modules`
2. Add modules: `add_module_to_record` (one at a time, in order)
3. Configure each module: `set_module_config_vars` with content
4. Add images if needed: `upload_record_image` or `generate_image`
### Editing an existing Builder page
1. List current modules: `list_page_modules`
2. Get module vars: `get_module_config_vars`
3. Update vars: `set_module_config_vars`
4. Or edit the module template: edit `index-base.tpl` with `acai-line-replace` or `acai-write` → compilation runs automatically
## Working with Standard Pages
### Adding content to a Standard page
Use `create_or_update_record` to set:
- `content` — HTML content (main body)
- `titulo_alternativo` — alternative title shown on the page
- `titulo_de_pagina` — browser tab title (SEO)
- `metatag_descripcion` — meta description (SEO)
### Example: Update a standard page
```
create_or_update_record with:
tableName: "apartados"
recordNum: "87"
fields:
content: "<h2>Our Services</h2><p>We offer...</p>"
titulo_de_pagina: "Services | My Site"
metatag_descripcion: "Discover our services..."
```
## The `apartados` Table (Special)
The `apartados` table is the main pages table in most Acai sites. Key characteristics:
- `menuType = "category"` — hierarchical with parent/child
- `parentNum` — the num of the parent page (0 = root level)
- `depth` — nesting level (0 = root, 1 = child, 2 = grandchild)
- `globalOrder` — display order across the entire tree
- `visible_en_el_menu` — whether the page shows in the navigation menu
- `breadcrumb` — auto-generated breadcrumb path
- Pages can be either Builder or Standard (check `controlador` field per record)