diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/blog/b2.go | 31 | ||||
| -rw-r--r-- | internal/blog/client.go | 1 | ||||
| -rw-r--r-- | internal/blog/index.go | 83 | ||||
| -rw-r--r-- | internal/blog/s3.go | 269 | ||||
| -rw-r--r-- | internal/mailer/mailer.go | 16 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-blog-search.go | 53 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-email-is-in-verification.go | 3 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-email-send-verification-code.go | 15 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-email-verify.go | 7 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-map-get.go | 116 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-subs-put.go | 7 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog-title.go | 25 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog.go | 16 | ||||
| -rw-r--r-- | internal/router/handlers/lang-map.go | 65 | ||||
| -rw-r--r-- | internal/router/handlers/lang-user-unsubscribe.go | 9 | ||||
| -rw-r--r-- | internal/router/handlers/lang-user.go | 9 | ||||
| -rw-r--r-- | internal/router/handlers/lang.go | 9 | ||||
| -rw-r--r-- | internal/router/router.go | 59 | ||||
| -rw-r--r-- | internal/templatemanager/templatemanager.go | 6 |
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) { |