Guides

Frontend stack

The default Tailwind CSS, DaisyUI and Alpine.js setup, and React, Vue or any other toolset.

Nimbly renders HTML on the server and leaves the browser side to you. A project gets Tailwind CSS, DaisyUI and Alpine.js set up by default, and the admin is built with them. Your own routes can use them, add to them, or use a different toolset altogether.

The default stack

Tool Used for
Tailwind CSS 4 Utility classes in templates
DaisyUI 5 Component classes and themes
Alpine.js Interaction, directly in the markup

./nimbly build scans the templates of core/ and ext/ for classes and builds ext/static/app.css and ext/static/app.js. ./nimbly watch keeps them current while you edit.

  • Theme. Colors and DaisyUI themes are in ext/tailwind.theme.js. The admin and the Nimbly bar use the same tokens, so the editing tools follow the brand.
  • Styles. ext/theme.css holds the project's own CSS, imported after the framework styles so it wins.
  • Classes in variables. Tailwind finds classes by reading the source. Quote a class list that is stored in a variable, [#set menu-text="text-stone-500"#], so the classes appear as a string in the template.

Server-rendered markup with Alpine

Alpine adds behavior to markup the server already rendered. Keep content in HTML when the browser only shows, hides, sorts or filters it, and give Alpine the few values it needs:

<article data-sector="[#item.sector#]" x-show="sector === 'all' || sector === $el.dataset.sector">
  [#get-html item.description#]
</article>

When the browser builds the interface from data, pass the records as JSON in a script block, which is safe for any text the records hold:

<script type="application/json" id="projects-data">[#fmt var=data.projects empty={} type=json#]</script>
<div x-data="{ projects: Object.values(JSON.parse(document.getElementById('projects-data').textContent)) }">
  <template x-for="project in projects" :key="project.uuid">
    <h3 x-text="project.title"></h3>
  </template>
</div>

For large or private data sets, let the page ask the API for one page at a time instead of embedding everything.

A library on one page

Load a library on the route that needs it. The projects example puts its map on one page with Leaflet:

[#set head="<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'>"#]
[#html#]

and loads the script with the route's own index.js, or a <script> in its main.tpl. Other pages never load it.

React, Vue or another framework

A route can host an application written with any framework. The pattern is the same for each:

  1. Build the application with its own toolchain, for example Vite, into a folder under ext/static/. Files in ext/static/ are served from the site root, so ext/static/planner/main.js is /planner/main.js.
  2. Mount it from a route: render the element it mounts on, and load the build.
  3. Pass data in the page, or let the application call the API.

A route that keeps the standard shell, ext/uri/planner/index.tpl:

[#set page-title="Planner"#]
[#set head="<link rel='stylesheet' href='[#base-url#]/planner/main.css'>"#]
[#html#]

and its main.tpl:

[#data projects filter=published:1#]
<div id="planner"></div>
<script type="application/json" id="planner-data">[#fmt var=data.projects empty={} type=json#]</script>
<script type="module" src="[#base-url#]/planner/main.js"></script>

The application reads its starting data from the script block:

import { createRoot } from 'react-dom/client';
import Planner from './Planner';

const projects = Object.values(JSON.parse(document.getElementById('planner-data').textContent));
createRoot(document.getElementById('planner')).render(<Planner projects={projects} />);

In the browser, the application can read and write through the REST API with the signed-in user's session, under the same permissions as the admin:

const response = await fetch(`${window.nb.base_url}/api/v1/projects?limit=20`, { credentials: 'same-origin' });
const { projects, total } = await response.json();

window.nb.base_url is set by the shell. On a route with a document of its own, write the base address into the page with [#base-url#].

Keep the application's source and toolchain in a folder of its own inside ext/, such as ext/planner/ with its own package.json, and commit or deploy its build output the same way as app.css.

A different stack for the whole site

To use another CSS framework or no framework on every public page, give each route a document of its own, or replace parts of the page shell in ext/tpl/html/. The admin uses the same shell and needs app.css and app.js, so add your assets next to them rather than removing them. See Your own HTML.

View this page as Markdown