| 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 -
.lvtrc exists (most common scenario)
- Agent context - User is working with
lvt-assistant agent
- 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:
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
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 {
category := r.URL.Query().Get("category")
var products []models.Product
var err error
if category != "" {
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:
<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:
SELECT * FROM products ORDER BY created_at DESC;
SELECT * FROM products
WHERE category = ?
ORDER BY created_at DESC;
SELECT * FROM products
WHERE featured = true
ORDER BY popularity DESC
LIMIT 10;
SELECT * FROM products
WHERE name LIKE '%' || ? || '%'
OR description LIKE '%' || ? || '%';
After editing queries.sql:
lvt migration up
4. Add Validation
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")
if name == "" {
return lt.Error("Name is required")
}
price, err := strconv.ParseFloat(priceStr, 64)
if err != nil || price < 0 {
return lt.Error("Invalid price")
}
product, err := queries.CreateProduct(r.Context(), models.CreateProductParams{
Name: name,
Price: price,
})
if err != nil {
return err
}
return lt.Redirect("/products")
}
})
}
5. Add Authentication
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
user := getUserFromSession(r)
if user == nil {
return lt.Redirect("/login")
}
if !user.IsAdmin {
return lt.Error("Unauthorized", http.StatusForbidden)
}
})
}
Changing CSS Framework
Generate with different kit:
lvt new myapp --kit multi
lvt new myapp --kit single
lvt new myapp --kit simple
lvt gen resource products name --css tailwind
lvt gen resource products name --css none
Template Helpers
LiveTemplate templates have access to Go template functions:
{{.CreatedAt.Format "2006-01-02"}}
{{if .Published}}
<span class="badge-success">Published</span>
{{else}}
<span class="badge-warning">Draft</span>
{{end}}
{{range $i, $product := .Products}}
<div class="item-{{$i}}">{{$product.Name}}</div>
{{end}}
<p>${{printf "%.2f" .Price}}</p>
{{range .Products}}
<h2>{{.Name}}</h2>
{{range .Reviews}}
<p>{{.Comment}}</p>
{{end}}
{{end}}
WebSocket Actions
Generated resources include WebSocket support for real-time updates:
func Handler(queries *models.Queries) http.HandlerFunc {
return livetemplate.Handler(func(r *http.Request, lt *livetemplate.LiveTemplate) error {
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
}
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:
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.Printf("Error loading products: %v", err)
return lt.Error("Unable to load products. Please try again.")
}
if len(products) == 0 {
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