summaryrefslogtreecommitdiff
path: root/internal
diff options
from:
to:
context:
space:
mode:
authorGravatar Saya Andy <145215889+SayaAndy@users.noreply.github.com> 2025-09-06 20:17:22 +0700
committerGravatar GitHub <noreply@github.com> 2025-09-06 20:17:22 +0700
commit4161e051f4437a4ab68430a4e542b76b1f335c3a (patch)
tree5c63fabb02fa9c55c9e7cf2c26c2a35a90806f35 /internal
parentd695dc09f44236be33fecc7a5ab9a326823170d9 (diff)
parent13a66428533c5f870901343dd505bcb835015430 (diff)
downloadweb-0.8.0.tar.gz
web-0.8.0.zip
v0.8.0 (#7)v0.8.0
- feat: seamless single page application (not including map & index page for now) - feat: add pages cache - feat: home page - feat: change to mobile view on 1.1/1 aspect ratio (instead of 0.8/1) - feat: always have page body in square aspect ratio - feat: remove label in like button - feat: stricter cors policy - refactor: move lightlibrary css onto my bucket - fix: use filename (not the whole path) for getting date in lightgallery - fix: don't put question mark in url if no params
Diffstat (limited to 'internal')
-rw-r--r--internal/factgiver/factgiver.go67
-rw-r--r--internal/frontmatter/parser.go2
-rw-r--r--internal/lightgallery/html_renderer.go12
-rw-r--r--internal/router/api-v1-blog-search.go16
-rw-r--r--internal/router/api-v1-general-page-body.go160
-rw-r--r--internal/router/api-v1-general-page-bottom-embeds.go75
-rw-r--r--internal/router/api-v1-general-page-footer.go75
-rw-r--r--internal/router/api-v1-general-page-header.go85
-rw-r--r--internal/router/api-v1-general-page-top-embeds.go75
-rw-r--r--internal/router/api-v1-general-page.go52
-rw-r--r--internal/router/client-cache.go5
-rw-r--r--internal/router/lang-blog-title.go1
-rw-r--r--internal/router/lang-blog.go1
-rw-r--r--internal/router/page-cache.go5
-rw-r--r--internal/templatemanager/templatemanager.go38
15 files changed, 650 insertions, 19 deletions
diff --git a/internal/factgiver/factgiver.go b/internal/factgiver/factgiver.go
new file mode 100644
index 0000000..6f77403
--- /dev/null
+++ b/internal/factgiver/factgiver.go
@@ -0,0 +1,67 @@
+package factgiver
+
+import (
+ "fmt"
+ "math/rand"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+)
+
+type FactGiver struct {
+ b2Client *b2.B2Client
+ cache map[string][]string
+ langs []string
+ factsFileName string
+ nlRe *regexp.Regexp
+ randGen *rand.Rand
+}
+
+func NewFactGiver(cfg *config.FactGiverConfig, langs []string) (*FactGiver, error) {
+ b2Client, err := b2.NewB2Client(&cfg.Storage.Config)
+ if err != nil {
+ return nil, fmt.Errorf("fail to init b2 client for a new fact giver: %s", err.Error())
+ }
+
+ factGiver := &FactGiver{
+ b2Client: b2Client,
+ cache: make(map[string][]string, len(langs)),
+ langs: langs,
+ factsFileName: cfg.FactsFileName,
+ nlRe: regexp.MustCompile(`\r?\n`),
+ randGen: rand.New(rand.NewSource(time.Now().UnixNano())),
+ }
+ if err = factGiver.initCache(); err != nil {
+ return nil, fmt.Errorf("fail to init cache for a new fact giver: %s", err.Error())
+ }
+
+ return factGiver, nil
+}
+
+func (g *FactGiver) Give(lang string) [3]string {
+ factSlice := make([]string, len(g.cache[lang]))
+ copy(factSlice, g.cache[lang])
+ g.randGen.Shuffle(len(factSlice), func(i, j int) {
+ factSlice[i], factSlice[j] = factSlice[j], factSlice[i]
+ })
+ return [3]string{factSlice[0], factSlice[1], factSlice[2]}
+}
+
+func (g *FactGiver) initCache() error {
+ for _, lang := range g.langs {
+ localFacts := strings.Replace(g.factsFileName, "*", lang, 1)
+ factsContentBytes, err := g.b2Client.ReadAll(localFacts)
+ if err != nil {
+ return fmt.Errorf("fail to read '%s' facts file: %s", lang, err.Error())
+ }
+ factsContent := string(factsContentBytes)
+ g.cache[lang] = g.nlRe.Split(factsContent, -1)
+ if g.cache[lang][len(g.cache[lang])-1] == "" {
+ g.cache[lang] = g.cache[lang][:len(g.cache[lang])-1]
+ }
+ }
+ return nil
+}
diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go
index 1a521c0..83312dc 100644
--- a/internal/frontmatter/parser.go
+++ b/internal/frontmatter/parser.go
@@ -16,6 +16,8 @@ type Metadata struct {
Thumbnail string `yaml:"thumbnail"`
Tags []string `yaml:"tags"`
Geolocation string `yaml:"geolocation"`
+ Medley string `yaml:"medley"`
+ MedleyPart int `yaml:"medleyPart"`
}
func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) {
diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go
index 17a6f2b..8e184e0 100644
--- a/internal/lightgallery/html_renderer.go
+++ b/internal/lightgallery/html_renderer.go
@@ -60,7 +60,8 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [
for _, img := range gallery.Images {
imageUrlSegments := strings.Split(img.URL, ".")
imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".")
- imageNameParts := strings.Split(imageUrlWithoutExt, "-")
+ imageUrlParts := strings.Split(imageUrlWithoutExt, "/")
+ imageNameParts := strings.Split(imageUrlParts[len(imageUrlParts)-1], "-")
var captionBuf bytes.Buffer
captionHTML := img.Caption
@@ -119,7 +120,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();
+ }
+}, {once: true});
</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID))
}
diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go
index e22fabe..41f2dec 100644
--- a/internal/router/api-v1-blog-search.go
+++ b/internal/router/api-v1-blog-search.go
@@ -1,6 +1,7 @@
package router
import (
+ "encoding/json"
"fmt"
"log/slog"
"net/url"
@@ -35,10 +36,17 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie
slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz))
}
- pages, err := b2Client.Scan(lang + "/")
- if err != nil {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err))
+ var pages []*b2.BlogPage
+ if pagesBytes, ok := PCache.Get("blog-search.pages-list"); pagesBytes != nil || ok {
+ json.Unmarshal(pagesBytes, &pages)
+ } else {
+ pages, err = b2Client.Scan(lang + "/")
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err))
+ }
+ pagesBytes, _ := json.Marshal(pages)
+ PCache.SetWithTTL("blog-search.pages-list", pagesBytes, int64(len(pagesBytes)), 5*time.Minute)
}
encodedQuery := c.Request().URI().QueryString()
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..f27e0c0
--- /dev/null
+++ b/internal/router/api-v1-general-page-body.go
@@ -0,0 +1,160 @@
+package router
+
+import (
+ "fmt"
+ "html/template"
+ "log/slog"
+ "math/rand"
+ "net/url"
+ "regexp"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/factgiver"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+ "github.com/yuin/goldmark"
+)
+
+var FactGiver *factgiver.FactGiver
+
+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()
+
+ cacheKey := fmt.Sprintf("body.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ 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,
+ "QueryString": string(c.Request().URI().QueryString()),
+ }
+ 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, ",")
+ values["Title"] = l[lang].BlogSearch.Header
+
+ 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["MapLocationX"] = x
+ values["MapLocationY"] = y
+ values["MapLocationAreaMeters"] = areaError
+ values["Title"] = metadata.Title
+ values["ParsedMarkdown"] = template.HTML(parsedMarkdown)
+
+ additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html")
+ } else if len(pathParts) == 1 {
+ values["Title"] = l[lang].HomePage.Header
+ values["FilledHeartCount"] = uint(40)
+ values["OutlineHeartCount"] = uint(40)
+ values["GifName"] = fmt.Sprintf("otter-%d.gif", rand.Int()%3+1)
+ values["FunFacts"] = FactGiver.Give(lang)
+ additionalTemplates = append(additionalTemplates, "views/pages/home-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")
+ }
+
+ go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute)
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(content)
+ }
+}
diff --git a/internal/router/api-v1-general-page-bottom-embeds.go b/internal/router/api-v1-general-page-bottom-embeds.go
new file mode 100644
index 0000000..c4d9543
--- /dev/null
+++ b/internal/router/api-v1-general-page-bottom-embeds.go
@@ -0,0 +1,75 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "net/url"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("general-page-bottom-embeds", "views/partials/general-page-bottom-embeds.html")
+}
+
+func Api_V1_GeneralPage_BottomEmbeds(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)
+
+ 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()
+ cacheKey := fmt.Sprintf("bottom-embeds.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ 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,
+ "QueryString": string(c.Request().URI().QueryString()),
+ }
+ 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")
+ } else if len(pathParts) == 1 {
+ additionalTemplates = append(additionalTemplates, "views/pages/home-page.html")
+ }
+
+ content, err := tm.Render("general-page-bottom-embeds", 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")
+ }
+
+ go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute)
+ 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..003f54c
--- /dev/null
+++ b/internal/router/api-v1-general-page-footer.go
@@ -0,0 +1,75 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "net/url"
+ "slices"
+ "strings"
+ "time"
+
+ "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()
+
+ cacheKey := fmt.Sprintf("footer.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ 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,
+ "QueryString": string(c.Request().URI().QueryString()),
+ }
+ 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")
+ } else if len(pathParts) == 1 {
+ additionalTemplates = append(additionalTemplates, "views/pages/home-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")
+ }
+
+ go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute)
+ 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
new file mode 100644
index 0000000..80228f7
--- /dev/null
+++ b/internal/router/api-v1-general-page-header.go
@@ -0,0 +1,85 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "net/url"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+)
+
+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)
+
+ 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()
+
+ cacheKey := fmt.Sprintf("header.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ 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,
+ "QueryString": string(c.Request().URI().QueryString()),
+ }
+ 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")
+ } else if len(pathParts) == 1 {
+ values["Title"] = l[lang].HomePage.Header
+ additionalTemplates = append(additionalTemplates, "views/pages/home-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")
+ }
+
+ go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute)
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(content)
+ }
+}
diff --git a/internal/router/api-v1-general-page-top-embeds.go b/internal/router/api-v1-general-page-top-embeds.go
new file mode 100644
index 0000000..78021b2
--- /dev/null
+++ b/internal/router/api-v1-general-page-top-embeds.go
@@ -0,0 +1,75 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "net/url"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("general-page-top-embeds", "views/partials/general-page-top-embeds.html")
+}
+
+func Api_V1_GeneralPage_TopEmbeds(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()
+
+ cacheKey := fmt.Sprintf("top-embeds.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ 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,
+ "QueryString": string(c.Request().URI().QueryString()),
+ }
+ 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")
+ } else if len(pathParts) == 1 {
+ additionalTemplates = append(additionalTemplates, "views/pages/home-page.html")
+ }
+
+ content, err := tm.Render("general-page-top-embeds", 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")
+ }
+
+ go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute)
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(content)
+ }
+}
diff --git a/internal/router/api-v1-general-page.go b/internal/router/api-v1-general-page.go
new file mode 100644
index 0000000..d0ea559
--- /dev/null
+++ b/internal/router/api-v1-general-page.go
@@ -0,0 +1,52 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("general-page", "views/layouts/general-page.html")
+}
+
+func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ path := c.Path()
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 0 {
+ return c.Status(fiber.ErrBadRequest.Code).SendString("url path 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))
+ }
+
+ cacheKey := "general-page." + lang
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(val)
+ }
+
+ content, err := tm.Render("general-page", fiber.Map{
+ "L": l[lang],
+ "Lang": lang,
+ "QueryString": string(c.Request().URI().QueryString()),
+ })
+ 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")
+ }
+
+ go PCache.Set(cacheKey, content, int64(len(content)))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
+ return c.Status(fiber.StatusOK).Type("html").Send(content)
+ }
+}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index 81d7641..e448fb3 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -11,11 +11,6 @@ import (
"golang.org/x/crypto/argon2"
)
-type PageLike struct {
- PageRef string
- UserId string
-}
-
type ClientCache struct {
hashMap map[string]string
hashMapMutex sync.RWMutex
diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go
index bafb0d2..d6ca917 100644
--- a/internal/router/lang-blog-title.go
+++ b/internal/router/lang-blog-title.go
@@ -56,6 +56,7 @@ func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client
"MapLocationY": y,
"MapLocationAreaMeters": areaError,
"Lang": lang,
+ "QueryString": string(c.Request().URI().QueryString()),
})
if err != nil {
slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error()))
diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go
index dd3f079..38a6597 100644
--- a/internal/router/lang-blog.go
+++ b/internal/router/lang-blog.go
@@ -76,6 +76,7 @@ func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B
content, err := tm.Render("blog-catalogue", fiber.Map{
"QuerySort": querySort,
"QueryTags": strings.Join(queryTags, ","),
+ "QueryString": string(c.Request().URI().QueryString()),
"Tags": tagsArray,
"Lang": lang,
"L": l[lang],
diff --git a/internal/router/page-cache.go b/internal/router/page-cache.go
new file mode 100644
index 0000000..01e4792
--- /dev/null
+++ b/internal/router/page-cache.go
@@ -0,0 +1,5 @@
+package router
+
+import "github.com/dgraph-io/ristretto/v2"
+
+var PCache *ristretto.Cache[string, []byte]
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index f89d0c8..4bf9a49 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -22,13 +22,22 @@ type TemplateManagerTemplates struct {
Files []string
}
+var templateFuncMap = template.FuncMap{
+ "contains": strings.Contains,
+ "iterate": func(count uint) []uint {
+ items := make([]uint, count)
+ for i := range count {
+ items[i] = i
+ }
+ return items
+ },
+}
+
func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) {
templateMap := make(map[string]templateManagerRender)
for _, tmplStruct := range templates {
- tmpl := template.New("").Funcs(template.FuncMap{
- "contains": strings.Contains,
- })
+ tmpl := template.New("").Funcs(templateFuncMap)
tmpl, err := tmpl.ParseFiles(tmplStruct.Files...)
if err != nil {
return nil, err
@@ -44,14 +53,29 @@ 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.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)
+ }
+ }
+
var buf bytes.Buffer
- err := tmpl.Tmpl.ExecuteTemplate(&buf, tmpl.Main, data)
+ err = tempTmpl.ExecuteTemplate(&buf, tmpl.Main, data)
return buf.Bytes(), err
}
@@ -60,9 +84,7 @@ func (tm *TemplateManager) Add(name string, files ...string) error {
return fmt.Errorf("you can't add template without any files")
}
- tmpl := template.New("").Funcs(template.FuncMap{
- "contains": strings.Contains,
- })
+ tmpl := template.New("").Funcs(templateFuncMap)
tmpl, err := tmpl.ParseFiles(files...)
if err != nil {