Add production rules discovered during real projects

- Correct hooks-and-api.md: file hooks vs module hooks param injection,
  FK naming not always _num, CmsApi.hook Promise pattern
- Add 9 new rules to hooks-and-api.md: reserved `tipo` var, ghost modules,
  cms_uploads schema, name vs title by menuType, menuOrder, localCache
  gotcha, slug generation, uploads from hooks, CocoEmail
- Add 5 rules to modular-system.md: minified/ dir, Docker workflow,
  debug tools (?compiletwig/?pruebas), general sections deploy, controlador
- Add 2 rules to css-js-conventions.md: Vue inline conflict, Vue mount delay
- Add testing section to quick-reference.md
- Create docs/deploy-and-sync.md for production deploy and sync rules
- Promote 3 critical rules to CLAUDE.md (rules 11-13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 16:17:00 +00:00
parent 2c079088ef
commit f2021361ec
6 changed files with 283 additions and 9 deletions

View File

@@ -102,6 +102,9 @@ Do NOT modify web-base files — they are shared across all projects.
8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed 8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
9. Twig concatenation uses `~` operator: `'value=' ~ variable` 9. Twig concatenation uses `~` operator: `'value=' ~ variable`
10. `enlace` (link) fields already include slashes 10. `enlace` (link) fields already include slashes
11. **File hooks (`hooks/*.php`) do NOT inject variables.** Always read params manually: `$params = json_decode(file_get_contents('php://input'), true) ?: [];` — Only module hooks (`modulos/*/hook.php`) receive variables directly.
12. **`tipo` is a reserved variable.** The `.htaccess` injects `tipo=barra` on every request. Never use `tipo` as a hook parameter name — it gets overwritten, causing 404s. Use `account_type`, `user_tipo`, etc.
13. **Never pass `html` via `save_module` to modules with configured builder vars** (banners, carousels, modules with images/colors set from admin). It corrupts the visual configuration. Only pass `js` and/or `css`. See [docs/deploy-and-sync.md](docs/deploy-and-sync.md).
## Documentation ## Documentation
@@ -114,3 +117,4 @@ Do NOT modify web-base files — they are shared across all projects.
- [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/deploy-and-sync.md](docs/deploy-and-sync.md) — Deploy to production (save_module, sync, minified regeneration)

View File

@@ -83,9 +83,17 @@ if (section) {
### CmsApi (Client-Side) ### CmsApi (Client-Side)
```js ```js
// Callback
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) { CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
console.log(response); console.log(response);
}); });
// Promise
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
console.log(res);
}).catch(function(err) {
console.error(err);
});
``` ```
### Cuándo usar Vue 3 ### Cuándo usar Vue 3
@@ -123,6 +131,8 @@ createApp({
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig. Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig.
**IMPORTANTE: JS Vue SIEMPRE en `script.js`, nunca inline.** Twig también interpreta `${ }`. Si el JS con Vue está inline en `<script>` dentro de `index-base.tpl`, Twig lo corrompe al renderizar. Todo el código Vue debe ir en el archivo `script.js` del módulo.
--- ---
## Variables Globales Disponibles ## Variables Globales Disponibles
@@ -216,6 +226,12 @@ Después de cambios dinámicos: `AOS.refresh()` en JavaScript.
--- ---
## Testing con Vue
Vue necesita 3-5 segundos para montar después de navegar a una página. El `display:none` inicial se quita cuando `checkAuth()` o el `mounted()` completan. Al testear con Playwright, esperar antes de verificar contenido Vue.
---
## Buenas prácticas ## Buenas prácticas
- HTML/Twig semántico - HTML/Twig semántico

56
docs/deploy-and-sync.md Normal file
View File

@@ -0,0 +1,56 @@
# Deploy & Sync
Reglas para desplegar módulos y secciones generales a producción via MCP, y para sincronizar con el servidor.
---
## Protocolo para `save_module`
1. `save_module` con `html`, `js`, `css`**LOS 3 SIEMPRE**. Si omites uno, se borra de prod (queda 0 bytes).
2. Ejecutar `check_module` → fuerza recompilación de `index-twig.tpl`
3. Visitar `?compiletwig` en una página que use el módulo
4. Verificar que renderiza
---
## NUNCA pasar `html` a módulos con builder vars configurados
Módulos con sample data en el builder (banners, carouseles, módulos con imágenes/colores configurados desde el admin) se ROMPEN al recompilar el HTML. El `save_module` puede perder/corromper la configuración visual.
**Para estos módulos:**
- Solo pasar `js` y/o `css`, nunca `html`
- Si se rompe: `recover_git` con `list_git_log` para revertir
- Editar el HTML desde el panel admin de Acai, no por MCP
---
## Forzar regeneración de minified
Si subes solo `js`, el `minified/script-{hash}.js` NO se regenera automáticamente.
**Solución:**
1. Leer el HTML actual de prod con `get_module` (sections: `["html"]`)
2. Añadir un espacio al final
3. `save_module` con los 3 (html+espacio, js, css)
**IMPORTANTE:** Usar SIEMPRE el HTML de prod (`get_module`), NUNCA el local.
---
## General Sections (`custom-*`)
Se despliegan con `save_general_section`, no `save_module`:
- El módulo se llama `custom-{nombre_tabla}` (ej: `custom-productos`)
- Se sube con `table: "productos"`, `content`, `javascript`, `css`
---
## Sync y Versionado
### Sync from server puede borrar módulos locales
El "Sync from server" puede eliminar módulos que existen en local pero no en el servidor. Si borra `custom-header` o `custom-footer`, TODAS las páginas crashean.
**Regla:** Hacer commit ANTES de cualquier sync.
### Sync solo sube archivos modificados
Para forzar un sync de un archivo sin cambios reales, añadir un espacio al final.

View File

@@ -4,16 +4,17 @@
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 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`.
### Estructura de un Hook ### Hooks de módulo vs hooks de archivo
**IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE.
#### Hooks de módulo (`modulos/MODULE_ID/hook.php`)
Los parámetros se inyectan como variables directamente:
```php ```php
<?php <?php
// Los parámetros se reciben como variables directamente // Si llamas hook con {param1: 100}, tendrás $param1 = 100
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
$resultado = $param1 * 2; $resultado = $param1 * 2;
// Retornar un array (se convierte a JSON)
return [ return [
"success" => true, "success" => true,
"message" => "Valor procesado: " . $resultado, "message" => "Valor procesado: " . $resultado,
@@ -22,6 +23,22 @@ return [
?> ?>
``` ```
#### Hooks de archivo (`hooks/*.php`)
Se ejecutan dentro de `function returnData() {}` via `eval()`. **NINGUNA variable del request se inyecta.** Hay que leer manualmente:
```php
<?php
// SIEMPRE hacer esto al inicio de cada hook de archivo:
$params = json_decode(file_get_contents('php://input'), true) ?: [];
$nombre = trim(@$params['nombre']);
$email = trim(@$params['email']);
return [
"success" => true,
"nombre" => $nombre
];
?>
```
### Testing Hooks ### Testing Hooks
El Docker debe estar corriendo. Hacer curl al endpoint del hook: El Docker debe estar corriendo. Hacer curl al endpoint del hook:
@@ -32,6 +49,18 @@ curl http://localhost:8080/hooks/example_hook/
No usar X-Hooks-Token en desarrollo local. No usar X-Hooks-Token en desarrollo local.
### Gotchas de Hooks
#### `tipo` es variable reservada
El `.htaccess` de Acai añade `tipo=barra` a cada request. Si envías `tipo` como parámetro en un hook, se sobrescribe → 404 o comportamiento inesperado.
**Usar nombres alternativos:** `account_type`, `user_tipo`, `record_tipo`, etc.
#### Módulos fantasma interceptan hooks
Si `save_module` crea un directorio `modulos/auth_signup/hook.php`, este intercepta la URL `/hooks/auth_signup/` porque `addModulesHooksToLayout()` se ejecuta ANTES que `addFilesHooksToLayout()`.
**Solución:** Los hooks generales van en `hooks/hooks.{nombre}.php`. Nunca crear hooks como módulos. Si aparecen fantasmas, borrar el directorio.
### Cómo Llamar Hooks ### Cómo Llamar Hooks
**Desde HTML (recomendado para módulos):** **Desde HTML (recomendado para módulos):**
@@ -161,7 +190,7 @@ CmsApi::delete('productos',
- Nombres de tabla **sin** prefijo `cms_` - Nombres de tabla **sin** prefijo `cms_`
- Primary key siempre es `num`, nunca `id` - Primary key siempre es `num`, nunca `id`
- Foreign keys: `categoria_num`, no `categoria_id` - Foreign keys: el nombre es **exactamente** el definido en el schema (`.ini.php`). A veces es `categoria_num`, a veces `categoria`. Siempre consultar el schema antes de asumir
- Upload fields: no se manejan via insert/update - Upload fields: no se manejan via insert/update
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN` - Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
@@ -170,11 +199,18 @@ CmsApi::delete('productos',
## CmsApi (JavaScript — Client-Side) ## CmsApi (JavaScript — Client-Side)
```js ```js
// Llamar hook // Llamar hook (callback)
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) { CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
// response es la salida del hook // response es la salida del hook
}); });
// Llamar hook (Promise)
CmsApi.hook('/hooks/module_id/', { param: 'value' }).then(function(res) {
// res = lo que devuelve el PHP del hook
}).catch(function(err) {
// error de red
});
// Leer registros (si está expuesto via hooks) // Leer registros (si está expuesto via hooks)
CmsApi.get('tableName', { where: conditions }, function(records) { CmsApi.get('tableName', { where: conditions }, function(records) {
// records array // records array
@@ -413,3 +449,111 @@ if (!empty($stock)) {
return ["success" => true, "total" => $total]; return ["success" => true, "total" => $total];
?> ?>
``` ```
---
## Gotchas de Base de Datos
### Nombres de columnas = nombres en el schema
Los campos foreign key NO añaden `_num` automáticamente. El nombre es exactamente el definido en el `.ini.php`. Siempre consultar `DESCRIBE cms_<tabla>` o el schema antes de escribir INSERT/UPDATE.
### Campos `name` vs `title` según `menuType`
El campo principal depende del `menuType` de la tabla:
| menuType | Campo principal | Ejemplos |
|----------|----------------|----------|
| `category` | `name` | apartados, categorias_productos, productos |
| `multi` | `title` | users, localizaciones, correos, favoritos |
| `single` | no aplica | configuracion, configuracion_tienda |
Las tablas `category` generan `name` automáticamente como campo de navegación. Las `multi` generan `title` como campo scaffold por defecto.
### Orden de tablas y campos
Al crear tablas o campos, usar `menuOrder` / `order` alto (99+) para que aparezcan al final del listado admin sin alterar el orden existente.
### `cms_uploads` tiene esquema diferente
La tabla usa `createdTime` (no `createdDate`). No tiene `createdByUserNum`, `updatedDate`, ni `updatedByUserNum`. Para insertar un upload manualmente:
```php
global $TABLE_PREFIX;
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
VALUES (NOW(), 'productos', $num, 'foto', '$urlPath', $nextOrder)";
```
### Upload fields en PHP
Incluso con una sola imagen, los uploads son arrays:
```php
$image = @$p['foto'][0]['urlPath'] ?: '';
```
---
## CocoDB Cache en Memoria
`CocoDB::localCache()` se llama en cada request. Todos los `CmsApi::get()` se cachean en memoria por hash de SQL+options. Si insertas un registro y luego haces get con la misma query exacta, devuelve el resultado cacheado (sin el nuevo registro).
**Solución:** Usar opciones diferentes en la segunda query (ej: `ignoreSchema: true` en una, sin en la otra) para que el hash sea distinto.
---
## Generación de Slug (enlace)
`CmsApi::insert()` desde hooks NO genera el slug automáticamente (el panel admin sí lo hace). Generar manualmente:
```php
$slug = strtolower(trim($name));
$slug = preg_replace('/[áàâäã]/u', 'a', $slug);
$slug = preg_replace('/[éèêë]/u', 'e', $slug);
$slug = preg_replace('/[íìîï]/u', 'i', $slug);
$slug = preg_replace('/[óòôöõ]/u', 'o', $slug);
$slug = preg_replace('/[úùûü]/u', 'u', $slug);
$slug = preg_replace('/[ñ]/u', 'n', $slug);
$slug = preg_replace('/[^a-z0-9\-]/', '-', $slug);
$slug = preg_replace('/-+/', '-', $slug);
$slug = trim($slug, '-');
$enlace = '/' . $slug . '-' . substr(uniqid(), -6) . '/';
```
---
## Uploads desde Hooks
CmsApi no gestiona uploads directamente. Para subir archivos desde un hook:
1. Recibir base64 via `php://input`
2. Decodificar y guardar en `$_SERVER['DOCUMENT_ROOT'] . '/cms/uploads/'`
3. Insertar manualmente en `cms_uploads`
```php
$params = json_decode(file_get_contents('php://input'), true) ?: [];
$data = base64_decode($params['file_b64']);
$filename = $num . '_' . time() . '.' . $extension;
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/cms/uploads';
file_put_contents($uploadsDir . '/' . $filename, $data);
global $TABLE_PREFIX;
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
VALUES (NOW(), 'productos', $num, 'foto', '/cms/uploads/$filename', 1)";
mysql_query($sql);
```
---
## Email (CocoEmail)
### Requiere template en BD
`CocoEmail::send('IDENTIFICADOR', $params, $to)` busca el template en `cms_correos` por `identificador`. Si no existe → excepción "Correo no encontrado". Si `$to` tiene emails inválidos/vacíos → "No hay destinatarios válidos".
**Siempre envolver en try/catch:**
```php
try {
hook("/hooks/customEmailHeader/", ["remote" => @$_SESSION["REMOTE_URL"]]);
CocoEmail::send("ACTIVAR_CUENTA", $user, [$user["email"]]);
} catch (Exception $e) {
// Email falló pero la operación principal sigue
}
```
### Variables en templates de email
Las variables se reemplazan con `{nombre_campo}`:
```
Hola {nombre}, tu código es {codigo}
```
El segundo parámetro de `send()` es el array de datos con las variables.

View File

@@ -13,7 +13,10 @@ Modules are the visual building blocks of Acai websites. Each module lives in `t
├── index-twig.tpl # Compiled Twig 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) ├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
├── style.css # Module-scoped styles ├── style.css # Module-scoped styles
── script.js # Module JavaScript ── script.js # Module JavaScript
└── minified/
├── script-{hash}.js # JS minificado (servido al browser, do NOT edit)
└── style-{hash}.css # CSS minificado (servido al browser, do NOT edit)
``` ```
### Template Syntax ### Template Syntax
@@ -63,7 +66,7 @@ General sections are database-backed templates used for record views, headers, f
- Access record data via the `thisrecord` variable - Access record data via the `thisrecord` variable
- Upload fields return **arrays**: `thisrecord.image[0].urlPath` - Upload fields return **arrays**: `thisrecord.image[0].urlPath`
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4` - Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
- Foreign key fields use `_num` suffix: `thisrecord.category_num` - Foreign key field names match the schema exactly (may or may not have `_num` suffix — always check the `.ini.php`)
- Saved via `save_general_section()` (not `save_module()`) - Saved via `save_general_section()` (not `save_module()`)
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax - Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
@@ -103,3 +106,46 @@ The `multiv2` builder field type creates repeatable groups of fields:
``` ```
Access individual items: `record.items[0].title`, `record.items[1].image`, etc. Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
---
## Workflow Local (Docker)
Al editar módulos en desarrollo local con Docker, los archivos compilados no se regeneran automáticamente. Hay que copiar manualmente:
```bash
# 1. Editar HTML y JS por separado
vim modulos/MODULE_ID/index-base.tpl # Solo HTML
vim modulos/MODULE_ID/script.js # Todo el JS
# 2. Copiar para que Docker los sirva
cp modulos/MODULE_ID/index-base.tpl modulos/MODULE_ID/index.tpl
cp modulos/MODULE_ID/script.js modulos/MODULE_ID/minified/script.js
```
### Herramientas de debug
- **`?compiletwig`** — Añadir a cualquier URL. Regenera los `index-twig.tpl` pero con un pipeline diferente al auto-compile. Útil para forzar recompilación, pero puede romper en ciertos contextos. Usar con precaución.
- **`?pruebas`** — Bypass de modo mantenimiento. Añadir a cualquier URL establece `$_SESSION["pruebas"]=true`. Solo hay que hacerlo una vez por sesión.
---
## General Sections — Deploy
Las general sections se identifican como `custom-{nombre_tabla}` (ej: `custom-productos`). Se despliegan con `save_general_section`, NO con `save_module`:
- `table`: nombre de la tabla (ej: `"productos"`)
- `content`: HTML del template
- `javascript`: JS
- `css`: CSS
---
## Páginas CMS (Apartados)
### Campo `controlador` obligatorio para builder
Si una página debe renderizar módulos del builder (drag-and-drop), necesita estos campos configurados:
```
controlador = "cms/lib/plugins/builder_saas/controlador.php"
precontrolador = "cms/lib/plugins/builder_saas/controlador_tabla.php"
```
Sin estos campos, la página muestra solo la general section (`custom-apartados`) en vez de los módulos asignados.

View File

@@ -83,3 +83,11 @@
| `loop.index is odd/even` | Para layouts alternados | | `loop.index is odd/even` | Para layouts alternados |
| `interno` | True dentro del editor CMS | | `interno` | True dentro del editor CMS |
| `thisrecord` | Registro actual (en secciones generales) | | `thisrecord` | Registro actual (en secciones generales) |
## Testing
| Regla | Detalle |
|-------|---------|
| Hooks devuelven JSON en network | Al testear con Playwright, usar `browser_network_requests` o `fetch()` en `browser_evaluate`. El snapshot visual no muestra respuestas de hooks |
| Vue tarda en montar | Después de navegar a una página con Vue, esperar 3-5s antes de verificar contenido |
| `?pruebas` | Añadir a URL para bypass de modo mantenimiento (una vez por sesión) |