diff --git a/Makefile b/Makefile
index 815cad9..a4f968b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
# Run the application
.PHONY: run
-run:
+run: sqlc templ
clear
go run cmd/web/main.go
@@ -14,6 +14,10 @@ sqlc:
rm -f pkg/models/sqlc/*
sqlc generate
+.PHONY: templ
+templ:
+ templ generate
+
# Check for direct dependency updates
.PHONY: check-updates
check-updates:
diff --git a/go.mod b/go.mod
index 404d900..ecf92a9 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
ariga.io/atlas v0.21.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
+ github.com/a-h/templ v0.2.747 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
diff --git a/go.sum b/go.sum
index ddeda86..f85c2d4 100644
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,8 @@ github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VP
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
+github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
+github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
diff --git a/pkg/funcmap/funcmap.go b/pkg/funcmap/funcmap.go
index b359183..f5d53b6 100644
--- a/pkg/funcmap/funcmap.go
+++ b/pkg/funcmap/funcmap.go
@@ -53,6 +53,10 @@ func (fm *funcMap) file(filepath string) string {
return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster)
}
+func File(filepath string) string {
+ return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster)
+}
+
// link outputs HTML for a link element, providing the ability to dynamically set the active class
func (fm *funcMap) link(url, text, currentPath string, classes ...string) template.HTML {
if currentPath == url {
diff --git a/pkg/handlers/pages.go b/pkg/handlers/pages.go
index 37701e5..ebe00c5 100644
--- a/pkg/handlers/pages.go
+++ b/pkg/handlers/pages.go
@@ -1,13 +1,13 @@
package handlers
import (
- "fmt"
"html/template"
"github.com/labstack/echo/v4"
"git.grosinger.net/tgrosinger/saasitone/pkg/page"
"git.grosinger.net/tgrosinger/saasitone/pkg/services"
+ "git.grosinger.net/tgrosinger/saasitone/templ/pages"
"git.grosinger.net/tgrosinger/saasitone/templates"
)
@@ -19,11 +19,7 @@ const (
type (
Pages struct {
*services.TemplateRenderer
- }
-
- post struct {
- Title string
- Body string
+ *services.DBClient
}
aboutData struct {
@@ -44,6 +40,7 @@ func init() {
func (h *Pages) Init(c *services.Container) error {
h.TemplateRenderer = c.TemplateRenderer
+ h.DBClient = c.DB
return nil
}
@@ -59,23 +56,11 @@ func (h *Pages) Home(ctx echo.Context) error {
p.Metatags.Description = "Welcome to the homepage."
p.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
p.Pager = page.NewPager(ctx, 4)
- p.Data = h.fetchPosts(&p.Pager)
- return h.RenderPage(ctx, p)
-}
+ data := h.Post.FetchAll(&p.Pager)
+ component := pages.Home(p, data)
-// fetchPosts is an mock example of fetching posts to illustrate how paging works
-func (h *Pages) fetchPosts(pager *page.Pager) []post {
- pager.SetItems(20)
- posts := make([]post, 20)
-
- for k := range posts {
- posts[k] = post{
- Title: fmt.Sprintf("Post example #%d", k+1),
- Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
- }
- }
- return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
+ return h.RenderPageTempl(ctx, p, component)
}
func (h *Pages) About(ctx echo.Context) error {
diff --git a/pkg/page/page.go b/pkg/page/page.go
index 7252de2..9826c24 100644
--- a/pkg/page/page.go
+++ b/pkg/page/page.go
@@ -1,7 +1,6 @@
package page
import (
- "html/template"
"net/http"
"time"
@@ -39,6 +38,9 @@ type Page struct {
// URL stores the URL of the current request
URL string
+ // ToURL is a function to convert a route name and optional route parameters to a URL
+ ToURL func(name string, params ...interface{}) string
+
// Data stores whatever additional data that needs to be passed to the templates.
// This is what the handler uses to pass the content of the page.
Data any
@@ -127,6 +129,7 @@ func New(ctx echo.Context) Page {
Context: ctx,
Path: ctx.Request().URL.Path,
URL: ctx.Request().URL.String(),
+ ToURL: ctx.Echo().Reverse,
StatusCode: http.StatusOK,
Pager: NewPager(ctx, DefaultItemsPerPage),
Headers: make(map[string]string),
@@ -151,11 +154,6 @@ func New(ctx echo.Context) Page {
// GetMessages gets all flash messages for a given type.
// This allows for easy access to flash messages from the templates.
-func (p Page) GetMessages(typ msg.Type) []template.HTML {
- strs := msg.Get(p.Context, typ)
- ret := make([]template.HTML, len(strs))
- for k, v := range strs {
- ret[k] = template.HTML(v)
- }
- return ret
+func (p Page) GetMessages(typ msg.Type) []string {
+ return msg.Get(p.Context, typ)
}
diff --git a/pkg/services/db.go b/pkg/services/db.go
index cc9673c..54e2ef3 100644
--- a/pkg/services/db.go
+++ b/pkg/services/db.go
@@ -18,6 +18,7 @@ type DBClient struct {
C *sqlc.Queries
User *DBUserClient
+ Post *DBPostClient
}
func NewDBClient(cfg *config.Config) (*DBClient, error) {
@@ -43,6 +44,7 @@ func NewDBClient(cfg *config.Config) (*DBClient, error) {
C: sqlc.New(db),
}
client.User = &DBUserClient{db: db}
+ client.Post = &DBPostClient{db: db}
migrationsDirPath := cfg.Storage.MigrationsDir
logger.Info("Loading schema migrations",
diff --git a/pkg/services/posts.go b/pkg/services/posts.go
new file mode 100644
index 0000000..a8206b8
--- /dev/null
+++ b/pkg/services/posts.go
@@ -0,0 +1,31 @@
+package services
+
+import (
+ "database/sql"
+ "fmt"
+
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+)
+
+type Post struct {
+ Title string
+ Body string
+}
+
+type DBPostClient struct {
+ db *sql.DB
+}
+
+// FetchAll is an mock example of fetching posts to illustrate how paging works
+func (c *DBPostClient) FetchAll(pager *page.Pager) []Post {
+ pager.SetItems(20)
+ posts := make([]Post, 20)
+
+ for k := range posts {
+ posts[k] = Post{
+ Title: fmt.Sprintf("Post example #%d", k+1),
+ Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
+ }
+ }
+ return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
+}
diff --git a/pkg/services/template_renderer.go b/pkg/services/template_renderer.go
index ef73097..c3cfa8f 100644
--- a/pkg/services/template_renderer.go
+++ b/pkg/services/template_renderer.go
@@ -9,12 +9,14 @@ import (
"net/http"
"sync"
+ "github.com/a-h/templ"
"github.com/labstack/echo/v4"
"git.grosinger.net/tgrosinger/saasitone/config"
"git.grosinger.net/tgrosinger/saasitone/pkg/context"
"git.grosinger.net/tgrosinger/saasitone/pkg/log"
"git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/templ/layouts"
"git.grosinger.net/tgrosinger/saasitone/templates"
)
@@ -96,6 +98,54 @@ func (t *TemplateRenderer) Parse() *templateBuilder {
}
}
+func (t *TemplateRenderer) RenderPageTempl(ctx echo.Context, page page.Page, content templ.Component) error {
+ // Page name is required
+ if page.Name == "" {
+ return echo.NewHTTPError(http.StatusInternalServerError, "page render failed due to missing name")
+ }
+
+ // Use the app name in configuration if a value was not set
+ if page.AppName == "" {
+ page.AppName = t.config.App.Name
+ }
+
+ layout := layouts.Main
+
+ // Check if this is an HTMX non-boosted request which indicates that only partial
+ // content should be rendered
+ if page.HTMX.Request.Enabled && !page.HTMX.Request.Boosted {
+ // TODO: Change layout to HTML layout
+ }
+
+ buf := bytes.Buffer{}
+ temp := layout(page, content)
+ err := temp.Render(ctx.Request().Context(), &buf)
+ if err != nil {
+ return echo.NewHTTPError(
+ http.StatusInternalServerError,
+ fmt.Sprintf("failed to parse and execute templates: %s", err),
+ )
+ }
+
+ // Set the status code
+ ctx.Response().Status = page.StatusCode
+
+ // Set any headers
+ for k, v := range page.Headers {
+ ctx.Response().Header().Set(k, v)
+ }
+
+ // Apply the HTMX response, if one
+ if page.HTMX.Response != nil {
+ page.HTMX.Response.Apply(ctx)
+ }
+
+ // Cache this page, if caching was enabled
+ t.cachePage(ctx, page, &buf)
+
+ return ctx.HTMLBlob(ctx.Response().Status, buf.Bytes())
+}
+
// RenderPage renders a Page as an HTTP response
func (t *TemplateRenderer) RenderPage(ctx echo.Context, page page.Page) error {
var buf *bytes.Buffer
diff --git a/templ/components/messages.templ b/templ/components/messages.templ
new file mode 100644
index 0000000..b53a2b4
--- /dev/null
+++ b/templ/components/messages.templ
@@ -0,0 +1,21 @@
+package components
+
+import (
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/msg"
+)
+
+templ Messages(p page.Page) {
+ for _, msgType := range []msg.Type{msg.TypeSuccess, msg.TypeInfo, msg.TypeWarning, msg.TypeDanger} {
+ for _, msg := range p.GetMessages(msgType) {
+ @message(msgType, msg)
+ }
+ }
+}
+
+templ message(msgType msg.Type, msg string) {
+
+
+ { msg }
+
+}
diff --git a/templ/components/messages_templ.go b/templ/components/messages_templ.go
new file mode 100644
index 0000000..0bf9d65
--- /dev/null
+++ b/templ/components/messages_templ.go
@@ -0,0 +1,99 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.707
+package components
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+import (
+ "git.grosinger.net/tgrosinger/saasitone/pkg/msg"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+)
+
+func Messages(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ for _, msgType := range []msg.Type{msg.TypeSuccess, msg.TypeInfo, msg.TypeWarning, msg.TypeDanger} {
+ for _, msg := range p.GetMessages(msgType) {
+ templ_7745c5c3_Err = message(msgType, msg).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func message(msgType msg.Type, msg string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var2 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var2 == nil {
+ templ_7745c5c3_Var2 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ var templ_7745c5c3_Var3 = []any{"notification is-light", "is-" + msgType}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/components/messages.templ`, Line: 19, Col: 7}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/templ/layouts/main.templ b/templ/layouts/main.templ
new file mode 100644
index 0000000..d9f2101
--- /dev/null
+++ b/templ/layouts/main.templ
@@ -0,0 +1,168 @@
+package layouts
+
+import (
+ "strings"
+
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap"
+ "git.grosinger.net/tgrosinger/saasitone/templ/components"
+)
+
+templ Main(p page.Page, content templ.Component) {
+
+
+
+ @metatags(p)
+ @css()
+ @js()
+
+
+
+
+
+
+
+
+
+
+
+
+ if p.Title != "" {
+
{ p.Title }
+ }
+ @components.Messages(p)
+ @content
+
+
+
+
+ @footer(p)
+
+
+}
+
+templ link(p page.Page, url, text, classes string) {
+ { text }
+}
+
+templ search(p page.Page) {
+
+
$refs.input.focus());"/>
+
+
+}
+
+templ metatags(p page.Page) {
+ if p.Title != "" {
+ { p.AppName } | { p.Title }
+ } else {
+ { p.AppName }
+ }
+
+
+
+
+ if p.Metatags.Description != "" {
+
+ }
+ if len(p.Metatags.Keywords) > 0 {
+
+ }
+}
+
+templ css() {
+
+}
+
+templ js() {
+
+
+}
+
+templ footer(p page.Page) {
+ if p.CSRF != "" {
+
+ }
+
+}
diff --git a/templ/layouts/main_templ.go b/templ/layouts/main_templ.go
new file mode 100644
index 0000000..3126add
--- /dev/null
+++ b/templ/layouts/main_templ.go
@@ -0,0 +1,506 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.707
+package layouts
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+import (
+ "strings"
+
+ "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/templ/components"
+)
+
+func Main(p page.Page, content templ.Component) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ 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 = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = metatags(p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = css().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = js().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p.Title != "" {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 77, Col: 35}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = components.Messages(p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = footer(p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func link(p page.Page, url, text, classes string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ var templ_7745c5c3_Var6 = []any{classes, templ.KV("is-active", p.Path == url)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(text)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 91, Col: 90}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func search(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var10 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var10 == nil {
+ templ_7745c5c3_Var10 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func metatags(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var12 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var12 == nil {
+ templ_7745c5c3_Var12 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ if p.Title != "" {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var13 string
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.AppName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 125, Col: 20}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var14 string
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 125, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var15 string
+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p.AppName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 127, Col: 20}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p.Metatags.Description != "" {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if len(p.Metatags.Keywords) > 0 {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func css() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var18 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var18 == nil {
+ templ_7745c5c3_Var18 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func js() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ 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 = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func footer(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var20 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var20 == nil {
+ templ_7745c5c3_Var20 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ if p.CSRF != "" {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/templ/pages/home.templ b/templ/pages/home.templ
new file mode 100644
index 0000000..15c9c74
--- /dev/null
+++ b/templ/pages/home.templ
@@ -0,0 +1,92 @@
+package pages
+
+import (
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/services"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap"
+)
+
+templ Home(p page.Page, posts []services.Post) {
+ if (p.HTMX.Request.Target != "posts") {
+ @topContent(p)
+ }
+
+ for _, post := range posts {
+
+
+
+
+
+
+
+
+
+ { post.Title }
+
+ { post.Body }
+
+
+
+
+ }
+
+ if !p.Pager.IsBeginning() {
+
+ Previous page
+
+ }
+ if !p.Pager.IsEnd() {
+
+ Next page
+
+ }
+
+
+ if (p.HTMX.Request.Target != "posts") {
+ @fileMsg(p)
+ }
+}
+
+templ topContent(p page.Page) {
+
+
+
+
+ if p.IsAuth {
+ Hello, { p.AuthUser.Name }
+ } else {
+ Hello
+ }
+
+
+ if p.IsAuth {
+ Welcome back!
+ } else {
+ Please login in to your account.
+ }
+
+
+
+
+
+ Recent posts
+
+ Below is an example of both paging and AJAX fetching using HTMX
+
+
+}
+
+templ fileMsg(p page.Page) {
+
+
+
+
+ In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted.
+ Static files also contain cache-control headers which are configured via middleware.
+ You can also use AlpineJS to dismiss this message.
+
+
+}
diff --git a/templ/pages/home_templ.go b/templ/pages/home_templ.go
new file mode 100644
index 0000000..9e5d307
--- /dev/null
+++ b/templ/pages/home_templ.go
@@ -0,0 +1,205 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.707
+package pages
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+import (
+ "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/page"
+ "git.grosinger.net/tgrosinger/saasitone/pkg/services"
+)
+
+func Home(p page.Page, posts []services.Post) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ if p.HTMX.Request.Target != "posts" {
+ templ_7745c5c3_Err = topContent(p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, post := range posts {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ 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: `templ/pages/home.templ`, Line: 24, Col: 27}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Body)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/pages/home.templ`, Line: 26, Col: 18}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !p.Pager.IsBeginning() {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Previous page
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if !p.Pager.IsEnd() {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Next page
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p.HTMX.Request.Target != "posts" {
+ templ_7745c5c3_Err = fileMsg(p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func topContent(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ 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 = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p.IsAuth {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Hello, ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(p.AuthUser.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/pages/home.templ`, Line: 56, Col: 30}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Hello")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p.IsAuth {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Welcome back!")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Please login in to your account.")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" Recent posts Below is an example of both paging and AJAX fetching using HTMX ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func fileMsg(p page.Page) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. Static files also contain cache-control headers which are configured via middleware. You can also use AlpineJS to dismiss this message.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}