diff --git a/pkg/page/page.go b/pkg/page/page.go index 4f0c755..cd64276 100644 --- a/pkg/page/page.go +++ b/pkg/page/page.go @@ -52,11 +52,6 @@ type Page struct { // messages and markup presented to the user Form any - // Layout stores the name of the layout base template file which will be used when the page is rendered. - // This should match a template file located within the layouts directory inside the templates directory. - // The template extension should not be included in this value. - Layout templates.Layout - LayoutComponent func(content templ.Component) templ.Component // Name stores the name of the page as well as the name of the template file which will be used to render diff --git a/pkg/services/template_renderer.go b/pkg/services/template_renderer.go index b5e518a..41d67c5 100644 --- a/pkg/services/template_renderer.go +++ b/pkg/services/template_renderer.go @@ -139,76 +139,6 @@ func (t *TemplateRenderer) RenderPageTempl(ctx echo.Context, page page.Page, con // 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 - var err error - templateGroup := "page" - - // 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 - } - - // 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 { - // Switch the layout which will only render the page content - page.Layout = templates.LayoutHTMX - - // Alter the template group so this is cached separately - templateGroup = "page:htmx" - } - - // Parse and execute the templates for the Page - // As mentioned in the documentation for the Page struct, the templates used for the page will be: - // 1. The layout/base template specified in Page.Layout - // 2. The content template specified in Page.Name - // 3. All templates within the components directory - // Also included is the function map provided by the funcmap package - buf, err = t. - Parse(). - Group(templateGroup). - Key(string(page.Name)). - Base(string(page.Layout)). - Files( - fmt.Sprintf("layouts/%s", page.Layout), - fmt.Sprintf("pages/%s", page.Name), - ). - Directories("components"). - Execute(page) - 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()) } @@ -271,7 +201,6 @@ func (t *TemplateRenderer) GetCachedPage(ctx echo.Context, url string) (*CachedP return p.(*CachedPage), nil } - // getCacheKey gets a cache key for a given group and ID func (t *TemplateRenderer) getCacheKey(group, key string) string { if group != "" { diff --git a/pkg/services/template_renderer_test.go b/pkg/services/template_renderer_test.go index 7411ca1..b2fa82f 100644 --- a/pkg/services/template_renderer_test.go +++ b/pkg/services/template_renderer_test.go @@ -1,21 +1,22 @@ package services import ( - "context" - "fmt" "net/http" "net/http/httptest" "testing" + "github.com/a-h/templ" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.grosinger.net/tgrosinger/saasitone/config" "git.grosinger.net/tgrosinger/saasitone/pkg/htmx" + "git.grosinger.net/tgrosinger/saasitone/pkg/models" "git.grosinger.net/tgrosinger/saasitone/pkg/page" "git.grosinger.net/tgrosinger/saasitone/pkg/tests" - "git.grosinger.net/tgrosinger/saasitone/templates" + "git.grosinger.net/tgrosinger/saasitone/templ/layouts" + "git.grosinger.net/tgrosinger/saasitone/templ/pages" ) func TestTemplateRenderer(t *testing.T) { @@ -31,9 +32,8 @@ func TestTemplateRenderer(t *testing.T) { Parse(). Group(group). Key(id). - Base("htmx"). - Files("layouts/htmx", "pages/error"). - Directories("components"). + Base("test"). + Files("emails/test"). Store() require.NoError(t, err) @@ -43,13 +43,7 @@ func TestTemplateRenderer(t *testing.T) { // Check that all expected templates are included expectedTemplates := make(map[string]bool) - expectedTemplates["htmx"+config.TemplateExt] = true - expectedTemplates["error"+config.TemplateExt] = true - components, err := templates.Get().ReadDir("components") - require.NoError(t, err) - for _, f := range components { - expectedTemplates[f.Name()] = true - } + expectedTemplates["test"+config.TemplateExt] = true for _, v := range parsed.Template.Templates() { delete(expectedTemplates, v.Name()) } @@ -63,20 +57,19 @@ func TestTemplateRenderer(t *testing.T) { buf, err := tpl.Execute(data) require.NoError(t, err) require.NotNil(t, buf) - assert.Contains(t, buf.String(), "Please try again") + assert.Contains(t, buf.String(), "Test email template") buf, err = c.TemplateRenderer. Parse(). Group(group). Key(id). - Base("htmx"). - Files("htmx", "pages/error"). - Directories("components"). + Base("test"). + Files("email/test"). Execute(data) require.NoError(t, err) require.NotNil(t, buf) - assert.Contains(t, buf.String(), "Please try again") + assert.Contains(t, buf.String(), "Test email template") } func TestTemplateRenderer_RenderPage(t *testing.T) { @@ -85,8 +78,7 @@ func TestTemplateRenderer_RenderPage(t *testing.T) { tests.InitSession(ctx) p := page.New(ctx) - p.Name = "home" - p.Layout = "main" + p.Name = "test" p.Cache.Enabled = false p.Headers["A"] = "b" p.Headers["C"] = "d" @@ -98,101 +90,27 @@ func TestTemplateRenderer_RenderPage(t *testing.T) { // Rendering should fail if the Page has no name ctx, _, p := setup() p.Name = "" - err := c.TemplateRenderer.RenderPage(ctx, p) + err := c.TemplateRenderer.RenderPageTempl(ctx, p, pages.Error(p)) assert.Error(t, err) }) - t.Run("no page cache", func(t *testing.T) { - ctx, _, p := setup() - err := c.TemplateRenderer.RenderPage(ctx, p) - require.NoError(t, err) - - // Check status code and headers - assert.Equal(t, http.StatusCreated, ctx.Response().Status) - for k, v := range p.Headers { - assert.Equal(t, v, ctx.Response().Header().Get(k)) - } - - // Check the template cache - parsed, err := c.TemplateRenderer.Load("page", string(p.Name)) - require.NoError(t, err) - - // Check that all expected templates were parsed. - // This includes the name, layout and all components - expectedTemplates := make(map[string]bool) - expectedTemplates[fmt.Sprintf("%s%s", p.Name, config.TemplateExt)] = true - expectedTemplates[fmt.Sprintf("%s%s", p.Layout, config.TemplateExt)] = true - components, err := templates.Get().ReadDir("components") - require.NoError(t, err) - for _, f := range components { - expectedTemplates[f.Name()] = true - } - - for _, v := range parsed.Template.Templates() { - delete(expectedTemplates, v.Name()) - } - assert.Empty(t, expectedTemplates) - }) - t.Run("htmx rendering", func(t *testing.T) { ctx, _, p := setup() p.HTMX.Request.Enabled = true p.HTMX.Response = &htmx.Response{ Trigger: "trigger", } - err := c.TemplateRenderer.RenderPage(ctx, p) + + component := pages.Home(p, []models.Post{}) + p.LayoutComponent = func(content templ.Component) templ.Component { + return layouts.Main(p, content) + } + + err := c.TemplateRenderer.RenderPageTempl(ctx, p, component) require.NoError(t, err) // Check HTMX header assert.Equal(t, "trigger", ctx.Response().Header().Get(htmx.HeaderTrigger)) - - // Check the template cache - parsed, err := c.TemplateRenderer.Load("page:htmx", string(p.Name)) - require.NoError(t, err) - - // Check that all expected templates were parsed. - // This includes the name, htmx and all components - expectedTemplates := make(map[string]bool) - expectedTemplates[fmt.Sprintf("%s%s", p.Name, config.TemplateExt)] = true - expectedTemplates["htmx"+config.TemplateExt] = true - components, err := templates.Get().ReadDir("components") - require.NoError(t, err) - for _, f := range components { - expectedTemplates[f.Name()] = true - } - - for _, v := range parsed.Template.Templates() { - delete(expectedTemplates, v.Name()) - } - assert.Empty(t, expectedTemplates) - }) - - t.Run("page cache", func(t *testing.T) { - ctx, rec, p := setup() - p.Cache.Enabled = true - p.Cache.Tags = []string{"tag1"} - err := c.TemplateRenderer.RenderPage(ctx, p) - require.NoError(t, err) - - // Fetch from the cache - cp, err := c.TemplateRenderer.GetCachedPage(ctx, p.URL) - require.NoError(t, err) - - // Compare the cached page - assert.Equal(t, p.URL, cp.URL) - assert.Equal(t, p.Headers, cp.Headers) - assert.Equal(t, p.StatusCode, cp.StatusCode) - assert.Equal(t, rec.Body.Bytes(), cp.HTML) - - // Clear the tag - err = c.Cache. - Flush(). - Tags(p.Cache.Tags[0]). - Execute(context.Background()) - require.NoError(t, err) - - // Refetch from the cache and expect no results - _, err = c.TemplateRenderer.GetCachedPage(ctx, p.URL) - assert.Error(t, err) }) } +