Files
acai-scaffold/docs/modular-system.md
Dmielgo f2021361ec 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>
2026-03-24 16:17:00 +00:00

5.2 KiB

Acai Modular System

Modules

Modules are the visual building blocks of Acai websites. Each module lives in template/estandar/modulos/<module-id>/.

File Structure

<module-id>/
├── 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
└── 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

Templates use a hybrid of Twig and Acai attributes. The source file is always index-base.tpl.

<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>

Including Modules from Other Modules

<module_id :param1="value1" :param2="'string value'"></module_id>

Parameters are received as variables inside the included module.

Global Variables

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 database-backed templates used for record views, headers, footers, and reusable layouts. They use the same template engine as modules.

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 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())
  • Parser type 2 = Twig (recommended), 0 = Acai legacy syntax

Example: Record Template

<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:

<set :categories="'categorias' | get()"></set>
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>

Repeatable Content (multiv2)

The multiv2 builder field type creates repeatable groups of fields:

<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>

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:

# 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.