202 lines
5.4 KiB
Markdown
202 lines
5.4 KiB
Markdown
|
|
|
||
|
|
## Adding a New Page — Step by Step
|
||
|
|
|
||
|
|
### Step 1 — Choose a layout
|
||
|
|
|
||
|
|
Declare the layout in your template's very first line using a Go comment:
|
||
|
|
|
||
|
|
```html
|
||
|
|
{{/* layout: base_public.html */}}
|
||
|
|
```
|
||
|
|
|
||
|
|
If you omit this line, `base.html` (sidebar layout) is used automatically.
|
||
|
|
|
||
|
|
**Built-in layouts:**
|
||
|
|
|
||
|
|
| Layout file | Navigation | Best for |
|
||
|
|
|---|---|---|
|
||
|
|
| `base.html` | Left sidebar | Authenticated app pages (dashboard, profile, etc.) |
|
||
|
|
| `base_public.html` | Top bar | Public/marketing pages, landing pages |
|
||
|
|
| `base_bare.html` | None | Minimal pages, print view, embeds |
|
||
|
|
|
||
|
|
**Custom layout:** Create `web/templates/layouts/my_layout.html`, then declare `{{/* layout: my_layout.html */}}` in your page. Any layout filename works as long as it lives in the `layouts/` directory and contains `{{define "base"}} ... {{end}}`.
|
||
|
|
|
||
|
|
### Step 2 — Create the template
|
||
|
|
|
||
|
|
`web/templates/pages/mypage.html`:
|
||
|
|
```html
|
||
|
|
{{/* layout: base.html */}}
|
||
|
|
{{template "base" .}}
|
||
|
|
|
||
|
|
{{define "title"}}My Page — GoApp{{end}}
|
||
|
|
|
||
|
|
{{define "content"}}
|
||
|
|
<div class="max-w-4xl mx-auto">
|
||
|
|
<h2 class="text-2xl font-semibold text-white mb-6">{{.Title}}</h2>
|
||
|
|
|
||
|
|
{{range .Items}}
|
||
|
|
<div class="card">
|
||
|
|
<p class="text-gray-300">{{.}}</p>
|
||
|
|
</div>
|
||
|
|
{{end}}
|
||
|
|
</div>
|
||
|
|
{{end}}
|
||
|
|
|
||
|
|
{{/* Optional: page-specific <head> additions */}}
|
||
|
|
{{define "head"}}<style>/* page CSS */</style>{{end}}
|
||
|
|
|
||
|
|
{{/* Optional: page-specific scripts */}}
|
||
|
|
{{define "scripts"}}<script>/* page JS */</script>{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
The `{{/* layout: ... */}}` comment is read only by Go at startup — it never appears in the HTML output.
|
||
|
|
|
||
|
|
### Step 3 — Create the handler
|
||
|
|
|
||
|
|
`internal/handlers/mypage.go`:
|
||
|
|
```go
|
||
|
|
package handlers
|
||
|
|
|
||
|
|
import "net/http"
|
||
|
|
|
||
|
|
// MyPageData holds everything the template needs.
|
||
|
|
// Always embed PageData — it carries User, Nav, flash messages, etc.
|
||
|
|
type MyPageData struct {
|
||
|
|
PageData
|
||
|
|
Items []string // your page-specific data
|
||
|
|
}
|
||
|
|
|
||
|
|
type MyPageHandler struct {
|
||
|
|
tmpl *Renderer
|
||
|
|
// Add other dependencies here: *db.DB, *mailer.Mailer, *logger.Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewMyPageHandler(r *Renderer) *MyPageHandler {
|
||
|
|
return &MyPageHandler{tmpl: r}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *MyPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != http.MethodGet {
|
||
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
data := MyPageData{
|
||
|
|
PageData: NewPageData(r, "My Page"),
|
||
|
|
Items: []string{"hello", "world"},
|
||
|
|
}
|
||
|
|
h.tmpl.Render(w, "mypage", data)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 4 — Register the route
|
||
|
|
|
||
|
|
Add one line to `internal/router/router.go`:
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Public page (no login required):
|
||
|
|
mux.Handle("/mypage", handlers.NewMyPageHandler(renderer))
|
||
|
|
|
||
|
|
// Authenticated page (redirects to /login if not signed in):
|
||
|
|
mux.Handle("/mypage", middleware.RequireAuth(handlers.NewMyPageHandler(renderer)))
|
||
|
|
|
||
|
|
// Admin-only page:
|
||
|
|
mux.Handle("/mypage", adminMW(handlers.NewMyPageHandler(renderer)))
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 5 — (Optional) Add to sidebar navigation
|
||
|
|
|
||
|
|
In `internal/handlers/renderer.go`, append to `DefaultNav`:
|
||
|
|
```go
|
||
|
|
var DefaultNav = []NavItem{
|
||
|
|
// existing items ...
|
||
|
|
{Label: "My Page", Href: "/mypage", Icon: "info"}, // all users
|
||
|
|
{Label: "Admin Page", Href: "/mypage", Icon: "shield", AdminOnly: true}, // admins only
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Available icon names: `home`, `info`, `mail`, `users`, `activity`, `settings`, `logout`, `shield`, `key`, `mfa`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Email — Using the Mailer
|
||
|
|
|
||
|
|
### Configure in `data/config.json`
|
||
|
|
|
||
|
|
```json
|
||
|
|
"email": {
|
||
|
|
"enabled": true,
|
||
|
|
"smtp_host": "smtp.gmail.com",
|
||
|
|
"smtp_port": 587,
|
||
|
|
"encryption": "starttls",
|
||
|
|
"auth": true,
|
||
|
|
"username": "you@gmail.com",
|
||
|
|
"password": "your-app-password",
|
||
|
|
"from_address": "GoApp <you@gmail.com>"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Gmail tip:** Create an [App Password](https://myaccount.google.com/apppasswords) (not your main password). Requires 2FA enabled on your Google account.
|
||
|
|
|
||
|
|
### Wire the Mailer into a handler
|
||
|
|
|
||
|
|
```go
|
||
|
|
// In router.go (m is already created from cfg.Email):
|
||
|
|
mux.Handle("/notify", handlers.NewNotifyHandler(renderer, m, log))
|
||
|
|
|
||
|
|
// In your handler:
|
||
|
|
type NotifyHandler struct {
|
||
|
|
tmpl *Renderer
|
||
|
|
mailer *mailer.Mailer
|
||
|
|
log *logger.Logger
|
||
|
|
}
|
||
|
|
func NewNotifyHandler(r *Renderer, m *mailer.Mailer, l *logger.Logger) *NotifyHandler {
|
||
|
|
return &NotifyHandler{tmpl: r, mailer: m, log: l}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Send plain-text email
|
||
|
|
|
||
|
|
```go
|
||
|
|
err := h.mailer.Send(mailer.Message{
|
||
|
|
To: []string{"user@example.com"},
|
||
|
|
Subject: "Welcome to GoApp",
|
||
|
|
Body: "Hello! Your account is ready.",
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
h.log.Warn("send welcome email", "err", err)
|
||
|
|
// Don't abort — email failure should not break the UX
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Send HTML email
|
||
|
|
|
||
|
|
```go
|
||
|
|
err := h.mailer.Send(mailer.Message{
|
||
|
|
To: []string{"user@example.com"},
|
||
|
|
Subject: "Welcome to GoApp",
|
||
|
|
Body: "<h1>Welcome!</h1><p>Your account is <strong>ready</strong>.</p>",
|
||
|
|
IsHTML: true,
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Multiple recipients + CC
|
||
|
|
|
||
|
|
```go
|
||
|
|
err := h.mailer.Send(mailer.Message{
|
||
|
|
To: []string{"alice@example.com", "bob@example.com"},
|
||
|
|
CC: []string{"manager@example.com"},
|
||
|
|
Subject: "Team notification",
|
||
|
|
Body: "Something happened that you should know about.",
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Safe error handling pattern
|
||
|
|
|
||
|
|
When `email.enabled` is `false`, `Send()` returns `nil` immediately — the rest of your handler works normally. Always log email errors rather than aborting the request:
|
||
|
|
|
||
|
|
```go
|
||
|
|
if err := h.mailer.Send(msg); err != nil {
|
||
|
|
h.log.Warn("email send failed", "err", err)
|
||
|
|
// continue — don't return an error page to the user
|
||
|
|
}
|
||
|
|
```
|