Dynamic pages
A .chtml file is an HTML page that can use your recipes. Recipes always run on the server.
Overview
There are three ways to use a recipe from a page:
| Write | When it runs | Arguments come from |
|---|---|---|
%{ recipe(args) }% | When the page is rendered. The tag is replaced by the result | The page itself |
%recipe%(args) | When your JavaScript calls it. It asks the server and returns the result | Your JavaScript, so also visitors |
%#recipe%(args) | The same, without blocking the page. Returns a Promise of the result | Your JavaScript, so also visitors |
The name is the one you saved the recipe under in the editor. Names can use letters, numbers, _ and -, and can't start with a number or -.
Results in the page
A %{ }% tag is replaced by what the recipe returns, before the page reaches the browser:
<p>%{ with_tax(10) }%</p>A page can have many tags, and they run together. The result is kept until you change the page or one of its recipes, so a popular page doesn't run its recipes for every visitor. Saved changes show up immediately. The exception is a page where a recipe uses randomness or storage: that one runs again on every view.
%{ }% tag can't depend on the visitor.
For that, use JavaScript.
Live values
Add | poll(seconds) to a tag, and the page keeps that value up to date without reloading:
<p>Price: %{ with_tax(10) | poll(5) }%</p>The value is in the page straight away, and a small script asks the server for a new one every 5 seconds. For a value that changes, like one from storage, that's all it takes to show something like a live visitor count on every open page.
- Between 1 and 3600 seconds. Everyone polling the same recipe with the same arguments shares one result per interval, so a popular page doesn't use more of your limits.
- Only in the page's text. Not in a
<script>, an attribute,<title>,<style>or<textarea>: those show an error page with the line number. The value goes in a<span>of its own. - Reading only. A recipe that stores something can't be polled, since it would run at times you don't control.
- Exactly these arguments. Unlike
%recipe%(, polling doesn't let visitors call the recipe with other values. - Background tabs wait. They catch up when someone comes back. If a request fails, the last value stays, and it tries again after a while.
To do something when a value changes, listen for the nh:update event, which bubbles up from the element:
<div id="stats">Visitors: %{ with_tax(1) | poll(10) }%</div>
<script>
document.getElementById("stats").addEventListener("nh:update", event => {
console.log("New value:", event.detail.value);
});
</script>Arguments
Arguments are plain values. Calculations and variables aren't allowed: compute those in the recipe.
| Type | Example |
|---|---|
| Number | 12, -5, 1.5 |
| Text | "hello" or 'hello' |
| Yes / no / nothing | true, false, null |
| List | [1, 2, "three"] |
| Object | {"size": "large"} |
Arguments without a name follow the order of the recipe's parameters. You can also name them, and leave out any that have a default:
%{ add_two(3, 4) }%|%{ add_two(x=3, y=4) }%|%{ add_two(3) }%This uses a recipe with an optional parameter, from the recipes guide. An argument can't contain the text }%, because that ends the tag.
Where results can go
Results are made safe for the place they end up in, so a < in a result can't break your page:
| Where the tag is | What you get |
|---|---|
| In HTML, including attributes | Escaped text. Lists and objects appear as JSON, true/false as words, and null as nothing |
Inside <script> | A JavaScript value, including quotes for text |
So inside a script, don't put quotes around the tag:
<script>
const price = %{ with_tax(10) }%;
</script>A result can't produce HTML, and tags inside a result don't run. To build markup, return data and create the elements in JavaScript.
Calling from JavaScript
%recipe%(args) turns into a call to a generated function that sends the arguments to the server, runs
the recipe there, and returns the result, like any other function. The function is added to your page automatically,
before your own scripts. Use it anywhere in the page, including attributes like onclick:
<input id="net" type="number" value="10">
<button onclick="update()">Calculate</button>
<p id="out"></p>
<script>
function update() {
const net = Number(document.getElementById("net").value);
const out = document.getElementById("out");
try {
out.textContent = %with_tax%(net);
} catch (error) {
out.textContent = error.message;
}
}
</script>- If the recipe fails, the function throws an
Errorwhosemessagesays why. - Arguments are positional only, in the order of the recipe's parameters.
- The
(is required:%with_tax%on its own stays as plain text, and so does%name%(when there's no such recipe. So you can't pass the function around by name, likelist.map(%with_tax%): writelist.map(x => %with_tax%(x)).
Without blocking the page
The browser freezes the page while it waits for the answer, usually for a fraction of a second, but longer on a slow connection.
Browsers also warn about this in the console. So for anything that runs while someone is typing or scrolling, in a loop, or as
the page loads, put a # before the name. %#recipe%(args) doesn't block, and returns a Promise of the result instead:
<script type="module">
const price = await %#with_tax%(10);
document.body.append(price);
</script>
<button onclick="%#with_tax%(20).then(alert).catch(e => alert(e.message))">Price</button>
If the recipe fails, the Promise rejects with the same Error. Use await inside an async function or a
<script type="module">, or .then() anywhere else.
%recipe%( or %#recipe%( can be called by visitors. A recipe you only use in
%{ }% tags can't be called from outside, whatever arguments someone tries.
Handling visitor input
Anyone can call a recipe that a page offers to JavaScript, with any arguments. Two habits keep that safe:
- Declare your parameter types. A
"numeric[0..100]"parameter is checked before the recipe runs. Useanyonly when you really mean it. - Show results with
textContent, notinnerHTML. If a recipe returns something a visitor typed,innerHTMLwould let them run scripts on your page.
Don't count on a calculation in the browser being honest either: if a price matters, work it out in a recipe, from values in the recipe.
When something goes wrong
A mistake in a page, or a recipe that fails during rendering, shows an error page with the line number, so you'll notice it straight away:
| Message | What to do |
|---|---|
| there is no recipe 'x' | Check that the recipe is saved under that name, with the same spelling |
| expected recipe_name(arguments...) | A %{ }% tag has to contain one recipe call and nothing else |
| the arguments could not be read | Check the quotes and commas, and whether an argument contains }% |
| arguments must be plain values | Move calculations and variables into the recipe |
| parameter 'x': ... | The value doesn't match the parameter's type or range |
| step 2 (divide): ... | A step in the recipe failed. Steps are counted from 0 |
| the recipe took longer than 1 second / used too many resources | It went over the limits. Look for steps that make numbers or text very large |
Calls from JavaScript report the same messages through the thrown Error or rejected Promise. Their HTTP status codes, if you look in the browser's network tab:
| Status | Meaning |
|---|---|
200 | The recipe ran |
400 | The arguments were refused, or a step failed |
403 | This page didn't offer this recipe |
404 | The recipe no longer exists |
429 | Over a rate limit. The Retry-After header says how many seconds to wait |
500 | The recipe was stopped for using too many resources |
Limits
These keep one site from slowing down the others.
| Limit | Value |
|---|---|
| Time for one recipe | 1 second |
| Time for all recipes of a page together | 3 seconds |
| Memory for one run | 256 MB |
| Page size | 2 MB |
| Tags in a page | 5000, of which 200 can be %{ }% |
| Different recipes in a page | 200 |
Size of one %{ }% tag | 8 KB |
| Size of a recipe file | 256 KB |
| Stored values, per site | 1024 KB |
| Size of a JavaScript call | 64 KB |
| Per visitor, per site | 120 page views, 120 calls and 600 polls a minute |
| Per site, all visitors together | 600 runs and 30 seconds of run time a minute |
A "run" is one page render, however many tags it has, or one JavaScript call. All the %{ }% tags of a page share one run. Page views that are served from the saved result don't use any.