summaryrefslogtreecommitdiffci
path: root/internal
diff options
from:
to:
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/blog/b2.go31
-rw-r--r--internal/blog/client.go1
-rw-r--r--internal/blog/index.go83
-rw-r--r--internal/blog/s3.go269
-rw-r--r--internal/mailer/mailer.go16
-rw-r--r--internal/router/handlers/api-v1-blog-search.go53
-rw-r--r--internal/router/handlers/api-v1-email-is-in-verification.go3
-rw-r--r--internal/router/handlers/api-v1-email-send-verification-code.go15
-rw-r--r--internal/router/handlers/api-v1-email-verify.go7
-rw-r--r--internal/router/handlers/api-v1-map-get.go116
-rw-r--r--internal/router/handlers/api-v1-subs-put.go7
-rw-r--r--internal/router/handlers/lang-blog-title.go25
-rw-r--r--internal/router/handlers/lang-blog.go16
-rw-r--r--internal/router/handlers/lang-map.go65
-rw-r--r--internal/router/handlers/lang-user-unsubscribe.go9
-rw-r--r--internal/router/handlers/lang-user.go9
-rw-r--r--internal/router/handlers/lang.go9
-rw-r--r--internal/router/router.go59
-rw-r--r--internal/templatemanager/templatemanager.go6
19 files changed, 459 insertions, 340 deletions
diff --git a/internal/blog/b2.go b/internal/blog/b2.go
index 0caa0ba..f2ac890 100644
--- a/internal/blog/b2.go
+++ b/internal/blog/b2.go
@@ -2,6 +2,7 @@ package blog
import (
"context"
+ "encoding/json"
"fmt"
"slices"
"strings"
@@ -37,6 +38,20 @@ func NewB2Client(cfg *config.StorageConfig) (Client, error) {
return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil
}
+func (c *B2Client) GetMedleys() ([]MedleyEntry, error) {
+ idxRaw, err := c.readAll(MedleysIndexFileName)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err)
+ }
+
+ var idx []MedleyEntry
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err)
+ }
+
+ return idx, nil
+}
+
func (c *B2Client) Scan(prefix string) ([]*Page, error) {
filePaths := []*Page{}
@@ -70,17 +85,15 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) {
return nil, fmt.Errorf("failed to parse published time metadata field: %w", err)
}
- linkParts := strings.Split(obj.Name(), "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(linkParts)-1], ".")
-
+ link := obj.Name()
+ fileName := link[strings.LastIndex(link, "/")+1 : strings.LastIndex(link, ".")]
tags := strings.Split(attrs.Info["tags"], ",")
slices.Sort(tags)
- lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
+ lang, _ := strings.CutPrefix(link[0:strings.Index(link, "/")], c.prefix)
filePaths = append(filePaths, &Page{
- Link: obj.Name(),
+ Link: link,
FileName: fileName,
Lang: lang,
ModifiedTime: attrs.LastModified,
@@ -104,7 +117,11 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) {
}
func (c *B2Client) ReadAll(path string) ([]byte, error) {
- obj := c.bucket.Object(c.prefix + path)
+ return c.readAll(c.prefix + path)
+}
+
+func (c *B2Client) readAll(path string) ([]byte, error) {
+ obj := c.bucket.Object(path)
if obj == nil {
return nil, fmt.Errorf("failed to reference object in B2 bucket")
}
diff --git a/internal/blog/client.go b/internal/blog/client.go
index 56bbee4..53eb0fd 100644
--- a/internal/blog/client.go
+++ b/internal/blog/client.go
@@ -17,6 +17,7 @@ type Page struct {
type Client interface {
Scan(prefix string) ([]*Page, error)
+ GetMedleys() ([]MedleyEntry, error)
ReadAll(path string) ([]byte, error)
ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error)
}
diff --git a/internal/blog/index.go b/internal/blog/index.go
index 861a90e..a09b59e 100644
--- a/internal/blog/index.go
+++ b/internal/blog/index.go
@@ -1,10 +1,17 @@
package blog
-import "time"
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/internal/frontmatter"
+)
const IndexFileName = "index.json"
+const MedleysIndexFileName = "medleys.json"
-const IndexSchemaVersion = 1
+const IndexSchemaVersion = 2
type IndexEntry struct {
Link string `json:"link"`
@@ -20,13 +27,77 @@ type IndexEntry struct {
MedleyPart int `json:"medleyPart,omitempty"`
}
-type IndexCategory struct {
+func (e IndexEntry) Metadata() *frontmatter.Metadata {
+ return &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ }
+}
+
+type IndexV2Category struct {
+ GeneratedAt time.Time `json:"generatedAt"`
+ Pages map[string]IndexEntry `json:"pages"`
+}
+
+type IndexV1Category struct {
GeneratedAt time.Time `json:"generatedAt"`
Pages []IndexEntry `json:"pages"`
}
type Index struct {
- SchemaVersion int `json:"schemaVersion"`
- GeneratedAt time.Time `json:"generatedAt"`
- Categories map[string]IndexCategory `json:"categories"`
+ SchemaVersion int `json:"schemaVersion"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ Categories any `json:"categories"`
+}
+
+func (idx *Index) UnmarshalJSON(data []byte) error {
+ var tmp struct {
+ SchemaVersion int `json:"schemaVersion"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ Categories json.RawMessage `json:"categories"`
+ }
+
+ if err := json.Unmarshal(data, &tmp); err != nil {
+ return err
+ }
+
+ idx.SchemaVersion = tmp.SchemaVersion
+ idx.GeneratedAt = tmp.GeneratedAt
+
+ switch tmp.SchemaVersion {
+ case 1:
+ var categories map[string]*IndexV1Category
+ if err := json.Unmarshal(tmp.Categories, &categories); err != nil {
+ return fmt.Errorf("unmarshal map[string]*IndexV1Category: %w", err)
+ }
+ idx.Categories = &categories
+ case 2:
+ var categories map[string]*IndexV2Category
+ if err := json.Unmarshal(tmp.Categories, &categories); err != nil {
+ return fmt.Errorf("unmarshal map[string]*IndexV2Category: %w", err)
+ }
+ idx.Categories = &categories
+ default:
+ return fmt.Errorf("unsupported index version: %d", tmp.SchemaVersion)
+ }
+
+ return nil
+}
+
+type MedleyEntry struct {
+ Codename string `json:"codename"`
+ Localnames map[string]string `json:"localnames"`
+ Content []string `json:"content"`
+}
+
+type MedleyPageEntry struct {
+ Codename string `json:"codename"`
+ Position int `json:"position"`
}
diff --git a/internal/blog/s3.go b/internal/blog/s3.go
index bbd5238..3c1a932 100644
--- a/internal/blog/s3.go
+++ b/internal/blog/s3.go
@@ -1,29 +1,22 @@
package blog
import (
+ "bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"io"
- "log/slog"
- "net/url"
- "slices"
"strings"
- "sync"
- "time"
"github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/frontmatter"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
- s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
)
-const s3ScanConcurrency = 32
-
type S3Client struct {
prefix string
bucketName string
@@ -66,39 +59,38 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil
}
-func (c *S3Client) Scan(prefix string) ([]*Page, error) {
- pages, err := c.scanFromIndex(prefix)
- if err == nil {
- return pages, nil
+func (c *S3Client) GetMedleys() ([]MedleyEntry, error) {
+ idxRaw, err := c.readAll(MedleysIndexFileName)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err)
}
- var nsk *s3types.NoSuchKey
- if !errors.As(err, &nsk) {
- return nil, err
+ var idx []MedleyEntry
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err)
}
- slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix))
- return c.scanByListing(prefix)
+ return idx, nil
}
-func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) {
+func (c *S3Client) Scan(prefix string) ([]*Page, error) {
out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(IndexFileName),
})
if err != nil {
- return nil, fmt.Errorf("get index.json: %w", err)
+ return nil, fmt.Errorf("get %s: %w", IndexFileName, err)
}
defer out.Body.Close()
raw, err := io.ReadAll(out.Body)
if err != nil {
- return nil, fmt.Errorf("read index.json: %w", err)
+ return nil, fmt.Errorf("read %s: %w", IndexFileName, err)
}
var idx Index
if err := json.Unmarshal(raw, &idx); err != nil {
- return nil, fmt.Errorf("unmarshal index.json: %w", err)
+ return nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err)
}
wantLang := ""
@@ -108,160 +100,93 @@ func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) {
fullPrefix := c.prefix + prefix
pages := make([]*Page, 0)
- for catKey, cat := range idx.Categories {
- lang, ok := strings.CutPrefix(catKey, c.prefix)
- if !ok {
- continue
- }
- if wantLang != "" && wantLang != lang {
- continue
- }
- for _, e := range cat.Pages {
- if !strings.HasPrefix(e.Link, fullPrefix) {
+
+ switch idx.SchemaVersion {
+ case 1:
+ for catKey, cat := range *idx.Categories.(*map[string]*IndexV1Category) {
+ lang, ok := strings.CutPrefix(catKey, c.prefix)
+ if !ok {
continue
}
- linkParts := strings.Split(e.Link, "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
- pages = append(pages, &Page{
- Link: e.Link,
- FileName: fileName,
- Lang: lang,
- ModifiedTime: e.ModifiedTime,
- Metadata: &frontmatter.Metadata{
- Title: e.Title,
- ShortDescription: e.ShortDescription,
- ActionDate: e.ActionDate,
- PublishedTime: e.PublishedTime,
- Thumbnail: e.Thumbnail,
- Tags: e.Tags,
- Geolocation: e.Geolocation,
- Medley: e.Medley,
- MedleyPart: e.MedleyPart,
- },
- })
- }
- }
- return pages, nil
-}
-
-func (c *S3Client) scanByListing(prefix string) ([]*Page, error) {
- fullPrefix := c.prefix + prefix
-
- type candidate struct {
- key string
- lastModified time.Time
- }
- var candidates []candidate
-
- paginator := s3.NewListObjectsV2Paginator(c.s3cl, &s3.ListObjectsV2Input{
- Bucket: aws.String(c.bucketName),
- Prefix: aws.String(fullPrefix),
- })
- for paginator.HasMorePages() {
- output, err := paginator.NextPage(context.Background())
- if err != nil {
- return nil, fmt.Errorf("list S3 objects: %w", err)
- }
- for _, obj := range output.Contents {
- key := aws.ToString(obj.Key)
- if !strings.HasSuffix(key, ".md") {
+ if wantLang != "" && wantLang != lang {
continue
}
- candidates = append(candidates, candidate{key, aws.ToTime(obj.LastModified)})
- }
- }
-
- pages := make([]*Page, len(candidates))
- sem := make(chan struct{}, s3ScanConcurrency)
- var wg sync.WaitGroup
- var firstErr error
- var errMu sync.Mutex
-
- for i, cand := range candidates {
- sem <- struct{}{}
- wg.Go(func() {
- defer func() { <-sem }()
-
- head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{
- Bucket: aws.String(c.bucketName),
- Key: aws.String(cand.key),
- })
- if err != nil {
- errMu.Lock()
- if firstErr == nil {
- firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err)
+ for _, e := range cat.Pages {
+ if !strings.HasPrefix(e.Link, fullPrefix) {
+ continue
}
- errMu.Unlock()
- return
+ fileName := e.Link[strings.LastIndex(e.Link, "/")+1 : strings.LastIndex(e.Link, ".")]
+ pages = append(pages, &Page{
+ Link: e.Link,
+ FileName: fileName,
+ Lang: lang,
+ ModifiedTime: e.ModifiedTime,
+ Metadata: &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ },
+ })
}
-
- if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") {
- return
+ }
+ case 2:
+ for catKey, cat := range *idx.Categories.(*map[string]*IndexV2Category) {
+ lang, ok := strings.CutPrefix(catKey, c.prefix)
+ if !ok {
+ continue
}
- meta := head.Metadata
- if meta["title"] == "" {
- return
+ if wantLang != "" && wantLang != lang {
+ continue
}
-
- publishedTime, err := time.Parse(time.RFC3339, meta["published-time"])
- if err != nil {
- errMu.Lock()
- if firstErr == nil {
- firstErr = fmt.Errorf("failed to parse published time metadata field: %w", err)
+ for codename, e := range cat.Pages {
+ if !strings.HasPrefix(e.Link, fullPrefix) {
+ continue
}
- errMu.Unlock()
- return
+ pages = append(pages, &Page{
+ Link: e.Link,
+ FileName: codename,
+ Lang: lang,
+ ModifiedTime: e.ModifiedTime,
+ Metadata: &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ },
+ })
}
-
- linkParts := strings.Split(cand.key, "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
- lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
-
- tags := strings.Split(meta["tags"], ",")
- slices.Sort(tags)
-
- title, _ := url.QueryUnescape(meta["title"])
- shortDescription, _ := url.QueryUnescape(meta["short-description"])
- thumbnail, _ := url.QueryUnescape(meta["thumbnail"])
-
- pages[i] = &Page{
- Link: cand.key,
- FileName: fileName,
- Lang: lang,
- ModifiedTime: cand.lastModified,
- Metadata: &frontmatter.Metadata{
- Title: title,
- ShortDescription: shortDescription,
- ActionDate: meta["action-date"],
- PublishedTime: publishedTime,
- Thumbnail: thumbnail,
- Tags: tags,
- Geolocation: meta["geolocation"],
- },
- }
- })
- }
- wg.Wait()
-
- if firstErr != nil {
- return nil, firstErr
+ }
}
- out := pages[:0]
- for _, p := range pages {
- if p != nil {
- out = append(out, p)
+ medleys, _ := c.GetMedleys()
+ for _, medley := range medleys {
+ for locale, localname := range medley.Localnames {
+ l10n.T.SetPath(localname, true, locale, "Medleys", medley.Codename)
}
}
- return out, nil
+
+ return pages, nil
}
func (c *S3Client) ReadAll(path string) ([]byte, error) {
+ return c.readAll(c.prefix + path)
+}
+
+func (c *S3Client) readAll(path string) ([]byte, error) {
output, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
- Key: aws.String(c.prefix + path),
+ Key: aws.String(path),
})
if err != nil {
return nil, fmt.Errorf("get S3 object: %w", err)
@@ -277,10 +202,40 @@ func (c *S3Client) ReadAll(path string) ([]byte, error) {
}
func (c *S3Client) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) {
+ idxRaw, err := c.readAll(IndexFileName)
+ if err != nil {
+ return nil, nil, fmt.Errorf("read %s: %w", IndexFileName, err)
+ }
+
+ var idx Index
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err)
+ }
+
contentBytes, err := c.ReadAll(path)
if err != nil {
return nil, nil, fmt.Errorf("failed to read file for frontmatter parsing: %w", err)
}
+ switch idx.SchemaVersion {
+ case 1:
+ return frontmatter.ParseFrontmatter(contentBytes)
+ case 2:
+ fullPath := c.prefix + path
+ page := (*idx.Categories.(*map[string]*IndexV2Category))[fullPath[:strings.LastIndex(fullPath, "/")]].Pages[fullPath[strings.LastIndex(fullPath, "/")+1:strings.LastIndex(fullPath, ".")]]
+ metadata = page.Metadata()
+
+ if !bytes.HasPrefix(contentBytes, []byte("---\n")) {
+ return metadata, contentBytes, nil
+ }
+
+ end := bytes.Index(contentBytes[4:], []byte("\n---\n"))
+ if end == -1 {
+ return metadata, contentBytes, nil
+ }
+
+ return metadata, contentBytes[end+9:], nil
+ }
+
return frontmatter.ParseFrontmatter(contentBytes)
}
diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go
index ffa7b30..33db4f7 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -16,7 +16,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
- "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/dgraph-io/ristretto/v2"
"github.com/gofiber/fiber/v2"
"github.com/wneessen/go-mail"
@@ -43,8 +43,6 @@ type Mailer struct {
hashMap map[string][]byte
hashMapMutex sync.RWMutex
-
- l map[string]*locale.LocaleConfig
}
type SubscriptionType int
@@ -55,7 +53,7 @@ const (
Specific
)
-func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) {
+func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) {
verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{
NumCounters: 10000,
MaxCost: 1 << 20, // 1 MB
@@ -111,7 +109,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
End time.Time
CodeExpiry time.Time
}, 0),
- l: localization}, nil
+ }, nil
}
func (m *Mailer) GetHash(id string) []byte {
@@ -261,10 +259,9 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string
verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address)))
m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8))
- message.Subject(m.l[lang].Mail.VerifyEmail.Subject)
+ message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string))
msg, err := m.tm.Render("verify-email", fiber.Map{
- "L": m.l[lang],
"Lang": lang,
"VerificationCode": fmt.Sprintf("%X", verificationCode),
"ClientHost": m.clientHost,
@@ -499,11 +496,10 @@ rowLoop:
rand.Read(unsubscribeCodeBytes)
unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes)
- unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
+ unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1)
msgBody, err := m.tm.Render("new-post", fiber.Map{
- "L": m.l[post.Lang],
"Lang": post.Lang,
"Post": post,
"ClientHost": m.clientHost,
@@ -529,7 +525,7 @@ rowLoop:
message.SetMessageID()
message.SetDate()
message.SetBulk()
- message.Subject(m.l[post.Lang].Mail.NewPost.Subject)
+ message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string))
message.SetBodyString(mail.TypeTextHTML, string(msgBody))
m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40)
diff --git a/internal/router/handlers/api-v1-blog-search.go b/internal/router/handlers/api-v1-blog-search.go
index 38004a1..b59f09a 100644
--- a/internal/router/handlers/api-v1-blog-search.go
+++ b/internal/router/handlers/api-v1-blog-search.go
@@ -1,13 +1,13 @@
package handlers
import (
+ "cmp"
"encoding/json"
"fmt"
"log/slog"
"net/url"
"regexp"
"slices"
- "strings"
"time"
"github.com/SayaAndy/saya-today-web/internal/blog"
@@ -51,6 +51,10 @@ func (r *BlogSearchHandler) RateLimiter() *fiber.Handler {
func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
sort := c.Query("sort")
tz := c.Query("tz")
+ medley := c.Query("medley")
+ highlight := c.Query("highlight")
+ hideTags := c.QueryBool("hideTags", false)
+ hidePublishedTime := c.QueryBool("hidePublishedTime", false)
loc, err := time.LoadLocation(tz)
if err != nil {
@@ -85,21 +89,26 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
for _, page := range pages {
for _, tag := range page.Metadata.Tags {
if len(tags) == 0 || slices.Contains(tags, tag) {
- pageMeta = append(pageMeta, fiber.Map{
- "Link": page.Link,
- "ArticleLink": "/" + lang + "/blog/" + page.FileName,
- "Title": page.Metadata.Title,
- "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"),
- "ActionDate": page.Metadata.ActionDate,
- "ShortDescription": page.Metadata.ShortDescription,
- "Thumbnail": page.Metadata.Thumbnail,
- "Tags": page.Metadata.Tags,
- "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName),
- "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName),
- "ViewCount": supplements.ClientCache.GetViewCount(page.FileName),
- "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName),
- })
- break
+ if medley == "" || medley == page.Metadata.Medley {
+ pageMeta = append(pageMeta, fiber.Map{
+ "Link": page.Link,
+ "ArticleLink": "/" + lang + "/blog/" + page.FileName,
+ "Title": page.Metadata.Title,
+ "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"),
+ "ActionDate": page.Metadata.ActionDate,
+ "ShortDescription": page.Metadata.ShortDescription,
+ "Thumbnail": page.Metadata.Thumbnail,
+ "Tags": page.Metadata.Tags,
+ "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName),
+ "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName),
+ "ViewCount": supplements.ClientCache.GetViewCount(page.FileName),
+ "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName),
+ "Medley": page.Metadata.Medley,
+ "MedleyPart": page.Metadata.MedleyPart,
+ "ToHighlight": page.FileName == highlight,
+ })
+ break
+ }
}
}
}
@@ -107,13 +116,13 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
slices.SortFunc(pageMeta, func(a, b fiber.Map) int {
switch sort {
case "titleAsc":
- return strings.Compare(a["Title"].(string), b["Title"].(string))
+ return cmp.Compare(a["Title"].(string), b["Title"].(string))
case "titleDesc":
- return strings.Compare(b["Title"].(string), a["Title"].(string))
+ return cmp.Compare(b["Title"].(string), a["Title"].(string))
case "actionDateAsc":
- return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string))
+ return cmp.Compare(a["ActionDate"].(string), b["ActionDate"].(string))
case "actionDateDesc":
- return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string))
+ return cmp.Compare(b["ActionDate"].(string), a["ActionDate"].(string))
case "publicationDateAsc":
publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
@@ -122,11 +131,15 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
return publishedTimeB.Compare(publishedTimeA)
+ case "medley":
+ return cmp.Compare(a["MedleyPart"].(int), b["MedleyPart"].(int))
}
return 0
})
templateMap["BlogPages"] = pageMeta
+ templateMap["HideTags"] = hideTags
+ templateMap["HidePublishedTime"] = hidePublishedTime
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/api-v1-email-is-in-verification.go b/internal/router/handlers/api-v1-email-is-in-verification.go
index 4f05f48..b8ae162 100644
--- a/internal/router/handlers/api-v1-email-is-in-verification.go
+++ b/internal/router/handlers/api-v1-email-is-in-verification.go
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -49,7 +50,7 @@ func (r *OngoingVerificationHandler) Render(c *fiber.Ctx, supplements *router.Su
if !isAllowed {
sterileDataset["striked-end-time"] = template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli()))
templateMap["Status"] = "Neutral"
- templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
+ templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
} else {
templateMap["Status"] = "OK"
templateMap["Message"] = ""
diff --git a/internal/router/handlers/api-v1-email-send-verification-code.go b/internal/router/handlers/api-v1-email-send-verification-code.go
index 0b53efa..ea8df4d 100644
--- a/internal/router/handlers/api-v1-email-send-verification-code.go
+++ b/internal/router/handlers/api-v1-email-send-verification-code.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -49,27 +50,27 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S
email := c.FormValue("email")
if email == "" {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailEmpty
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailEmpty").(string)
return fiber.StatusUnprocessableEntity, nil
}
isTaken, err := supplements.Mailer.MailIsTaken(email)
if err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string)
slog.Error("failed to check if address is already taken", slog.String("error", err.Error()))
return fiber.StatusUnprocessableEntity, nil
}
if isTaken {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailTaken
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailTaken").(string)
return fiber.StatusUnprocessableEntity, nil
}
if isAllowed, whenAllowed, _ := supplements.Mailer.IsAllowedToRetryVerification(id); !isAllowed {
templateMap["Status"] = "Failed"
- templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
+ templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
templateMap["DataAttributes"] = map[string]any{
"striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())),
}
@@ -78,20 +79,20 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S
if previousEmail, _, _ := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(id)); previousEmail == email {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailAlreadyValidated
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailAlreadyValidated").(string)
return fiber.StatusUnprocessableEntity, nil
}
if err = supplements.Mailer.SendVerificationCode(id, email, lang); err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string)
slog.Error("failed to send a verification code", slog.String("error", err.Error()))
return fiber.StatusUnprocessableEntity, nil
}
_, endTime, codeExpiry := supplements.Mailer.IsAllowedToRetryVerification(id)
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSent
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSent").(string)
templateMap["DataAttributes"] = map[string]any{
"striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", endTime.UnixMilli())),
"code-expiry-time": template.HTMLAttr(fmt.Sprintf("data-code-expiry-time=\"%d\"", codeExpiry.UnixMilli())),
diff --git a/internal/router/handlers/api-v1-email-verify.go b/internal/router/handlers/api-v1-email-verify.go
index ace1870..65ad6d9 100644
--- a/internal/router/handlers/api-v1-email-verify.go
+++ b/internal/router/handlers/api-v1-email-verify.go
@@ -5,6 +5,7 @@ import (
"log/slog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -46,19 +47,19 @@ func (r *VerifyCodeHandler) Render(c *fiber.Ctx, supplements *router.Supplements
if verificationCode == "" {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationEmpty
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationEmpty").(string)
return fiber.StatusUnprocessableEntity, nil
}
if err = supplements.Mailer.Verify(verificationCode, lang); err != nil {
slog.Warn("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error()))
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationFailed
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationFailed").(string)
return fiber.StatusUnprocessableEntity, nil
}
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationSuccess
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationSuccess").(string)
templateMap["DataAttributes"] = map[string]any{
"hide-verification-panel": template.HTMLAttr("data-code-expiry-time=\"true\""),
}
diff --git a/internal/router/handlers/api-v1-map-get.go b/internal/router/handlers/api-v1-map-get.go
new file mode 100644
index 0000000..e19c54a
--- /dev/null
+++ b/internal/router/handlers/api-v1-map-get.go
@@ -0,0 +1,116 @@
+package handlers
+
+import (
+ "fmt"
+ "log/slog"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/internal/blog"
+ "github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/gofiber/fiber/v2"
+)
+
+type GetMapHandler struct {
+ router.BasicHandler
+}
+
+func init() {
+ router.Routes = append(router.Routes, &GetMapHandler{})
+}
+
+func (r *GetMapHandler) Filter() (method string, path string) {
+ return "GET", "/api/v1/map"
+}
+
+func (r *GetMapHandler) IsTemplated() bool {
+ return false
+}
+
+func (r *GetMapHandler) TemplatesToInject() []string {
+ return []string{"views/partials/global-map-widget.html"}
+}
+
+func (r *GetMapHandler) ToCache() router.CacheSetting {
+ return router.ByUrlAndQuery
+}
+
+func (r *GetMapHandler) ToValidateLang() router.LangSetting {
+ return router.InReferer
+}
+
+func (r *GetMapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
+ codename := c.Query("codename")
+ zoom := c.QueryInt("zoom", 4)
+ zoomPosition := c.Query("zoomPosition")
+
+ pages, err := supplements.BlogClient.Scan(lang + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ slog.Error("received an error while scanning blog pages",
+ slog.String("error", err.Error()),
+ slog.String("lang", lang),
+ )
+ status = fiber.StatusPartialContent
+ pages = []*blog.Page{}
+ }
+ slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int {
+ return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime)
+ })
+
+ type MapMarker struct {
+ Index int `json:"Index"`
+ Title string `json:"Title"`
+ PageLink string `json:"PageLink"`
+ Lat float64 `json:"Lat"`
+ Long float64 `json:"Long"`
+ AccuracyMeters int64 `json:"AccuracyMeters"`
+ Thumbnail string `json:"Thumbnail"`
+ ToHighlight bool `json:"ToHighlight"`
+ }
+
+ templateMap["MapLocationLat"] = 45.4507
+ templateMap["MapLocationLong"] = 68.8319
+ templateMap["MapLocationZoom"] = zoom
+ templateMap["ZoomPosition"] = zoomPosition
+
+ mapMarkers := make([]*MapMarker, 0, len(pages))
+ for i, page := range pages {
+ geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
+ if len(geolocationParts) < 2 {
+ continue
+ }
+
+ var x, y float64
+ var areaError int64
+ if len(geolocationParts) >= 2 {
+ x, _ = strconv.ParseFloat(geolocationParts[0], 64)
+ y, _ = strconv.ParseFloat(geolocationParts[1], 64)
+ }
+ if len(geolocationParts) >= 3 {
+ areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
+ }
+
+ toHighlight := page.FileName == codename
+ if toHighlight {
+ templateMap["MapLocationLat"] = x
+ templateMap["MapLocationLong"] = y
+ }
+
+ mapMarkers = append(mapMarkers, &MapMarker{
+ Index: i,
+ Title: page.Metadata.Title,
+ PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
+ Lat: x,
+ Long: y,
+ AccuracyMeters: areaError,
+ Thumbnail: page.Metadata.Thumbnail,
+ ToHighlight: toHighlight,
+ })
+ }
+
+ templateMap["MapMarkers"] = mapMarkers
+
+ return status, nil
+}
diff --git a/internal/router/handlers/api-v1-subs-put.go b/internal/router/handlers/api-v1-subs-put.go
index 32fba7e..12dc5cf 100644
--- a/internal/router/handlers/api-v1-subs-put.go
+++ b/internal/router/handlers/api-v1-subs-put.go
@@ -3,6 +3,7 @@ package handlers
import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -52,18 +53,18 @@ func (r *PutSubsHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l
subscriptionTypeEnum = mailer.Specific
default:
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribeInvalidType
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribeInvalidType").(string)
return fiber.StatusUnprocessableEntity, nil
}
specificTags := c.FormValue("tags_picked")
if err = supplements.Mailer.Subscribe(supplements.Mailer.GetHash(c.IP()), subscriptionTypeEnum, specificTags); err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.FailedToSubscribe
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "FailedToSubscribe").(string)
return fiber.StatusUnprocessableEntity, nil
}
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribedSuccessfully
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribedSuccessfully").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-blog-title.go b/internal/router/handlers/lang-blog-title.go
index 49744c1..5a1155d 100644
--- a/internal/router/handlers/lang-blog-title.go
+++ b/internal/router/handlers/lang-blog-title.go
@@ -10,6 +10,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/frontmatter"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
"github.com/yuin/goldmark"
)
@@ -70,11 +71,15 @@ func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements,
if err != nil {
return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err)
}
+ title := metadata.Title
+ if metadata.Medley != "" {
+ title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string)
+ }
return []router.MetaField{
- {Property: "og:title", Content: metadata.Title},
+ {Property: "og:title", Content: title},
{Property: "og:description", Content: fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate)},
- {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail320p.BaseUrl, metadata.Thumbnail)},
+ {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail560p.BaseUrl, metadata.Thumbnail)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/blog/%s", templateMap["CanonicalEndpoint"], lang, c.Params("title"))},
{Property: "og:type", Content: "website"},
{Name: "twitter:card", Content: "summary_large_image"},
@@ -86,11 +91,15 @@ func (r *BlogPageHandler) AddLinkedData(c *fiber.Ctx, supplements *router.Supple
if err != nil {
return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err)
}
+ title := metadata.Title
+ if metadata.Medley != "" {
+ title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string)
+ }
return map[string]any{
"@context": "https://schema.org",
"@type": "Article",
- "headline": metadata.Title,
+ "headline": title,
"description": fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate),
"author": map[string]string{"@type": "Person", "name": "Saya Andy"},
"datePublished": metadata.PublishedTime.UTC().Format(time.RFC3339),
@@ -102,10 +111,11 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen
if err != nil {
return fiber.StatusBadRequest, fmt.Errorf("failed to get path from referer: %w", err)
}
+ title := pathParts[2]
- metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+pathParts[2])
+ metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+title)
if err != nil {
- return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", pathParts[2], err)
+ return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", title, err)
}
geolocationParts := strings.Split(metadata.Geolocation, " ")
@@ -122,12 +132,15 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen
templateMap["MapLocationY"] = y
templateMap["MapLocationAreaMeters"] = areaError
templateMap["Title"] = metadata.Title
+ templateMap["Codename"] = title
templateMap["ParsedMarkdown"] = template.HTML(parsedMarkdown)
templateMap["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00")
templateMap["ActionDate"] = metadata.ActionDate
templateMap["ShortDescription"] = metadata.ShortDescription
+ templateMap["Thumbnail"] = metadata.Thumbnail
+ templateMap["Medley"] = metadata.Medley
- go supplements.ClientCache.View(c.IP(), pathParts[2])
+ go supplements.ClientCache.View(c.IP(), title)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go
index 79ec9f3..c31c2eb 100644
--- a/internal/router/handlers/lang-blog.go
+++ b/internal/router/handlers/lang-blog.go
@@ -11,6 +11,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -57,18 +58,16 @@ func (r *CatalogueHandler) SitemapInfo(supplements *router.Supplements) []router
func (r *CatalogueHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
- {Property: "og:title", Content: supplements.Localization[lang].BlogSearch.Header},
- {Property: "og:description", Content: supplements.Localization[lang].BlogSearch.Description},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "BlogSearch", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "BlogSearch", "Description").(string)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/blog", templateMap["CanonicalEndpoint"], lang)},
{Property: "og:type", Content: "website"},
}, nil
}
func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- querySort := c.Query("sort")
- if querySort == "" {
- querySort = "publicationDateDesc"
- }
+ querySort := c.Query("sort", "publicationDateDesc")
+ previousBlogPage := c.Query("codename")
encodedQuery := c.Request().URI().QueryString()
decodedQuery, _ := url.QueryUnescape(string(encodedQuery))
@@ -87,13 +86,14 @@ func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Suppleme
templateMap["Tags"] = tagsArray
templateMap["QuerySort"] = querySort
templateMap["QueryTags"] = strings.Join(queryTags, ",")
- templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string)
+ templateMap["PreviousBlogPage"] = previousBlogPage
return fiber.StatusOK, nil
}
func (r *CatalogueHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-map.go b/internal/router/handlers/lang-map.go
index d452bc7..32ab35e 100644
--- a/internal/router/handlers/lang-map.go
+++ b/internal/router/handlers/lang-map.go
@@ -1,13 +1,6 @@
package handlers
import (
- "fmt"
- "log/slog"
- "slices"
- "strconv"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/router"
"github.com/gofiber/fiber/v2"
)
@@ -49,61 +42,5 @@ func (r *MapHandler) SitemapInfo(supplements *router.Supplements) []router.Sitem
}
func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- pages, err := supplements.BlogClient.Scan(lang + "/")
- slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int {
- return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime)
- })
- status := fiber.StatusOK
- if err != nil {
- slog.Error("received an error while scanning b2 pages",
- slog.String("error", err.Error()),
- slog.String("lang", lang),
- )
- status = fiber.StatusPartialContent
- pages = []*blog.Page{}
- }
-
- type MapMarker struct {
- Index int `json:"Index"`
- Title string `json:"Title"`
- PageLink string `json:"PageLink"`
- Lat float64 `json:"Lat"`
- Long float64 `json:"Long"`
- AccuracyMeters int64 `json:"AccuracyMeters"`
- Thumbnail string `json:"Thumbnail"`
- }
-
- mapMarkers := make([]*MapMarker, 0, len(pages))
- for i, page := range pages {
- geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
- if len(geolocationParts) < 2 {
- continue
- }
-
- var x, y float64
- var areaError int64
- if len(geolocationParts) >= 2 {
- x, _ = strconv.ParseFloat(geolocationParts[0], 64)
- y, _ = strconv.ParseFloat(geolocationParts[1], 64)
- }
- if len(geolocationParts) >= 3 {
- areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
- }
-
- mapMarkers = append(mapMarkers, &MapMarker{
- Index: i,
- Title: page.Metadata.Title,
- PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
- Lat: x,
- Long: y,
- AccuracyMeters: areaError,
- Thumbnail: page.Metadata.Thumbnail,
- })
- }
-
- templateMap["MapMarkers"] = mapMarkers
- templateMap["MapLocationLat"] = 45.4507
- templateMap["MapLocationLong"] = 68.8319
-
- return status, nil
+ return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-user-unsubscribe.go b/internal/router/handlers/lang-user-unsubscribe.go
index 4fbd244..e22d854 100644
--- a/internal/router/handlers/lang-user-unsubscribe.go
+++ b/internal/router/handlers/lang-user-unsubscribe.go
@@ -4,6 +4,7 @@ import (
"log/slog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -39,24 +40,24 @@ func (r *UnsubscribeHandler) Render(c *fiber.Ctx, supplements *router.Supplement
if unsubscribeCode == "" {
statusColor = "0, 0, 255"
statusEmoji = "(╭ರ_•́)"
- statusText = supplements.Localization[lang].UnsubscribePage.UnsetCode
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "UnsetCode").(string)
status = fiber.ErrBadRequest.Code
} else if clientError, serverError := supplements.Mailer.Unsubscribe(unsubscribeCode); clientError != nil {
slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error()))
statusColor = "255, 0, 0"
statusEmoji = "(͠≖~≖ ͡ )"
- statusText = supplements.Localization[lang].UnsubscribePage.InvalidCode
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "InvalidCode").(string)
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 = supplements.Localization[lang].UnsubscribePage.OnServerError
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "OnServerError").(string)
status = fiber.ErrInternalServerError.Code
} else {
statusColor = "0, 255, 0"
statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡"
- statusText = supplements.Localization[lang].UnsubscribePage.Success
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "Success").(string)
status = fiber.StatusOK
}
diff --git a/internal/router/handlers/lang-user.go b/internal/router/handlers/lang-user.go
index 0dda529..085a9aa 100644
--- a/internal/router/handlers/lang-user.go
+++ b/internal/router/handlers/lang-user.go
@@ -6,6 +6,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -40,15 +41,15 @@ func (r *UserHandler) ToValidateLang() router.LangSetting {
func (r *UserHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
{Name: "robots", Content: "noindex,nofollow"},
- {Property: "og:title", Content: supplements.Localization[lang].UserProfile.Header},
- {Property: "og:description", Content: supplements.Localization[lang].UserProfile.Description},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "UserProfile", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "UserProfile", "Description").(string)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/user", templateMap["CanonicalEndpoint"], lang)},
{Property: "og:type", Content: "website"},
}, nil
}
func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].UserProfile.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string)
email, _, err := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(c.IP()))
if err != nil {
@@ -83,6 +84,6 @@ func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements,
}
func (r *UserHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].UserProfile.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang.go b/internal/router/handlers/lang.go
index 25b5036..cc0b33b 100644
--- a/internal/router/handlers/lang.go
+++ b/internal/router/handlers/lang.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -51,8 +52,8 @@ func (r *HomeHandler) SitemapInfo(supplements *router.Supplements) []router.Site
func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
- {Property: "og:title", Content: supplements.Localization[lang].HomePage.Header},
- {Property: "og:description", Content: supplements.Localization[lang].HomePage.HomePageDescription},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "HomePage", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "HomePage", "HomePageDescription").(string)},
{Property: "og:image", Content: fmt.Sprintf(
supplements.PhotoStorage.HomePageGifs.BaseUrl,
supplements.PhotoStorage.HomePageGifs.Indexes[rand.Int()%len(supplements.PhotoStorage.HomePageGifs.Indexes)],
@@ -63,7 +64,7 @@ func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan
}
func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].HomePage.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string)
templateMap["FilledHeartCount"] = uint(40)
templateMap["OutlineHeartCount"] = uint(40)
templateMap["GifUrl"] = fmt.Sprintf(
@@ -75,6 +76,6 @@ func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements,
}
func (r *HomeHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].HomePage.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/router.go b/internal/router/router.go
index 7f3511a..c8b5686 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -23,7 +23,6 @@ import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/tailwind"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
- "github.com/SayaAndy/saya-today-web/locale"
"github.com/dgraph-io/ristretto/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
@@ -95,7 +94,6 @@ type Route interface {
type Supplements struct {
DB *sql.DB
BlogClient blog.Client
- Localization map[string]*locale.LocaleConfig
AvailableLanguages []config.AvailableLanguageConfig
ClientCache *ClientCache
PageCache *ristretto.Cache[string, []byte]
@@ -106,7 +104,7 @@ type Supplements struct {
MarkdownRenderer goldmark.Markdown
Meta config.MetaConfig
PhotoStorage config.PhotoStorageConfig
- StaticStorage config.PhotoTypeConfig
+ StaticStorage config.StaticStorageConfig
}
type Router struct {
@@ -149,16 +147,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage)
if err != nil {
- return nil, fmt.Errorf("fail to initialize b2 client: %w", err)
- }
-
- supplements.Localization = make(map[string]*locale.LocaleConfig, len(cfg.AvailableLanguages))
- for _, lang := range cfg.AvailableLanguages {
- localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile)
- if err != nil {
- return nil, fmt.Errorf("fail to initialize a locale: %w", err)
- }
- supplements.Localization[lang.Name] = localeCfg
+ return nil, fmt.Errorf("fail to initialize blog client: type %s: %w", cfg.BlogPages.Storage.Type, err)
}
supplements.MarkdownRenderer = goldmark.New(
@@ -200,7 +189,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
}
supplements.Mailer, err = mailer.NewMailer(supplements.DB, cfg.Mail.ClientHost, cfg.Mail.MailHost,
- cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt), supplements.Localization)
+ cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt))
if err != nil {
return nil, fmt.Errorf("fail to initialize mailer: %w", err)
}
@@ -314,13 +303,12 @@ func (r *Router) InitRoutes() (err error) {
}
defaultMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "Path": trimmedPath,
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap)
@@ -467,13 +455,12 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error {
}
valueMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "Path": trimmedPath,
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
var err error
@@ -576,11 +563,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error {
cacheKey := ""
trimmedPath := strings.Trim(path, "/")
+ requestQuery := string(c.Request().URI().QueryString())
switch route.ToCache() {
case ByUrlOnly:
cacheKey = fmt.Sprintf("%s.%s.%s", method, part, trimmedPath)
case ByUrlAndQuery:
- cacheKey = fmt.Sprintf("%s.%s.%s.%s", method, part, trimmedPath, queryString)
+ cacheKey = fmt.Sprintf("%s.%s.%s.%s.%s", method, part, trimmedPath, queryString, requestQuery)
}
if route.ToCache() != Disabled {
@@ -592,13 +580,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error {
var statusCode int
defaultMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": strings.Trim(path, "/"),
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
switch part {
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index 9d14e15..ff1214a 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -8,6 +8,8 @@ import (
"path/filepath"
"strings"
"time"
+
+ "github.com/SayaAndy/saya-today-web/l10n"
)
type TemplateManager struct {
@@ -38,6 +40,10 @@ var templateFuncMap = template.FuncMap{
"fdiv": func(a, b int) float64 {
return float64(a) / float64(b)
},
+ "l": func(path ...any) any {
+ return l10n.T.GetPath(path...)
+ },
+ "join": strings.Join,
}
func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) {