Zotonic Module Porting
Module Structure
A Zotonic 1.x module/app usually has:
app_or_module/
├── rebar.config
├── Makefile
├── Taskfile.yml (optional, delegate to Makefile if present)
├── priv/
│ ├── dispatch/dispatch
│ ├── lib/ (compiled static output)
│ ├── lib-src/ (SCSS/JS source and build Makefile)
│ └── templates/
└── src/
├── app_or_module.app.src
├── mod_app_or_module.erl
├── actions/
├── filters/
├── scomps/
├── validators/
├── controllers/
├── models/
└── support/
- Add
.app.src in src/.
- In the main module, add
-mod_config([...]) for all configurable options.
- Use dependencies via
-mod_depends([...]) and .app.src applications as appropriate.
- Module application names must start with
zotonic_mod_....
- The main Erlang module of
zotonic_mod_mymod is src/mod_mymod.erl.
- Actions, validators, and scomps should include the module name in their Erlang module name to support Zotonic override behavior.
- A site app has the same shape but uses a site module such as
src/sitename.erl and must include priv/zotonic_site.config, .json, .yaml, or .yml.
- Keep source files UTF-8 with LF line endings.
Module Boundaries
- Keep
src/mod_*.erl small and Zotonic-facing. It should declare module metadata and config, handle lifecycle callbacks, observe notifications, install data, and connect the module to Zotonic.
- Put public module APIs in
src/models/m_*.erl when templates, other modules, or model lookups need to call them. Models should normalize external/template input, apply ACL checks for model data, read module configuration, and return template-friendly data structures.
- Put worker processes and domain implementation in
src/support/. This includes gen_server workers, parsers, protocol adapters, batching internals, external command/process communication, and pure transformation helpers.
- Let the model mediate support modules when the functionality is exposed to Zotonic. For example, have the model ensure a shared worker is started, pass timeouts/configuration, call the support worker, and normalize
{ok, ...} | {error, ...} results for callers.
- Move code out of
mod_*.erl once it stops being module lifecycle or observer glue. Move code out of m_*.erl when it becomes reusable implementation detail rather than a Zotonic-facing API.
Porting From Zotonic 0.x
- Replace Webmachine-style controllers with Zotonic 1.x/Cowmachine callbacks.
- Look at
zotonic_mod_base controllers and Cowmachine defaults before implementing callbacks.
- Omit callbacks where defaults are correct.
- Replace old request APIs and
wrq usage with z_context, m_req, cowmachine_req, and Zotonic controller helpers.
- Replace string query keys with binary keys:
z_context:get_q(<<"payment_nr">>, Context)
- For JSON request bodies, use:
z_controller_helper:decode_request_noz(AcceptedCT, Context)
- For raw request-body authorization checks, read the body once and store it in the context if it must be reused.
- When porting templates, remove old convenience patterns that do not work well in Zotonic 1.x, such as
{% with m.rsc[id] as r %} followed by r.foo; use id.foo directly in admin edit templates.
Data And Types
- In Zotonic 1.x, request keys, JSON keys, and query keys are generally binaries.
- For text manipulation, use Unicode-aware
unicode or z_string routines. Do not slice or truncate text with byte-oriented binary operations, as those can split UTF-8 codepoints.
- Use maps for decoded JSON and payment/resource data.
- Avoid atoms for validation of external values unless they are already internal finite-state values.
- Use named
-spec variables with when clauses:
-spec callback(Body, Context) -> Result
when
Body :: binary(),
Context :: z:context(),
Result :: {true, z:context()} | {{halt, pos_integer()}, z:context()}.
Logging Conversion
- Replace older logging with
?LOG_* structured maps.
- Include
in => module_or_app_name.
- If
result => error, include reason => ....
- Branch on operation results and log success/failure after meaningful side effects.
- If
zotonic_core/include/zotonic.hrl is included, do not also include kernel/include/logger.hrl.
Crypto And JSON
- Prefer Zotonic JSON helpers such as
z_json.
- Replace deprecated crypto calls:
- old
crypto:hmac(...)
- new
crypto:mac(hmac, sha256, Key, Data)
- Use binary-safe signing/authorization code. Keep values as binaries when constructing signatures.
Payment Modules
- For PSP modules, observe payment notifications and return
#payment_psp_handler{} where expected.
- Use
m_payment:get/2, m_payment:get_by_psp/3, m_payment_log:log/4, and mod_payment:set_payment_status/4.
- Add
-mod_config entries for all PSP settings, such as live/test flags, API keys, webhook host, invoice prefix, and selectable/excluded services.
- For PSP redirect controllers, validate signatures before changing payment status and log the result of status updates.
- For webhook controllers, authorize before decoding and return a 500 halt on processing errors that should be retried.
Asset Builds
- Move SCSS source from
priv/lib/scss to priv/lib-src/scss.
- Output compiled CSS to
priv/lib/css; a dist directory is not required unless the app already uses one.
- Put the SCSS build command in
priv/lib-src/Makefile.
- Add an app-level
Makefile that delegates to priv/lib-src.
- Update
Taskfile.yml to run the app-level Makefile and remove obsolete Elm build tasks after migration.
- Do not run PostCSS unless the user asks or the app already requires it.
- If SCSS imports need npm packages, copy only the needed
node_modules packages into the build directory and verify the CSS build.
Translations In Modules And Sites
- Use English template source strings and
{_ ... _} translation tags for user-facing text.
- Do not leave old 0.x Dutch literals in templates; replace them with English msgids and update PO files.
- Do not regenerate or commit POT files during normal feature work. Zotonic POT files are generated on the
master branch with:
bin/zotonic pot zotonic
- The POT command connects to an already-running Zotonic node. If a feature/test command creates POT diffs, restore or leave them out unless the user explicitly asks to update POT files.
- Merge existing translations:
msgmerge --backup=none --update priv/translations/nl.po priv/translations/template/site.pot
- Create additional languages with
msginit, for example:
msginit --no-translator --locale=de --input=priv/translations/template/site.pot --output-file=priv/translations/de.po
- Validate PO files with
msgfmt --check --output-file=/dev/null.
- When creating a new PO file, update default headers such as
Project-Id-Version and PO-Revision-Date so validation warnings do not linger.
Verification
- Run
make for asset changes.
- Run
./rebar3 compile for Erlang/module changes.
- Do not run POT generation for normal translation-related template changes; POT files are generated on
master with bin/zotonic pot zotonic. Run msgmerge/msginit and msgfmt --check only when intentionally updating PO files.
- After compile, check
git diff -- rebar.lock; remove unrelated generated lockfile dependency churn unless the task intentionally changed dependencies.
- Ignore
erl_crash.dump; it is already in .gitignore and should not be reported as actionable worktree noise.
1---2name: zotonic-module-porting3description: Use when creating Zotonic 1.x modules or porting Zotonic 0.x modules. Covers app structure, .app.src, -mod_config, controllers, binary request keys, JSON decoding, logging, specs, SCSS builds, Makefile/Taskfile usage, and payment module conventions.4---56# Zotonic Module Porting78## Module Structure910A Zotonic 1.x module/app usually has:1112```text13app_or_module/14├── rebar.config15├── Makefile16├── Taskfile.yml (optional, delegate to Makefile if present)17├── priv/18│ ├── dispatch/dispatch19│ ├── lib/ (compiled static output)20│ ├── lib-src/ (SCSS/JS source and build Makefile)21│ └── templates/22└── src/23 ├── app_or_module.app.src24 ├── mod_app_or_module.erl25 ├── actions/26 ├── filters/27 ├── scomps/28 ├── validators/29 ├── controllers/30 ├── models/31 └── support/32```3334- Add `.app.src` in `src/`.35- In the main module, add `-mod_config([...])` for all configurable options.36- Use dependencies via `-mod_depends([...])` and `.app.src` applications as appropriate.37- Module application names must start with `zotonic_mod_...`.38- The main Erlang module of `zotonic_mod_mymod` is `src/mod_mymod.erl`.39- Actions, validators, and scomps should include the module name in their Erlang module name to support Zotonic override behavior.40- A site app has the same shape but uses a site module such as `src/sitename.erl` and must include `priv/zotonic_site.config`, `.json`, `.yaml`, or `.yml`.41- Keep source files UTF-8 with LF line endings.4243## Module Boundaries4445- Keep `src/mod_*.erl` small and Zotonic-facing. It should declare module metadata and config, handle lifecycle callbacks, observe notifications, install data, and connect the module to Zotonic.46- Put public module APIs in `src/models/m_*.erl` when templates, other modules, or model lookups need to call them. Models should normalize external/template input, apply ACL checks for model data, read module configuration, and return template-friendly data structures.47- Put worker processes and domain implementation in `src/support/`. This includes `gen_server` workers, parsers, protocol adapters, batching internals, external command/process communication, and pure transformation helpers.48- Let the model mediate support modules when the functionality is exposed to Zotonic. For example, have the model ensure a shared worker is started, pass timeouts/configuration, call the support worker, and normalize `{ok, ...} | {error, ...}` results for callers.49- Move code out of `mod_*.erl` once it stops being module lifecycle or observer glue. Move code out of `m_*.erl` when it becomes reusable implementation detail rather than a Zotonic-facing API.5051## Porting From Zotonic 0.x5253- Replace Webmachine-style controllers with Zotonic 1.x/Cowmachine callbacks.54- Look at `zotonic_mod_base` controllers and Cowmachine defaults before implementing callbacks.55- Omit callbacks where defaults are correct.56- Replace old request APIs and `wrq` usage with `z_context`, `m_req`, `cowmachine_req`, and Zotonic controller helpers.57- Replace string query keys with binary keys:5859```erlang60z_context:get_q(<<"payment_nr">>, Context)61```6263- For JSON request bodies, use:6465```erlang66z_controller_helper:decode_request_noz(AcceptedCT, Context)67```6869- For raw request-body authorization checks, read the body once and store it in the context if it must be reused.70- When porting templates, remove old convenience patterns that do not work well in Zotonic 1.x, such as `{% with m.rsc[id] as r %}` followed by `r.foo`; use `id.foo` directly in admin edit templates.7172## Data And Types7374- In Zotonic 1.x, request keys, JSON keys, and query keys are generally binaries.75- For text manipulation, use Unicode-aware `unicode` or `z_string` routines. Do not slice or truncate text with byte-oriented binary operations, as those can split UTF-8 codepoints.76- Use maps for decoded JSON and payment/resource data.77- Avoid atoms for validation of external values unless they are already internal finite-state values.78- Use named `-spec` variables with `when` clauses:7980```erlang81-spec callback(Body, Context) -> Result82 when83 Body :: binary(),84 Context :: z:context(),85 Result :: {true, z:context()} | {{halt, pos_integer()}, z:context()}.86```8788## Logging Conversion8990- Replace older logging with `?LOG_*` structured maps.91- Include `in => module_or_app_name`.92- If `result => error`, include `reason => ...`.93- Branch on operation results and log success/failure after meaningful side effects.94- If `zotonic_core/include/zotonic.hrl` is included, do not also include `kernel/include/logger.hrl`.9596## Crypto And JSON9798- Prefer Zotonic JSON helpers such as `z_json`.99- Replace deprecated crypto calls:100 - old `crypto:hmac(...)`101 - new `crypto:mac(hmac, sha256, Key, Data)`102- Use binary-safe signing/authorization code. Keep values as binaries when constructing signatures.103104## Payment Modules105106- For PSP modules, observe payment notifications and return `#payment_psp_handler{}` where expected.107- Use `m_payment:get/2`, `m_payment:get_by_psp/3`, `m_payment_log:log/4`, and `mod_payment:set_payment_status/4`.108- Add `-mod_config` entries for all PSP settings, such as live/test flags, API keys, webhook host, invoice prefix, and selectable/excluded services.109- For PSP redirect controllers, validate signatures before changing payment status and log the result of status updates.110- For webhook controllers, authorize before decoding and return a 500 halt on processing errors that should be retried.111112## Asset Builds113114- Move SCSS source from `priv/lib/scss` to `priv/lib-src/scss`.115- Output compiled CSS to `priv/lib/css`; a `dist` directory is not required unless the app already uses one.116- Put the SCSS build command in `priv/lib-src/Makefile`.117- Add an app-level `Makefile` that delegates to `priv/lib-src`.118- Update `Taskfile.yml` to run the app-level Makefile and remove obsolete Elm build tasks after migration.119- Do not run PostCSS unless the user asks or the app already requires it.120- If SCSS imports need npm packages, copy only the needed `node_modules` packages into the build directory and verify the CSS build.121122## Translations In Modules And Sites123124- Use English template source strings and `{_ ... _}` translation tags for user-facing text.125- Do not leave old 0.x Dutch literals in templates; replace them with English msgids and update PO files.126- Do not regenerate or commit POT files during normal feature work. Zotonic POT files are generated on the `master` branch with:127128```sh129bin/zotonic pot zotonic130```131132- The POT command connects to an already-running Zotonic node. If a feature/test command creates POT diffs, restore or leave them out unless the user explicitly asks to update POT files.133- Merge existing translations:134135```sh136msgmerge --backup=none --update priv/translations/nl.po priv/translations/template/site.pot137```138139- Create additional languages with `msginit`, for example:140141```sh142msginit --no-translator --locale=de --input=priv/translations/template/site.pot --output-file=priv/translations/de.po143```144145- Validate PO files with `msgfmt --check --output-file=/dev/null`.146- When creating a new PO file, update default headers such as `Project-Id-Version` and `PO-Revision-Date` so validation warnings do not linger.147148## Verification149150- Run `make` for asset changes.151- Run `./rebar3 compile` for Erlang/module changes.152- Do not run POT generation for normal translation-related template changes; POT files are generated on `master` with `bin/zotonic pot zotonic`. Run `msgmerge`/`msginit` and `msgfmt --check` only when intentionally updating PO files.153- After compile, check `git diff -- rebar.lock`; remove unrelated generated lockfile dependency churn unless the task intentionally changed dependencies.154- Ignore `erl_crash.dump`; it is already in `.gitignore` and should not be reported as actionable worktree noise.