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 and appends .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.
The response copies the model’s entries into a read-only map, null values and iteration order kept, so the renderer receives a map it cannot change.
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, taking a template name, a model, and the writer to render into.
Implement it for an engine that is not jte, and pass it to templates(renderer) to take the 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.
A template that throws therefore reaches app.exception(…) like any other failure, and a HEAD of a rendered page reports the length the GET would have sent.
The handler returns the template’s name and model, and AppServlet renders them before the response is written:
Development and production
jte compiles templates at runtime by default, reading them from the classpath.
There is nothing to configure, but the deployment machine needs a JDK, 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()
}
The spider-silk plugin's spiderSilk { jte() } writes this exact block for you, with jte’s native-image resources extension attached besides.
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.
A native image has no compiler at runtime at all, so precompiling is the only mode there.