diff --git a/examples/README.md b/examples/README.md
index d7f2a55..74c5e5c 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,6 +1,74 @@
-To run these examples:
+# Structpages Examples
+
+This directory contains examples demonstrating various features of the structpages library.
+
+## Available Examples
+
+### 1. Simple
+Basic routing and page structure demonstration.
+- Simple struct-based routing
+- Basic templ integration
+- URL generation with `urlFor`
+
+### 2. HTMX
+HTMX integration with partial rendering.
+- HTMX partial component rendering
+- Custom error handling for HTMX requests
+- Middleware usage
+- Dynamic content updates
+
+### 3. Todo
+Complete CRUD application with forms.
+- Full CRUD operations
+- Form handling with struct tags
+- Custom ServeHTTP handlers
+- In-memory data storage
+- HTMX interactions
+
+### 4. Blog-Admin
+Advanced blog platform with admin panel demonstrating:
+- **Dependency Injection**: Multiple services (DB, Auth, Session, Config)
+- **Nested Routing**: 3-level deep route structure (`/admin/posts/{id}/edit`)
+- **Authentication & Authorization**: Session-based auth with role-based access
+- **Middleware Patterns**: Global and per-route middleware
+- **Props Pattern**: Type-safe data loading with complex queries
+- **Advanced HTMX**: Custom PageConfig, partial rendering, auto-save
+- **Form Handling**: Structured form parsing with validation
+- **Database Integration**: SQLite with transactions and indexes
+- **Component Composition**: Reusable Templ components
+- **API Endpoints**: JSON APIs alongside HTML pages
+- **Query Parameters**: Pagination, filtering, search
+- **Real-time Features**: Auto-save, live updates
+- **Error Handling**: Custom error pages and graceful degradation
+
+## Running the Examples
+
+Each example is a standalone Go module. To run an example:
```shell
+# Navigate to example directory
cd examples/simple/
+
+# Download dependencies
+go mod download
+
+# Generate templ files
+templ generate
+
+# Run the server
+go run .
+
+# Or use templ's watch mode for development
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
```
+
+Open http://localhost:8080 in your browser.
+
+## Learning Path
+
+1. Start with **simple** to understand basic routing
+2. Move to **htmx** to learn about partial rendering
+3. Study **todo** for form handling and CRUD operations
+4. Explore **blog-admin** for production-ready patterns
+
+Each example builds on concepts from the previous ones, demonstrating increasingly sophisticated use of structpages features.
\ No newline at end of file
diff --git a/examples/blog-admin/.gitignore b/examples/blog-admin/.gitignore
new file mode 100644
index 0000000..efe05cb
--- /dev/null
+++ b/examples/blog-admin/.gitignore
@@ -0,0 +1,17 @@
+# Binary
+blog-admin
+
+# Logs
+*.log
+
+# Database
+*.db
+
+# Session/cookie files
+cookies.txt
+
+# Generated templ files
+*_templ.go
+
+# OS files
+.DS_Store
\ No newline at end of file
diff --git a/examples/blog-admin/README.md b/examples/blog-admin/README.md
new file mode 100644
index 0000000..ad076af
--- /dev/null
+++ b/examples/blog-admin/README.md
@@ -0,0 +1,170 @@
+# Blog Admin Example
+
+This is a comprehensive example demonstrating advanced features of the structpages framework. It implements a full-featured blog platform with public-facing pages and an admin panel.
+
+## Features Demonstrated
+
+### 1. **Dependency Injection**
+- Multiple services injected via `MountPages`: Store, AuthService, SessionManager, FormDecoder, Config
+- Type-based injection with automatic parameter resolution
+- Services available in Props methods, ServeHTTP handlers, and middleware
+
+### 2. **Nested Routing Structure**
+- Three-level deep routes (e.g., `/admin/posts/{id}/edit`)
+- Struct embedding for hierarchical organization
+- Clean URL patterns with path parameters
+
+### 3. **Authentication & Authorization**
+- Session-based authentication with bcrypt password hashing
+- Role-based access control (admin, author, reader)
+- Protected routes using middleware composition
+- Login/logout flow with redirect support
+
+### 4. **Advanced Middleware Patterns**
+- Global middleware (session loading, logging)
+- Per-route middleware (authentication, CSRF protection)
+- Middleware composition with proper execution order
+- Context-aware middleware with PageNode access
+
+### 5. **Props Pattern**
+- Type-safe props with complex data loading
+- Dependency injection in Props methods
+- Error handling and data validation
+- Efficient database queries with relationship loading
+
+### 6. **HTMX Integration**
+- Partial component rendering
+- Custom PageConfig for different HX-Target values
+- Progressive enhancement patterns
+- Real-time updates (publish/unpublish)
+- Auto-save functionality
+- Form submissions with URL updates
+
+### 7. **Form Handling**
+- Structured form parsing with go-playground/form
+- Multi-value form fields (categories, tags)
+- File upload handling (media library)
+- Validation and error display
+
+### 8. **Database Integration**
+- SQLite with proper schema and indexes
+- Transaction support for complex operations
+- Efficient queries with pagination
+- Analytics and view tracking
+
+### 9. **Component Composition**
+- Reusable Templ components (layouts, cards, forms)
+- Conditional rendering based on user roles
+- Dynamic component selection
+- Shared UI components library
+
+### 10. **URL Generation**
+- Type-safe URL generation with `urlFor`
+- Support for path parameters
+- Query string building with `join` helper
+- HTMX-aware URL handling
+
+### 11. **Error Handling**
+- Custom error handler with status codes
+- Graceful degradation
+- User-friendly error messages
+- Proper HTTP status responses
+
+### 12. **Advanced ServeHTTP Patterns**
+- Error-returning ServeHTTP (buffered response)
+- Standard http.Handler interface (direct write)
+- API endpoints with JSON responses
+- Mixed content types handling
+
+## Project Structure
+
+```
+blog-admin/
+├── main.go # Application entry point
+├── routes.go # Route definitions and middleware
+├── models.go # Data models and database schema
+├── store.go # Database operations
+├── auth.go # Authentication service
+├── components.templ # Reusable UI components
+├── pages.templ # Public pages (home, post, search, login)
+├── admin_pages.templ # Admin dashboard and post management
+├── admin_users.templ # User management and settings
+├── api_pages.templ # API endpoints and advanced patterns
+└── static/ # CSS and static assets
+ ├── styles.css # Public site styles
+ └── admin.css # Admin panel styles
+```
+
+## Running the Example
+
+1. Install dependencies:
+```bash
+go mod download
+```
+
+2. Generate Templ files:
+```bash
+templ generate
+```
+
+3. Run the application:
+```bash
+go run .
+```
+
+4. Access the application:
+- Public site: http://localhost:8080
+- Admin panel: http://localhost:8080/admin
+- Login credentials: admin / admin123
+
+## Key Patterns to Study
+
+### Dependency Injection
+See how services are registered in `main.go` and used throughout the application:
+```go
+if err := sp.MountPages(r, pages{}, "/", "Blog", store, auth, sessionManager, formDecoder, config); err != nil {
+ log.Fatal(err)
+}
+```
+
+### Nested Routes with Middleware
+Check `routes.go` for the hierarchical structure and middleware application:
+```go
+type adminPages struct {
+ dashboard `route:"/{$} Dashboard"`
+ posts adminPostPages `route:"/posts Posts"`
+}
+
+func (a adminPages) Middlewares(auth *AuthService) []structpages.MiddlewareFunc {
+ return []structpages.MiddlewareFunc{
+ requireAuthMiddleware(auth),
+ requireAdminMiddleware(auth),
+ }
+}
+```
+
+### Props Pattern with Data Loading
+See `pages.templ` for examples of loading complex data:
+```go
+func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) {
+ slug := r.PathValue("slug")
+ post, err := store.GetPostBySlug(slug)
+ // ... load related data
+}
+```
+
+### HTMX Partial Rendering
+Check `api_pages.templ` for custom PageConfig:
+```go
+func (s searchPage) PageConfig(r *http.Request) (string, error) {
+ hxTarget := r.Header.Get("HX-Target")
+ switch hxTarget {
+ case "search-results":
+ return "Results", nil
+ default:
+ return "Page", nil
+ }
+}
+```
+
+This example showcases how structpages can be used to build production-ready applications with minimal boilerplate while maintaining type safety and clean architecture.
\ No newline at end of file
diff --git a/examples/blog-admin/admin_pages.templ b/examples/blog-admin/admin_pages.templ
new file mode 100644
index 0000000..8e786d5
--- /dev/null
+++ b/examples/blog-admin/admin_pages.templ
@@ -0,0 +1,488 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-playground/form/v4"
+)
+
+// Admin Dashboard
+type dashboard struct{}
+
+type dashboardProps struct {
+ User *User
+ TotalPosts int
+ TotalUsers int
+ TotalComments int
+ RecentPosts []*Post
+ Analytics *Analytics
+}
+
+func (d dashboard) Props(r *http.Request, store *Store, auth *AuthService) (dashboardProps, error) {
+ user := auth.GetUser(r)
+
+ // Get counts
+ _, totalPosts, _ := store.ListPosts(PostFilter{Limit: 1})
+ _, totalUsers, _ := store.ListUsers(1, 0)
+
+ // Get recent posts
+ recentPosts, _, _ := store.ListPosts(PostFilter{
+ Limit: 5,
+ Offset: 0,
+ })
+
+ // Get today's analytics
+ analytics, _ := store.GetAnalytics(time.Now())
+
+ return dashboardProps{
+ User: user,
+ TotalPosts: totalPosts,
+ TotalUsers: totalUsers,
+ TotalComments: 0, // Simplified
+ RecentPosts: recentPosts,
+ Analytics: analytics,
+ }, nil
+}
+
+templ (d dashboard) Page(props dashboardProps) {
+ @adminLayout("Dashboard", props.User) {
+
Dashboard
+
+ @statsCard("Total Posts", fmt.Sprint(props.TotalPosts), "+5 this week")
+ @statsCard("Total Users", fmt.Sprint(props.TotalUsers), "+2 this week")
+ @statsCard("Page Views Today", fmt.Sprint(props.Analytics.PageViews), "")
+ @statsCard("Unique Visitors", fmt.Sprint(props.Analytics.Visitors), "")
+
+
+
+ Recent Posts
+ @dataTable([]string{"Title", "Author", "Status", "Date", "Actions"}) {
+ for _, post := range props.RecentPosts {
+
+ | { post.Title } |
+ { post.Author.Username } |
+
+
+ { post.Status }
+
+ |
+ { post.CreatedAt.Format("Jan 2, 2006") } |
+
+ Edit
+ |
+
+ }
+ }
+
+
+ Top Posts Today
+ if len(props.Analytics.TopPosts) > 0 {
+
+ for _, p := range props.Analytics.TopPosts {
+ -
+ { p.PostTitle }
+ { fmt.Sprintf("%d views", p.Views) }
+
+ }
+
+ } else {
+ No data available
+ }
+
+
+ }
+}
+
+// Admin Post List
+type adminPostListPage struct{}
+
+type adminPostListProps struct {
+ User *User
+ Posts []*Post
+ Total int
+ Page int
+ Filter PostFilter
+}
+
+func (p adminPostListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminPostListProps, error) {
+ user := auth.GetUser(r)
+
+ // Parse query parameters
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ filter := PostFilter{
+ Status: r.URL.Query().Get("status"),
+ Search: r.URL.Query().Get("search"),
+ Limit: 20,
+ Offset: (page - 1) * 20,
+ }
+
+ // If not admin, only show own posts
+ if user.Role != "admin" {
+ filter.AuthorID = user.ID
+ }
+
+ posts, total, err := store.ListPosts(filter)
+ if err != nil {
+ return adminPostListProps{}, err
+ }
+
+ return adminPostListProps{
+ User: user,
+ Posts: posts,
+ Total: total,
+ Page: page,
+ Filter: filter,
+ }, nil
+}
+
+templ (p adminPostListPage) Page(props adminPostListProps) {
+ @adminLayout("Posts", props.User) {
+
+
+
+
+ @dataTable([]string{"Title", "Author", "Status", "Published", "Views", "Actions"}) {
+ for _, post := range props.Posts {
+
+ |
+
+ { post.Title }
+
+ |
+ { post.Author.Username } |
+
+
+ { post.Status }
+
+ |
+
+ if post.PublishedAt != nil {
+ { post.PublishedAt.Format("Jan 2, 2006") }
+ } else {
+ Not published
+ }
+ |
+ { fmt.Sprint(post.ViewCount) } |
+
+ Edit
+ if post.Status == "draft" {
+
+ }
+
+ |
+
+ }
+ }
+ if baseURL, err := urlFor(ctx, adminPostListPage{}); err == nil {
+ @pagination(props.Page, (props.Total+19)/20, baseURL)
+ }
+ }
+}
+
+// Admin Post Form (New/Edit)
+type adminPostNewPage struct{}
+type adminPostEditPage struct{}
+
+type postForm struct {
+ Title string `form:"title"`
+ Slug string `form:"slug"`
+ Content string `form:"content"`
+ Excerpt string `form:"excerpt"`
+ Status string `form:"status"`
+ Categories []string `form:"categories"`
+ Tags string `form:"tags"`
+}
+
+func (p adminPostNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+
+ if r.Method == "POST" {
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Create post
+ post := &Post{
+ Title: f.Title,
+ Slug: f.Slug,
+ Content: f.Content,
+ Excerpt: f.Excerpt,
+ Status: f.Status,
+ AuthorID: user.ID,
+ }
+
+ if f.Slug == "" {
+ post.Slug = GenerateSlug(f.Title)
+ }
+
+ if f.Status == "published" {
+ now := time.Now()
+ post.PublishedAt = &now
+ }
+
+ // Parse tags
+ if f.Tags != "" {
+ tags := strings.Split(f.Tags, ",")
+ for _, tag := range tags {
+ post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)})
+ }
+ }
+
+ // Add categories
+ for _, catID := range f.Categories {
+ if id, err := strconv.Atoi(catID); err == nil {
+ post.Categories = append(post.Categories, Category{ID: id})
+ }
+ }
+
+ if err := store.CreatePost(post); err != nil {
+ return err
+ }
+
+ // Redirect to edit page
+ http.Redirect(w, r, fmt.Sprintf("/admin/posts/%d/edit", post.ID), http.StatusSeeOther)
+ return nil
+ }
+
+ // GET - show form
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, nil, &postForm{Status: "draft"}, categories, ""))
+}
+
+func (p adminPostEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update post
+ post.Title = f.Title
+ post.Slug = f.Slug
+ post.Content = f.Content
+ post.Excerpt = f.Excerpt
+ post.Status = f.Status
+
+ if f.Status == "published" && post.PublishedAt == nil {
+ now := time.Now()
+ post.PublishedAt = &now
+ }
+
+ // Parse tags
+ post.Tags = nil
+ if f.Tags != "" {
+ tags := strings.Split(f.Tags, ",")
+ for _, tag := range tags {
+ post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)})
+ }
+ }
+
+ // Update categories
+ post.Categories = nil
+ for _, catID := range f.Categories {
+ if id, err := strconv.Atoi(catID); err == nil {
+ post.Categories = append(post.Categories, Category{ID: id})
+ }
+ }
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Show success message (simplified - in production use flash messages)
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, post, &f, categories, "Post updated successfully"))
+ }
+
+ // GET - show form with existing data
+ form := &postForm{
+ Title: post.Title,
+ Slug: post.Slug,
+ Content: post.Content,
+ Excerpt: post.Excerpt,
+ Status: post.Status,
+ }
+
+ // Build tags string
+ var tags []string
+ for _, tag := range post.Tags {
+ tags = append(tags, tag.Name)
+ }
+ form.Tags = strings.Join(tags, ", ")
+
+ // Build categories
+ for _, cat := range post.Categories {
+ form.Categories = append(form.Categories, fmt.Sprint(cat.ID))
+ }
+
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, post, form, categories, ""))
+}
+
+// Post form template
+templ postFormPage(user *User, post *Post, form *postForm, categories []*Category, message string) {
+ @adminLayout(ternary(post == nil, "New Post", "Edit Post"), user) {
+
+ if message != "" {
+ @alert("success", message)
+ }
+
+ }
+}
+
+// Delete post handler
+type adminPostDeletePage struct{}
+
+func (p adminPostDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ if err := store.DeletePost(postID); err != nil {
+ return err
+ }
+
+ // Return empty response for HTMX to remove the row
+ w.WriteHeader(http.StatusOK)
+ return nil
+}
+
+// Helper functions
+func contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
diff --git a/examples/blog-admin/admin_pages_templ.go b/examples/blog-admin/admin_pages_templ.go
new file mode 100644
index 0000000..17e10fe
--- /dev/null
+++ b/examples/blog-admin/admin_pages_templ.go
@@ -0,0 +1,1038 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/a-h/templ"
+ templruntime "github.com/a-h/templ/runtime"
+ "github.com/go-playground/form/v4"
+)
+
+// Admin Dashboard
+type dashboard struct{}
+
+type dashboardProps struct {
+ User *User
+ TotalPosts int
+ TotalUsers int
+ TotalComments int
+ RecentPosts []*Post
+ Analytics *Analytics
+}
+
+func (d dashboard) Props(r *http.Request, store *Store, auth *AuthService) (dashboardProps, error) {
+ user := auth.GetUser(r)
+
+ // Get counts
+ _, totalPosts, _ := store.ListPosts(PostFilter{Limit: 1})
+ _, totalUsers, _ := store.ListUsers(1, 0)
+
+ // Get recent posts
+ recentPosts, _, _ := store.ListPosts(PostFilter{
+ Limit: 5,
+ Offset: 0,
+ })
+
+ // Get today's analytics
+ analytics, _ := store.GetAnalytics(time.Now())
+
+ return dashboardProps{
+ User: user,
+ TotalPosts: totalPosts,
+ TotalUsers: totalUsers,
+ TotalComments: 0, // Simplified
+ RecentPosts: recentPosts,
+ Analytics: analytics,
+ }, nil
+}
+
+func (d dashboard) Page(props dashboardProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Dashboard
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = statsCard("Total Posts", fmt.Sprint(props.TotalPosts), "+5 this week").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = statsCard("Total Users", fmt.Sprint(props.TotalUsers), "+2 this week").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = statsCard("Page Views Today", fmt.Sprint(props.Analytics.PageViews), "").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = statsCard("Unique Visitors", fmt.Sprint(props.Analytics.Visitors), "").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Recent Posts
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ for _, post := range props.RecentPosts {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 68, Col: 23}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 69, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 = []any{"status", "status-" + post.Status}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 72, Col: 22}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("Jan 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 75, Col: 49}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | Edit |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = dataTable([]string{"Title", "Author", "Status", "Date", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Top Posts Today
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(props.Analytics.TopPosts) > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, p := range props.Analytics.TopPosts {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "- ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(p.PostTitle)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 90, Col: 27}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d views", p.Views))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 91, Col: 62}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "No data available
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout("Dashboard", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Admin Post List
+type adminPostListPage struct{}
+
+type adminPostListProps struct {
+ User *User
+ Posts []*Post
+ Total int
+ Page int
+ Filter PostFilter
+}
+
+func (p adminPostListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminPostListProps, error) {
+ user := auth.GetUser(r)
+
+ // Parse query parameters
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ filter := PostFilter{
+ Status: r.URL.Query().Get("status"),
+ Search: r.URL.Query().Get("search"),
+ Limit: 20,
+ Offset: (page - 1) * 20,
+ }
+
+ // If not admin, only show own posts
+ if user.Role != "admin" {
+ filter.AuthorID = user.ID
+ }
+
+ posts, total, err := store.ListPosts(filter)
+ if err != nil {
+ return adminPostListProps{}, err
+ }
+
+ return adminPostListProps{
+ User: user,
+ Posts: posts,
+ Total: total,
+ Page: page,
+ Filter: filter,
+ }, nil
+}
+
+func (p adminPostListPage) Page(props adminPostListProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var13 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var13 == nil {
+ templ_7745c5c3_Var13 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Var18 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ for _, post := range props.Posts {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var20 string
+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 180, Col: 19}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var21 string
+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 183, Col: 31}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var22 = []any{"status", "status-" + post.Status}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var24 string
+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 186, Col: 20}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if post.PublishedAt != nil {
+ var templ_7745c5c3_Var25 string
+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(post.PublishedAt.Format("Jan 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 191, Col: 47}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Not published")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var26 string
+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(post.ViewCount))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 196, Col: 37}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " | Edit ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if post.Status == "draft" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = dataTable([]string{"Title", "Author", "Status", "Published", "Views", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if baseURL, err := urlFor(ctx, adminPostListPage{}); err == nil {
+ templ_7745c5c3_Err = pagination(props.Page, (props.Total+19)/20, baseURL).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout("Posts", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Admin Post Form (New/Edit)
+type adminPostNewPage struct{}
+type adminPostEditPage struct{}
+
+type postForm struct {
+ Title string `form:"title"`
+ Slug string `form:"slug"`
+ Content string `form:"content"`
+ Excerpt string `form:"excerpt"`
+ Status string `form:"status"`
+ Categories []string `form:"categories"`
+ Tags string `form:"tags"`
+}
+
+func (p adminPostNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+
+ if r.Method == "POST" {
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Create post
+ post := &Post{
+ Title: f.Title,
+ Slug: f.Slug,
+ Content: f.Content,
+ Excerpt: f.Excerpt,
+ Status: f.Status,
+ AuthorID: user.ID,
+ }
+
+ if f.Slug == "" {
+ post.Slug = GenerateSlug(f.Title)
+ }
+
+ if f.Status == "published" {
+ now := time.Now()
+ post.PublishedAt = &now
+ }
+
+ // Parse tags
+ if f.Tags != "" {
+ tags := strings.Split(f.Tags, ",")
+ for _, tag := range tags {
+ post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)})
+ }
+ }
+
+ // Add categories
+ for _, catID := range f.Categories {
+ if id, err := strconv.Atoi(catID); err == nil {
+ post.Categories = append(post.Categories, Category{ID: id})
+ }
+ }
+
+ if err := store.CreatePost(post); err != nil {
+ return err
+ }
+
+ // Redirect to edit page
+ http.Redirect(w, r, fmt.Sprintf("/admin/posts/%d/edit", post.ID), http.StatusSeeOther)
+ return nil
+ }
+
+ // GET - show form
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, nil, &postForm{Status: "draft"}, categories, ""))
+}
+
+func (p adminPostEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update post
+ post.Title = f.Title
+ post.Slug = f.Slug
+ post.Content = f.Content
+ post.Excerpt = f.Excerpt
+ post.Status = f.Status
+
+ if f.Status == "published" && post.PublishedAt == nil {
+ now := time.Now()
+ post.PublishedAt = &now
+ }
+
+ // Parse tags
+ post.Tags = nil
+ if f.Tags != "" {
+ tags := strings.Split(f.Tags, ",")
+ for _, tag := range tags {
+ post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)})
+ }
+ }
+
+ // Update categories
+ post.Categories = nil
+ for _, catID := range f.Categories {
+ if id, err := strconv.Atoi(catID); err == nil {
+ post.Categories = append(post.Categories, Category{ID: id})
+ }
+ }
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Show success message (simplified - in production use flash messages)
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, post, &f, categories, "Post updated successfully"))
+ }
+
+ // GET - show form with existing data
+ form := &postForm{
+ Title: post.Title,
+ Slug: post.Slug,
+ Content: post.Content,
+ Excerpt: post.Excerpt,
+ Status: post.Status,
+ }
+
+ // Build tags string
+ var tags []string
+ for _, tag := range post.Tags {
+ tags = append(tags, tag.Name)
+ }
+ form.Tags = strings.Join(tags, ", ")
+
+ // Build categories
+ for _, cat := range post.Categories {
+ form.Categories = append(form.Categories, fmt.Sprint(cat.ID))
+ }
+
+ categories, _ := store.ListCategories()
+ return render(w, r, postFormPage(user, post, form, categories, ""))
+}
+
+// Post form template
+func postFormPage(user *User, post *Post, form *postForm, categories []*Category, message string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var30 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var30 == nil {
+ templ_7745c5c3_Var30 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var31 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if message != "" {
+ templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout(ternary(post == nil, "New Post", "Edit Post"), user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var31), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Delete post handler
+type adminPostDeletePage struct{}
+
+func (p adminPostDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ if err := store.DeletePost(postID); err != nil {
+ return err
+ }
+
+ // Return empty response for HTMX to remove the row
+ w.WriteHeader(http.StatusOK)
+ return nil
+}
+
+// Helper functions
+func contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/examples/blog-admin/admin_users.templ b/examples/blog-admin/admin_users.templ
new file mode 100644
index 0000000..4d40c85
--- /dev/null
+++ b/examples/blog-admin/admin_users.templ
@@ -0,0 +1,339 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/go-playground/form/v4"
+)
+
+// Admin User List
+type adminUserListPage struct{}
+
+type adminUserListProps struct {
+ User *User
+ Users []*User
+ Total int
+ Page int
+}
+
+func (u adminUserListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminUserListProps, error) {
+ user := auth.GetUser(r)
+
+ // Only admins can view users
+ if user.Role != "admin" {
+ return adminUserListProps{}, fmt.Errorf("forbidden")
+ }
+
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ limit := 20
+ offset := (page - 1) * limit
+
+ users, total, err := store.ListUsers(limit, offset)
+ if err != nil {
+ return adminUserListProps{}, err
+ }
+
+ return adminUserListProps{
+ User: user,
+ Users: users,
+ Total: total,
+ Page: page,
+ }, nil
+}
+
+templ (u adminUserListPage) Page(props adminUserListProps) {
+ @adminLayout("Users", props.User) {
+
+ @dataTable([]string{"Username", "Email", "Role", "Created", "Actions"}) {
+ for _, user := range props.Users {
+
+ | { user.Username } |
+ { user.Email } |
+
+
+ { user.Role }
+
+ |
+ { user.CreatedAt.Format("Jan 2, 2006") } |
+
+ Edit
+ if user.ID != props.User.ID {
+
+ }
+ |
+
+ }
+ }
+ if baseURL, err := urlFor(ctx, adminUserListPage{}); err == nil {
+ @pagination(props.Page, (props.Total+19)/20, baseURL)
+ }
+ }
+}
+
+// Admin User Form (New/Edit)
+type adminUserNewPage struct{}
+type adminUserEditPage struct{}
+
+type userForm struct {
+ Username string `form:"username"`
+ Email string `form:"email"`
+ Password string `form:"password"`
+ Role string `form:"role"`
+}
+
+func (u adminUserNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f userForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Hash password
+ hash, err := HashPassword(f.Password)
+ if err != nil {
+ return err
+ }
+
+ // Create user
+ user := &User{
+ Username: f.Username,
+ Email: f.Email,
+ PasswordHash: hash,
+ Role: f.Role,
+ }
+
+ if err := store.CreateUser(user); err != nil {
+ return err
+ }
+
+ // Redirect to users list
+ http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
+ return nil
+ }
+
+ // GET - show form
+ return render(w, r, userFormPage(currentUser, nil, &userForm{Role: "reader"}, ""))
+}
+
+func (u adminUserEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ userID, _ := strconv.Atoi(r.PathValue("id"))
+
+ user, err := store.GetUserByID(userID)
+ if err != nil {
+ return err
+ }
+
+ if r.Method == "POST" {
+ var f userForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update user
+ user.Username = f.Username
+ user.Email = f.Email
+ user.Role = f.Role
+
+ // Update password if provided
+ if f.Password != "" {
+ hash, err := HashPassword(f.Password)
+ if err != nil {
+ return err
+ }
+ user.PasswordHash = hash
+ }
+
+ // In a real app, you'd have an UpdateUser method
+ // For now, show success
+ return render(w, r, userFormPage(currentUser, user, &f, "User updated successfully"))
+ }
+
+ // GET - show form
+ form := &userForm{
+ Username: user.Username,
+ Email: user.Email,
+ Role: user.Role,
+ }
+
+ return render(w, r, userFormPage(currentUser, user, form, ""))
+}
+
+templ userFormPage(currentUser *User, user *User, form *userForm, message string) {
+ @adminLayout(ternary(user == nil, "New User", "Edit User"), currentUser) {
+
+ if message != "" {
+ @alert("success", message)
+ }
+
+ }
+}
+
+// Delete user handler
+type adminUserDeletePage struct{}
+
+func (u adminUserDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ userID, _ := strconv.Atoi(r.PathValue("id"))
+
+ // Can't delete yourself
+ if userID == currentUser.ID {
+ return fmt.Errorf("cannot delete yourself")
+ }
+
+ // In a real app, you'd have a DeleteUser method
+ // For now, return success
+ w.WriteHeader(http.StatusOK)
+ return nil
+}
+
+// Admin Settings
+type adminSettingsPage struct{}
+
+type settingsForm struct {
+ SiteName string `form:"site_name"`
+ SiteDescription string `form:"site_description"`
+ AdminEmail string `form:"admin_email"`
+}
+
+func (s adminSettingsPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, config *Config, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+ if user.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f settingsForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update config (in memory only for this example)
+ config.SiteName = f.SiteName
+ config.SiteDescription = f.SiteDescription
+ config.AdminEmail = f.AdminEmail
+
+ // Show success
+ return render(w, r, s.Page(user, config, "Settings updated successfully"))
+ }
+
+ // GET - show form
+ return render(w, r, s.Page(user, config, ""))
+}
+
+templ (s adminSettingsPage) Page(user *User, config *Config, message string) {
+ @adminLayout("Settings", user) {
+ Settings
+ if message != "" {
+ @alert("success", message)
+ }
+
+ }
+}
+
+// Media Library
+type mediaLibraryPage struct{}
+
+func (m mediaLibraryPage) Props(r *http.Request, auth *AuthService) (*User, error) {
+ return auth.GetUser(r), nil
+}
+
+templ (m mediaLibraryPage) Page(user *User) {
+ @adminLayout("Media Library", user) {
+
+
+ }
+}
diff --git a/examples/blog-admin/admin_users_templ.go b/examples/blog-admin/admin_users_templ.go
new file mode 100644
index 0000000..ed4f14b
--- /dev/null
+++ b/examples/blog-admin/admin_users_templ.go
@@ -0,0 +1,702 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/a-h/templ"
+ templruntime "github.com/a-h/templ/runtime"
+ "github.com/go-playground/form/v4"
+)
+
+// Admin User List
+type adminUserListPage struct{}
+
+type adminUserListProps struct {
+ User *User
+ Users []*User
+ Total int
+ Page int
+}
+
+func (u adminUserListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminUserListProps, error) {
+ user := auth.GetUser(r)
+
+ // Only admins can view users
+ if user.Role != "admin" {
+ return adminUserListProps{}, fmt.Errorf("forbidden")
+ }
+
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ limit := 20
+ offset := (page - 1) * limit
+
+ users, total, err := store.ListUsers(limit, offset)
+ if err != nil {
+ return adminUserListProps{}, err
+ }
+
+ return adminUserListProps{
+ User: user,
+ Users: users,
+ Total: total,
+ Page: page,
+ }, nil
+}
+
+func (u adminUserListPage) Page(props adminUserListProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ for _, user := range props.Users {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 62, Col: 24}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 63, Col: 21}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 = []any{"role", "role-" + user.Role}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.Role)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 66, Col: 18}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.CreatedAt.Format("Jan 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 69, Col: 47}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | Edit ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if user.ID != props.User.ID {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = dataTable([]string{"Username", "Email", "Role", "Created", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if baseURL, err := urlFor(ctx, adminUserListPage{}); err == nil {
+ templ_7745c5c3_Err = pagination(props.Page, (props.Total+19)/20, baseURL).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout("Users", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Admin User Form (New/Edit)
+type adminUserNewPage struct{}
+type adminUserEditPage struct{}
+
+type userForm struct {
+ Username string `form:"username"`
+ Email string `form:"email"`
+ Password string `form:"password"`
+ Role string `form:"role"`
+}
+
+func (u adminUserNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f userForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Hash password
+ hash, err := HashPassword(f.Password)
+ if err != nil {
+ return err
+ }
+
+ // Create user
+ user := &User{
+ Username: f.Username,
+ Email: f.Email,
+ PasswordHash: hash,
+ Role: f.Role,
+ }
+
+ if err := store.CreateUser(user); err != nil {
+ return err
+ }
+
+ // Redirect to users list
+ http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
+ return nil
+ }
+
+ // GET - show form
+ return render(w, r, userFormPage(currentUser, nil, &userForm{Role: "reader"}, ""))
+}
+
+func (u adminUserEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ userID, _ := strconv.Atoi(r.PathValue("id"))
+
+ user, err := store.GetUserByID(userID)
+ if err != nil {
+ return err
+ }
+
+ if r.Method == "POST" {
+ var f userForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update user
+ user.Username = f.Username
+ user.Email = f.Email
+ user.Role = f.Role
+
+ // Update password if provided
+ if f.Password != "" {
+ hash, err := HashPassword(f.Password)
+ if err != nil {
+ return err
+ }
+ user.PasswordHash = hash
+ }
+
+ // In a real app, you'd have an UpdateUser method
+ // For now, show success
+ return render(w, r, userFormPage(currentUser, user, &f, "User updated successfully"))
+ }
+
+ // GET - show form
+ form := &userForm{
+ Username: user.Username,
+ Email: user.Email,
+ Role: user.Role,
+ }
+
+ return render(w, r, userFormPage(currentUser, user, form, ""))
+}
+
+func userFormPage(currentUser *User, user *User, form *userForm, message string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var13 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var13 == nil {
+ templ_7745c5c3_Var13 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if message != "" {
+ templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout(ternary(user == nil, "New User", "Edit User"), currentUser).Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Delete user handler
+type adminUserDeletePage struct{}
+
+func (u adminUserDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ currentUser := auth.GetUser(r)
+ if currentUser.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ userID, _ := strconv.Atoi(r.PathValue("id"))
+
+ // Can't delete yourself
+ if userID == currentUser.ID {
+ return fmt.Errorf("cannot delete yourself")
+ }
+
+ // In a real app, you'd have a DeleteUser method
+ // For now, return success
+ w.WriteHeader(http.StatusOK)
+ return nil
+}
+
+// Admin Settings
+type adminSettingsPage struct{}
+
+type settingsForm struct {
+ SiteName string `form:"site_name"`
+ SiteDescription string `form:"site_description"`
+ AdminEmail string `form:"admin_email"`
+}
+
+func (s adminSettingsPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, config *Config, decoder *form.Decoder) error {
+ user := auth.GetUser(r)
+ if user.Role != "admin" {
+ return fmt.Errorf("forbidden")
+ }
+
+ if r.Method == "POST" {
+ var f settingsForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update config (in memory only for this example)
+ config.SiteName = f.SiteName
+ config.SiteDescription = f.SiteDescription
+ config.AdminEmail = f.AdminEmail
+
+ // Show success
+ return render(w, r, s.Page(user, config, "Settings updated successfully"))
+ }
+
+ // GET - show form
+ return render(w, r, s.Page(user, config, ""))
+}
+
+func (s adminSettingsPage) Page(user *User, config *Config, message string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var16 == nil {
+ templ_7745c5c3_Var16 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Settings
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if message != "" {
+ templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout("Settings", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Media Library
+type mediaLibraryPage struct{}
+
+func (m mediaLibraryPage) Props(r *http.Request, auth *AuthService) (*User, error) {
+ return auth.GetUser(r), nil
+}
+
+func (m mediaLibraryPage) Page(user *User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var19 == nil {
+ templ_7745c5c3_Var19 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var20 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = adminLayout("Media Library", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var20), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/examples/blog-admin/api_pages.templ b/examples/blog-admin/api_pages.templ
new file mode 100644
index 0000000..7799d84
--- /dev/null
+++ b/examples/blog-admin/api_pages.templ
@@ -0,0 +1,211 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/go-playground/form/v4"
+)
+
+// API endpoints demonstrating direct HTTP handler pattern
+
+// Publish post API
+type apiPostPublishPage struct{}
+
+func (p apiPostPublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Update status
+ post.Status = "published"
+ now := time.Now()
+ post.PublishedAt = &now
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return updated row for HTMX
+ w.Header().Set("Content-Type", "text/html")
+ return render(w, r, postTableRow(post))
+}
+
+// Unpublish post API
+type apiPostUnpublishPage struct{}
+
+func (u apiPostUnpublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Update status
+ post.Status = "draft"
+ post.PublishedAt = nil
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return updated row for HTMX
+ w.Header().Set("Content-Type", "text/html")
+ return render(w, r, postTableRow(post))
+}
+
+// Auto-save API
+type apiPostAutosavePage struct{}
+
+func (a apiPostAutosavePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Parse form data
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update only content fields (not status)
+ post.Title = f.Title
+ post.Content = f.Content
+ post.Excerpt = f.Excerpt
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return JSON response
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": true,
+ "saved_at": time.Now().Format("3:04 PM"),
+ })
+ return nil
+}
+
+// Media upload API
+type apiMediaUploadPage struct{}
+
+func (m apiMediaUploadPage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // This demonstrates the standard http.Handler interface (no error return)
+ // In a real app, you'd handle file uploads here
+
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, ``)
+}
+
+// Template for post table row (used by publish/unpublish APIs)
+templ postTableRow(post *Post) {
+
+ |
+
+ { post.Title }
+
+ |
+ { post.Author.Username } |
+
+
+ { post.Status }
+
+ |
+
+ if post.PublishedAt != nil {
+ { post.PublishedAt.Format("Jan 2, 2006") }
+ } else {
+ Not published
+ }
+ |
+ { fmt.Sprint(post.ViewCount) } |
+
+ Edit
+ if post.Status == "draft" {
+
+ } else {
+
+ }
+
+ |
+
+}
+
+// Advanced HTMX PageConfig example
+func (s searchPage) PageConfig(r *http.Request) (string, error) {
+ // Custom PageConfig for different HTMX targets
+ hxTarget := r.Header.Get("HX-Target")
+
+ switch hxTarget {
+ case "search-results":
+ // Return just the results component
+ return "Results", nil
+ case "search-suggestions":
+ // Could return a suggestions component
+ return "Suggestions", nil
+ default:
+ // Default to full page
+ return "Page", nil
+ }
+}
+
+// Example of Init method
+func (m mediaLibraryPage) Init() {
+ // This would be called during route parsing
+ // Could be used to set up file storage, etc.
+ fmt.Println("Initializing media library page")
+}
diff --git a/examples/blog-admin/api_pages_templ.go b/examples/blog-admin/api_pages_templ.go
new file mode 100644
index 0000000..42d6494
--- /dev/null
+++ b/examples/blog-admin/api_pages_templ.go
@@ -0,0 +1,369 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/a-h/templ"
+ templruntime "github.com/a-h/templ/runtime"
+ "github.com/go-playground/form/v4"
+)
+
+// API endpoints demonstrating direct HTTP handler pattern
+
+// Publish post API
+type apiPostPublishPage struct{}
+
+func (p apiPostPublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Update status
+ post.Status = "published"
+ now := time.Now()
+ post.PublishedAt = &now
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return updated row for HTMX
+ w.Header().Set("Content-Type", "text/html")
+ return render(w, r, postTableRow(post))
+}
+
+// Unpublish post API
+type apiPostUnpublishPage struct{}
+
+func (u apiPostUnpublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Update status
+ post.Status = "draft"
+ post.PublishedAt = nil
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return updated row for HTMX
+ w.Header().Set("Content-Type", "text/html")
+ return render(w, r, postTableRow(post))
+}
+
+// Auto-save API
+type apiPostAutosavePage struct{}
+
+func (a apiPostAutosavePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error {
+ postID, _ := strconv.Atoi(r.PathValue("id"))
+
+ post, err := store.GetPostByID(postID)
+ if err != nil {
+ return err
+ }
+
+ // Check permission
+ user := auth.GetUser(r)
+ if user.Role != "admin" && post.AuthorID != user.ID {
+ return fmt.Errorf("forbidden")
+ }
+
+ // Parse form data
+ var f postForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ // Update only content fields (not status)
+ post.Title = f.Title
+ post.Content = f.Content
+ post.Excerpt = f.Excerpt
+
+ if err := store.UpdatePost(post); err != nil {
+ return err
+ }
+
+ // Return JSON response
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": true,
+ "saved_at": time.Now().Format("3:04 PM"),
+ })
+ return nil
+}
+
+// Media upload API
+type apiMediaUploadPage struct{}
+
+func (m apiMediaUploadPage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // This demonstrates the standard http.Handler interface (no error return)
+ // In a real app, you'd handle file uploads here
+
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, ``)
+}
+
+// Template for post table row (used by publish/unpublish APIs)
+func postTableRow(post *Post) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 139, Col: 16}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 142, Col: 28}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 = []any{"status", "status-" + post.Status}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 145, Col: 17}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if post.PublishedAt != nil {
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(post.PublishedAt.Format("Jan 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 150, Col: 44}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Not published")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(post.ViewCount))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 155, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " | Edit ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if post.Status == "draft" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Advanced HTMX PageConfig example
+func (s searchPage) PageConfig(r *http.Request) (string, error) {
+ // Custom PageConfig for different HTMX targets
+ hxTarget := r.Header.Get("HX-Target")
+
+ switch hxTarget {
+ case "search-results":
+ // Return just the results component
+ return "Results", nil
+ case "search-suggestions":
+ // Could return a suggestions component
+ return "Suggestions", nil
+ default:
+ // Default to full page
+ return "Page", nil
+ }
+}
+
+// Example of Init method
+func (m mediaLibraryPage) Init() {
+ // This would be called during route parsing
+ // Could be used to set up file storage, etc.
+ fmt.Println("Initializing media library page")
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/examples/blog-admin/auth.go b/examples/blog-admin/auth.go
new file mode 100644
index 0000000..66d3997
--- /dev/null
+++ b/examples/blog-admin/auth.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/alexedwards/scs/v2"
+ "golang.org/x/crypto/bcrypt"
+)
+
+// AuthService handles authentication
+type AuthService struct {
+ store *Store
+ session *scs.SessionManager
+}
+
+func NewAuthService(store *Store, session *scs.SessionManager) *AuthService {
+ return &AuthService{
+ store: store,
+ session: session,
+ }
+}
+
+// Login authenticates a user
+func (a *AuthService) Login(r *http.Request, username, password string) (*User, error) {
+ user, err := a.store.GetUserByUsername(username)
+ if err != nil {
+ return nil, fmt.Errorf("invalid credentials")
+ }
+
+ // Check password
+ err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
+ if err != nil {
+ return nil, fmt.Errorf("invalid credentials")
+ }
+
+ // Store user ID in session
+ a.session.Put(r.Context(), "userID", user.ID)
+
+ return user, nil
+}
+
+// Logout logs out the current user
+func (a *AuthService) Logout(r *http.Request) {
+ a.session.Remove(r.Context(), "userID")
+}
+
+// GetUser returns the currently logged in user
+func (a *AuthService) GetUser(r *http.Request) *User {
+ userID := a.session.GetInt(r.Context(), "userID")
+ if userID == 0 {
+ return nil
+ }
+
+ user, err := a.store.GetUserByID(userID)
+ if err != nil {
+ return nil
+ }
+
+ return user
+}
+
+// IsAuthenticated checks if a user is logged in
+func (a *AuthService) IsAuthenticated(r *http.Request) bool {
+ return a.GetUser(r) != nil
+}
+
+// IsAdmin checks if the current user is an admin
+func (a *AuthService) IsAdmin(r *http.Request) bool {
+ user := a.GetUser(r)
+ return user != nil && user.Role == "admin"
+}
+
+// IsAuthor checks if the current user is an author or admin
+func (a *AuthService) IsAuthor(r *http.Request) bool {
+ user := a.GetUser(r)
+ return user != nil && (user.Role == "author" || user.Role == "admin")
+}
+
+// HashPassword creates a bcrypt hash of the password
+func HashPassword(password string) (string, error) {
+ bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ return string(bytes), err
+}
+
+// Context keys for passing auth data
+type contextKey string
+
+const (
+ userContextKey contextKey = "user"
+)
+
+// WithUser adds user to context
+func WithUser(ctx context.Context, user *User) context.Context {
+ return context.WithValue(ctx, userContextKey, user)
+}
+
+// UserFromContext retrieves user from context
+func UserFromContext(ctx context.Context) *User {
+ user, ok := ctx.Value(userContextKey).(*User)
+ if !ok {
+ return nil
+ }
+ return user
+}
diff --git a/examples/blog-admin/components.templ b/examples/blog-admin/components.templ
new file mode 100644
index 0000000..e9caa51
--- /dev/null
+++ b/examples/blog-admin/components.templ
@@ -0,0 +1,272 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "github.com/jackielii/structpages"
+)
+
+// URL generation helpers
+func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) {
+ url, err := structpages.URLFor(ctx, page, args...)
+ return templ.URL(url), err
+}
+
+func join(page any, pattern string) string {
+ // Simplified join for query parameters
+ return fmt.Sprintf("%T%s", page, pattern)
+}
+
+// Layout components
+templ layout(title string, user *User) {
+
+
+
+
+
+ { title }
+
+
+
+
+
+ @navbar(user)
+
+ { children... }
+
+ @footer()
+
+
+}
+
+templ adminLayout(title string, user *User) {
+
+
+
+
+
+ { title } - Admin
+
+
+
+
+
+ @adminNavbar(user)
+
+ @adminSidebar()
+
+ { children... }
+
+
+
+
+}
+
+// Navigation components
+templ navbar(user *User) {
+
+}
+
+templ adminNavbar(user *User) {
+
+}
+
+templ adminSidebar() {
+
+}
+
+templ footer() {
+
+}
+
+// UI Components
+templ card(title string) {
+
+ if title != "" {
+
+ }
+
+ { children... }
+
+
+}
+
+templ alert(variant string, message string) {
+
+ { message }
+
+}
+
+templ formField(label, name, inputType string, value string, errors []string) {
+
+
+ 0) }
+ />
+ for _, err := range errors {
+ { err }
+ }
+
+}
+
+templ textareaField(label, name string, value string, rows int, errors []string) {
+
+
+
+ for _, err := range errors {
+ { err }
+ }
+
+}
+
+templ selectField(label, name string, value string, options map[string]string, errors []string) {
+
+
+
+ for _, err := range errors {
+ { err }
+ }
+
+}
+
+templ pagination(currentPage, totalPages int, baseURL templ.SafeURL) {
+ if totalPages > 1 {
+
+ }
+}
+
+templ statsCard(title, value, change string) {
+
+
{ title }
+
{ value }
+ if change != "" {
+
{ change }
+ }
+
+}
+
+templ dataTable(headers []string) {
+
+
+
+ for _, header := range headers {
+ | { header } |
+ }
+
+
+
+ { children... }
+
+
+}
+
+templ confirmModal(id, title, message, action string) {
+
+
+
+
{ title }
+
{ message }
+
+
+
+
+
+
+}
+
+// Loading states
+templ loading() {
+
+}
+
+templ htmxIndicator() {
+
+}
diff --git a/examples/blog-admin/components_templ.go b/examples/blog-admin/components_templ.go
new file mode 100644
index 0000000..ad43d2e
--- /dev/null
+++ b/examples/blog-admin/components_templ.go
@@ -0,0 +1,1532 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/a-h/templ"
+ templruntime "github.com/a-h/templ/runtime"
+ "github.com/jackielii/structpages"
+)
+
+// URL generation helpers
+func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) {
+ url, err := structpages.URLFor(ctx, page, args...)
+ return templ.URL(url), err
+}
+
+func join(page any, pattern string) string {
+ // Simplified join for query parameters
+ return fmt.Sprintf("%T%s", page, pattern)
+}
+
+// Layout components
+func layout(title string, user *User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 28, Col: 17}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = navbar(user).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func adminLayout(title string, user *User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var3 == nil {
+ templ_7745c5c3_Var3 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 49, Col: 17}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " - Admin")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = adminNavbar(user).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = adminSidebar().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Navigation components
+func navbar(user *User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func adminNavbar(user *User) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var14 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var14 == nil {
+ templ_7745c5c3_Var14 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func adminSidebar() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var19 == nil {
+ templ_7745c5c3_Var19 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func footer() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var25 == nil {
+ templ_7745c5c3_Var25 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// UI Components
+func card(title string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var26 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var26 == nil {
+ templ_7745c5c3_Var26 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if title != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ_7745c5c3_Var26.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func alert(variant string, message string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var28 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var28 == nil {
+ templ_7745c5c3_Var28 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ var templ_7745c5c3_Var29 = []any{"alert", "alert-" + variant}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var31 string
+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(message)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 144, Col: 11}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func formField(label, name, inputType string, value string, errors []string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var32 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var32 == nil {
+ templ_7745c5c3_Var32 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var35 = []any{templ.KV("error", len(errors) > 0)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var35...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, err := range errors {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var41 string
+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(err)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 159, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func textareaField(label, name string, value string, rows int, errors []string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var42 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var42 == nil {
+ templ_7745c5c3_Var42 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var45 = []any{templ.KV("error", len(errors) > 0)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var45...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, err := range errors {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var51 string
+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(err)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 174, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func selectField(label, name string, value string, options map[string]string, errors []string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var52 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var52 == nil {
+ templ_7745c5c3_Var52 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var55 = []any{templ.KV("error", len(errors) > 0)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var55...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, err := range errors {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var61 string
+ templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(err)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 192, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func pagination(currentPage, totalPages int, baseURL templ.SafeURL) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var62 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var62 == nil {
+ templ_7745c5c3_Var62 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ if totalPages > 1 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+}
+
+func statsCard(title, value, change string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var68 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var68 == nil {
+ templ_7745c5c3_Var68 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var69 string
+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 221, Col: 13}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var70 string
+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(value)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 222, Col: 28}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if change != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var71 string
+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(change)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 224, Col: 31}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func dataTable(headers []string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var72 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var72 == nil {
+ templ_7745c5c3_Var72 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, header := range headers {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var73 string
+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(header)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 234, Col: 17}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ_7745c5c3_Var72.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func confirmModal(id, title, message, action string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var74 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var74 == nil {
+ templ_7745c5c3_Var74 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var76 string
+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 248, Col: 14}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var77 string
+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(message)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 249, Col: 15}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Loading states
+func loading() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var80 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var80 == nil {
+ templ_7745c5c3_Var80 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func htmxIndicator() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var81 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var81 == nil {
+ templ_7745c5c3_Var81 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/examples/blog-admin/errors.go b/examples/blog-admin/errors.go
new file mode 100644
index 0000000..bbd4e9e
--- /dev/null
+++ b/examples/blog-admin/errors.go
@@ -0,0 +1,18 @@
+package main
+
+import "fmt"
+
+// HTTPError represents an HTTP error with a status code
+type HTTPError struct {
+ Code int
+ Message string
+}
+
+func (e HTTPError) Error() string {
+ return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
+}
+
+// NewHTTPError creates a new HTTP error
+func NewHTTPError(code int, message string) HTTPError {
+ return HTTPError{Code: code, Message: message}
+}
diff --git a/examples/blog-admin/go.mod b/examples/blog-admin/go.mod
new file mode 100644
index 0000000..4835737
--- /dev/null
+++ b/examples/blog-admin/go.mod
@@ -0,0 +1,20 @@
+module github.com/jackielii/structpages/examples/blog-admin
+
+go 1.24.0
+
+toolchain go1.24.3
+
+replace github.com/jackielii/structpages => ../..
+
+require (
+ github.com/alexedwards/scs/v2 v2.8.0
+ github.com/go-playground/form/v4 v4.2.1
+ github.com/jackielii/structpages v0.0.0-00010101000000-000000000000
+ github.com/mattn/go-sqlite3 v1.14.24
+ golang.org/x/crypto v0.31.0
+)
+
+require (
+ github.com/a-h/templ v0.3.898 // indirect
+ github.com/jackielii/ctxkey v1.0.1 // indirect
+)
diff --git a/examples/blog-admin/go.sum b/examples/blog-admin/go.sum
new file mode 100644
index 0000000..64101a6
--- /dev/null
+++ b/examples/blog-admin/go.sum
@@ -0,0 +1,16 @@
+github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
+github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
+github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
+github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
+github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg=
+github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
diff --git a/examples/blog-admin/main.go b/examples/blog-admin/main.go
new file mode 100644
index 0000000..3837cc4
--- /dev/null
+++ b/examples/blog-admin/main.go
@@ -0,0 +1,196 @@
+package main
+
+import (
+ "database/sql"
+ "embed"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/alexedwards/scs/v2"
+ "github.com/alexedwards/scs/v2/memstore"
+ "github.com/go-playground/form/v4"
+ "github.com/jackielii/structpages"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+//go:embed static/*
+var staticFS embed.FS
+
+var (
+ flagPort = flag.String("port", "8080", "Port to listen on")
+ flagDB = flag.String("db", "blog.db", "Database file path")
+)
+
+func main() {
+ flag.Parse()
+
+ // Initialize database
+ db, err := initDB(*flagDB)
+ if err != nil {
+ log.Fatal("Failed to initialize database:", err)
+ }
+ defer db.Close()
+
+ // Initialize services
+ sessionManager := initSessionManager()
+ store := NewStore(db)
+ auth := NewAuthService(store, sessionManager)
+ formDecoder := form.NewDecoder()
+ formDecoder.SetTagName("form")
+ config := &Config{
+ SiteName: "Structpages Blog",
+ SiteDescription: "A powerful blog platform built with structpages",
+ AdminEmail: "admin@example.com",
+ }
+
+ // Initialize structpages
+ sp := structpages.New(
+ structpages.WithDefaultPageConfig(structpages.HTMXPageConfig),
+ structpages.WithErrorHandler(customErrorHandler),
+ structpages.WithMiddlewares(
+ wrapMiddleware(sessionMiddleware(sessionManager)),
+ wrapMiddleware(loggingMiddleware),
+ ),
+ )
+
+ // Create router
+ mux := http.NewServeMux()
+ r := structpages.NewRouter(mux)
+
+ // Mount pages with dependency injection
+ if err := sp.MountPages(r, pages{}, "/", "Structpages Blog",
+ store,
+ auth,
+ sessionManager,
+ formDecoder,
+ config,
+ ); err != nil {
+ log.Fatal("Failed to mount pages:", err)
+ }
+
+ // Serve static files
+ fileServer := http.FileServer(http.FS(staticFS))
+ mux.Handle("/static/", fileServer)
+
+ // Print available routes
+ fmt.Println("\nAvailable routes:")
+ printRoutes(mux, "")
+
+ // Start server
+ addr := fmt.Sprintf(":%s", *flagPort)
+ log.Printf("Starting blog server on http://localhost%s", addr)
+ if err := http.ListenAndServe(addr, r); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func initDB(dbPath string) (*sql.DB, error) {
+ db, err := sql.Open("sqlite3", dbPath)
+ if err != nil {
+ return nil, err
+ }
+
+ // Enable foreign keys
+ if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
+ return nil, err
+ }
+
+ // Create tables
+ if err := createTables(db); err != nil {
+ return nil, err
+ }
+
+ return db, nil
+}
+
+func initSessionManager() *scs.SessionManager {
+ sessionManager := scs.New()
+ sessionManager.Store = memstore.New()
+ sessionManager.Lifetime = 24 * time.Hour
+ sessionManager.Cookie.Name = "blog_session"
+ sessionManager.Cookie.HttpOnly = true
+ sessionManager.Cookie.Persist = true
+ sessionManager.Cookie.SameSite = http.SameSiteLaxMode
+ sessionManager.Cookie.Secure = false // Set to true in production with HTTPS
+ return sessionManager
+}
+
+func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
+ log.Printf("Error handling request %s: %v", r.URL.Path, err)
+
+ // Check if it's an HTTP error
+ var httpErr HTTPError
+ if errors.As(err, &httpErr) {
+ w.WriteHeader(httpErr.Code)
+ if httpErr.Code == http.StatusNotFound {
+ fmt.Fprintf(w, "Page not found: %s", r.URL.Path)
+ } else {
+ fmt.Fprintf(w, "Error %d: %s", httpErr.Code, httpErr.Message)
+ }
+ return
+ }
+
+ // Default error response
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintf(w, "Internal server error")
+}
+
+// Middleware
+
+func sessionMiddleware(sm *scs.SessionManager) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return sm.LoadAndSave(next)
+ }
+}
+
+func loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ next.ServeHTTP(w, r)
+ log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
+ })
+}
+
+// wrapMiddleware converts a standard middleware to structpages MiddlewareFunc
+func wrapMiddleware(mw func(http.Handler) http.Handler) structpages.MiddlewareFunc {
+ return func(next http.Handler, pn *structpages.PageNode) http.Handler {
+ return mw(next)
+ }
+}
+
+// Print routes helper
+func printRoutes(mux *http.ServeMux, prefix string) {
+ // This is a simplified version - in production you'd use reflection
+ // or a more sophisticated approach to list all routes
+ routes := []string{
+ "/",
+ "/posts/{slug}",
+ "/category/{slug}",
+ "/search",
+ "/login",
+ "/logout",
+ "/admin",
+ "/admin/posts",
+ "/admin/posts/new",
+ "/admin/posts/{id}/edit",
+ "/admin/users",
+ "/admin/settings",
+ "/api/posts/{id}/publish",
+ "/static/",
+ }
+
+ for _, route := range routes {
+ fmt.Printf("%s%s\n", prefix, route)
+ }
+}
+
+// Config holds application configuration
+type Config struct {
+ SiteName string
+ SiteDescription string
+ AdminEmail string
+}
diff --git a/examples/blog-admin/models.go b/examples/blog-admin/models.go
new file mode 100644
index 0000000..bdfa1e5
--- /dev/null
+++ b/examples/blog-admin/models.go
@@ -0,0 +1,224 @@
+package main
+
+import (
+ "database/sql"
+ "time"
+)
+
+// User represents a blog user
+type User struct {
+ ID int
+ Username string
+ Email string
+ PasswordHash string
+ Role string // "admin", "author", "reader"
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// Post represents a blog post
+type Post struct {
+ ID int
+ Slug string
+ Title string
+ Content string
+ Excerpt string
+ AuthorID int
+ Author *User
+ Status string // "draft", "published"
+ PublishedAt *time.Time
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ ViewCount int
+ Tags []Tag
+ Categories []Category
+}
+
+// Category represents a blog category
+type Category struct {
+ ID int
+ Slug string
+ Name string
+ Description string
+ PostCount int
+ CreatedAt time.Time
+}
+
+// Tag represents a blog tag
+type Tag struct {
+ ID int
+ Name string
+ PostCount int
+}
+
+// Comment represents a blog comment
+type Comment struct {
+ ID int
+ PostID int
+ Post *Post
+ AuthorID *int
+ Author *User
+ Name string // For anonymous comments
+ Email string // For anonymous comments
+ Content string
+ Status string // "pending", "approved", "spam"
+ CreatedAt time.Time
+}
+
+// Media represents uploaded media files
+type Media struct {
+ ID int
+ Filename string
+ Path string
+ MimeType string
+ Size int64
+ UploadedBy int
+ UploadedAt time.Time
+ Alt string
+ Description string
+}
+
+// Analytics represents basic analytics data
+type Analytics struct {
+ Date time.Time
+ PageViews int
+ Visitors int
+ NewUsers int
+ TopPosts []PostAnalytics
+}
+
+type PostAnalytics struct {
+ PostID int
+ PostTitle string
+ Views int
+}
+
+// Database schema
+func createTables(db *sql.DB) error {
+ schema := `
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'reader',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE IF NOT EXISTS posts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug TEXT UNIQUE NOT NULL,
+ title TEXT NOT NULL,
+ content TEXT NOT NULL,
+ excerpt TEXT,
+ author_id INTEGER NOT NULL,
+ status TEXT NOT NULL DEFAULT 'draft',
+ published_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ view_count INTEGER DEFAULT 0,
+ FOREIGN KEY (author_id) REFERENCES users(id)
+ );
+
+ CREATE TABLE IF NOT EXISTS categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug TEXT UNIQUE NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS post_categories (
+ post_id INTEGER NOT NULL,
+ category_id INTEGER NOT NULL,
+ PRIMARY KEY (post_id, category_id),
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS post_tags (
+ post_id INTEGER NOT NULL,
+ tag_id INTEGER NOT NULL,
+ PRIMARY KEY (post_id, tag_id),
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS comments (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ post_id INTEGER NOT NULL,
+ author_id INTEGER,
+ name TEXT,
+ email TEXT,
+ content TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS media (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ filename TEXT NOT NULL,
+ path TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ size INTEGER NOT NULL,
+ uploaded_by INTEGER NOT NULL,
+ uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ alt TEXT,
+ description TEXT,
+ FOREIGN KEY (uploaded_by) REFERENCES users(id)
+ );
+
+ CREATE TABLE IF NOT EXISTS page_views (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ post_id INTEGER,
+ path TEXT NOT NULL,
+ ip_hash TEXT NOT NULL,
+ user_agent TEXT,
+ referrer TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
+ );
+
+ -- Create indexes
+ CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
+ CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
+ CREATE INDEX IF NOT EXISTS idx_posts_published_at ON posts(published_at);
+ CREATE INDEX IF NOT EXISTS idx_categories_slug ON categories(slug);
+ CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments(post_id);
+ CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status);
+ CREATE INDEX IF NOT EXISTS idx_page_views_created_at ON page_views(created_at);
+
+ -- Insert default admin user (password: admin123)
+ INSERT OR IGNORE INTO users (id, username, email, password_hash, role)
+ VALUES (1, 'admin', 'admin@example.com', '$2a$10$i/CMm4KCafFhALzoVAqRxO8MpyllRakjq5gwOFH4KMdeIWodOjKyS', 'admin');
+
+ -- Insert sample categories
+ INSERT OR IGNORE INTO categories (id, slug, name, description) VALUES
+ (1, 'technology', 'Technology', 'Posts about technology and programming'),
+ (2, 'design', 'Design', 'Posts about design and UX'),
+ (3, 'business', 'Business', 'Posts about business and entrepreneurship');
+
+ -- Insert sample posts
+ INSERT OR IGNORE INTO posts (id, slug, title, content, excerpt, author_id, status, published_at, view_count) VALUES
+ (1, 'welcome-to-structpages', 'Welcome to Structpages Blog', 'This is a demo blog built with the structpages framework. It demonstrates advanced features like nested routing, authentication, and HTMX integration.
', 'A demo blog showcasing structpages features', 1, 'published', CURRENT_TIMESTAMP, 42),
+ (2, 'building-with-go', 'Building Web Apps with Go', 'Go is an excellent choice for building web applications. Its simplicity and performance make it ideal for modern web development.
', 'Why Go is great for web development', 1, 'published', CURRENT_TIMESTAMP, 15),
+ (3, 'draft-post', 'Work in Progress', 'This is a draft post that is not yet published.
', 'Coming soon...', 1, 'draft', NULL, 0);
+
+ -- Link posts to categories
+ INSERT OR IGNORE INTO post_categories (post_id, category_id) VALUES
+ (1, 1), (1, 2),
+ (2, 1),
+ (3, 3);
+ `
+
+ _, err := db.Exec(schema)
+ return err
+}
diff --git a/examples/blog-admin/pages.templ b/examples/blog-admin/pages.templ
new file mode 100644
index 0000000..3c79b64
--- /dev/null
+++ b/examples/blog-admin/pages.templ
@@ -0,0 +1,475 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/go-playground/form/v4"
+)
+
+// Home page
+type homePage struct{}
+
+type homePageProps struct {
+ RecentPosts []*Post
+ PopularPosts []*Post
+ Categories []*Category
+ User *User
+}
+
+func (h homePage) Props(r *http.Request, store *Store, auth *AuthService) (homePageProps, error) {
+ // Get recent posts
+ recentPosts, _, err := store.ListPosts(PostFilter{
+ Status: "published",
+ Limit: 5,
+ Offset: 0,
+ })
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ // Get popular posts (simplified - just get posts with high view count)
+ popularPosts, _, err := store.ListPosts(PostFilter{
+ Status: "published",
+ Limit: 5,
+ Offset: 0,
+ })
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ // Get categories
+ categories, err := store.ListCategories()
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ return homePageProps{
+ RecentPosts: recentPosts,
+ PopularPosts: popularPosts,
+ Categories: categories,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+templ (h homePage) Page(props homePageProps) {
+ @layout("Home", props.User) {
+
+
+ Welcome to Structpages Blog
+ A powerful blog platform demonstrating advanced structpages features
+
+
+
+ Recent Posts
+ for _, post := range props.RecentPosts {
+ @postCard(post)
+ }
+
+
+
+
+ }
+}
+
+// Post card component
+templ postCard(post *Post) {
+
+
+
+ By { post.Author.Username }
+ { post.CreatedAt.Format("Jan 2, 2006") }
+
+ { post.Excerpt }
+ Read more →
+
+}
+
+// Single post page
+type postPage struct{}
+
+type postPageProps struct {
+ Post *Post
+ RelatedPosts []*Post
+ User *User
+}
+
+func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) {
+ slug := r.PathValue("slug")
+
+ post, err := store.GetPostBySlug(slug)
+ if err != nil {
+ return postPageProps{}, err
+ }
+
+ // Only show published posts to non-authors
+ user := auth.GetUser(r)
+ if post.Status != "published" && (user == nil || (user.ID != post.AuthorID && user.Role != "admin")) {
+ return postPageProps{}, fmt.Errorf("post not found")
+ }
+
+ // Increment view count
+ go store.IncrementPostViews(post.ID)
+
+ // Get related posts (simplified - just get posts from same category)
+ var relatedPosts []*Post
+ if len(post.Categories) > 0 {
+ related, _, _ := store.ListPosts(PostFilter{
+ Status: "published",
+ CategoryID: post.Categories[0].ID,
+ Limit: 3,
+ Offset: 0,
+ })
+ for _, p := range related {
+ if p.ID != post.ID {
+ relatedPosts = append(relatedPosts, p)
+ }
+ }
+ }
+
+ return postPageProps{
+ Post: post,
+ RelatedPosts: relatedPosts,
+ User: user,
+ }, nil
+}
+
+templ (p postPage) Page(props postPageProps) {
+ @layout(props.Post.Title, props.User) {
+
+
+ { props.Post.Title }
+
+ By { props.Post.Author.Username }
+ { props.Post.CreatedAt.Format("January 2, 2006") }
+ { fmt.Sprintf("%d views", props.Post.ViewCount) }
+
+ if props.Post.Status == "draft" {
+ @alert("warning", "This post is a draft and not yet published")
+ }
+
+
+ @templ.Raw(props.Post.Content)
+
+
+ if len(props.RelatedPosts) > 0 {
+
+ Related Posts
+
+ for _, post := range props.RelatedPosts {
+ @postCard(post)
+ }
+
+
+ }
+
+ }
+}
+
+// Category page
+type categoryPage struct{}
+
+type categoryPageProps struct {
+ Category *Category
+ Posts []*Post
+ Total int
+ Page int
+ User *User
+}
+
+func (c categoryPage) Props(r *http.Request, store *Store, auth *AuthService) (categoryPageProps, error) {
+ slug := r.PathValue("slug")
+
+ category, err := store.GetCategoryBySlug(slug)
+ if err != nil {
+ return categoryPageProps{}, err
+ }
+
+ // Get page number from query
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ limit := 10
+ offset := (page - 1) * limit
+
+ posts, total, err := store.ListPosts(PostFilter{
+ Status: "published",
+ CategoryID: category.ID,
+ Limit: limit,
+ Offset: offset,
+ })
+ if err != nil {
+ return categoryPageProps{}, err
+ }
+
+ return categoryPageProps{
+ Category: category,
+ Posts: posts,
+ Total: total,
+ Page: page,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+templ (c categoryPage) Page(props categoryPageProps) {
+ @layout(props.Category.Name, props.User) {
+
+
+
+ for _, post := range props.Posts {
+ @postCard(post)
+ }
+
+ if url, err := urlFor(ctx, categoryPage{}, props.Category.Slug); err == nil {
+ @pagination(props.Page, (props.Total+9)/10, url)
+ }
+
+ }
+}
+
+// Search page
+type searchPage struct{}
+
+type searchPageProps struct {
+ Query string
+ Results []*Post
+ Total int
+ Page int
+ User *User
+}
+
+func (s searchPage) Props(r *http.Request, store *Store, auth *AuthService) (searchPageProps, error) {
+ query := r.URL.Query().Get("q")
+
+ // Get page number from query
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ var results []*Post
+ var total int
+
+ if query != "" {
+ limit := 10
+ offset := (page - 1) * limit
+
+ var err error
+ results, total, err = store.ListPosts(PostFilter{
+ Status: "published",
+ Search: query,
+ Limit: limit,
+ Offset: offset,
+ })
+ if err != nil {
+ return searchPageProps{}, err
+ }
+ }
+
+ return searchPageProps{
+ Query: query,
+ Results: results,
+ Total: total,
+ Page: page,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+templ (s searchPage) Page(props searchPageProps) {
+ @layout("Search", props.User) {
+
+
Search
+
+ if props.Query != "" {
+
+
+ { fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query) }
+
+ if len(props.Results) > 0 {
+
+ for _, post := range props.Results {
+ @postCard(post)
+ }
+
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil {
+ @pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query)))
+ }
+ } else {
+
No posts found matching your search.
+ }
+
+ }
+
+ }
+}
+
+// HTMX partial rendering for search
+templ (s searchPage) Results(props searchPageProps) {
+
+
+ { fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query) }
+
+ if len(props.Results) > 0 {
+
+ for _, post := range props.Results {
+ @postCard(post)
+ }
+
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil {
+ @pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query)))
+ }
+ } else {
+
No posts found matching your search.
+ }
+
+}
+
+// Login page
+type loginPage struct{}
+
+type loginForm struct {
+ Username string `form:"username"`
+ Password string `form:"password"`
+ Redirect string `form:"redirect"`
+}
+
+func (l loginPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, decoder *form.Decoder) error {
+ // Already logged in?
+ if auth.IsAuthenticated(r) {
+ http.Redirect(w, r, "/admin", http.StatusSeeOther)
+ return nil
+ }
+
+ if r.Method == "POST" {
+ var f loginForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ _, err := auth.Login(r, f.Username, f.Password)
+ if err != nil {
+ // Show error
+ return render(w, r, l.Page(auth.GetUser(r), &f, err.Error()))
+ }
+
+ // Redirect to admin or requested page
+ redirect := f.Redirect
+ if redirect == "" {
+ redirect = "/admin"
+ }
+ http.Redirect(w, r, redirect, http.StatusSeeOther)
+ return nil
+ }
+
+ // GET request - show form
+ redirect := r.URL.Query().Get("redirect")
+ return render(w, r, l.Page(auth.GetUser(r), &loginForm{Redirect: redirect}, ""))
+}
+
+templ (l loginPage) Page(user *User, form *loginForm, errorMsg string) {
+ @layout("Login", user) {
+
+ }
+}
+
+// Logout page
+type logoutPage struct{}
+
+func (l logoutPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService) error {
+ auth.Logout(r)
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ return nil
+}
diff --git a/examples/blog-admin/pages_templ.go b/examples/blog-admin/pages_templ.go
new file mode 100644
index 0000000..60711e7
--- /dev/null
+++ b/examples/blog-admin/pages_templ.go
@@ -0,0 +1,1182 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/a-h/templ"
+ templruntime "github.com/a-h/templ/runtime"
+ "github.com/go-playground/form/v4"
+)
+
+// Home page
+type homePage struct{}
+
+type homePageProps struct {
+ RecentPosts []*Post
+ PopularPosts []*Post
+ Categories []*Category
+ User *User
+}
+
+func (h homePage) Props(r *http.Request, store *Store, auth *AuthService) (homePageProps, error) {
+ // Get recent posts
+ recentPosts, _, err := store.ListPosts(PostFilter{
+ Status: "published",
+ Limit: 5,
+ Offset: 0,
+ })
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ // Get popular posts (simplified - just get posts with high view count)
+ popularPosts, _, err := store.ListPosts(PostFilter{
+ Status: "published",
+ Limit: 5,
+ Offset: 0,
+ })
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ // Get categories
+ categories, err := store.ListCategories()
+ if err != nil {
+ return homePageProps{}, err
+ }
+
+ return homePageProps{
+ RecentPosts: recentPosts,
+ PopularPosts: popularPosts,
+ Categories: categories,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+func (h homePage) Page(props homePageProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Welcome to Structpages Blog
A powerful blog platform demonstrating advanced structpages features
Recent Posts
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range props.RecentPosts {
+ templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = layout("Home", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Post card component
+func postCard(post *Post) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var9 == nil {
+ templ_7745c5c3_Var9 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "By ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 113, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var13 string
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("Jan 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 114, Col: 47}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var14 string
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(post.Excerpt)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 116, Col: 19}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Read more →")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Single post page
+type postPage struct{}
+
+type postPageProps struct {
+ Post *Post
+ RelatedPosts []*Post
+ User *User
+}
+
+func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) {
+ slug := r.PathValue("slug")
+
+ post, err := store.GetPostBySlug(slug)
+ if err != nil {
+ return postPageProps{}, err
+ }
+
+ // Only show published posts to non-authors
+ user := auth.GetUser(r)
+ if post.Status != "published" && (user == nil || (user.ID != post.AuthorID && user.Role != "admin")) {
+ return postPageProps{}, fmt.Errorf("post not found")
+ }
+
+ // Increment view count
+ go store.IncrementPostViews(post.ID)
+
+ // Get related posts (simplified - just get posts from same category)
+ var relatedPosts []*Post
+ if len(post.Categories) > 0 {
+ related, _, _ := store.ListPosts(PostFilter{
+ Status: "published",
+ CategoryID: post.Categories[0].ID,
+ Limit: 3,
+ Offset: 0,
+ })
+ for _, p := range related {
+ if p.ID != post.ID {
+ relatedPosts = append(relatedPosts, p)
+ }
+ }
+ }
+
+ return postPageProps{
+ Post: post,
+ RelatedPosts: relatedPosts,
+ User: user,
+ }, nil
+}
+
+func (p postPage) Page(props postPageProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var16 == nil {
+ templ_7745c5c3_Var16 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var18 string
+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 174, Col: 26}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
By ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var19 string
+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.Author.Username)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 176, Col: 42}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var20 string
+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.CreatedAt.Format("January 2, 2006"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 177, Col: 59}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var21 string
+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d views", props.Post.ViewCount))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 178, Col: 58}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if props.Post.Status == "draft" {
+ templ_7745c5c3_Err = alert("warning", "This post is a draft and not yet published").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ.Raw(props.Post.Content).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(props.RelatedPosts) > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "Related Posts
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range props.RelatedPosts {
+ templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = layout(props.Post.Title, props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Category page
+type categoryPage struct{}
+
+type categoryPageProps struct {
+ Category *Category
+ Posts []*Post
+ Total int
+ Page int
+ User *User
+}
+
+func (c categoryPage) Props(r *http.Request, store *Store, auth *AuthService) (categoryPageProps, error) {
+ slug := r.PathValue("slug")
+
+ category, err := store.GetCategoryBySlug(slug)
+ if err != nil {
+ return categoryPageProps{}, err
+ }
+
+ // Get page number from query
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ limit := 10
+ offset := (page - 1) * limit
+
+ posts, total, err := store.ListPosts(PostFilter{
+ Status: "published",
+ CategoryID: category.ID,
+ Limit: limit,
+ Offset: offset,
+ })
+ if err != nil {
+ return categoryPageProps{}, err
+ }
+
+ return categoryPageProps{
+ Category: category,
+ Posts: posts,
+ Total: total,
+ Page: page,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+func (c categoryPage) Page(props categoryPageProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var25 == nil {
+ templ_7745c5c3_Var25 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range props.Posts {
+ templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if url, err := urlFor(ctx, categoryPage{}, props.Category.Slug); err == nil {
+ templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, url).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = layout(props.Category.Name, props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Search page
+type searchPage struct{}
+
+type searchPageProps struct {
+ Query string
+ Results []*Post
+ Total int
+ Page int
+ User *User
+}
+
+func (s searchPage) Props(r *http.Request, store *Store, auth *AuthService) (searchPageProps, error) {
+ query := r.URL.Query().Get("q")
+
+ // Get page number from query
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
+ page = parsed
+ }
+ }
+
+ var results []*Post
+ var total int
+
+ if query != "" {
+ limit := 10
+ offset := (page - 1) * limit
+
+ var err error
+ results, total, err = store.ListPosts(PostFilter{
+ Status: "published",
+ Search: query,
+ Limit: limit,
+ Offset: offset,
+ })
+ if err != nil {
+ return searchPageProps{}, err
+ }
+ }
+
+ return searchPageProps{
+ Query: query,
+ Results: results,
+ Total: total,
+ Page: page,
+ User: auth.GetUser(r),
+ }, nil
+}
+
+func (s searchPage) Page(props searchPageProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var30 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var30 == nil {
+ templ_7745c5c3_Var30 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var31 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "Search
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if props.Query != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var34 string
+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 371, Col: 76}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(props.Results) > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range props.Results {
+ templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil {
+ templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
No posts found matching your search.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = layout("Search", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var31), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// HTMX partial rendering for search
+func (s searchPage) Results(props searchPageProps) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var35 == nil {
+ templ_7745c5c3_Var35 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var37 string
+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 397, Col: 73}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(props.Results) > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range props.Results {
+ templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil {
+ templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
No posts found matching your search.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Login page
+type loginPage struct{}
+
+type loginForm struct {
+ Username string `form:"username"`
+ Password string `form:"password"`
+ Redirect string `form:"redirect"`
+}
+
+func (l loginPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, decoder *form.Decoder) error {
+ // Already logged in?
+ if auth.IsAuthenticated(r) {
+ http.Redirect(w, r, "/admin", http.StatusSeeOther)
+ return nil
+ }
+
+ if r.Method == "POST" {
+ var f loginForm
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ if err := decoder.Decode(&f, r.PostForm); err != nil {
+ return err
+ }
+
+ _, err := auth.Login(r, f.Username, f.Password)
+ if err != nil {
+ // Show error
+ return render(w, r, l.Page(auth.GetUser(r), &f, err.Error()))
+ }
+
+ // Redirect to admin or requested page
+ redirect := f.Redirect
+ if redirect == "" {
+ redirect = "/admin"
+ }
+ http.Redirect(w, r, redirect, http.StatusSeeOther)
+ return nil
+ }
+
+ // GET request - show form
+ redirect := r.URL.Query().Get("redirect")
+ return render(w, r, l.Page(auth.GetUser(r), &loginForm{Redirect: redirect}, ""))
+}
+
+func (l loginPage) Page(user *User, form *loginForm, errorMsg string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var38 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var38 == nil {
+ templ_7745c5c3_Var38 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+ templ_7745c5c3_Err = layout("Login", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Logout page
+type logoutPage struct{}
+
+func (l logoutPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService) error {
+ auth.Logout(r)
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ return nil
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/examples/blog-admin/routes.go b/examples/blog-admin/routes.go
new file mode 100644
index 0000000..12d35e9
--- /dev/null
+++ b/examples/blog-admin/routes.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+ "net/http"
+
+ "github.com/jackielii/structpages"
+)
+
+// Root pages structure demonstrating nested routing
+type pages struct {
+ // Public pages
+ homePage `route:"/{$} Home"`
+ postPage `route:"/posts/{slug} "`
+ categoryPage `route:"/category/{slug} "`
+ searchPage `route:"/search Search"`
+
+ // Auth pages
+ loginPage `route:"/login Login"`
+ logoutPage `route:"POST /logout"`
+
+ // Admin section with nested routes
+ admin adminPages `route:"/admin Admin"`
+
+ // API endpoints
+ api apiPages `route:"/api"`
+}
+
+// Admin pages demonstrating nested structure
+type adminPages struct {
+ dashboard `route:"/{$} Dashboard"`
+ posts adminPostPages `route:"/posts Posts"`
+ users adminUserPages `route:"/users Users"`
+ settings adminSettingsPage `route:"/settings Settings"`
+ mediaLibrary mediaLibraryPage `route:"/media Media Library"`
+}
+
+// Admin post management pages
+type adminPostPages struct {
+ list adminPostListPage `route:"/{$} All Posts"`
+ new adminPostNewPage `route:"/new New Post"`
+ edit adminPostEditPage `route:"/{id}/edit"`
+ delete adminPostDeletePage `route:"POST /{id}/delete"`
+}
+
+// Admin user management pages
+type adminUserPages struct {
+ list adminUserListPage `route:"/{$} All Users"`
+ new adminUserNewPage `route:"/new New User"`
+ edit adminUserEditPage `route:"/{id}/edit"`
+ delete adminUserDeletePage `route:"POST /{id}/delete"`
+}
+
+// API pages for programmatic access
+type apiPages struct {
+ posts apiPostPages `route:"/posts"`
+ media apiMediaPages `route:"/media"`
+}
+
+type apiPostPages struct {
+ publish apiPostPublishPage `route:"POST /{id}/publish"`
+ unpublish apiPostUnpublishPage `route:"POST /{id}/unpublish"`
+ autosave apiPostAutosavePage `route:"POST /{id}/autosave"`
+}
+
+type apiMediaPages struct {
+ upload apiMediaUploadPage `route:"POST /upload"`
+}
+
+// Middleware implementations
+
+// adminPages implements middleware for authentication
+func (a adminPages) Middlewares(auth *AuthService) []structpages.MiddlewareFunc {
+ return []structpages.MiddlewareFunc{
+ requireAuthMiddleware(auth),
+ requireAdminMiddleware(auth),
+ }
+}
+
+// adminPostPages can have additional middleware
+func (p adminPostPages) Middlewares() []structpages.MiddlewareFunc {
+ return []structpages.MiddlewareFunc{
+ csrfProtectionMiddleware,
+ }
+}
+
+// Helper middleware functions
+func requireAuthMiddleware(auth *AuthService) structpages.MiddlewareFunc {
+ return func(next http.Handler, pn *structpages.PageNode) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ user := auth.GetUser(r)
+ if user == nil {
+ http.Redirect(w, r, "/login?redirect="+r.URL.Path, http.StatusSeeOther)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func requireAdminMiddleware(auth *AuthService) structpages.MiddlewareFunc {
+ return func(next http.Handler, pn *structpages.PageNode) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ user := auth.GetUser(r)
+ if user == nil || user.Role != "admin" {
+ http.Error(w, "Forbidden", http.StatusForbidden)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func csrfProtectionMiddleware(next http.Handler, pn *structpages.PageNode) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Simplified CSRF check - in production use a proper CSRF library
+ if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
+ token := r.FormValue("csrf_token")
+ if token == "" {
+ token = r.Header.Get("X-CSRF-Token")
+ }
+ // Validate token here
+ }
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/examples/blog-admin/static/admin.css b/examples/blog-admin/static/admin.css
new file mode 100644
index 0000000..e62fd42
--- /dev/null
+++ b/examples/blog-admin/static/admin.css
@@ -0,0 +1,431 @@
+/* Admin styles */
+body.admin {
+ background: #f5f7fa;
+}
+
+/* Admin navbar */
+.admin-navbar {
+ background: #2c3e50;
+ color: white;
+ padding: 1rem 0;
+}
+
+.admin-navbar .container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.admin-navbar .logo {
+ color: white;
+ font-size: 1.25rem;
+ font-weight: bold;
+}
+
+.admin-navbar .nav-right {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.admin-navbar a {
+ color: white;
+}
+
+.admin-navbar button {
+ background: transparent;
+ border: 1px solid white;
+ color: white;
+ padding: 0.25rem 0.75rem;
+}
+
+/* Admin layout */
+.admin-container {
+ display: grid;
+ grid-template-columns: 250px 1fr;
+ min-height: calc(100vh - 60px);
+}
+
+/* Admin sidebar */
+.admin-sidebar {
+ background: white;
+ border-right: 1px solid #e1e4e8;
+}
+
+.admin-sidebar ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.admin-sidebar a {
+ display: block;
+ padding: 0.75rem 1.5rem;
+ color: #333;
+ border-bottom: 1px solid #f0f0f0;
+ transition: background 0.2s;
+}
+
+.admin-sidebar a:hover {
+ background: #f8f9fa;
+ text-decoration: none;
+}
+
+.admin-sidebar a.active {
+ background: #e9ecef;
+ font-weight: 500;
+}
+
+/* Admin main */
+.admin-main {
+ padding: 2rem;
+}
+
+.admin-main h1 {
+ margin: 0 0 2rem;
+}
+
+/* Page header */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ margin: 0;
+}
+
+/* Stats grid */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+/* Stats card */
+.stats-card {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.stats-card h4 {
+ margin: 0 0 0.5rem;
+ color: #666;
+ font-weight: normal;
+}
+
+.stats-card .value {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #333;
+}
+
+.stats-card .change {
+ color: #28a745;
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+}
+
+/* Dashboard grid */
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+}
+
+.dashboard-grid section {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.dashboard-grid h2 {
+ margin: 0 0 1rem;
+ font-size: 1.25rem;
+}
+
+/* Data table */
+.data-table {
+ width: 100%;
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.data-table th {
+ background: #f8f9fa;
+ padding: 0.75rem;
+ text-align: left;
+ font-weight: 500;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.data-table td {
+ padding: 0.75rem;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.data-table tr:last-child td {
+ border-bottom: none;
+}
+
+.data-table .actions {
+ white-space: nowrap;
+}
+
+.data-table .actions a,
+.data-table .actions button {
+ margin-right: 0.5rem;
+ font-size: 0.875rem;
+ padding: 0.25rem 0.5rem;
+}
+
+/* Status badges */
+.status {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.status-draft {
+ background: #ffeeba;
+ color: #856404;
+}
+
+.status-published {
+ background: #d4edda;
+ color: #155724;
+}
+
+/* Role badges */
+.role {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+}
+
+.role-admin {
+ background: #d1ecf1;
+ color: #0c5460;
+}
+
+.role-author {
+ background: #e2e3e5;
+ color: #383d41;
+}
+
+.role-reader {
+ background: #f8f9fa;
+ color: #6c757d;
+}
+
+/* Filters */
+.filters {
+ background: white;
+ padding: 1rem;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.filter-form {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-end;
+}
+
+.filter-form input,
+.filter-form select {
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+
+/* Post form */
+.post-form {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: 1fr 300px;
+ gap: 2rem;
+}
+
+.form-grid .sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.form-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.checkbox {
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.checkbox input {
+ margin-right: 0.5rem;
+ width: auto;
+}
+
+/* User form */
+.user-form,
+.settings-form {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ max-width: 600px;
+}
+
+/* Media grid */
+.media-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+}
+
+.media-item {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ text-align: center;
+}
+
+.media-item img {
+ width: 100%;
+ height: 150px;
+ object-fit: cover;
+}
+
+.media-item span {
+ display: block;
+ padding: 0.5rem;
+ font-size: 0.875rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+/* Top posts list */
+.top-posts {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.top-posts li {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.top-posts li:last-child {
+ border-bottom: none;
+}
+
+/* Card component */
+.card {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+.card-header {
+ background: #f8f9fa;
+ padding: 1rem;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.card-header h3 {
+ margin: 0;
+ font-size: 1.125rem;
+}
+
+.card-body {
+ padding: 1rem;
+}
+
+/* Modal */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.5);
+}
+
+.modal-content {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ position: relative;
+ max-width: 500px;
+ width: 90%;
+}
+
+.modal-content h3 {
+ margin: 0 0 1rem;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+ margin-top: 1.5rem;
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+ .admin-container {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-sidebar {
+ display: none;
+ }
+
+ .form-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .dashboard-grid {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/examples/blog-admin/static/styles.css b/examples/blog-admin/static/styles.css
new file mode 100644
index 0000000..45905ea
--- /dev/null
+++ b/examples/blog-admin/static/styles.css
@@ -0,0 +1,416 @@
+/* Base styles */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ background: #f5f5f5;
+}
+
+a {
+ color: #0066cc;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* Container */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+/* Navbar */
+.navbar {
+ background: white;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ padding: 1rem 0;
+ margin-bottom: 2rem;
+}
+
+.navbar .container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.navbar .logo {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #333;
+}
+
+.nav-links {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+}
+
+.nav-links form {
+ margin: 0;
+}
+
+/* Hero section */
+.hero {
+ text-align: center;
+ padding: 3rem 0;
+ background: white;
+ border-radius: 8px;
+ margin-bottom: 2rem;
+}
+
+.hero h1 {
+ margin: 0 0 0.5rem;
+ font-size: 2.5rem;
+}
+
+.hero p {
+ margin: 0;
+ color: #666;
+ font-size: 1.25rem;
+}
+
+/* Grid layouts */
+.home-grid {
+ display: grid;
+ grid-template-columns: 1fr 300px;
+ gap: 2rem;
+}
+
+.posts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+/* Post card */
+.post-card {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.post-card h3 {
+ margin: 0 0 0.5rem;
+}
+
+.post-card h3 a {
+ color: #333;
+}
+
+.post-meta {
+ color: #666;
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+}
+
+.post-meta span:not(:last-child)::after {
+ content: " • ";
+}
+
+.read-more {
+ color: #0066cc;
+ font-weight: 500;
+}
+
+/* Sidebar */
+.sidebar section {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.sidebar h3 {
+ margin: 0 0 1rem;
+ font-size: 1.25rem;
+}
+
+.sidebar ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.sidebar li {
+ padding: 0.5rem 0;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.sidebar li:last-child {
+ border-bottom: none;
+}
+
+.views, .count {
+ color: #666;
+ font-size: 0.875rem;
+}
+
+/* Single post */
+.post-single {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.post-single header {
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #eee;
+}
+
+.post-single h1 {
+ margin: 0 0 1rem;
+}
+
+.post-content {
+ font-size: 1.125rem;
+ line-height: 1.8;
+ margin-bottom: 2rem;
+}
+
+.post-footer {
+ padding-top: 1rem;
+ border-top: 1px solid #eee;
+ color: #666;
+}
+
+.post-footer > div {
+ margin-bottom: 0.5rem;
+}
+
+.tag {
+ background: #f0f0f0;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+}
+
+/* Forms */
+.form-field {
+ margin-bottom: 1.5rem;
+}
+
+.form-field label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+}
+
+.form-field input,
+.form-field textarea,
+.form-field select {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+.form-field input.error,
+.form-field textarea.error,
+.form-field select.error {
+ border-color: #dc3545;
+}
+
+.error-text {
+ color: #dc3545;
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+ display: block;
+}
+
+/* Buttons */
+.btn, button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+ text-decoration: none;
+ display: inline-block;
+ background: #f0f0f0;
+ color: #333;
+}
+
+.btn.primary, button[type="submit"] {
+ background: #0066cc;
+ color: white;
+}
+
+.btn.primary:hover, button[type="submit"]:hover {
+ background: #0052a3;
+}
+
+.btn.danger, button.danger {
+ background: #dc3545;
+ color: white;
+}
+
+.btn.danger:hover, button.danger:hover {
+ background: #c82333;
+}
+
+/* Alerts */
+.alert {
+ padding: 1rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
+
+.alert-success {
+ background: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.alert-error {
+ background: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.alert-warning {
+ background: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeeba;
+}
+
+/* Search form */
+.search-form {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.search-form input {
+ flex: 1;
+ padding: 0.75rem;
+ font-size: 1.125rem;
+}
+
+/* Login form */
+.login-form {
+ max-width: 400px;
+ margin: 0 auto;
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.login-form h1 {
+ margin: 0 0 1.5rem;
+ text-align: center;
+}
+
+.hint {
+ text-align: center;
+ color: #666;
+ margin-top: 1rem;
+}
+
+/* Pagination */
+.pagination {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+ margin-top: 2rem;
+}
+
+.pagination a,
+.pagination span {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+
+.pagination .current {
+ background: #0066cc;
+ color: white;
+ border-color: #0066cc;
+}
+
+/* Footer */
+.footer {
+ background: #333;
+ color: white;
+ padding: 2rem 0;
+ margin-top: 4rem;
+ text-align: center;
+}
+
+.footer a {
+ color: #66b3ff;
+}
+
+/* Utilities */
+.inline {
+ display: inline;
+}
+
+.text-muted {
+ color: #666;
+}
+
+/* Loading */
+.loading {
+ text-align: center;
+ padding: 2rem;
+}
+
+.spinner {
+ display: inline-block;
+ width: 40px;
+ height: 40px;
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #0066cc;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.spinner-small {
+ width: 20px;
+ height: 20px;
+ border-width: 2px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.htmx-indicator {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ display: none;
+}
+
+.htmx-request .htmx-indicator {
+ display: block;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .home-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .nav-links {
+ flex-wrap: wrap;
+ }
+}
\ No newline at end of file
diff --git a/examples/blog-admin/store.go b/examples/blog-admin/store.go
new file mode 100644
index 0000000..296795f
--- /dev/null
+++ b/examples/blog-admin/store.go
@@ -0,0 +1,515 @@
+package main
+
+import (
+ "database/sql"
+ "strings"
+ "time"
+)
+
+// Store handles all database operations
+type Store struct {
+ db *sql.DB
+}
+
+func NewStore(db *sql.DB) *Store {
+ return &Store{db: db}
+}
+
+// User operations
+
+func (s *Store) GetUserByID(id int) (*User, error) {
+ var u User
+ err := s.db.QueryRow(`
+ SELECT id, username, email, password_hash, role, created_at, updated_at
+ FROM users WHERE id = ?
+ `, id).Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &u, nil
+}
+
+func (s *Store) GetUserByUsername(username string) (*User, error) {
+ var u User
+ err := s.db.QueryRow(`
+ SELECT id, username, email, password_hash, role, created_at, updated_at
+ FROM users WHERE username = ?
+ `, username).Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &u, nil
+}
+
+func (s *Store) CreateUser(user *User) error {
+ result, err := s.db.Exec(`
+ INSERT INTO users (username, email, password_hash, role)
+ VALUES (?, ?, ?, ?)
+ `, user.Username, user.Email, user.PasswordHash, user.Role)
+ if err != nil {
+ return err
+ }
+ id, err := result.LastInsertId()
+ if err != nil {
+ return err
+ }
+ user.ID = int(id)
+ return nil
+}
+
+func (s *Store) ListUsers(limit, offset int) ([]*User, int, error) {
+ // Get total count
+ var total int
+ err := s.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // Get users
+ rows, err := s.db.Query(`
+ SELECT id, username, email, password_hash, role, created_at, updated_at
+ FROM users
+ ORDER BY created_at DESC
+ LIMIT ? OFFSET ?
+ `, limit, offset)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var users []*User
+ for rows.Next() {
+ var u User
+ err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
+ if err != nil {
+ return nil, 0, err
+ }
+ users = append(users, &u)
+ }
+ return users, total, nil
+}
+
+// Post operations
+
+func (s *Store) GetPostBySlug(slug string) (*Post, error) {
+ var p Post
+ err := s.db.QueryRow(`
+ SELECT id, slug, title, content, excerpt, author_id, status,
+ published_at, created_at, updated_at, view_count
+ FROM posts WHERE slug = ?
+ `, slug).Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt,
+ &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount)
+ if err != nil {
+ return nil, err
+ }
+
+ // Load author
+ p.Author, _ = s.GetUserByID(p.AuthorID)
+
+ // Load tags and categories
+ p.Tags, _ = s.GetPostTags(p.ID)
+ p.Categories, _ = s.GetPostCategories(p.ID)
+
+ return &p, nil
+}
+
+func (s *Store) GetPostByID(id int) (*Post, error) {
+ var p Post
+ err := s.db.QueryRow(`
+ SELECT id, slug, title, content, excerpt, author_id, status,
+ published_at, created_at, updated_at, view_count
+ FROM posts WHERE id = ?
+ `, id).Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt,
+ &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount)
+ if err != nil {
+ return nil, err
+ }
+
+ // Load author
+ p.Author, _ = s.GetUserByID(p.AuthorID)
+
+ // Load tags and categories
+ p.Tags, _ = s.GetPostTags(p.ID)
+ p.Categories, _ = s.GetPostCategories(p.ID)
+
+ return &p, nil
+}
+
+func (s *Store) ListPosts(filter PostFilter) ([]*Post, int, error) {
+ query := `SELECT id, slug, title, content, excerpt, author_id, status,
+ published_at, created_at, updated_at, view_count FROM posts WHERE 1=1`
+ args := []interface{}{}
+
+ if filter.Status != "" {
+ query += " AND status = ?"
+ args = append(args, filter.Status)
+ }
+ if filter.AuthorID > 0 {
+ query += " AND author_id = ?"
+ args = append(args, filter.AuthorID)
+ }
+ if filter.CategoryID > 0 {
+ query += " AND id IN (SELECT post_id FROM post_categories WHERE category_id = ?)"
+ args = append(args, filter.CategoryID)
+ }
+ if filter.Search != "" {
+ query += " AND (title LIKE ? OR content LIKE ?)"
+ searchTerm := "%" + filter.Search + "%"
+ args = append(args, searchTerm, searchTerm)
+ }
+
+ // Get total count
+ var total int
+ // Build count query from scratch
+ countQuery := "SELECT COUNT(*) FROM posts WHERE 1=1"
+ countArgs := []interface{}{}
+
+ if filter.Status != "" {
+ countQuery += " AND status = ?"
+ countArgs = append(countArgs, filter.Status)
+ }
+ if filter.AuthorID > 0 {
+ countQuery += " AND author_id = ?"
+ countArgs = append(countArgs, filter.AuthorID)
+ }
+ if filter.CategoryID > 0 {
+ countQuery += " AND id IN (SELECT post_id FROM post_categories WHERE category_id = ?)"
+ countArgs = append(countArgs, filter.CategoryID)
+ }
+ if filter.Search != "" {
+ countQuery += " AND (title LIKE ? OR content LIKE ?)"
+ searchTerm := "%" + filter.Search + "%"
+ countArgs = append(countArgs, searchTerm, searchTerm)
+ }
+
+ err := s.db.QueryRow(countQuery, countArgs...).Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // Add ordering and pagination
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
+ args = append(args, filter.Limit, filter.Offset)
+
+ rows, err := s.db.Query(query, args...)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var posts []*Post
+ for rows.Next() {
+ var p Post
+ err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt,
+ &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // Load author
+ p.Author, _ = s.GetUserByID(p.AuthorID)
+
+ posts = append(posts, &p)
+ }
+
+ return posts, total, nil
+}
+
+func (s *Store) CreatePost(post *Post) error {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ result, err := tx.Exec(`
+ INSERT INTO posts (slug, title, content, excerpt, author_id, status, published_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, post.Slug, post.Title, post.Content, post.Excerpt, post.AuthorID, post.Status, post.PublishedAt)
+ if err != nil {
+ return err
+ }
+
+ id, err := result.LastInsertId()
+ if err != nil {
+ return err
+ }
+ post.ID = int(id)
+
+ // Update tags and categories
+ if err := s.updatePostTags(tx, post.ID, post.Tags); err != nil {
+ return err
+ }
+ if err := s.updatePostCategories(tx, post.ID, post.Categories); err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}
+
+func (s *Store) UpdatePost(post *Post) error {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ _, err = tx.Exec(`
+ UPDATE posts
+ SET slug = ?, title = ?, content = ?, excerpt = ?,
+ status = ?, published_at = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `, post.Slug, post.Title, post.Content, post.Excerpt,
+ post.Status, post.PublishedAt, post.ID)
+ if err != nil {
+ return err
+ }
+
+ // Update tags and categories
+ if err := s.updatePostTags(tx, post.ID, post.Tags); err != nil {
+ return err
+ }
+ if err := s.updatePostCategories(tx, post.ID, post.Categories); err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}
+
+func (s *Store) DeletePost(id int) error {
+ _, err := s.db.Exec("DELETE FROM posts WHERE id = ?", id)
+ return err
+}
+
+// Category operations
+
+func (s *Store) ListCategories() ([]*Category, error) {
+ rows, err := s.db.Query(`
+ SELECT c.id, c.slug, c.name, c.description, c.created_at, COUNT(pc.post_id) as post_count
+ FROM categories c
+ LEFT JOIN post_categories pc ON c.id = pc.category_id
+ GROUP BY c.id
+ ORDER BY c.name
+ `)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var categories []*Category
+ for rows.Next() {
+ var c Category
+ err := rows.Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt, &c.PostCount)
+ if err != nil {
+ return nil, err
+ }
+ categories = append(categories, &c)
+ }
+ return categories, nil
+}
+
+func (s *Store) GetCategoryBySlug(slug string) (*Category, error) {
+ var c Category
+ err := s.db.QueryRow(`
+ SELECT c.id, c.slug, c.name, c.description, c.created_at, COUNT(pc.post_id) as post_count
+ FROM categories c
+ LEFT JOIN post_categories pc ON c.id = pc.category_id
+ WHERE c.slug = ?
+ GROUP BY c.id
+ `, slug).Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt, &c.PostCount)
+ if err != nil {
+ return nil, err
+ }
+ return &c, nil
+}
+
+// Tag operations
+
+func (s *Store) GetPostTags(postID int) ([]Tag, error) {
+ rows, err := s.db.Query(`
+ SELECT t.id, t.name
+ FROM tags t
+ JOIN post_tags pt ON t.id = pt.tag_id
+ WHERE pt.post_id = ?
+ ORDER BY t.name
+ `, postID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var tags []Tag
+ for rows.Next() {
+ var t Tag
+ err := rows.Scan(&t.ID, &t.Name)
+ if err != nil {
+ return nil, err
+ }
+ tags = append(tags, t)
+ }
+ return tags, nil
+}
+
+func (s *Store) GetPostCategories(postID int) ([]Category, error) {
+ rows, err := s.db.Query(`
+ SELECT c.id, c.slug, c.name, c.description, c.created_at
+ FROM categories c
+ JOIN post_categories pc ON c.id = pc.category_id
+ WHERE pc.post_id = ?
+ ORDER BY c.name
+ `, postID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var categories []Category
+ for rows.Next() {
+ var c Category
+ err := rows.Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ categories = append(categories, c)
+ }
+ return categories, nil
+}
+
+// Analytics operations
+
+func (s *Store) IncrementPostViews(postID int) error {
+ _, err := s.db.Exec("UPDATE posts SET view_count = view_count + 1 WHERE id = ?", postID)
+ return err
+}
+
+func (s *Store) RecordPageView(postID *int, path, ipHash, userAgent, referrer string) error {
+ _, err := s.db.Exec(`
+ INSERT INTO page_views (post_id, path, ip_hash, user_agent, referrer)
+ VALUES (?, ?, ?, ?, ?)
+ `, postID, path, ipHash, userAgent, referrer)
+ return err
+}
+
+func (s *Store) GetAnalytics(date time.Time) (*Analytics, error) {
+ // This is a simplified version - in production you'd have more sophisticated analytics
+ var analytics Analytics
+ analytics.Date = date
+
+ // Get daily stats
+ err := s.db.QueryRow(`
+ SELECT COUNT(DISTINCT ip_hash) as visitors, COUNT(*) as page_views
+ FROM page_views
+ WHERE DATE(created_at) = DATE(?)
+ `, date).Scan(&analytics.Visitors, &analytics.PageViews)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get top posts
+ rows, err := s.db.Query(`
+ SELECT p.id, p.title, COUNT(pv.id) as views
+ FROM posts p
+ JOIN page_views pv ON p.id = pv.post_id
+ WHERE DATE(pv.created_at) = DATE(?)
+ GROUP BY p.id
+ ORDER BY views DESC
+ LIMIT 10
+ `, date)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var pa PostAnalytics
+ err := rows.Scan(&pa.PostID, &pa.PostTitle, &pa.Views)
+ if err != nil {
+ return nil, err
+ }
+ analytics.TopPosts = append(analytics.TopPosts, pa)
+ }
+
+ return &analytics, nil
+}
+
+// Helper methods
+
+func (s *Store) updatePostTags(tx *sql.Tx, postID int, tags []Tag) error {
+ // Delete existing tags
+ _, err := tx.Exec("DELETE FROM post_tags WHERE post_id = ?", postID)
+ if err != nil {
+ return err
+ }
+
+ // Insert new tags
+ for _, tag := range tags {
+ // Get or create tag
+ var tagID int
+ err := tx.QueryRow("SELECT id FROM tags WHERE name = ?", tag.Name).Scan(&tagID)
+ if err == sql.ErrNoRows {
+ result, err := tx.Exec("INSERT INTO tags (name) VALUES (?)", tag.Name)
+ if err != nil {
+ return err
+ }
+ id, err := result.LastInsertId()
+ if err != nil {
+ return err
+ }
+ tagID = int(id)
+ } else if err != nil {
+ return err
+ }
+
+ // Link tag to post
+ _, err = tx.Exec("INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)", postID, tagID)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *Store) updatePostCategories(tx *sql.Tx, postID int, categories []Category) error {
+ // Delete existing categories
+ _, err := tx.Exec("DELETE FROM post_categories WHERE post_id = ?", postID)
+ if err != nil {
+ return err
+ }
+
+ // Insert new categories
+ for _, cat := range categories {
+ _, err = tx.Exec("INSERT INTO post_categories (post_id, category_id) VALUES (?, ?)", postID, cat.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Filter types
+
+type PostFilter struct {
+ Status string
+ AuthorID int
+ CategoryID int
+ Search string
+ Limit int
+ Offset int
+}
+
+// Generate slug from title
+func GenerateSlug(title string) string {
+ // Simple slug generation - in production use a proper slugify library
+ slug := strings.ToLower(title)
+ slug = strings.ReplaceAll(slug, " ", "-")
+ slug = strings.ReplaceAll(slug, "'", "")
+ slug = strings.ReplaceAll(slug, "\"", "")
+ slug = strings.ReplaceAll(slug, ".", "")
+ slug = strings.ReplaceAll(slug, ",", "")
+ slug = strings.ReplaceAll(slug, "!", "")
+ slug = strings.ReplaceAll(slug, "?", "")
+ slug = strings.ReplaceAll(slug, "&", "and")
+ return slug
+}
diff --git a/examples/blog-admin/utils.go b/examples/blog-admin/utils.go
new file mode 100644
index 0000000..3c5bf29
--- /dev/null
+++ b/examples/blog-admin/utils.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/a-h/templ"
+)
+
+// Render helper for templ components
+func render(w http.ResponseWriter, r *http.Request, component templ.Component) error {
+ templ.Handler(component).ServeHTTP(w, r)
+ return nil
+}
+
+// Ternary helper for strings
+func ternary(condition bool, ifTrue, ifFalse string) string {
+ if condition {
+ return ifTrue
+ }
+ return ifFalse
+}
+
+// Ternary helper for URLs
+func ternaryURL(condition bool, ifTrue, ifFalse templ.SafeURL) templ.SafeURL {
+ if condition {
+ return ifTrue
+ }
+ return ifFalse
+}
+
+// Helper for conditional URLs with error handling
+func ternaryURLWithError(condition bool, ctx context.Context, pageTrue, pageFalse any, args ...any) templ.SafeURL {
+ if condition {
+ url, _ := urlFor(ctx, pageTrue)
+ return url
+ }
+ url, _ := urlFor(ctx, pageFalse, args...)
+ return url
+}
+
+// Helper to get URL or empty string
+func urlForOrEmpty(ctx context.Context, page any, args ...any) templ.SafeURL {
+ url, _ := urlFor(ctx, page, args...)
+ return url
+}
diff --git a/examples/htmx/pages_templ.go b/examples/htmx/pages_templ.go
index 08e1748..3b2e5de 100644
--- a/examples/htmx/pages_templ.go
+++ b/examples/htmx/pages_templ.go
@@ -356,7 +356,7 @@ func html() templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, index{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 76, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 76, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -369,7 +369,7 @@ func html() templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, product{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 77, Col: 44}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 77, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -382,7 +382,7 @@ func html() templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, team{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 78, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 78, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -395,7 +395,7 @@ func html() templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, contact{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 79, Col: 44}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 79, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -492,7 +492,7 @@ func errorComp(err error) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 98, Col: 17}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 98, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
diff --git a/examples/simple/pages_templ.go b/examples/simple/pages_templ.go
index 3d7af30..4593007 100644
--- a/examples/simple/pages_templ.go
+++ b/examples/simple/pages_templ.go
@@ -240,7 +240,7 @@ func html() templ.Component {
var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, index{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 57, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 57, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -253,7 +253,7 @@ func html() templ.Component {
var templ_7745c5c3_Var11 templ.SafeURL
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, product{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 58, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 58, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -266,7 +266,7 @@ func html() templ.Component {
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, team{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 59, Col: 39}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 59, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func html() templ.Component {
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, contact{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 60, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 60, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
diff --git a/examples/todo/pages_templ.go b/examples/todo/pages_templ.go
index 9e6e09d..7de8f7b 100644
--- a/examples/todo/pages_templ.go
+++ b/examples/todo/pages_templ.go
@@ -60,7 +60,7 @@ func (p index) Page() templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, add{}))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 21, Col: 32}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 21, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -167,7 +167,7 @@ func todoList() templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 1, Col: 0}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -190,7 +190,7 @@ func todoList() templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, toggle{}, "id", todo.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 92, Col: 52}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 92, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -203,7 +203,7 @@ func todoList() templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Text)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 96, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 96, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -216,7 +216,7 @@ func todoList() templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, deleteTodo{}, "id", todo.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 100, Col: 57}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 100, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -353,7 +353,7 @@ func errorComp(err error) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 232, Col: 17}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 232, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {