Skip to content

templ components

Most of Laju Go’s HTML is rendered by Inertia + Svelte. The exception is server-rendered pages that are not part of the SPA — the public landing page, email bodies, any standalone HTML that should not ship JS. Those use templ, a typed template language that compiles to Go.

You write .templ files. The templ generate command compiles each .templ into a *_templ.go file containing a normal Go function you call directly.

templates/index.templ → templ generate → templates/index_templ.go
  1. Edit templates/<name>.templ.
  2. Run templ generate (or make templ).
  3. Call the generated function from a handler: templates.LandingPage(title, isDev, viteURL, mainCSS).Render(ctx, w).

The generated function is type-checked by the Go compiler, so a typo in a prop name or a missing argument is a compile error, not a runtime 500.

File Edit? Purpose
templates/index.templ ✅ Yes Source — templ syntax, components, layout
templates/index_templ.go ❌ Never Generated Go — overwritten by templ generate

If you see a bug in rendered HTML, fix the .templ and regenerate. Do not patch the _templ.go.

The public landing page is a templ component — it is a full standalone HTML document (not an Inertia page) so it can render with zero JS on first paint.

templates/index.templ
package templates
templ LandingPage(title string, isDev bool, viteURL string, mainCSS string) {
{{
productLinks := FooterLinks{
{FLabel: "Features", FURL: "#features"},
{FLabel: "How it works", FURL: "#how-it-works"},
// ...
}
}}
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"/>
<title>{ title } - Laju Go</title>
// ...
</head>
<body class="bg-neutral-950 text-white antialiased min-h-screen">
// ...
@FeatureCardLarge("auth", "Authentication", "Email and password with Argon2id hashing...", "brand")
</body>
</html>
}

Notes:

  • {{ ... }} blocks run Go at render time — building link lists, computing values.
  • { title } interpolates a typed parameter.
  • @FeatureCardLarge(...) calls another templ component, also type-checked.

The handler calls it directly — no string templating, no fmt.Sprintf:

// app/handlers/public.go (illustrative)
func (h *PublicHandler) Index(c *fiber.Ctx) error {
w := c.Response().BodyWriter()
return templates.LandingPage("Laju Go", h.assetService.IsDev(), viteURL, mainCSS).Render(c.UserContext(), w)
}

Always self-close and quote every attribute:

<!--Correctself-closing, quoted -->
<svg width="28" height="28" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="url(#logoGrad)"/>
<path d="M19 7L10 17h5l-1 8 9-10h-5l1-8Z" fill="white"/>
</svg>
<!--Wrongunclosed tags, will not compile -->
<svg width="28" height="28">
<rect width="32" height="32" rx="8" fill="url(#logoGrad)">
<path d="..." fill="white">
</svg>

The landing page in templates/index.templ has many inline SVGs (logo, icons, social marks) — all self-closing. Copy that style.

The dev loop for templ is:

  1. Edit templates/<name>.templ.
  2. Run templ generate in another terminal (or make templ).
  3. Air picks up the regenerated *_templ.go and rebuilds the Go binary.

Vite HMR covers Svelte; Air covers Go; templ is the manual step between them. If your templ edit is not showing up, you forgot step 2.

Use templ when Use Inertia + Svelte when
Standalone HTML with no client interactivity (landing page, marketing) Anything inside the authenticated app
Email bodies Forms, dashboards, any page that needs state
SEO-critical pages that must render with zero JS Pages where a SPA feel is desired

The split is deliberate: templ keeps the public surface fast and JS-free, while Inertia gives the app a modern SPA experience without a separate API.