Go can be used for server-rendered web apps as well as APIs. With templ for views and htmx for browser interactions, it is possible to build an admin UI in a mostly Go-first workflow.
In this series, we will build an e-commerce admin dashboard with Fiber, HTMX, Templ UI, and GORM. Part 1 focuses on the UI foundation: starting a small server, generating UI components, building a landing page, and composing a reusable dashboard layout.
The code samples below are intentionally simplified. They are closer to pseudocode than copy-paste production code, so the focus stays on structure instead of project-specific details.
Prerequisites
Set up the server
To begin, a very small Fiber server is enough. It only needs to serve static assets, render the landing page, and listen on a port.
func main() {
app := fiber.New()
app.Static("/assets", "./assets")
app.Get("/", RenderTempl(LandingPage))
app.Listen(":3000")
}
That is enough for a first success point. If / renders correctly, the server, the template engine, and the static files are all connected.
Import UI components
Templ UI follows a generated-source approach. Instead of depending on a remote component package at runtime, you generate the component files into your own project and edit them like normal code.
Install the CLI:
go install github.com/templui/templui/cmd/templui@latest
Then initialize it and add components:
templui init
templui add "*"
For a tutorial, generating everything is convenient because it makes experimentation easier. In a real project, you can add only the components you plan to use.
Make a landing page
The landing page is a good first screen because it proves the layout works and gives us somewhere to place the first navigation buttons.
Here is a simplified .templ page component:
templ LandingPage(c fiber.Ctx) {
@layout.LandingLayout() {
<div class="h-full flex justify-center items-center">
<div class="flex flex-col gap-4 items-center justify-center px-4 w-full max-w-2xl">
<div class="text-center space-y-4">
<h1 class="text-5xl font-bold tracking-tight">Fanta Admin</h1>
<p class="text-muted-foreground text-lg max-w-md mx-auto">
A simple admin panel built with Go, HTMX, and templ.
</p>
</div>
<div hx-boost="true" hx-indicator="#gprogress" class="flex gap-3">
@button.Button(button.Props{Href: "/login"}) {
Login
}
@button.Button(button.Props{Variant: "outline", Href: "/signup"}) {
Sign Up
}
</div>
</div>
</div>
}
}
The page itself stays small because the shared wrapper is moved into a layout component:
templ LandingLayout() {
@BaseLayout() {
<div class="h-full flex flex-col">
@block.Navbar()
<main class="flex-1">
{ children... }
</main>
</div>
}
}
This pattern is useful early on: put shared structure in layouts, and keep page components focused on their own content.
Make the dashboard layout
An admin page usually needs more structure than a landing page. A common shape is a sidebar for navigation and a main content area for the current page.
With templ, that can still be written as a normal component:
templ AdminLayout(c fiber.Ctx) {
@layout.BaseLayout() {
@sidebar.Layout() {
@sidebar.Sidebar(sidebar.Props{
Collapsible: sidebar.CollapsibleIcon,
Variant: sidebar.VariantFloating,
}) {
@sidebar.Header() {
<div class="px-2 py-2 font-semibold">Admin</div>
}
@sidebar.Content() {
@sidebar.Menu() {
@sidebar.MenuItem() {
@sidebar.MenuButton(sidebar.MenuButtonProps{Href: "/api/dashboard"}) {
Dashboard
}
}
@sidebar.MenuItem() {
@sidebar.MenuButton(sidebar.MenuButtonProps{Href: "/api/products"}) {
Products
}
}
@sidebar.MenuItem() {
@sidebar.MenuButton(sidebar.MenuButtonProps{Href: "/api/users"}) {
Users
}
}
}
}
}
@sidebar.Inset() {
<div class="flex h-full flex-col">
<header class="px-6 py-4 border-b font-semibold">
Admin Panel
</header>
<main class="px-10 py-6">
{ children... }
</main>
</div>
}
}
}
}
Once that layout exists, a page can plug into it with very little code:
templ DashboardPage(c fiber.Ctx) {
@AdminLayout(c) {
<div class="space-y-2">
<h1 class="text-3xl font-bold">Dashboard</h1>
<p class="text-muted-foreground">
Welcome to the admin panel.
</p>
</div>
}
}
And the route can stay simple as well:
app.Get("/api/dashboard", RenderTempl(DashboardPage))
That parent-child composition is the main idea. The layout owns the shell, and each page provides the content.
A Fiber adapter for templ
templ renders through Go's standard net/http flow, while Fiber uses its own handler type. To connect the two, a small adapter is useful.
For tutorial purposes, the idea can be expressed like this:
func RenderTempl(page func(fiber.Ctx) templ.Component) fiber.Handler {
return func(c fiber.Ctx) error {
component := page(c)
httpHandler := templ.Handler(component)
return adaptor.HTTPHandler(httpHandler)(c)
}
}
The important part is not the exact implementation. The important part is that templ can render through an ordinary HTTP handler, and Fiber can wrap that handler through its adapter.
Once this helper exists, using templ pages in routes feels natural.
Wire things up with HTMX
At this point, the pages already render. HTMX adds smoother navigation and form handling without requiring a separate frontend framework.
The root layout can load the scripts and define a global loading indicator:
templ BaseLayout() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="/assets/css/output.css" rel="stylesheet"/>
<link href="/assets/css/gprogress.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hyperscript.org@0.9.14/dist/_hyperscript.min.js"></script>
<title>Admin</title>
</head>
<body class="h-full" hx-indicator="#gprogress">
<div id="gprogress" class="gprogress"></div>
{ children... }
</body>
</html>
}
Then page-level navigation can stay declarative. For example, the landing page buttons already use hx-boost="true", which tells HTMX to enhance the links inside that container.
Forms can use the same idea. A simplified login component might look like this:
templ LoginPage(c fiber.Ctx) {
@layout.LandingLayout() {
<div class="h-full flex justify-center items-center">
<form
hx-boost="true"
hx-swap="none"
method="post"
action="/auth/unpw/login"
class="flex flex-col gap-3 min-w-sm"
_="on htmx:afterRequest
if event.detail.successful then
set window.location.href to '/api/dashboard'
end"
>
<h1 class="text-2xl font-bold">Login</h1>
<input name="user" type="text" placeholder="Username"/>
<input name="passwd" type="password" placeholder="Password"/>
<button type="submit">Login</button>
</form>
</div>
}
}
The main idea is that the page still looks like normal server-rendered HTML, but HTMX can enhance link clicks and form submissions when needed.
Part 1 Summary
At this point, we have the basic UI foundation in place:
- a small Fiber server
- generated Templ UI components
- a landing page
- a reusable admin layout
- a small
templadapter for Fiber - HTMX-enhanced navigation and form submission
You should end up with a result similar to this:
If you want the full boilerplate, the complete source is here:

Top comments (0)