Write your pages in Tera
You'll build a noticeboard whose pages are Tera templates the host renders straight from the file. The routing, the loaders and the prerendering are the same ones every other tutorial uses. Only the page files change, from TSX to Tera, while the build still lowers the loaders.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
520 is a different setup: it embeds Tera in your own Rust binary with no app/ directory and no CLI. Here the project is an ordinary FSR app and only the page files change.
Scaffold it#
tera is a direction like react or vue, so fsr use is how you take it. A template runs on the server and ships no code to the browser. Add --example and it writes a page.tera beside its loader to start from, which this tutorial replaces.
It needs an
fsrbuilt with theterafeature, which the published binary is.
Lay out the routes#
Put page.tera where page.tsx would go and nothing else about the route changes:
app/
routes/
layout.tera the frame
layout.loader.ts
page.tera /
page.loader.ts
notice/[id]/
page.tera /notice/{id}
page.loader.ts
templates/
nav.tera a partial
src/
notices.ts
Delete the scaffold's
routes/layout.tsxandroutes/page.tsxas you add the.terafiles. A directory holding both is refused, naming the two files.
app/src/notices.ts:
;
;Write the loader#
The loader is TypeScript, lowered into the plan and run by the host exactly as it is beside a page.tsx. What it returns is the template's context.
app/routes/page.loader.ts:
;
;
app/routes/page.tera reads that notices the way a page component would have taken it as a prop:
<ul class="notices">
{% for notice in notices %}
<li><a href="/notice/{{ notice.id }}">{{ notice.title }}</a> <span>{{ notice.who }}</span></li>
{% endfor %}
</ul>
params, identity and locale reach the template through the loader rather than directly. A template only renders data the loader fetched, the same as a page component.
Write the layout#
app/routes/layout.tera uses slot where a TSX layout writes {children}:
<div class="board">
<header>{% include "templates/nav.tera" %}</header>
<main>{{ slot(name="content") }}</main>
<footer>{{ count }} notices, rendered from templates under <code>routes/</code>.</footer>
</div>
with its own loader beside it, independent of the page's:
;
;
app/templates/nav.tera is a partial and can sit anywhere outside vendor/, dist/ and generated/:
<a href="/">Noticeboard</a>
Every .tera under app/ is loaded into one Tera instance and named by its path under the app, which is why the include says templates/nav.tera. extends resolves the same way.
Add a parameterised route#
It has the same shape, plus a paths export so 110's prerendering applies here too:
;
;
;
;<article>
<h1>{{ title }}</h1>
<p>{{ body }}</p>
<p>Posted by {{ who }}.</p>
</article>
Build it#
A TSX page says lowered static, meaning the build turned it into a tree the host walks; a .tera page says template, meaning the host reads the file. The three loaders are still lowered.
The .tera files are listed among tsconfig.build.json's includes and snapfirec compiles nothing from them. error.tsx and not-found.tsx are still TSX here, since the scaffold wrote them; they'd take templates too.
Run it#
The <!--sf-g:...--> comments are the segment markers the navigator uses, the same ones a TSX page gets. Everything between them is your template's own output, whitespace included: the blank lines are where the {% for %} and {% endfor %} tags stood. Write {%- for notice in notices %} and {%- endfor %} if you want Tera to trim them.
2 notices in the footer comes from the layout's loader, which runs independently of the page's, as it does with TSX.
Prerender the notices#
paths works the same with a template:
The static/js/fsr/ files are the client modules, so the directory is a site anything can serve. prerender.build is the hash of the build it rendered from. A host of a different build ignores the directory.
Point [server] prerender at the directory in config/app.toml:
[]
= "127.0.0.1:3000"
= "dist/prerender"A development host renders every page live, so build, prerender and serve under RELEASE_ENV=production. The production layer refuses the placeholder session key, so give it one in C5_SESSION__KEY:
A notice in the set is answered from a file:
The miss ran the loader live, raised fail("not_found", ...) and answered 404 with the message:
there is no notice nope
Boot-time templates and islands#
Templates are read at boot. Edit a .tera and the running server keeps serving the old one, because the host loaded them all at startup. Restart it. Under fsr dev the loop does that for you.
That also means a renamed partial is a boot error rather than an empty page: the host refuses a plan naming a template the tree doesn't hold.
An island still works. A template places one with a literal:
{{ island(module="src/ui/Thing.tsx#default") }}
The build reads the literal and bundles that module, so you can hydrate one corner of a page nothing else in the application hydrates. It has to be a literal, since the build can only bundle a module it can name.
The board above has no framework, so placing a React component there stops the build on the adapter check rather than on anything to do with templates:
Run fsr use board/app react first if you want one.
Try it#
Put a page.tsx next to one of the page.tera files and build:
Rename templates/nav.tera and start the server. It refuses at boot, with Tera's own error and the line of the include:
Take paths out of the notice loader and prerender again: the two notice files stop being written and the route drops off the prerender list fsr serve --verbose prints. Every notice now renders per request. With two notices that costs nothing, but with thousands you want paths back.