Templates
WebResponse.template(name, model) answers with a rendered page.
The name carries no extension: the engine appends its own.
app.get("/decks/{deckId}", req ->
WebResponse.template("deck", Map.of("deck", service.deck(req.pathParamLong("deckId")))));
Nothing has to be configured for that: an App renders with jte over classpath:/jte, appending .jte, so the handler above renders classpath:/jte/deck.jte.
The model is a Map, passed to jte’s parameters by name, and ContentType.Html escapes ${} output for you.
A root or a suffix of your own
app.templates(new JteTemplates("templates") // classpath:/templates
.suffix(".html")); // .../deck.html
The suffix is appended, never checked for, so a name that still carries its extension is looked up with the suffix twice over.
Another engine
TemplateRenderer is a single method — a template name, a model, and the writer to render into.
An engine that is not jte is that one method, and templates(renderer) puts it in place of the default.
app.templates((template, model, out) -> mustache.compile(template + ".mustache").execute(out, model));
Three engines come as modules of their own, with the root, the suffix, and the escaping already settled: FreeMarker, Handlebars, and Thymeleaf.
app.templates(new FreeMarkerTemplates("freemarker")); // classpath:/freemarker/deck.ftlh
app.templates(new HandlebarsTemplates("hbs")); // classpath:/hbs/deck.hbs
app.templates(new ThymeleafTemplates("thymeleaf")); // classpath:/thymeleaf/deck.html
Anything else is the one-liner above; a module only earns its place when there is something to settle beyond compile(name).render(model).
Rendering happens while the handler’s exception handling still applies, so a template that throws reaches app.exception(…) like any other failure, and a HEAD of a rendered page reports the length the GET would have sent.
Development and production
jte compiles templates at runtime by default, reading them from the classpath: nothing to configure, but it needs a JDK on the deployment machine, and an edited template is not seen until the classpath copy is rebuilt.
The engine has a mode for each side of that trade, and JteTemplates has a constructor for each.
Development resolves templates from the source directory instead of the classpath:
app.templates(new JteTemplates(
new DirectoryCodeResolver(Path.of("src/main/resources/jte"))));
Before each render, jte checks the modification timestamp of the template file and everything it depends on, and recompiles what changed — saving a .jte file and refreshing the browser is the whole loop.
Production precompiles every template at build time with the jte Gradle plugin:
plugins {
id 'gg.jte.gradle' version '3.2.4'
}
jte {
sourceDirectory = file('src/main/resources/jte').toPath()
contentType = gg.jte.ContentType.Html
generate()
}
generate() turns each template into a Java class compiled along with the application, and the matching constructor hands the engine those classes:
app.templates(new JteTemplates(TemplateEngine.createPrecompiled(ContentType.Html)));
The jar then renders from compiled classes: no runtime compiler, no JDK on the machine, and a template that does not compile fails the build instead of the request.
The suffix applies either way, so no handler changes.
The example project wires both modes behind a --dev flag, and a native image has no compiler at runtime at all, so there this mode is the only one.