Guides

What a route returns

Every route is a blank canvas: the standard page, your own HTML, JSON, XML, text, CSV, files and PDF.

Every route is a blank canvas. A route is a folder with an index.tpl, and what that template produces is the answer, nothing more. Nimbly adds no layout, wrapper or format of its own.

  • To get the standard page, with the head, the assets and in-place editing, end the template with [#html#].
  • To get your own HTML document, write it.
  • To get JSON, XML, plain text, CSV, a file or a PDF, produce that instead.

All of them use the same shortcodes, data, templates, libraries and permissions. A site can mix them freely: a public site with the standard shell, a landing page with its own document, a JSON feed, a sitemap and a members-only download, side by side.

The route returns How
The standard page [#html#] at the end of index.tpl
Your own HTML document Write the document in index.tpl; see Your own HTML
JSON [#http-header json#] and the JSON
XML, a feed [#http-header xml#] and the XML
Plain text [#http-header txt#] and the text
CSV [#http-header csv#] and the rows
A file A library that sends the file's headers and contents
A PDF The PDF module, from an HTML template
A redirect [#redirect url#]
A status code [#http-header 404#], 403 or 500

The address

A route folder may contain a dot, so the address can carry an extension:

ext/uri/projects.json/index.tpl     /projects.json
ext/uri/feed.xml/index.tpl          /feed.xml
ext/uri/export/projects.csv/        /export/projects.csv

A dynamic route works the same way, for example ext/uri/api/projects/(slug)/ that answers JSON for one project. See Routing.

The content type

[#http-header type#] at the start of a template sets the Content-Type:

Type Content-Type
json application/json
xml application/xml; charset=UTF-8
txt text/plain; charset=UTF-8
csv application/csv; charset=UTF-8
css text/css
js application/javascript

For any other type, or for extra headers, use a library and PHP's header(), as shown below.

JSON

ext/uri/projects.json/index.tpl:

[#http-header json#][#data projects filter=published:1 sort=year|numeric|desc#][#fmt var=data.projects empty={} type=json#]

[#fmt ... type=json#] encodes a variable; [#get project.title json#] encodes a single value, with quotes and escaping. Start the template with the header and put nothing before the JSON.

For a shape of your own, write it in the template:

[#http-header json#][#data .config.site#]{"site": [#get data.config.site.name json#], "projects": [#data-count projects#]}

When the answer needs logic, build it in a library:

<?php
// ext/lib/project-stats.php

function project_stats_sc($params)
{
    load_library('data');
    $projects = data_read('projects') ?: [];
    header('Content-Type: application/json');
    return json_encode([
        'total' => count($projects),
        'per_sector' => array_count_values(array_column($projects, 'sector')),
    ]);
}

ext/uri/stats.json/index.tpl holds one line: [#project-stats#].

XML and feeds

ext/uri/feed.xml/index.tpl:

[#http-header xml#]<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Fieldwork projects</title>
    [#data projects filter=published:1 sort=year|numeric|desc limit=20#]
    [#repeat data.projects tpl=feed-item#]
  </channel>
</rss>

with a template per item, ext/uri/feed.xml/feed-item.tpl:

<item>
  <title>[#fmt var=item.title type=plain#]</title>
  <link>[#base-url#]/projects/[#item.title_slug#]</link>
  <description>[#fmt var=item.description type=plain#]</description>
</item>

Every site already answers sitemap.xml and robots.txt. A resource adds its records to the sitemap with a sitemap setting in its .meta.

Plain text

[#http-header txt#][#data .config.site#][#get data.config.site.name#]
Projects: [#data-count projects#]

CSV

[#http-header csv#]title,sector,year
[#data projects filter=published:1#][#repeat data.projects tpl=csv-row#]

with csv-row.tpl holding one line per record. The admin also exports any resource for users with export-<resource>.

Files

Files uploaded to the media library are served at /download/<uuid>, with their stored type and name.

To serve a file under your own rules, such as a document for members only or a file generated on request, let a library send it:

<?php
// ext/lib/member-document.php

function member_document_sc($params)
{
    load_library('access');
    if (!access_by_feature('view-member-documents')) {
        http_response_code(403);
        exit();
    }
    $path = $GLOBALS['SYSTEM']['file_base'] . 'ext/data/.member-files/handbook.pdf';
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="Member handbook.pdf"');
    header('Content-Length: ' . filesize($path));
    readfile($path);
    exit();
}

ext/uri/members/handbook.pdf/index.tpl holds [#member-document#]. The same pattern sends a ZIP, an image made on the fly or a calendar file: the headers and the bytes are yours.

PDF documents

The PDF module renders HTML from your own templates to PDF, caches the result and sends it as a download:

./nimbly module:install pdf
load_library('pdf');
$pdf = pdf_render_cached_document([
    'paper_size' => 'a4',
    'orientation' => 'portrait',
    'pages' => [$html],
]);
pdf_send_download($pdf, 'Project-overview.pdf');
exit();

The cache key covers the document and the renderer, so a changed text or template produces a new PDF by itself.

Images

Uploaded images are served at /img/<uuid>/<size>, in the size the address asks for and as WebP: 800w for a width, 400x400c for a crop. In templates, [#get-img-html#] writes the responsive markup. See Media and inline editing.

Redirects and status codes

[#redirect [#base-url#]/en/#]
[#http-header 404#][#http-header json#]{"error": "not found"}

A dynamic route that does not accept a request falls through to the site's 404 page, which ext/uri/errors/404/ can replace.

The REST API

Every resource also answers at /api/v1/<resource> with JSON, for list, read, create, update and delete, under the permissions of the caller. Use it for apps and integrations, and write routes of your own for anything it does not cover. See the REST API reference.

View this page as Markdown