---
title: Routing
description: How Nibiru maps URLs to controllers, actions and parameters — including SEO-friendly URL forms.
---

Nibiru routing is **convention-based** with optional regex routes from `settings.<env>.ini` for special cases. There's no big route file to maintain.

## The default convention

```
/<controller>/<action>/<param1>/<param2>/...
```

Examples:

| URL | Controller | Action | $_REQUEST |
|---|---|---|---|
| `/` | `indexController` | (none) | — |
| `/products` | `productsController` | (none) | — |
| `/products/detail` | `productsController` | `detailAction` | — |
| `/products/detail/42` | `productsController` | `detailAction` | `id => 42` |
| `/users/edit/42` | `usersController` | `editAction` | `id => 42` |

Without an `_action`, only `navigationAction()` and `pageAction()` run. With `_action`, the named action runs in between.

## SEO URLs

Nibiru auto-detects SEO-friendly URLs without any configuration. The pattern:

```
/<controller>/<slug>/<numeric-id>
```

If the second segment is **not** a method name on the controller and the third segment is numeric, Nibiru rewrites the request internally to:

```
/<controller>/detail/?id=<numeric-id>&slug=<slug>
```

So a URL like `/maschine/marduk-gold-plating/42` reaches `maschineController::detailAction()` with `$_REQUEST['id'] === '42'` and `$_REQUEST['slug'] === 'marduk-gold-plating'`. This is exactly how the production e-commerce site `prod.maschinen-stockert.de` produces clean URLs without a custom router.

## Reading parameters

Anything past the action segment becomes a `$_REQUEST` key paired by position:

```php
// /users/edit/42  →  $_REQUEST['id'] = '42'
public function editAction() {
    $id = (int) ($_REQUEST['id'] ?? 0);
    // ...
}
```

For non-numeric or named parameters, prefer query strings:

```
/users/search?q=marduk&page=2
```

## Custom routes via INI

The `[NIBIRU_ROUTING]` section in `settings.<env>.ini` lets you map regex URL patterns to controller/action pairs and named params:

```ini
[NIBIRU_ROUTING]
; Map /api/v1/products/42 to apiController::productsAction with id=42
api.v1.products.pattern    = "^/api/v1/products/(\d+)$"
api.v1.products.controller = "api"
api.v1.products.action     = "products"
api.v1.products.params[]   = "id"
```

When a URL matches the pattern, capture groups are assigned to the named `params[]` keys (in order) as `$_REQUEST` entries.

## Routing helpers

```php
Router::getInstance()->currentPage();   // 'products'
Router::getInstance()->tplName();       // 'products'  (controller stem for templates)
Router::getInstance()->getController(); // alias for currentPage()
```

These are useful inside controllers and templates:

```smarty
<a href="/products" class="{if Router::currentPage() == 'products'}active{/if}">
  Products
</a>
```

(In Smarty you'd use a Smarty plugin or a pre-assigned variable; the helper itself is for PHP.)

## Forwarding

To redirect at the framework level (sets the right HTTP headers and exits):

```php
View::forwardTo('/login');
```

For a JSON response (sets `Content-Type: application/json`):

```php
View::forwardToJsonHeader();
View::assign(['data' => ['ok' => true]]);
```

This pattern is used heavily by API endpoints in production — see the `apiController` in `data.maschinen-stockert.de`.

## Pagination URLs

Nibiru's [pagination](/core/pagination/) expects URLs of the form:

```
/<controller>/<action>/page/<N>
```

The `Pageination` class parses the trailing `page/N` segment, so any route format that preserves it is fine.

## Trailing slashes & case

URLs are matched **case-sensitively**. `/Users/edit` and `/users/edit` will hit different controllers (the second exists, the first 404s). Trailing slashes are tolerated.

## Common pitfalls

- **Action collision with SEO URL.** If you name a method `aboutAction()` and try to use `/products/about/42` as an SEO URL, the SEO rewrite *won't* trigger, because `about` is a known action. Pick slugs that don't collide with action names.
- **Action without `_action`.** Just typing `/products/detail/` does **not** call `detailAction()` — `_action` must be set. The dispatcher does this automatically when the URL has at least two segments, but a stripped query string won't.
- **`index` is the root controller.** `/` → `indexController`. There is no separate "home" route.
