{"result":{"depiction_url":null,"edges":{"hasreference":{"objects":[{"created":"2026-10-07T11:13:48Z","object_id":{"id":1576,"is_a":["text","documentation","cookbook"],"name":"doc_cookbook_writing_module","title":{"_type":"trans","tr":{"en":"Create a reusable module"}},"uri":"https:\/\/zotonic.com\/id\/1576"},"seq":1},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2919,"is_a":["text","documentation","developerguide"],"name":"developer_application_identities","title":{"_type":"trans","tr":{"en":"Erlang applications, Zotonic modules, and sites"}},"uri":"https:\/\/zotonic.com\/id\/2919"},"seq":2},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2962,"is_a":["text","documentation","developerguide"],"name":"developer_schema_upgrades","title":{"_type":"trans","tr":{"en":"Upgrade a module&#39;s data schema"}},"uri":"https:\/\/zotonic.com\/id\/2962"},"seq":3},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2924,"is_a":["text","documentation","developerguide"],"name":"developer_module_priority","title":{"_type":"trans","tr":{"en":"Dependencies, capabilities, and module priority"}},"uri":"https:\/\/zotonic.com\/id\/2924"},"seq":4}],"predicate":{"id":2774,"is_a":["meta","predicate"],"name":"hasreference","title":{"_type":"trans","tr":{"en":"Documentation reference"}},"uri":"https:\/\/zotonic.com\/id\/hasreference"}},"references":{"objects":[{"created":"2020-05-30T05:47:40Z","object_id":{"id":1352,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_templates","title":{"_type":"trans","tr":{"en":"Create a page layout"}},"uri":"https:\/\/zotonic.com\/id\/1352"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1540,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_controllers","title":{"_type":"trans","tr":{"en":"Add an HTTP controller"}},"uri":"https:\/\/zotonic.com\/id\/1540"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1541,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_dispatch_rules","title":{"_type":"trans","tr":{"en":"Follow a request to its template"}},"uri":"https:\/\/zotonic.com\/id\/1541"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1476,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_admin_modules","title":"mod_admin_modules","uri":"https:\/\/zotonic.com\/id\/1476"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1728,"is_a":["text","documentation","reference"],"name":"doc_developerguide_configuration_site_configuration","title":"Site configuration","uri":"https:\/\/zotonic.com\/id\/1728"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1528,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_status_site","title":{"_type":"trans","tr":{"en":"Find the status site and your site&#39;s admin"}},"uri":"https:\/\/zotonic.com\/id\/1528"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1372,"is_a":["text","documentation","reference","template_tag"],"name":"doc_template_tag_tag_lib","title":"lib","uri":"https:\/\/zotonic.com\/id\/1372"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1347,"is_a":["text","documentation","reference","template_tag"],"name":"doc_template_tag_tag_extends","title":"extends","uri":"https:\/\/zotonic.com\/id\/1347"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1539,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_wires","title":{"_type":"trans","tr":{"en":"Handle a browser event with a wire"}},"uri":"https:\/\/zotonic.com\/id\/1539"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1419,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_forms_and_validation","title":{"_type":"trans","tr":{"en":"Validate a form on both sides"}},"uri":"https:\/\/zotonic.com\/id\/1419"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1752,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_acl_user_groups","title":"mod_acl_user_groups","uri":"https:\/\/zotonic.com\/id\/1752"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1274,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_notifications","title":{"_type":"trans","tr":{"en":"Respond to a notification"}},"uri":"https:\/\/zotonic.com\/id\/1274"},"seq":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"refers":{"objects":[{"created":"2026-10-07T11:13:00Z","object_id":{"id":2926,"is_a":["text","documentation","developerguide"],"name":"developer_startup_activation","title":{"_type":"trans","tr":{"en":"Application startup and per-site module activation"}},"uri":"https:\/\/zotonic.com\/id\/2926"},"seq":1000000},{"created":"2026-10-07T11:13:00Z","object_id":{"id":2960,"is_a":["text","documentation","developerguide"],"name":"developer_model_api","title":{"_type":"trans","tr":{"en":"Expose data through a model"}},"uri":"https:\/\/zotonic.com\/id\/2960"},"seq":1000000}],"predicate":{"id":2409,"is_a":["meta","predicate"],"name":"refers","title":{"_type":"trans","tr":{"en":"Refers"}},"uri":"https:\/\/zotonic.com\/id\/refers"}},"relation":{"objects":[{"created":"2020-05-30T05:47:40Z","object_id":{"id":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1576,"is_a":["text","documentation","cookbook"],"name":"doc_cookbook_writing_module","title":{"_type":"trans","tr":{"en":"Create a reusable module"}},"uri":"https:\/\/zotonic.com\/id\/1576"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1541,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_dispatch_rules","title":{"_type":"trans","tr":{"en":"Follow a request to its template"}},"uri":"https:\/\/zotonic.com\/id\/1541"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1372,"is_a":["text","documentation","reference","template_tag"],"name":"doc_template_tag_tag_lib","title":"lib","uri":"https:\/\/zotonic.com\/id\/1372"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1352,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_templates","title":{"_type":"trans","tr":{"en":"Create a page layout"}},"uri":"https:\/\/zotonic.com\/id\/1352"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1539,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_wires","title":{"_type":"trans","tr":{"en":"Handle a browser event with a wire"}},"uri":"https:\/\/zotonic.com\/id\/1539"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1540,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_controllers","title":{"_type":"trans","tr":{"en":"Add an HTTP controller"}},"uri":"https:\/\/zotonic.com\/id\/1540"},"seq":1000000},{"created":"2020-05-30T05:47:40Z","object_id":{"id":1419,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_forms_and_validation","title":{"_type":"trans","tr":{"en":"Validate a form on both sides"}},"uri":"https:\/\/zotonic.com\/id\/1419"},"seq":1000000}],"predicate":{"id":303,"is_a":["meta","predicate"],"name":"relation","title":{"_type":"trans","tr":{"nl":"Relatie","en":"Relation"}},"uri":"http:\/\/purl.org\/dc\/terms\/relation"}},"subject":{"objects":[{"created":"2026-10-07T11:13:48Z","object_id":{"id":2554,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_how_to_guide","title":"How-to guide","uri":"https:\/\/zotonic.com\/id\/2554"},"seq":1},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":3},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2604,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_module_management","title":"Module management","uri":"https:\/\/zotonic.com\/id\/2604"},"seq":4},{"created":"2026-10-07T11:13:48Z","object_id":{"id":2658,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_migrate","title":"Migrate","uri":"https:\/\/zotonic.com\/id\/2658"},"seq":5}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":1353,"is_a":["text","documentation","developerguide"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1353"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_developerguide_modules","page_url":{"en":"https:\/\/zotonic.com\/docs\/1353\/create-a-reusable-module","x-default":"https:\/\/zotonic.com\/docs\/1353\/create-a-reusable-module"},"preview_url":null,"resource":{"version":42,"pivot_location_lat":null,"title":{"_type":"trans","tr":{"en":"Create a reusable module"}},"is_authoritative":true,"body":{"_type":"trans","tr":{"en":"<p>Create <code>apps_user\/zotonic_mod_garden<\/code> with the following files. This example adds a discoverable module without starting an extra process.<\/p>\n<p><code>rebar.config<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{erl_opts, [debug_info]}.\n<\/code><\/pre>\n<p><code>src\/zotonic_mod_garden.app.src<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{application, zotonic_mod_garden, [\n    {description, &quot;Community garden features&quot;},\n    {vsn, &quot;0.1.0&quot;},\n    {registered, []},\n    {applications, [kernel, stdlib, zotonic_core]},\n    {env, []},\n    {modules, []}\n]}.\n<\/code><\/pre>\n<p><code>src\/mod_garden.erl<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-module(mod_garden).\n-mod_title(&quot;Garden&quot;).\n-mod_description(&quot;Community garden features.&quot;).\n-mod_prio(500).\n<\/code><\/pre>\n<p>Run <code>make<\/code> from the Zotonic workspace root. On the running development node, use <code>bin\/zotonic update<\/code> to refresh discovery. Open module management in the site&#39;s admin and activate Garden.<\/p>\n<p>Check that the module is active. Add the model from <a href=\"\/id\/2960\">Expose data through a model<\/a> for a first data lookup, and put templates in <code>priv\/templates<\/code>. Keep installation hooks and observers in the main module; move domain logic into models or support modules.<\/p>\n<h2>Erlang applications, Zotonic modules, and sites<\/h2>\n<p>These terms describe different parts of the system.<\/p>\n<aside><p>Putting a package on the code path is different from activating its Zotonic module for a particular site. Use <a href=\"\/id\/2926\">Application startup and per-site module activation<\/a> to understand this distinction when a compiled feature is missing.<\/p><\/aside>\n<p>An <strong>Erlang application<\/strong> is a package described by a <code>.app<\/code> file, normally generated from <code>src\/&lt;name&gt;.app.src<\/code>. It can provide modules and an OTP application callback.<\/p>\n<p>A <strong>Zotonic module<\/strong> is functionality that can be activated for a site. Its main Erlang module declares attributes such as <code>-mod_title<\/code> and <code>-mod_depends<\/code>.<\/p>\n<p>A <strong>site<\/strong> is a named website with its own configuration and runtime context. Several sites share the same Erlang VM and loaded code, while module activation and site data are site-specific.<\/p>\n<h2>Upgrade a module&#39;s data schema<\/h2>\n<p>Version schema changes through the module&#39;s installation and upgrade mechanism. Inspect a module using <code>-mod_schema<\/code> and <code>manage_schema\/2<\/code> before adding your own versioned step.<\/p>\n<p>Make each step safe for the state it may encounter after a partial attempt. Separate database structure changes from long-running data conversion when a single startup transaction would be too costly.<\/p>\n<p>Test a fresh install and an upgrade from the previously released schema. Keep a database backup for the upgrade test and verify application behavior after migration, not just the table definitions.<\/p>\n<p>If version 1 installed the Garden fixture, change the declaration to <code>-mod_schema(2).<\/code> for the next release. Keep the fresh-install clause and add a <code>manage_schema({upgrade, 2}, Context)<\/code> clause for the version-1-to-2 change. Erlang function clauses are separated with semicolons. Do not return success for an upgrade you have not implemented.<\/p>\n<p>The manager calls upgrades in sequence, within a database transaction per schema step, then applies any returned datamodel and records the version. A code reload alone is not an upgrade test: run the module activation\/startup lifecycle on the test site. Use <code>manage_data\/2<\/code> for a deliberate follow-up after schema work, or queue longer conversions. Compare fresh-install and upgraded data before release.<\/p>\n<h2>Dependencies, capabilities, and module priority<\/h2>\n<p>The main site or module can declare:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-mod_depends([mod_base]).\n-mod_prio(500).\n<\/code><\/pre>\n<p>Dependencies express requirements for activation. Modules can also declare provided capabilities with <code>-mod_provides<\/code>; depend on an established capability when your feature requires that service rather than one particular implementation.<\/p>\n<p>Priority determines ordering when modules supply competing resources such as templates. Lower numerical priorities take precedence. Sites commonly use a low priority so their templates override generic module templates.<\/p>\n<p>Do not use priority to hide a missing dependency or duplicate Erlang module name. Check which modules are active, inspect the selected template, and make one intentional override at a time.<\/p>"}},"slug":"create-a-reusable-module","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"is_featured":false,"content_group_id":{"id":339,"is_a":["meta","content_group"],"name":"default_content_group","title":{"_type":"trans","tr":{"en":"Default Content Group"}},"uri":"https:\/\/zotonic.com\/id\/default_content_group"},"category_id":{"id":317,"is_a":["meta","category"],"name":"developerguide","title":"Developer guide","uri":"https:\/\/zotonic.com\/id\/developerguide"},"publication_start":"2022-02-15T10:01:00Z","is_website_redirect":false,"github_url":"https:\/\/github.com\/zotonic\/zotonic\/tree\/master\/doc\/developer-guide\/modules.rst","pivot_location_lng":null,"name":"doc_developerguide_modules","is_unfindable":false,"is_published":true,"doc_editorial_edges":{"hasreference":[1576,2919,2924,2962],"subject":[2554,2564,2604,2635,2658]},"summary":{"_type":"trans","tr":{"en":"Create apps_user\/zotonic_mod_garden with the following files. This example adds a discoverable module without starting an extra process."}},"pivot_geocode":null,"custom_slug":false,"created":"2020-05-30T05:47:19Z","uri":null,"date_is_all_day":false,"is_dependent":false,"is_page_path_multiple":false,"publication_end":"9999-08-17T12:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_editorial_bundle":true,"creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"seo_noindex":false,"modified":"2026-10-07T11:14:01Z","title_slug":{"_type":"trans","tr":{"en":"create-a-reusable-module"}}},"uri":"https:\/\/zotonic.com\/id\/1353","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1353"}},"status":"ok"}