From 9340e7022d95c912db5f1d9fcb0313743ae3cb4a Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sun, 31 Aug 2025 11:16:59 +0700 Subject: refactor: prepare layout for SPA --- internal/router/api-v1-general-page-header.go | 39 +++++++++++++++++++++++++++ internal/templatemanager/templatemanager.go | 15 +++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 internal/router/api-v1-general-page-header.go (limited to 'internal') diff --git a/internal/router/api-v1-general-page-header.go b/internal/router/api-v1-general-page-header.go new file mode 100644 index 0000000..8ac7c03 --- /dev/null +++ b/internal/router/api-v1-general-page-header.go @@ -0,0 +1,39 @@ +package router + +import ( + "fmt" + "net/url" + "slices" + "strings" + + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + pathParts := strings.SplitN(strings.Trim(path, "/"), "/", 2) + if len(pathParts) < 2 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + return nil + } +} diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index f89d0c8..640a706 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -44,14 +44,25 @@ func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager }, nil } -func (tm *TemplateManager) Render(name string, data interface{}) ([]byte, error) { +func (tm *TemplateManager) Render(name string, data any, files ...string) ([]byte, error) { tmpl, exists := tm.templates[name] if !exists { return nil, fmt.Errorf("template %s is not found", name) } + var err error + var tempTmpl *template.Template + if len(files) == 0 { + tempTmpl = tmpl.Tmpl + } else { + tempTmpl, err = tmpl.Tmpl.ParseFiles(files...) + if err != nil { + return nil, fmt.Errorf("couldn't include additional files in template rendering: %w", err) + } + } + var buf bytes.Buffer - err := tmpl.Tmpl.ExecuteTemplate(&buf, tmpl.Main, data) + err = tempTmpl.ExecuteTemplate(&buf, tmpl.Main, data) return buf.Bytes(), err } -- cgit v1.3.1+13 From 5eb7b64b3da54e769cc6e52fd6918e4b465b8c0a Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sun, 31 Aug 2025 13:16:45 +0700 Subject: feat: integrate partials in page generating --- internal/lightgallery/html_renderer.go | 9 +- internal/router/api-v1-general-page-body.go | 137 ++++++++++++++++++++++++++ internal/router/api-v1-general-page-footer.go | 63 ++++++++++++ internal/router/api-v1-general-page-header.go | 39 +++++++- internal/templatemanager/templatemanager.go | 6 +- main.go | 4 + views/layouts/general-page.html | 55 +++++------ 7 files changed, 276 insertions(+), 37 deletions(-) create mode 100644 internal/router/api-v1-general-page-body.go create mode 100644 internal/router/api-v1-general-page-footer.go (limited to 'internal') diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go index 17a6f2b..21bd049 100644 --- a/internal/lightgallery/html_renderer.go +++ b/internal/lightgallery/html_renderer.go @@ -119,7 +119,14 @@ function createLightGallery%s() { galleryMap.set(inlineGallery, createLightGallery%s); } -document.addEventListener('DOMContentLoaded', createLightGallery%s); +document.addEventListener('htmx:afterRequest', (e) => { + if (e.detail.xhr.status == 404 || e.detail.successful != true) { + return console.error(e); + } + if (e.detail.target.id == 'general-page-body') { + createLightGallery%s(); + } +}); `, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID)) } diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go new file mode 100644 index 0000000..419bad6 --- /dev/null +++ b/internal/router/api-v1-general-page-body.go @@ -0,0 +1,137 @@ +package router + +import ( + "fmt" + "html/template" + "log/slog" + "net/url" + "regexp" + "slices" + "strings" + + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" + "github.com/yuin/goldmark" +) + +func init() { + tm.Add("general-page-body", "views/partials/general-page-body.html") +} + +func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) < 2 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + querySort := c.Query("sort") + if querySort == "" { + querySort = "publicationDateDesc" + } + + encodedQuery := c.Request().URI().QueryString() + re, err := regexp.Compile(`tags\[\]=([\w]+)`) + if err != nil { + slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering") + } + decodedQuery, _ := url.QueryUnescape(string(encodedQuery)) + matches := re.FindAllStringSubmatch(decodedQuery, -1) + + queryTags := make([]string, 0, len(matches)) + for _, match := range matches { + queryTags = append(queryTags, string(match[1])) + } + + pages, err := b2Client.Scan(lang + "/") + if err != nil { + slog.Warn("failed to scan pages via b2", slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages via b2: %s", slog.String("error", err.Error()))) + } + + tagsMap := make(map[string]int) + for _, page := range pages { + for _, tag := range page.Metadata.Tags { + tagsMap[tag]++ + } + } + slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("path", c.Path())) + + type Tag struct { + Name string `json:"Name" yaml:"name"` + Count int `json:"Count" yaml:"count"` + } + + tagsArray := make([]Tag, 0, len(tagsMap)) + for tag, count := range tagsMap { + tagsArray = append(tagsArray, Tag{tag, count}) + } + slices.SortFunc(tagsArray, func(a Tag, b Tag) int { + return strings.Compare(a.Name, b.Name) + }) + + values["Tags"] = tagsArray + values["QuerySort"] = querySort + values["QueryTags"] = strings.Join(queryTags, ",") + + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+pathParts[2]) + if err != nil { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", pathParts[2])) + } + + geolocationParts := strings.Split(metadata.Geolocation, " ") + var x, y, areaError string + if len(geolocationParts) >= 2 { + x = geolocationParts[0] + y = geolocationParts[1] + } + if len(geolocationParts) >= 3 { + areaError = geolocationParts[2] + } + + values["ParsedMarkdown"] = template.HTML(parsedMarkdown) + values["MapLocationX"] = x + values["MapLocationY"] = y + values["MapLocationAreaMeters"] = areaError + + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } + + content, err := tm.Render("general-page-body", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page-footer.go b/internal/router/api-v1-general-page-footer.go new file mode 100644 index 0000000..4c3a35e --- /dev/null +++ b/internal/router/api-v1-general-page-footer.go @@ -0,0 +1,63 @@ +package router + +import ( + "fmt" + "log/slog" + "net/url" + "slices" + "strings" + + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("general-page-footer", "views/partials/general-page-footer.html") +} + +func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) < 2 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } + + content, err := tm.Render("general-page-footer", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page-header.go b/internal/router/api-v1-general-page-header.go index 8ac7c03..045ff8b 100644 --- a/internal/router/api-v1-general-page-header.go +++ b/internal/router/api-v1-general-page-header.go @@ -2,15 +2,21 @@ package router import ( "fmt" + "log/slog" "net/url" "slices" "strings" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" ) -func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { +func init() { + tm.Add("general-page-header", "views/partials/general-page-header.html") +} + +func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) @@ -24,7 +30,7 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string } path := urlStruct.EscapedPath() - pathParts := strings.SplitN(strings.Trim(path, "/"), "/", 2) + pathParts := strings.Split(strings.Trim(path, "/"), "/") if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -34,6 +40,33 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) } - return nil + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + values["Title"] = l[lang].BlogSearch.Header + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + metadata, _, err := b2Client.ReadFrontmatter(lang + "/" + pathParts[2] + ".md") + if err != nil { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("could not read '%s' for content", path)) + } + values["Title"] = metadata.Title + values["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") + values["ActionDate"] = metadata.ActionDate + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } + + content, err := tm.Render("general-page-header", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) } } diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index 640a706..d8adab6 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -55,7 +55,11 @@ func (tm *TemplateManager) Render(name string, data any, files ...string) ([]byt if len(files) == 0 { tempTmpl = tmpl.Tmpl } else { - tempTmpl, err = tmpl.Tmpl.ParseFiles(files...) + tempTmpl, err = tmpl.Tmpl.Clone() + if err != nil { + return nil, fmt.Errorf("couldn't clone existing template for rendering: %w", err) + } + tempTmpl, err = tempTmpl.ParseFiles(files...) if err != nil { return nil, fmt.Errorf("couldn't include additional files in template rendering: %w", err) } diff --git a/main.go b/main.go index 020f8ef..ace9916 100644 --- a/main.go +++ b/main.go @@ -124,10 +124,14 @@ func main() { app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client)) app.Get("/:lang/blog", router.Lang_Blog(localization, availableLanguages, b2Client)) app.Get("/:lang/blog/:title", router.Lang_Blog_Title(localization, availableLanguages, b2Client, md)) + app.Get("/api/v1/tz", router.Api_V1_TZ()) app.Get("/api/v1/blog-search", router.Api_V1_BlogSearch(localization, availableLanguages, b2Client)) app.Get("/api/v1/like", router.Api_V1_Like_Get(localization, b2Client)) app.Put("/api/v1/like", router.Api_V1_Like_Put(localization, b2Client)) + app.Get("/api/v1/general-page/header", router.Api_V1_GeneralPage_Header(localization, availableLanguages, b2Client)) + app.Get("/api/v1/general-page/body", router.Api_V1_GeneralPage_Body(localization, availableLanguages, b2Client, md)) + app.Get("/api/v1/general-page/footer", router.Api_V1_GeneralPage_Footer(localization, availableLanguages)) app.Static("/", "./static") diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 675b6e9..4e352f4 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -12,9 +12,7 @@ - {{- if .MapLocationX }} - {{- end }} @@ -176,31 +174,18 @@
-
-
- -

//

-

{{ .Title }}

-

{{ .PublishedDate }}

-
- {{ block "header" . }}{{ end }} +

-
- {{ block "body" . }}{{ end }} +
-