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.