12 KiB
12 KiB
Templ Migration Guide
Convert HTML prototypes to Go templ templates with HTMX for server-driven UI.
Architecture
Browser Go Server
------- ---------
[Web Awesome UI] <-- HTMX --> [Templ Renderer]
| |
| hx-post="/api/sign" |
| hx-target="#result" |
+--------------------------------->+
|
[Handler Logic]
|
+<---------------------------------+
| HTML Fragment Response
|
[DOM Update via HTMX]
Directory Structure
project/
├── views/
│ ├── welcome.templ
│ ├── login.templ
│ ├── register.templ
│ ├── authorize.templ
│ ├── accounts.templ
│ ├── nfts.templ
│ └── service.templ
├── layouts/
│ ├── base.templ
│ ├── centered.templ # Auth pages
│ └── dashboard.templ # Authenticated pages
├── components/
│ ├── stepper.templ
│ ├── auth_method_card.templ
│ ├── permission_item.templ
│ ├── token_row.templ
│ ├── nft_card.templ
│ └── session_item.templ
└── handlers/
└── routes.go
Page Overview
Auth Pages (Popup/Webview Optimized)
| Page | File | Viewport | Description |
|---|---|---|---|
| Welcome | welcome.templ |
360-480px | 3-step onboarding stepper |
| Login | login.templ |
360-480px | WebAuthn sign-in with passkey/security key/QR |
| Register | register.templ |
360-480px | Device detection + registration wizard |
| Authorize | authorize.templ |
360-480px | OAuth consent for connect/sign/transaction |
Dashboard Pages
| Page | File | Description |
|---|---|---|
| Accounts | accounts.templ |
Token balances, transactions, drawers |
| NFTs | nfts.templ |
Gallery with collections, filters, actions |
| Service | service.templ |
Connected service detail with permissions |
Component Extraction
Shared Components
// components/stepper.templ
templ ProgressDots(current int, total int)
templ OnboardingStepper(current int, steps []string)
// components/auth_method_card.templ
templ AuthMethodCard(method, icon, title, desc string, disabled bool)
// components/permission_item.templ
templ PermissionItem(icon, iconClass, title, desc string)
// components/qr_section.templ
templ QRCodeSection(value, label, fallbackText string)
Page-Specific Components
Login
templ LoginStep1(caps DeviceCapabilities)
templ QRAuthStep()
templ RecoveryStep()
templ SuccessStep(title, redirectUrl string)
Register
templ DeviceDetectionStep(caps DeviceCapabilities)
templ RegistrationMethodStep(method string)
templ RegistrationSuccessStep()
templ CapabilityItem(id, label string, supported, loading bool)
templ RecoveryKeyDisplay(publicKey, recoveryKey string)
Authorize
templ AppIdentityHeader(app AppInfo)
templ WalletSelector(wallets []Wallet, selected string)
templ PermissionList(permissions []Permission)
templ MessagePreview(message, signType string)
templ TransactionPreview(tx Transaction)
templ SwapPreview(from, to TokenAmount)
templ GasEstimate(network, gas, maxFee string)
Accounts
templ StatsGrid(stats WalletStats)
templ TokenBalancesCard(tokens []Token)
templ TokenRow(token Token)
templ RecentTransactionsCard(txs []Transaction)
templ TransactionRow(tx Transaction)
templ ConnectedAccountsCard(accounts []Account)
templ ReceiveDrawer(token string)
templ SendDrawer(tokens []Token)
templ SwapDrawer(tokens []Token)
templ CreateAccountDialog(step int, network string)
NFTs
templ NFTsStatsGrid(stats NFTStats)
templ CollectionsCard(collections []Collection)
templ CollectionCard(collection Collection)
templ NFTsGalleryCard(nfts []NFTItem, filter NFTFilter)
templ NFTFilterBar(filter NFTFilter)
templ NFTGrid(nfts []NFTItem)
templ NFTCard(nft NFTItem)
templ NFTBadge(badgeType string)
Service
templ ServiceHero(info ServiceInfo, status ServiceStatus)
templ UsageStatsGrid(stats UsageStats)
templ PermissionsCard(permissions []Permission)
templ PermissionToggle(scope string, enabled bool, desc string)
templ ServiceInfoCard(oauth OAuthInfo)
templ SessionLogCard(sessions []SessionEvent)
templ SessionLogItem(event SessionEvent)
templ DataAccessCard(access DataAccess)
State Types
Auth Pages
type DeviceCapabilities struct {
PlatformAuth bool // Face ID, Touch ID, Windows Hello
CrossPlatform bool // Security keys (YubiKey)
ConditionalUI bool // Passkey autofill support
}
type LoginState struct {
Step string
SelectedMethod string // "passkey", "security-key", "qr-code"
Username string
Error string
}
type RegisterState struct {
Step int
SelectedMethod string
Username string
DisplayName string
PublicKey string
RecoveryKey string
Error string
}
type AuthRequest struct {
Type string // "connect", "sign", "transaction"
App AppInfo
Scopes []string
Message string
Transaction *TxDetails
State string
Nonce string
}
type AppInfo struct {
Name string
Domain string
LogoURL string
Verified bool
}
Dashboard Pages
type WalletData struct {
Stats WalletStats
Tokens []Token
Transactions []Transaction
Accounts []Account
}
type WalletStats struct {
TotalBalance float64
Change24h float64
ChangePercent float64
AssetCount int
PendingCount int
}
type Token struct {
Symbol string
Name string
Balance float64
ValueUSD float64
IconColor string
}
type NFTsData struct {
Stats NFTStats
Collections []Collection
NFTs []NFTItem
Filter NFTFilter
}
type NFTFilter struct {
Tab string // "all", "listed", "unlisted"
Collection string
Sort string // "recent", "value-high", "value-low", "name"
View string // "grid", "list"
}
type ServiceData struct {
Info ServiceInfo
Status ServiceStatus
Stats UsageStats
Permissions []Permission
OAuth OAuthInfo
Sessions []SessionEvent
DataAccess DataAccess
}
HTMX Patterns
Step Navigation
<!-- Before: JavaScript -->
<wa-button onclick="goToStep(2)">Next</wa-button>
<!-- After: HTMX -->
<wa-button hx-get="/welcome/step/2" hx-target="#step-content">Next</wa-button>
Form Submission
<form hx-post="/api/auth/register"
hx-target="#step-content"
hx-indicator="#register-spinner">
Auth Method Selection
<div class="auth-method-card"
hx-post="/login/select-method"
hx-vals='{"method":"passkey"}'
hx-target="#btn-signin"
hx-swap="outerHTML">
QR Code Polling
<div id="qr-status"
hx-get="/api/auth/qr-status?session=xyz789"
hx-trigger="every 2s"
hx-target="this">
Tab Switching
<wa-tab-group hx-on:wa-tab-show="
htmx.ajax('GET', '/authorize/panel/' + event.detail.name, {target: '#panel-content'})
">
Drawer Opening
<wa-button hx-get="/drawers/receive"
hx-target="#drawer-container"
hx-swap="innerHTML">
Receive
</wa-button>
Filter with URL State
<wa-tab-group hx-on:wa-tab-show="
htmx.ajax('GET', '/nfts?filter=' + event.detail.name, {target: '#nft-grid', pushUrl: true})
">
Permission Toggle
<wa-switch hx-post="/api/connections/{id}/scopes"
hx-vals='{"scope": "wallet:sign", "enabled": !this.checked}'
hx-target="#permission-status"
hx-swap="innerHTML">
Out-of-Band Updates
templ TransactionResult(tx Transaction) {
<div id="tx-result">
<wa-callout variant="success">
Transaction submitted: { tx.Hash }
</wa-callout>
</div>
<div id="balance-display" hx-swap-oob="true">
@BalanceCard(tx.NewBalance)
</div>
<wa-drawer id="send-drawer" hx-swap-oob="true"></wa-drawer>
}
Stats Auto-Refresh
<div id="stats-grid"
hx-get="/api/wallet/stats"
hx-trigger="every 30s"
hx-swap="innerHTML">
Layout Templates
Base Layout
// layouts/base.templ
templ Base(title string) {
<!DOCTYPE html>
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{ title } - Sonr Motr Wallet</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
</head>
<body>
{ children... }
</body>
</html>
}
Centered Card (Auth Pages)
// layouts/centered.templ
templ CenteredCard(title string) {
@Base(title) {
<wa-page>
<main class="main-centered">
<wa-card>
{ children... }
</wa-card>
</main>
</wa-page>
}
}
Dashboard Layout
// layouts/dashboard.templ
templ DashboardLayout(activePage string) {
@Base("Dashboard") {
<wa-page>
<div class="dashboard-layout">
@Sidebar(activePage)
<main class="main-content">
{ children... }
</main>
</div>
</wa-page>
}
}
Viewport Constraints
Auth pages optimized for popup windows and webviews:
| Size | Dimensions | Use Case |
|---|---|---|
| Small | 360 x 540 | Mobile popup |
| Medium | 420 x 600 | Default popup |
| Large | 480 x 680 | Desktop popup |
| Mobile | 375 x 667 | iPhone viewport |
CSS breakpoints:
@media (max-width: 400px) {
.main-centered { padding: var(--wa-space-m); }
.main-centered wa-card { max-width: 100%; }
}
@media (max-height: 600px) {
.hero-icon { width: 56px; height: 56px; }
}
Migration Checklist
-
Layouts first
base.templ- HTML head, scripts, body wrappercentered.templ- Auth pages card layoutdashboard.templ- Sidebar + main content
-
Shared components
- Stepper/Progress dots
- Auth method cards
- Permission items
- QR code sections
- Token rows
- NFT cards
-
Auth pages
welcome.templ- Onboarding flowlogin.templ- Sign-in with WebAuthnregister.templ- Registration wizardauthorize.templ- OAuth consent
-
Dashboard pages
accounts.templ- Main wallet viewnfts.templ- NFT galleryservice.templ- Connected service detail
-
Wire HTMX
- Replace all
onclickhandlers withhx-*attributes - Add polling for real-time updates
- Implement drawer/dialog triggers
- Replace all
-
Connect handlers
- Route registration
- API endpoints for HTMX partials
- WebAuthn integration points
CSS Variables
Primary brand color:
:root {
--wa-color-primary: #17c2ff;
}
Layout patterns:
.main-centered {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
padding: var(--wa-space-l);
box-sizing: border-box;
}
.main-centered wa-card {
width: 100%;
max-width: 45ch; /* Auth pages */
max-width: 48ch; /* Wider content */
}
Web Awesome Components Reference
Layout Classes
wa-stack- Vertical flex withwa-gap-{size}wa-cluster- Horizontal flex withwa-gap-{size}wa-grid- CSS grid with--min-column-sizewa-flank- Space-between horizontal layout
Gap Sizes
3xs, 2xs, xs, s, m, l, xl, 2xl, 3xl
Typography
- Headings:
wa-heading-xsthroughwa-heading-2xl - Captions:
wa-caption-xs,wa-caption-s,wa-caption-m - Labels:
wa-label-s,wa-label-m
Common Components
<wa-button variant="brand">Primary</wa-button>
<wa-button variant="neutral" appearance="outlined">Secondary</wa-button>
<wa-input label="Field" placeholder="Enter value"></wa-input>
<wa-card><div slot="header">Title</div>Content</wa-card>
<wa-drawer label="Title" placement="end"></wa-drawer>
<wa-dialog label="Title"></wa-dialog>
<wa-callout variant="warning"><wa-icon slot="icon" name="alert"></wa-icon>Message</wa-callout>