name: lvt-customize description: Use when customizing generated LiveTemplate code - covers editing handlers/templates, understanding generated structure, changing behavior, and common customization patterns
lvt:customize
Customize generated LiveTemplate resources, views, and templates.
๐ฏ ACTIVATION RULES
Context Detection
This skill typically runs in existing LiveTemplate projects (.lvtrc exists).
โ Context Established By:
- Project context -
.lvtrcexists (most common scenario) - Agent context - User is working with
lvt-assistantagent - Keyword context - User mentions "lvt", "livetemplate", or "lt"
Keyword matching (case-insensitive): lvt, livetemplate, lt
Trigger Patterns
With Context: โ Generic prompts related to this skill's purpose
Without Context (needs keywords): โ Must mention "lvt", "livetemplate", or "lt" โ Generic requests without keywords
Overview
LiveTemplate generates working code that you customize for your needs. All generated files are meant to be edited - lvt won't overwrite your changes.
Safe to customize:
- โ Handler files (
.go) - โ Template files (
.tmpl) - โ CSS/styling
- โ Business logic
Don't customize directly:
- โ Generated database models (
database/models/) - โ Schema files (
schema.sql) - use migrations instead
Generated Code Structure
project/
โโโ app/
โ โโโ products/
โ โ โโโ products.go โ Handler (customize this)
โ โ โโโ products.tmpl โ Template (customize this)
โ โ โโโ products_test.go โ Tests (customize this)
โ โโโ dashboard/
โ โโโ dashboard.go โ View handler
โ โโโ dashboard.tmpl โ View template
โโโ database/
โ โโโ models/ โ Generated by sqlc (don't edit)
โ โโโ schema.sql โ Use migrations, not direct edits
โ โโโ queries.sql โ Safe to add custom queries
โโโ shared/ โ Shared utilities
โโโ cmd/
โโโ myapp/
โโโ main.go โ Routes auto-injected (safe to edit)
Common Customizations
1. Change Handler Logic
Generated handler:
// app/products/products.go
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
// Default: List all products
products, err := queries.ListProducts(r.Context())
return lt.Render("products", products)
})
}
Add filtering:
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
// Get query parameter
category := r.URL.Query().Get("category")
var products []models.Product
var err error
if category != "" {
// Custom filtered query
products, err = queries.ListProductsByCategory(r.Context(), category)
} else {
products, err = queries.ListProducts(r.Context())
}
if err != nil {
return err
}
return lt.Render("products", map[string]interface{}{
"Products": products,
"Category": category,
})
})
}
2. Customize Templates
Generated template:
<!-- app/products/products.tmpl -->
<div class="container">
<h1>Products</h1>
{{range .Products}}
<div class="product">
<h2>{{.Name}}</h2>
<p>{{.Price}}</p>
</div>
{{end}}
</div>
Add custom styling and structure:
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Products</h1>
<button class="btn-primary">Add Product</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{{range .Products}}
<div class="card hover:shadow-lg transition-shadow">
<img src="/images/{{.ImageURL}}" alt="{{.Name}}" />
<h2 class="font-semibold text-xl">{{.Name}}</h2>
<p class="text-gray-600">${{printf "%.2f" .Price}}</p>
<button class="btn-secondary">Add to Cart</button>
</div>
{{end}}
</div>
</div>
3. Add Custom Queries
Add to queries.sql:
-- database/queries.sql
-- Generated queries (don't remove)
-- name: ListProducts :many
SELECT * FROM products ORDER BY created_at DESC;
-- Your custom queries (add below)
-- name: ListProductsByCategory :many
SELECT * FROM products
WHERE category = ?
ORDER BY created_at DESC;
-- name: GetFeaturedProducts :many
SELECT * FROM products
WHERE featured = true
ORDER BY popularity DESC
LIMIT 10;
-- name: SearchProducts :many
SELECT * FROM products
WHERE name LIKE '%' || ? || '%'
OR description LIKE '%' || ? || '%';
After editing queries.sql:
# Regenerate models
lvt migration up
4. Add Validation
// app/products/products.go
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
if r.Method == http.MethodPost {
name := r.FormValue("name")
priceStr := r.FormValue("price")
// Validation
if name == "" {
return lt.Error("Name is required")
}
price, err := strconv.ParseFloat(priceStr, 64)
if err != nil || price < 0 {
return lt.Error("Invalid price")
}
// Create product
product, err := queries.CreateProduct(r.Context(), models.CreateProductParams{
Name: name,
Price: price,
})
if err != nil {
return err
}
return lt.Redirect("/products")
}
// ... rest of handler
})
}
5. Add Authentication
// app/admin/admin.go
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
// Check authentication
user := getUserFromSession(r)
if user == nil {
return lt.Redirect("/login")
}
if !user.IsAdmin {
return lt.Error("Unauthorized", http.StatusForbidden)
}
// ... admin logic
})
}
Changing CSS Framework
Generate with different kit:
# Create app with specific kit
lvt new myapp --kit multi # default, Tailwind CSS
lvt new myapp --kit single # Tailwind CSS SPA
lvt new myapp --kit simple # no CSS (semantic HTML)
# Generate resource with specific CSS
lvt gen resource products name --css tailwind
lvt gen resource products name --css none
Template Helpers
LiveTemplate templates have access to Go template functions:
<!-- Date formatting -->
{{.CreatedAt.Format "2006-01-02"}}
<!-- Conditional -->
{{if .Published}}
<span class="badge-success">Published</span>
{{else}}
<span class="badge-warning">Draft</span>
{{end}}
<!-- Range with index -->
{{range $i, $product := .Products}}
<div class="item-{{$i}}">{{$product.Name}}</div>
{{end}}
<!-- Printf for formatting -->
<p>${{printf "%.2f" .Price}}</p>
<!-- Nested data -->
{{range .Products}}
<h2>{{.Name}}</h2>
{{range .Reviews}}
<p>{{.Comment}}</p>
{{end}}
{{end}}
WebSocket Actions
Generated resources include WebSocket support for real-time updates:
// app/products/products.go
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
// Listen for WebSocket actions
action := lt.Action()
switch action {
case "add":
name := lt.FormValue("name")
price := lt.FormValueFloat("price")
product, err := queries.CreateProduct(r.Context(), models.CreateProductParams{
Name: name,
Price: price,
})
if err != nil {
return err
}
// Re-render with new data
products, _ := queries.ListProducts(r.Context())
return lt.Render("products", products)
case "delete":
id := lt.FormValueInt("id")
err := queries.DeleteProduct(r.Context(), id)
if err != nil {
return err
}
products, _ := queries.ListProducts(r.Context())
return lt.Render("products", products)
default:
// Initial load
products, err := queries.ListProducts(r.Context())
if err != nil {
return err
}
return lt.Render("products", products)
}
})
}
Common Patterns
Pagination
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
page := 1
if p := r.URL.Query().Get("page"); p != "" {
page, _ = strconv.Atoi(p)
}
limit := 20
offset := (page - 1) * limit
products, err := queries.ListProductsPaginated(r.Context(), models.ListProductsPaginatedParams{
Limit: int64(limit),
Offset: int64(offset),
})
return lt.Render("products", map[string]interface{}{
"Products": products,
"Page": page,
"NextPage": page + 1,
"PrevPage": page - 1,
})
})
}
Error Handling
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
products, err := queries.ListProducts(r.Context())
if err != nil {
// Log error
log.Printf("Error loading products: %v", err)
// Show user-friendly message
return lt.Error("Unable to load products. Please try again.")
}
if len(products) == 0 {
// Handle empty state
return lt.Render("products", map[string]interface{}{
"Products": products,
"Empty": true,
})
}
return lt.Render("products", products)
})
}
Complex Data Structures
type ProductWithReviews struct {
Product models.Product
Reviews []models.Review
AvgRating float64
}
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
products, _ := queries.ListProducts(r.Context())
var enriched []ProductWithReviews
for _, p := range products {
reviews, _ := queries.GetProductReviews(r.Context(), p.ID)
var total float64
for _, r := range reviews {
total += r.Rating
}
avg := 0.0
if len(reviews) > 0 {
avg = total / float64(len(reviews))
}
enriched = append(enriched, ProductWithReviews{
Product: p,
Reviews: reviews,
AvgRating: avg,
})
}
return lt.Render("products", enriched)
})
}
File Organization Tips
Keep related code together:
app/products/
โโโ products.go # Main handler
โโโ products.tmpl # Main template
โโโ products_test.go # Tests
โโโ helpers.go # Product-specific helpers
โโโ validation.go # Product validation logic
Shared utilities:
project/
โโโ app/
โ โโโ products/
โโโ shared/
โ โโโ auth/ # Shared auth logic
โ โโโ validation/ # Shared validators
โ โโโ helpers/ # Shared helpers
โโโ ...
Quick Reference
| I want to... | How |
|---|---|
| Change handler logic | Edit app/<name>/<name>.go |
| Customize HTML/UI | Edit app/<name>/<name>.tmpl |
| Add custom query | Edit database/queries.sql + run lvt migration up |
| Add validation | Add checks in handler before database calls |
| Change CSS framework | Use --css flag when generating |
| Add authentication | Check session/user in handler |
| Handle WebSocket actions | Use lt.Action() in handler |
| Pass complex data | Use map[string]interface{} or custom structs |
Remember
โ All generated .go and .tmpl files are meant to be edited
โ lvt won't overwrite your customizations
โ Add custom SQL queries to queries.sql
โ Run lvt migration up after changing queries
โ Use Go template syntax in .tmpl files
โ WebSocket actions update the page automatically
โ Don't edit generated models in database/models/
โ Don't edit schema.sql directly - use migrations
โ Don't forget to run migrations after query changes