Layouts

Page layouts wrap content with common structure (nav, footer).

Basic Layout

class Nav implements Template { public Element render() { return nav("Nav"); } }
class Footer implements Template { public Element render() { return footer("Footer"); } }

public class Layout implements Template {
    private final String title;
    private final Element content;

    public Layout(String title, Element content) {
        this.title = title;
        this.content = content;
    }

    public Element render() {
        return html(
            head(title(title)),
            body(new Nav(), main(content), new Footer())
        );
    }
}

Register Layout

class Layout implements Template { public Element render() { return html(); } }
class HomePage implements Template { public Element render() { return div(); } }
class AboutPage implements Template { public Element render() { return div(); } }

// In Routes.java - set default layout
app.layout(Layout.class);

// Pages automatically wrapped
app.pages(
    "/", HomePage.class,
    "/about", AboutPage.class
);

The layout's stylesheet

A layout owns the global CSS: the reset, the design tokens, the document shell. Return it from styles() and the render puts it in <head> — no page has to remember to include it.

class Head implements Template {
    private final String title;
    Head(String title) { this.title = title; }
    public Element render() { return head(title(title)); }
}
class Nav implements Template { public Element render() { return nav("Nav"); } }
class Footer implements Template { public Element render() { return footer("Footer"); } }

public class Layout implements Template {
    private final String title = "My App";
    private final Element content = div("content");
    private final CSSValue TEXT = hex("#1e293b");
    private final CSSValue BG = hex("#ffffff");
    private final jweb.css.Theme TOKENS = jweb.css.Theme.light();

    @Override
    public Element render() {
        return html(new Head(title), body(new Nav(), main(content), new Footer()));
    }

    @Override
    public Stylesheet styles() {
        return stylesheet()
            .add(TOKENS)
            .rule(Selector.any().or(Selector.any().before()).or(Selector.any().after()), style().boxSizing(borderBox).margin(zero))
            .rule(Selector.type("body"), style().color(TEXT).backgroundColor(BG));
    }
}

Theme Tokens

public final class Theme {

    public static final jweb.css.Theme TOKENS = jweb.css.Theme.light()
        .color("primary", hex("#6366f1"))
        .color("text",    hex("#1e293b"))
        .space("4",       rem(1))
        .space("8",       rem(2))
        .dark(jweb.css.Theme.dark().color("text", hex("#e2e8f0")));

    // The names pages use — each one a var() reference
    public static final CSSValue PRIMARY = jweb.css.Theme.color("primary");
    public static final CSSValue TEXT    = jweb.css.Theme.color("text");
    public static final CSSValue SP_4    = jweb.css.Theme.space("4");
    public static final CSSValue SP_8    = jweb.css.Theme.space("8");
}

// Usage — unchanged (import static ...Theme.* to drop the prefix)
div(style().color(Theme.PRIMARY).padding(Theme.SP_4))
Define design tokens in Theme.java for consistent styling across your app. As custom properties they are visible in devtools and swappable at runtime.

Internationalization (i18n)

Build multilingual applications with translated messages.

import jweb.I18n;

String username = "John";

// Get translated message
String title = I18n.t("page.title");

// With parameters
String greeting = I18n.t("welcome", username);
// "welcome" = "Hello, {0}!" → "Hello, John!"

Message Files

Create message files in src/main/resources/messages/:

// messages_en.properties
page.title=Welcome
nav.home=Home
nav.about=About
welcome=Hello, {0}!
errors.required=This field is required

// messages_es.properties
page.title=Bienvenido
nav.home=Inicio
nav.about=Acerca de
welcome=¡Hola, {0}!
errors.required=Este campo es obligatorio

In Templates

public class HomePage implements Template {
    public Element render() {
        return div(
            h1(I18n.t("page.title")),
            nav(
                a(href("/"), I18n.t("nav.home")),
                a(href("/about"), I18n.t("nav.about"))
            )
        );
    }
}

Locale Detection

I18n detects locale from (in order):

// 1. Query parameter: /page?lang=es
// 2. Session attribute: lang
// 3. Cookie: lang
// 4. Accept-Language header
// 5. Default locale

// Get current locale
Locale locale = I18n.getLocale(req);

// Use middleware for automatic detection
app.use(I18n.middleware());

Language Picker

// Get supported locales
List<I18n.LocaleInfo> locales = I18n.getSupportedLocales();
// Each has: code, nativeName, englishName

// In template
form(method("post"), action("/language"),
    select(name("lang"), attrs().set("onchange", "this.form.submit()"),
        each(locales, locale -> {
            var a = attrs();
            if (locale.code().equals(I18n.current().getLanguage())) {
                a.selected();
            }
            return option(value(locale.code()), a, locale.nativeName());  // "Español" for Spanish
        })
    )
)

Switch Language

import jweb.http.Cookie;

// Via session
app.post("/language", req -> {
    String lang = req.formParam("lang");
    req.session().setAttribute("lang", lang);
    return Response.redirect(req.header("Referer"));
});

// Via cookie (persistent) — the builder renders the Set-Cookie value
app.post("/language/cookie", req -> Response.status(204)
    .header("Set-Cookie", Cookie.of("lang", req.formParam("lang"))
        .maxAge(Duration.ofDays(365))
        .path("/")
        .toHeaderValue())
    .build());
Use nativeName() in language pickers - users recognize their language better.