summaryrefslogtreecommitdiff
path: root/internal/router
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'internal/router')
-rw-r--r--internal/router/api-v1-blog-search.go34
-rw-r--r--internal/router/api-v1-email-is-in-verification.go58
-rw-r--r--internal/router/api-v1-email-send-verification-code.go98
-rw-r--r--internal/router/api-v1-email-verify.go52
-rw-r--r--internal/router/api-v1-general-page-body.go243
-rw-r--r--internal/router/api-v1-general-page-bottom-embeds.go86
-rw-r--r--internal/router/api-v1-general-page-footer.go86
-rw-r--r--internal/router/api-v1-general-page-header.go98
-rw-r--r--internal/router/api-v1-general-page-top-embeds.go86
-rw-r--r--internal/router/api-v1-general-page.go59
-rw-r--r--internal/router/api-v1-like.go17
-rw-r--r--internal/router/api-v1-subs.go67
-rw-r--r--internal/router/client-cache.go270
-rw-r--r--internal/router/lang-blog-title.go82
-rw-r--r--internal/router/lang-blog.go93
-rw-r--r--internal/router/lang-map.go15
-rw-r--r--internal/router/lang-user-unsubscribe.go71
-rw-r--r--internal/router/page-cache.go5
-rw-r--r--internal/router/root.go27
19 files changed, 247 insertions, 1300 deletions
diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go
index 5b1d14d..a809783 100644
--- a/internal/router/api-v1-blog-search.go
+++ b/internal/router/api-v1-blog-search.go
@@ -1,7 +1,6 @@
package router
import (
- "encoding/json"
"fmt"
"log/slog"
"net/url"
@@ -10,49 +9,36 @@ import (
"strings"
"time"
- "github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/b2"
"github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
)
func init() {
- assert(0, tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html"))
+ tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html")
}
-func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
+func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
sort := c.Query("sort")
lang := c.Query("lang")
tz := c.Query("tz")
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
+ if !slices.Contains(langs, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
}
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
- langIsAvailable:
loc, err := time.LoadLocation(tz)
if err != nil {
loc = time.UTC
slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz))
}
- cacheKey := "blog-search." + lang + ".pages-list"
- var pages []*b2.BlogPage
- if pagesBytes, ok := PCache.Get(cacheKey); 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(cacheKey, pagesBytes, int64(len(pagesBytes)), 5*time.Minute)
+ 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))
}
encodedQuery := c.Request().URI().QueryString()
@@ -83,8 +69,6 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []config.Availab
"ShortDescription": page.Metadata.ShortDescription,
"Thumbnail": page.Metadata.Thumbnail,
"Tags": page.Metadata.Tags,
- "LikeCount": CCache.GetLikeCount(page.FileName),
- "ViewCount": CCache.GetViewCount(page.FileName),
})
break
}
diff --git a/internal/router/api-v1-email-is-in-verification.go b/internal/router/api-v1-email-is-in-verification.go
deleted file mode 100644
index 83c783c..0000000
--- a/internal/router/api-v1-email-is-in-verification.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package router
-
-import (
- "fmt"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_IsInVerification(l map[string]*locale.LocaleConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
-
- id := c.IP()
- lang := "en"
-
- var referer, path string
- var pathParts []string
- var urlStruct *url.URL
- var err error
-
- referer = c.Get("Referer", "")
- if referer == "" {
- goto skipFetchingLang
- }
-
- urlStruct, err = url.ParseRequestURI(referer)
- if err != nil {
- goto skipFetchingLang
- }
-
- path = urlStruct.EscapedPath()
- pathParts = strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 0 {
- goto skipFetchingLang
- }
-
- lang = pathParts[0]
-
- skipFetchingLang:
- isAllowed, whenAllowed, codeExpiry := Mailer.IsAllowedToRetryVerification(id)
- if !isAllowed {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Neutral", fiber.StatusOK, lang, strings.ReplaceAll(l[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(whenAllowed.UnixMilli()),
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
- return api_v1_email_sendStatusHtml(c, "email-message", l, "OK", fiber.StatusOK, lang, "", map[string]string{
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
-}
diff --git a/internal/router/api-v1-email-send-verification-code.go b/internal/router/api-v1-email-send-verification-code.go
deleted file mode 100644
index 9372eb3..0000000
--- a/internal/router/api-v1-email-send-verification-code.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package router
-
-import (
- "fmt"
- "html/template"
- "log/slog"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_SendVerificationCode(l map[string]*locale.LocaleConfig) 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}/user'")
- }
-
- lang := pathParts[0]
- id := c.IP()
-
- email := c.FormValue("email")
- if email == "" {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailEmpty, map[string]string{})
- }
-
- isTaken, err := Mailer.MailIsTaken(email)
- if err != nil {
- slog.Error("failed to check if address is already taken", slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationCodeSendingError, map[string]string{})
- }
- if isTaken {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailTaken, map[string]string{})
- }
-
- if isAllowed, whenAllowed, _ := Mailer.IsAllowedToRetryVerification(id); !isAllowed {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, strings.ReplaceAll(l[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(whenAllowed.UnixMilli()),
- })
- }
-
- if previousEmail, _, _ := Mailer.GetInfo(Mailer.GetHash(id)); previousEmail == email {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailAlreadyValidated, map[string]string{})
- }
-
- if err = Mailer.SendVerificationCode(id, email, lang); err != nil {
- slog.Error("failed to send a verification code", slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationCodeSendingError, map[string]string{})
- }
-
- _, endTime, codeExpiry := Mailer.IsAllowedToRetryVerification(id)
- return api_v1_email_sendStatusHtml(c, "email-message", l, "OK", fiber.StatusOK, lang, strings.ReplaceAll(l[lang].UserProfile.VerificationCodeSent, "{}", endTime.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(endTime.UnixMilli()),
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
-}
-
-func api_v1_email_sendStatusHtml(c *fiber.Ctx, divId string, l map[string]*locale.LocaleConfig, status string, code int, lang string, message string, dataAttributes map[string]string) error {
- sterileDataset := make(map[string]interface{})
- for k, v := range dataAttributes {
- sterileDataset[k] = template.HTMLAttr(fmt.Sprintf("data-%s=\"%s\"", k, template.HTMLEscapeString(v)))
- }
-
- content, err := tm.Render("personal-page-status", fiber.Map{
- "L": l[lang],
- "Status": status,
- "Message": message,
- "StatusId": divId,
- "DataAttributes": sterileDataset,
- })
- if err != nil {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- slog.Error("failed to render the email status message", slog.String("error", err.Error()), slog.String("div_id", divId), slog.String("message", message))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("Failed to render the email status message, please ask administrator for a more detailed cause")
- }
- c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
- return c.Status(code).Send(content)
-}
diff --git a/internal/router/api-v1-email-verify.go b/internal/router/api-v1-email-verify.go
deleted file mode 100644
index 7fbfdec..0000000
--- a/internal/router/api-v1-email-verify.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_Verify(l map[string]*locale.LocaleConfig) 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}/user'")
- }
-
- lang := pathParts[0]
-
- verificationCode := c.FormValue("email_code")
- if verificationCode == "" {
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationEmpty, map[string]string{})
- }
-
- if err = Mailer.Verify(verificationCode, lang); err != nil {
- slog.Error("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationFailed, map[string]string{})
- }
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "OK", fiber.StatusOK, lang, l[lang].UserProfile.VerificationSuccess+"\n\n"+l[lang].UserProfile.RefreshPage, map[string]string{
- "hide-verification-panel": "true",
- })
- }
-}
diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go
deleted file mode 100644
index b5567b7..0000000
--- a/internal/router/api-v1-general-page-body.go
+++ /dev/null
@@ -1,243 +0,0 @@
-package router
-
-import (
- "bytes"
- "fmt"
- "html/template"
- "log/slog"
- "math/rand"
- "net/url"
- "regexp"
- "slices"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/internal/blogtrigger"
- "github.com/SayaAndy/saya-today-web/internal/factgiver"
- "github.com/SayaAndy/saya-today-web/internal/frontmatter"
- "github.com/SayaAndy/saya-today-web/internal/mailer"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
- "github.com/yuin/goldmark"
-)
-
-var FactGiver *factgiver.FactGiver
-var Mailer *mailer.Mailer
-var BlogTrigger *blogtrigger.BlogTriggerScheduler
-
-func init() {
- assert(0, tm.Add("general-page-body", "views/partials/general-page-body.html"))
-}
-
-func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, 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()
- trimmedPath := strings.Trim(path, "/")
-
- cacheKey := fmt.Sprintf("body.%s", trimmedPath)
- if val, ok := PCache.Get(cacheKey); !strings.HasSuffix(trimmedPath, "user") && val != nil && ok {
- c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
- return c.Status(fiber.StatusOK).Type("html").Send(val)
- }
-
- lang := ""
- pathParts := strings.Split(trimmedPath, "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- values := fiber.Map{
- "L": l[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "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]))
- }
-
- tagsArray, err := getTags(b2Client, lang)
- if err != nil {
- slog.Warn("failed to get the available tags", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to gather available tags")
- }
-
- 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) == 2 && pathParts[1] == "user" {
- values["Title"] = l[lang].UserProfile.Header
-
- email, _, err := Mailer.GetInfo(Mailer.GetHash(c.IP()))
- if err != nil {
- slog.Error("get info from mailer about a client", slog.String("error", err.Error()))
- }
-
- tagsArray, err := getTags(b2Client, lang)
- if err != nil {
- slog.Warn("failed to get the available tags", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to gather available tags")
- }
-
- subscriptionType, tags, err := Mailer.GetSubscriptions(c.IP())
- if err != nil {
- slog.Warn("failed to get the user subscriptions", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to get the user subscriptions")
- }
-
- switch subscriptionType {
- case mailer.None:
- values["TagsPicked"] = "none"
- case mailer.All:
- values["TagsPicked"] = "all"
- case mailer.Specific:
- values["TagsPicked"] = "specific"
- }
-
- values["TagsPickedList"] = tags
-
- values["Email"] = email
- values["EmailCode"] = c.Query("email_code")
- values["ExistingTags"] = tagsArray
-
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.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)
- 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")
-
- go CCache.View(c.IP(), pathParts[2])
- } 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")
- } else if len(pathParts) == 0 {
- values["Title"] = "Choose Your Language"
- values["AvailableLanguages"] = langs
- additionalTemplates = append(additionalTemplates, "views/pages/language-pick.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)
- }
-}
-
-func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
- metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
- if err != nil {
- return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
- }
-
- var buf bytes.Buffer
- if err := md.Convert(markdown, &buf); err != nil {
- return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
- }
-
- return metadata, buf.String(), nil
-}
-
-type Tag struct {
- Name string `json:"Name" yaml:"name"`
- Count int `json:"Count" yaml:"count"`
-}
-
-func getTags(b2Client *b2.B2Client, lang string) (tags []Tag, err error) {
- pages, err := b2Client.Scan(lang + "/")
- if err != nil {
- slog.Warn("failed to scan pages via b2", slog.String("error", err.Error()))
- return nil, fmt.Errorf("failed to scan pages via b2: %w", err)
- }
-
- 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("lang", lang))
-
- 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)
- })
-
- return tagsArray, nil
-}
diff --git a/internal/router/api-v1-general-page-bottom-embeds.go b/internal/router/api-v1-general-page-bottom-embeds.go
deleted file mode 100644
index b408187..0000000
--- a/internal/router/api-v1-general-page-bottom-embeds.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("general-page-bottom-embeds", "views/partials/general-page-bottom-embeds.html"))
-}
-
-func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, 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)
- }
-
- lang := ""
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- values := fiber.Map{
- "L": l[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "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) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.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")
- } else if len(pathParts) == 0 {
- additionalTemplates = append(additionalTemplates, "views/pages/language-pick.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
deleted file mode 100644
index 99d899a..0000000
--- a/internal/router/api-v1-general-page-footer.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("general-page-footer", "views/partials/general-page-footer.html"))
-}
-
-func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) 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)
- }
-
- lang := ""
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- values := fiber.Map{
- "L": l[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "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) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.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")
- } else if len(pathParts) == 0 {
- additionalTemplates = append(additionalTemplates, "views/pages/language-pick.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
deleted file mode 100644
index 768417d..0000000
--- a/internal/router/api-v1-general-page-header.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("general-page-header", "views/partials/general-page-header.html"))
-}
-
-func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, 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)
- }
-
- lang := ""
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- values := fiber.Map{
- "L": l[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "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) == 2 && pathParts[1] == "user" {
- values["Title"] = l[lang].UserProfile.Header
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.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")
- } else if len(pathParts) == 0 {
- values["Title"] = "Choose Your Language"
- additionalTemplates = append(additionalTemplates, "views/pages/language-pick.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
deleted file mode 100644
index f3e480b..0000000
--- a/internal/router/api-v1-general-page-top-embeds.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("general-page-top-embeds", "views/partials/general-page-top-embeds.html"))
-}
-
-func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) 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)
- }
-
- lang := ""
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- values := fiber.Map{
- "L": l[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "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) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.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")
- } else if len(pathParts) == 0 {
- additionalTemplates = append(additionalTemplates, "views/pages/language-pick.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
deleted file mode 100644
index c8b4362..0000000
--- a/internal/router/api-v1-general-page.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("general-page", "views/layouts/general-page.html"))
-}
-
-func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- path := c.Path()
-
- lang := ""
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 1 && pathParts[0] == "" {
- pathParts = []string{}
- }
- if len(pathParts) > 0 {
- lang = pathParts[0]
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'")
- }
-
- langIsAvailable:
- queryString := string(c.Request().URI().QueryString())
- cacheKey := fmt.Sprintf("general-page.%s?%s", lang, queryString)
- 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": 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/api-v1-like.go b/internal/router/api-v1-like.go
index 323c457..1732c77 100644
--- a/internal/router/api-v1-like.go
+++ b/internal/router/api-v1-like.go
@@ -8,15 +8,14 @@ import (
"strings"
"github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
)
func init() {
- assert(0, tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html"))
+ tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html")
}
-func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error {
+func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
@@ -42,12 +41,12 @@ func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c
return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink))
}
+ ip := c.IP()
newLikeStatus, err := strconv.ParseBool(c.FormValue("like", "true"))
if err != nil {
return c.Status(fiber.ErrBadRequest.Code).SendString("invalid 'like' value")
}
- ip := c.IP()
if newLikeStatus {
CCache.LikeOn(ip, page)
} else {
@@ -57,9 +56,7 @@ func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c
slog.Debug("someone pressed the like button!", slog.String("ip", ip), slog.String("page", page), slog.String("new_like_status", fmt.Sprint(newLikeStatus)))
if c.Get("HX-Request", "false") == "true" {
content, err := tm.Render("blog-page-like-button", fiber.Map{
- "L": l[lang],
- "Liked": newLikeStatus,
- "LikedCount": CCache.GetLikeCount(page),
+ "Liked": newLikeStatus,
})
if err != nil {
slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error()))
@@ -73,7 +70,7 @@ func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c
}
}
-func Api_V1_Like_Get(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error {
+func Api_V1_Like_Get(b2 *b2.B2Client) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
@@ -105,9 +102,7 @@ func Api_V1_Like_Get(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c
slog.Debug("someone requested the like status!", slog.String("ip", ip), slog.String("page", page), slog.Bool("like_status", likeStatus))
if c.Get("HX-Request", "false") == "true" {
content, err := tm.Render("blog-page-like-button", fiber.Map{
- "L": l[lang],
- "Liked": likeStatus,
- "LikedCount": CCache.GetLikeCount(page),
+ "Liked": likeStatus,
})
if err != nil {
slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error()))
diff --git a/internal/router/api-v1-subs.go b/internal/router/api-v1-subs.go
deleted file mode 100644
index 72f16e0..0000000
--- a/internal/router/api-v1-subs.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package router
-
-import (
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/mailer"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Subs_Put(l map[string]*locale.LocaleConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
-
- id := c.IP()
- lang := "en"
-
- var referer, path string
- var pathParts []string
- var urlStruct *url.URL
- var err error
-
- referer = c.Get("Referer", "")
- if referer == "" {
- goto skipFetchingLang
- }
-
- urlStruct, err = url.ParseRequestURI(referer)
- if err != nil {
- goto skipFetchingLang
- }
-
- path = urlStruct.EscapedPath()
- pathParts = strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 0 {
- goto skipFetchingLang
- }
-
- lang = pathParts[0]
-
- skipFetchingLang:
- subscriptionType := c.FormValue("tags")
- var subscriptionTypeEnum mailer.SubscriptionType
- switch subscriptionType {
- case "all":
- subscriptionTypeEnum = mailer.All
- case "none":
- subscriptionTypeEnum = mailer.None
- case "specific":
- subscriptionTypeEnum = mailer.Specific
- default:
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "Failed", fiber.StatusUnprocessableEntity, lang, l[lang].UserProfile.SubscribeInvalidType, map[string]string{})
- }
-
- specificTags := c.FormValue("tags_picked")
- if err = Mailer.Subscribe(Mailer.GetHash(id), subscriptionTypeEnum, specificTags); err != nil {
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "Failed", fiber.StatusUnprocessableEntity, lang, l[lang].UserProfile.FailedToSubscribe, map[string]string{})
- }
-
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "OK", fiber.StatusOK, lang, l[lang].UserProfile.SubscribedSuccessfully, map[string]string{})
- }
-}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index e7cda30..3104a66 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -1,11 +1,8 @@
package router
import (
- "database/sql"
"encoding/base64"
- "fmt"
"log/slog"
- "strings"
"sync"
"golang.org/x/crypto/argon2"
@@ -13,133 +10,35 @@ import (
type ClientCache struct {
hashMap map[string]string
- hashMapMutex sync.RWMutex
-
- likePageMap map[string]map[string]struct{}
- viewPageMap map[string]map[string]struct{}
- pageMutexMap map[string]*sync.RWMutex
- pageMutexMapMutex sync.Mutex
-
- salt []byte
- db *sql.DB
+ mutexLikeMap map[string]*sync.Mutex
+ mutexHashMap map[string]*sync.Mutex
+ likePageMap map[string]map[string]struct{}
+ salt []byte
}
var CCache *ClientCache
-func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
- tx, err := db.Begin()
- if err != nil {
- return nil, fmt.Errorf("fail to init transaction with db to fill cache: %w", err)
- }
- slog.Debug("began db transaction", slog.String("method", "NewClientCache"))
-
- rows, err := tx.Query("select * from blog_likes;")
- if err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
- return nil, fmt.Errorf("fail to query db for blog_likes to fill cache: %w", err)
- }
-
- likePageMap := make(map[string]map[string]struct{})
- viewPageMap := make(map[string]map[string]struct{})
- pageMutexMap := make(map[string]*sync.RWMutex)
-
- for rows.Next() {
- var pageRef string
- var userId []byte
- if err = rows.Scan(&pageRef, &userId); err != nil {
- tx.Rollback()
- return nil, fmt.Errorf("fail scanning blog_likes to fill cache: %w", err)
- }
- userIdString := base64.RawStdEncoding.EncodeToString(userId)
- if _, ok := likePageMap[pageRef]; !ok {
- likePageMap[pageRef] = make(map[string]struct{})
- viewPageMap[pageRef] = make(map[string]struct{})
- pageMutexMap[pageRef] = &sync.RWMutex{}
- }
- likePageMap[pageRef][userIdString] = struct{}{}
- viewPageMap[pageRef][userIdString] = struct{}{}
- }
-
- rows, err = tx.Query("select * from blog_views;")
- if err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
- return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err)
- }
-
- for rows.Next() {
- var pageRef string
- var userId []byte
- if err = rows.Scan(&pageRef, &userId); err != nil {
- tx.Rollback()
- return nil, fmt.Errorf("fail scanning blog_views to fill cache: %w", err)
- }
- userIdString := base64.RawStdEncoding.EncodeToString(userId)
- if _, ok := viewPageMap[pageRef]; !ok {
- viewPageMap[pageRef] = make(map[string]struct{})
- pageMutexMap[pageRef] = &sync.RWMutex{}
- }
- viewPageMap[pageRef][userIdString] = struct{}{}
- }
-
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
- return nil, fmt.Errorf("fail to commit transaction in db: %w", err)
- }
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
-
+func NewClientCache(salt []byte) *ClientCache {
return &ClientCache{
hashMap: make(map[string]string),
- likePageMap: likePageMap,
- viewPageMap: viewPageMap,
- pageMutexMap: pageMutexMap,
+ mutexLikeMap: make(map[string]*sync.Mutex),
+ mutexHashMap: make(map[string]*sync.Mutex),
+ likePageMap: make(map[string]map[string]struct{}),
salt: salt,
- db: db,
- }, nil
-}
-
-func (c *ClientCache) Close() error {
- tx, err := c.db.Begin()
- if err != nil {
- return fmt.Errorf("fail to init transaction with db to dump cache: %w", err)
- }
- slog.Debug("began db transaction in ClientCache.Close")
-
- if err = batchSave(tx, "blog_likes", c.likePageMap); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
- return fmt.Errorf("fail to save blog_likes: %s", err)
}
-
- if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
- return fmt.Errorf("fail to save blog_views: %s", err)
- }
-
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
- return fmt.Errorf("fail to commit all the changes related to cache: %s", err)
- }
-
- slog.Debug("ended db transaction in ClientCache.Close")
- return nil
}
func (c *ClientCache) GetHash(id string) string {
- c.hashMapMutex.RLock()
if val, ok := c.hashMap[id]; ok {
- c.hashMapMutex.RUnlock()
slog.Debug("gave an old hash", slog.String("hash", val))
return val
}
- c.hashMapMutex.RUnlock()
- c.hashMapMutex.Lock()
- defer c.hashMapMutex.Unlock()
+ if _, ok := c.mutexHashMap[id]; !ok {
+ c.mutexHashMap[id] = &sync.Mutex{}
+ }
+ c.mutexHashMap[id].Lock()
+ defer c.mutexHashMap[id].Unlock()
if val, ok := c.hashMap[id]; ok {
slog.Debug("gave a newly generated hash", slog.String("hash", val))
@@ -151,25 +50,7 @@ func (c *ClientCache) GetHash(id string) string {
return c.hashMap[id]
}
-func (c *ClientCache) getPageMutex(page string) *sync.RWMutex {
- c.pageMutexMapMutex.Lock()
- defer c.pageMutexMapMutex.Unlock()
-
- if mutex, ok := c.pageMutexMap[page]; ok {
- return mutex
- }
-
- c.pageMutexMap[page] = &sync.RWMutex{}
- return c.pageMutexMap[page]
-}
-
func (c *ClientCache) GetLikeStatus(id string, page string) bool {
- page = strings.Clone(page)
-
- mutex := c.getPageMutex(page)
- mutex.RLock()
- defer mutex.RUnlock()
-
if _, ok := c.likePageMap[page]; !ok {
return false
}
@@ -177,27 +58,15 @@ func (c *ClientCache) GetLikeStatus(id string, page string) bool {
return ok
}
-func (c *ClientCache) GetLikeCount(page string) int {
- page = strings.Clone(page)
-
- mutex := c.getPageMutex(page)
- mutex.RLock()
- defer mutex.RUnlock()
-
- if userSet, ok := c.likePageMap[page]; ok {
- return len(userSet)
+func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) {
+ if _, ok := c.mutexLikeMap[id]; !ok {
+ c.mutexLikeMap[id] = &sync.Mutex{}
}
- return 0
-}
+ c.mutexLikeMap[id].Lock()
+ defer c.mutexLikeMap[id].Unlock()
-func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) {
- page = strings.Clone(page)
hash := c.GetHash(id)
- mutex := c.getPageMutex(page)
- mutex.Lock()
- defer mutex.Unlock()
-
if userSet, ok := c.likePageMap[page]; ok {
_, alreadyLiked = userSet[hash]
c.likePageMap[page][hash] = struct{}{}
@@ -210,16 +79,16 @@ func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) {
}
func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) {
- page = strings.Clone(page)
- hash := c.GetHash(id)
-
- mutex := c.getPageMutex(page)
- mutex.Lock()
- defer mutex.Unlock()
+ if _, ok := c.mutexLikeMap[id]; !ok {
+ c.mutexLikeMap[id] = &sync.Mutex{}
+ }
+ c.mutexLikeMap[id].Lock()
+ defer c.mutexLikeMap[id].Unlock()
if _, ok := c.likePageMap[page]; !ok {
return true
}
+ hash := c.GetHash(id)
if _, ok := c.likePageMap[page][hash]; !ok {
return true
}
@@ -227,94 +96,3 @@ func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) {
delete(c.likePageMap[page], hash)
return false
}
-
-func (c *ClientCache) GetViewStatus(id string, page string) bool {
- page = strings.Clone(page)
-
- mutex := c.getPageMutex(page)
- mutex.RLock()
- defer mutex.RUnlock()
-
- if _, ok := c.viewPageMap[page]; !ok {
- return false
- }
- _, ok := c.viewPageMap[page][c.GetHash(id)]
- return ok
-}
-
-func (c *ClientCache) GetViewCount(page string) int {
- page = strings.Clone(page)
-
- mutex := c.getPageMutex(page)
- mutex.RLock()
- defer mutex.RUnlock()
-
- if userSet, ok := c.viewPageMap[page]; ok {
- return len(userSet)
- }
- return 0
-}
-
-func (c *ClientCache) View(id string, page string) {
- page = strings.Clone(page)
- hash := c.GetHash(id)
-
- mutex := c.getPageMutex(page)
- mutex.Lock()
- defer mutex.Unlock()
-
- if _, ok := c.viewPageMap[page]; !ok {
- c.viewPageMap[page] = make(map[string]struct{})
- }
- c.viewPageMap[page][hash] = struct{}{}
-}
-
-func batchSave(tx *sql.Tx, table string, pageMap map[string]map[string]struct{}) (err error) {
- if _, err = tx.Exec(fmt.Sprintf("delete from %s;", table)); err != nil {
- return fmt.Errorf("fail to truncate table %s: %w", table, err)
- }
-
- userIdBytes := make(map[string][]byte)
-
- sqlStatement := fmt.Sprintf(`
- INSERT OR IGNORE INTO %s (page_ref, user_id)
- VALUES %s(?, ?);
- `, table, strings.Repeat("(?, ?), ", 99))
- sqlStatementVars := make([]any, 0, 200)
-
- for pageRef, userSet := range pageMap {
- for userId := range userSet {
- if _, ok := userIdBytes[userId]; !ok {
- userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId)
- if err != nil {
- slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error()))
- continue
- }
- }
-
- sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId]))
- if len(sqlStatementVars) < 200 {
- continue
- }
-
- if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil {
- slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error()))
- }
-
- sqlStatementVars = make([]any, 0, 200)
- }
- }
-
- if len(sqlStatementVars) > 0 {
- sqlStatement = fmt.Sprintf(`
- INSERT OR IGNORE INTO %s (page_ref, user_id)
- VALUES %s(?, ?);
- `, table, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1))
-
- if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil {
- slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error()))
- }
- }
-
- return nil
-}
diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go
new file mode 100644
index 0000000..bafb0d2
--- /dev/null
+++ b/internal/router/lang-blog-title.go
@@ -0,0 +1,82 @@
+package router
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "log/slog"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/frontmatter"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+ "github.com/yuin/goldmark"
+)
+
+func init() {
+ tm.Add("blog-page", "views/layouts/general-page.html", "views/pages/blog-page.html")
+}
+
+func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ ip := c.IP()
+ slog.Debug("client entering blog page", slog.String("ip", ip), slog.String("page", c.Path()))
+
+ lang := c.Params("lang")
+ if !slices.Contains(langs, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
+ }
+
+ metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+c.Params("title"))
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title")))
+ }
+
+ 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]
+ }
+
+ content, err := tm.Render("blog-page", fiber.Map{
+ "Title": metadata.Title,
+ "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"),
+ "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()),
+ "ParsedMarkdown": template.HTML(parsedMarkdown),
+ "MapLocationX": x,
+ "MapLocationY": y,
+ "MapLocationAreaMeters": areaError,
+ "Lang": lang,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ }
+}
+
+func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
+ metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
+ }
+
+ var buf bytes.Buffer
+ if err := md.Convert(markdown, &buf); err != nil {
+ return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
+ }
+
+ return metadata, buf.String(), nil
+}
diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go
new file mode 100644
index 0000000..e5220c6
--- /dev/null
+++ b/internal/router/lang-blog.go
@@ -0,0 +1,93 @@
+package router
+
+import (
+ "fmt"
+ "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"
+)
+
+func init() {
+ tm.Add("blog-catalogue", "views/layouts/general-page.html", "views/pages/blog-catalogue.html")
+}
+
+func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ lang := c.Params("lang")
+ if !slices.Contains(langs, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
+ }
+
+ 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()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ 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 + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ status = fiber.StatusPartialContent
+ pages = []*b2.BlogPage{}
+ }
+
+ 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)
+ })
+
+ content, err := tm.Render("blog-catalogue", fiber.Map{
+ "QuerySort": querySort,
+ "QueryTags": strings.Join(queryTags, ","),
+ "Tags": tagsArray,
+ "Lang": lang,
+ "L": l[lang],
+ "PublishedYear": "2025",
+ "Title": l[lang].BlogSearch.Header,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Status(status).Send(content)
+ }
+}
diff --git a/internal/router/lang-map.go b/internal/router/lang-map.go
index 619b05b..e94d756 100644
--- a/internal/router/lang-map.go
+++ b/internal/router/lang-map.go
@@ -3,30 +3,27 @@ package router
import (
"fmt"
"log/slog"
+ "slices"
"strconv"
"strings"
- "github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/b2"
"github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
)
func init() {
- assert(0, tm.Add("global-map", "views/pages/global-map.html"))
+ tm.Add("global-map", "views/pages/global-map.html")
}
-func Lang_Map(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
+func Lang_Map(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
lang := c.Params("lang")
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
+ if !slices.Contains(langs, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
}
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
- langIsAvailable:
pages, err := b2Client.Scan(lang + "/")
status := fiber.StatusOK
if err != nil {
diff --git a/internal/router/lang-user-unsubscribe.go b/internal/router/lang-user-unsubscribe.go
deleted file mode 100644
index c39f5e7..0000000
--- a/internal/router/lang-user-unsubscribe.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package router
-
-import (
- "fmt"
- "html/template"
- "log/slog"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("unsubscribe-page", "views/pages/unsubscribe-page.html"))
-}
-
-func Lang_User_Unsubscribe(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
-
- langIsAvailable:
- var statusEmoji, statusText, statusColor string
- var status int
-
- unsubscribeCode := c.FormValue("code")
- if unsubscribeCode == "" {
- statusColor = "0, 0, 255"
- statusEmoji = "(╭ರ_•́)"
- statusText = l[lang].UnsubscribePage.UnsetCode
- status = fiber.ErrBadRequest.Code
- } else if clientError, serverError := Mailer.Unsubscribe(unsubscribeCode); clientError != nil {
- slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error()))
- statusColor = "255, 0, 0"
- statusEmoji = "(͠≖~≖ ͡ )"
- statusText = l[lang].UnsubscribePage.InvalidCode
- status = fiber.ErrBadRequest.Code
- } else if serverError != nil {
- slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error()))
- statusColor = "255, 128, 0"
- statusEmoji = "( ˶°ㅁ°) !!"
- statusText = l[lang].UnsubscribePage.OnServerError
- status = fiber.ErrInternalServerError.Code
- } else {
- statusColor = "0, 255, 0"
- statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡"
- statusText = l[lang].UnsubscribePage.Success
- status = fiber.StatusOK
- }
-
- content, err := tm.Render("unsubscribe-page", fiber.Map{
- "Lang": lang,
- "L": l[lang],
- "StatusEmoji": statusEmoji,
- "StatusText": statusText,
- "StatusColor": template.HTML(statusColor),
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/user/unsubscribe"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Status(status).Send(content)
- }
-}
diff --git a/internal/router/page-cache.go b/internal/router/page-cache.go
deleted file mode 100644
index 01e4792..0000000
--- a/internal/router/page-cache.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package router
-
-import "github.com/dgraph-io/ristretto/v2"
-
-var PCache *ristretto.Cache[string, []byte]
diff --git a/internal/router/root.go b/internal/router/root.go
new file mode 100644
index 0000000..f4a0692
--- /dev/null
+++ b/internal/router/root.go
@@ -0,0 +1,27 @@
+package router
+
+import (
+ "log/slog"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("index", "views/index.html")
+}
+
+func Root(localeCfg []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ content, err := tm.Render("index", fiber.Map{
+ "AvailableLanguages": localeCfg,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ }
+}