# Modules

Package reusable application behavior, templates and resources.

A module is a self-contained feature that bundles its own routes, templates, libraries, and install logic.

Use a module when:
- A feature has **both a library (shortcode) and a dedicated route** that belong together - e.g. a Stripe integration with a webhook endpoint and a `stripe-webhook` shortcode.
- A feature ships as a **reusable unit** - e.g. an event system, a shop, a blog - rather than a loose set of pages.
- A feature needs an **install script** to create resources or register routes.

Use `ext/lib/` and `ext/uri/` directly (without a module) for things that don't naturally group together - a standalone utility shortcode, or a one-off page with no associated library.

## Module directory structure

```
ext/modules/<name>/
  .install.inc     # Install script - runs once when the module is installed
  lib/             # Shortcode libraries scoped to this module
  tpl/             # Templates scoped to this module
  uri/             # Routes scoped to this module (merged into the URL space)
```

Module templates follow the same `index.tpl` convention as `ext/tpl/`. A template at `ext/modules/member/tpl/member-status/index.tpl` is callable as `[#member-status#]`. Flat `.tpl` files inside the folder are partials - only `index.tpl` is the public shortcode entry point.

Routes inside `modules/<name>/uri/` are served at the same paths as if they were in `ext/uri/`. A route at `ext/modules/event/uri/event/(slug)/index.tpl` is accessible at `/event/<slug>/`.

## Module auto-discovery

All modules in `ext/modules/` and `core/modules/` are discovered automatically on the first template lookup of any request. There is no need to declare or load a module explicitly - its templates, libraries, and routes are always available.

The `[#module name#]` shortcode is a no-op and should not be used. Use `[#nop module event#]` or a comment if you want to document a dependency inline.

## `.install.inc`

The install script runs when `./nimbly module:install <name>` (or the admin install button) is executed. Use it to create the resource(s) and register any dynamic routes the module needs.

```php
<?php

load_library("data");

// Create the resource if it doesn't exist yet
$result = data_create_resource("events", [
    "fields" => [
        "title"     => ["name" => "Title", "type" => "text", "required" => true],
        "title_slug" => ["name" => "URL slug", "type" => "slug", "source" => "title"],
        "published" => ["name" => "Published", "type" => "boolean"],
        "date"      => ["name" => "Date", "type" => "date"],
        "body"      => ["name" => "Body", "type" => "html",
                        "buttons" => "h2,h3,bold,italic,anchor",
                        "media" => true, "media_sizes" => "sm-90,md-70,lg-60",
                        "admin_col" => false],
        "sort_order" => ["name" => "Sort order", "type" => "text", "admin_col" => false]
    ],
    "index" => ["title_slug"],
    "sort" => ["field" => "sort_order", "flags" => "numeric", "order" => "asc"]
]);

// Register any dynamic routes this module needs
$route = 'events/(slug)';
$result &= data_exists(".routes", md5($route))
    || data_create(".routes", md5($route), ["route" => $route, "order" => 200]);

return $result;
```

## PHP data API (available in `.install.inc` and `route.inc`)

| Function | Description |
|---|---|
| `data_create_resource($name, $meta)` | Creates a new resource with the given `.meta` definition |
| `data_create($resource, $uuid, $data)` | Creates a new record |
| `data_read($resource, $uuid)` | Reads a single record by UUID |
| `data_exists($resource, $uuid)` | Returns true if a record exists |
| `load_library($name)` | Loads a shortcode library so its PHP functions are available |

## Slug-to-UUID routing

Use the index system to resolve a slug from the URL to a record. Add `title_slug` to the `index` array in `.meta`, then look it up in `route.inc`:

```php
<?php

$parts = router_match(__FILE__);
if ($parts === false || count($parts) !== 1) return;

$slug = $parts[0];
load_library('data');
load_library('util');

$records = data_read_index('events', 'title_slug', md5_uuid($slug));
if (empty($records)) return;

$record = reset($records);
set_variable_dot('record', $record);

router_accept();
```

The record UUID is stable and random - slugs are stored as regular fields and indexed for fast lookup. If the slug field changes, the index updates automatically on next save.

---
