This guide shows you how to add simple static pages to your Gojang application, such as an About page, Contact page, or Terms of Service page.
Creating a static page involves three simple steps:
- Create the HTML template
- Create the handler function
- Register the route
Estimated time: 5 minutes per page
Top-level public page templates are located in app/views/templates/. Feature-specific templates can live beside their feature package, such as app/posts/templates/.
{{define "title"}}About Us{{end}}
{{define "content"}}
<div class="container">
<h1>About Us</h1>
<div class="card">
<p>
Welcome to our application! We're building amazing things with Go and HTMX.
</p>
<h2>Our Mission</h2>
<p>
To provide the best web framework experience using modern technologies
while maintaining simplicity and performance.
</p>
<h2>Technology Stack</h2>
<ul>
<li>Go - Fast, reliable backend</li>
<li>HTMX - Dynamic interactions without heavy JavaScript</li>
<li>Ent - Type-safe ORM</li>
<li>Custom CSS - Clean, semantic styling</li>
</ul>
<div class="alert alert-info" style="margin-top: 2rem;">
<p>
<strong>Fun Fact:</strong> This entire page was created in less than 5 minutes!
</p>
</div>
</div>
</div>
{{end}}Your template must define two blocks:
{{define "title"}}- Page title shown in browser tab and<h1>tags{{define "content"}}- Main page content
The template automatically inherits from base.html, which includes:
- β Header with navigation
- β Footer
- β All CSS/JS dependencies
- β Flash messages
- β HTMX integration
Handlers are located in app/pages/pages.handler.go.
// About renders the about page
func (h *PageHandler) About(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "about.html", &renderers.TemplateData{
Title: "About Us",
Data: map[string]interface{}{},
})
}func (h *PageHandler) PageName(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "template.html", &renderers.TemplateData{
Title: "Page Title",
Data: map[string]interface{}{},
})
}func (h *PageHandler) Contact(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "contact.html", &renderers.TemplateData{
Title: "Contact Us",
Data: map[string]interface{}{
"Email": "support@example.com",
"Phone": "+1-234-567-8900",
"Address": "123 Main St, City, State",
},
})
}Then access in template:
{{define "content"}}
<p>Email: {{.Data.Email}}</p>
<p>Phone: {{.Data.Phone}}</p>
{{end}}func (h *PageHandler) Profile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r.Context())
h.Renderer.Render(w, r, "profile.html", &renderers.TemplateData{
Title: "My Profile",
Data: map[string]interface{}{
"User": user,
},
})
}Routes are located in app/pages/pages.route.go.
Find the PageRoutes function and add your route:
func PageRoutes(handler *PageHandler, sm *scs.SessionManager, client *models.Client) chi.Router {
r := chi.NewRouter()
// Existing routes
r.Get("/", handler.Home)
// Add your new route
r.Get("/about", handler.About)
return r
}r.Get("/about", handler.About)
r.Get("/contact", handler.Contact)
r.Get("/terms", handler.Terms)r.Group(func(r chi.Router) {
r.Use(middleware.RequireAuth(sm, client))
r.Get("/dashboard", handler.Dashboard)
r.Get("/settings", handler.Settings)
})r.Group(func(r chi.Router) {
r.Use(middleware.RequireAuth(sm, client))
r.Use(middleware.RequireStaff)
r.Get("/staff-panel", handler.StaffPanel)
})r.Get("/blog/{slug}", handler.BlogPost)
// In handler:
func (h *PageHandler) BlogPost(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
// ... fetch blog post by slug
}Let's create a complete contact page from scratch.
{{define "title"}}Contact Us{{end}}
{{define "content"}}
<div class="container">
<h1>Contact Us</h1>
<div class="card">
<h2>Get in Touch</h2>
<div class="contact-info">
<div class="contact-item">
<strong>Email:</strong>
<p>{{.Data.Email}}</p>
</div>
<div class="contact-item">
<strong>Phone:</strong>
<p>{{.Data.Phone}}</p>
</div>
<div class="contact-item">
<strong>Address:</strong>
<p>{{.Data.Address}}</p>
</div>
</div>
</div>
<div class="alert alert-info">
<p>
π‘ <strong>Tip:</strong> You can extend this page with an HTMX-powered contact form!
</p>
</div>
</div>
<style>
.contact-info {
display: grid;
gap: 1.5rem;
margin-top: 1rem;
}
.contact-item strong {
display: block;
margin-bottom: 0.25rem;
}
</style>
{{end}}// Contact renders the contact page
func (h *PageHandler) Contact(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "contact.html", &renderers.TemplateData{
Title: "Contact Us",
Data: map[string]interface{}{
"Email": "support@gojang.dev",
"Phone": "+1 (555) 123-4567",
"Address": "123 Framework Street, Go City, GC 12345",
},
})
}func PageRoutes(handler *PageHandler, sm *scs.SessionManager, client *models.Client) chi.Router {
r := chi.NewRouter()
r.Get("/", handler.Home)
r.Get("/about", handler.About)
r.Get("/contact", handler.Contact) // β
Add this line
return r
}- Restart the server:
go run ./app/cmd/web - Visit: http://localhost:8080/contact
- β Done!
To add your new page to the navigation menu:
Find the navigation section and add your link:
<nav class="flex space-x-4">
<a href="/" class="text-white hover:text-blue-200">Home</a>
<a href="/posts" class="text-white hover:text-blue-200">Posts</a>
<a href="/about" class="text-white hover:text-blue-200">About</a>
<a href="/contact" class="text-white hover:text-blue-200">Contact</a>
</nav>Want to make your static page interactive? Add HTMX attributes!
{{define "content"}}
<div class="container">
<h1>About Us</h1>
<div class="card">
<!-- Button that loads content via HTMX -->
<button
class="btn btn-primary"
hx-get="/about/team"
hx-target="#team-section"
hx-swap="innerHTML">
Load Team Members
</button>
<!-- Content will appear here -->
<div id="team-section" style="margin-top: 1.5rem;">
<!-- Team data loads here -->
</div>
</div>
</div>
{{end}}Then create a handler that returns just the HTML fragment:
func (h *PageHandler) AboutTeam(w http.ResponseWriter, r *http.Request) {
// Return just the fragment, not the full page
w.Write([]byte(`
<div class="team-grid">
<div class="card">John Doe - CEO</div>
<div class="card">Jane Smith - CTO</div>
<div class="card">Bob Johnson - Designer</div>
</div>
<style>
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
</style>
`))
}{{define "content"}}
<div class="container">
<h1>Frequently Asked Questions</h1>
<div class="faq-list">
{{range .Data.FAQs}}
<details class="card">
<summary style="cursor: pointer; font-weight: 600;">{{.Question}}</summary>
<p style="margin-top: 0.5rem;">{{.Answer}}</p>
</details>
{{end}}
</div>
</div>
<style>
.faq-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
{{end}}{{define "content"}}
<div class="container">
<h1>Privacy Policy</h1>
<div class="card">
<h2>Table of Contents</h2>
<ul>
<li><a href="#section-1">Information We Collect</a></li>
<li><a href="#section-2">How We Use Your Data</a></li>
<li><a href="#section-3">Your Rights</a></li>
</ul>
</div>
<section id="section-1" class="card">
<h2>1. Information We Collect</h2>
<p>...</p>
</section>
</div>
{{end}}- β
Check route is registered in
app/pages/pages.route.go - β
Check handler method exists in
app/pages/pages.handler.go - β Restart the server after changes
- β
Check template file exists in
app/views/templates/ - β
Check
{{define "title"}}and{{define "content"}}are present - β Check for syntax errors in template
- β Check Tailwind CSS classes are correct
- β
Ensure
base.htmlis loading CSS properly - β Clear browser cache
- β
Check
Datamap in handler contains the key - β
Check template uses correct syntax:
{{.Data.KeyName}} - β Check for nil values
- β Read: Creating Pages with Data Models
- β Learn: HTMX Integration Patterns
- β
Explore: Check existing templates in
app/views/templates/for more examples
| Task | File | Function |
|---|---|---|
| Create template | app/views/templates/page.html |
Define title and content blocks |
| Add handler | app/pages/pages.handler.go |
Create func (h *PageHandler) PageName() |
| Register route | app/pages/pages.route.go |
Add r.Get("/path", handler.Method) |
| Add navigation | app/views/templates/base.html |
Add link in <nav> section |