DEV Community

Cover image for Go Admin Dashboard for E-Commerce with HTMX, Templ UI, and GORM - Part 1
ColaFanta
ColaFanta

Posted on

Go Admin Dashboard for E-Commerce with HTMX, Templ UI, and GORM - Part 1

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")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then initialize it and add components:

templui init
templui add "*"
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

And the route can stay simple as well:

app.Get("/api/dashboard", RenderTempl(DashboardPage))
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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 templ adapter 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:

https://github.com/ColaFanta/go-simple-admin-template

Top comments (0)