diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/b2/client.go | 17 | ||||
| -rw-r--r-- | internal/router/basic-handler.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog-title.go | 19 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/lang-map.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/lang.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/root.go | 4 | ||||
| -rw-r--r-- | internal/router/handlers/sitemap.xml.go | 79 | ||||
| -rw-r--r-- | internal/router/router.go | 59 | ||||
| -rw-r--r-- | internal/templatemanager/templatemanager.go | 50 |
10 files changed, 225 insertions, 35 deletions
diff --git a/internal/b2/client.go b/internal/b2/client.go index d712736..fda79e0 100644 --- a/internal/b2/client.go +++ b/internal/b2/client.go @@ -33,10 +33,11 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) { } type BlogPage struct { - Link string - FileName string - Lang string - Metadata *frontmatter.Metadata + Link string + FileName string + Lang string + ModifiedTime time.Time + Metadata *frontmatter.Metadata } func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { @@ -79,9 +80,13 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { tags := strings.Split(attrs.Info["tags"], ",") slices.Sort(tags) + lang, _ := strings.CutPrefix(linkParts[0], c.prefix) + filePaths = append(filePaths, &BlogPage{ - Link: obj.Name(), - FileName: fileName, + Link: obj.Name(), + FileName: fileName, + Lang: lang, + ModifiedTime: attrs.LastModified, Metadata: &frontmatter.Metadata{ Title: attrs.Info["title"], ShortDescription: attrs.Info["short-description"], diff --git a/internal/router/basic-handler.go b/internal/router/basic-handler.go index 4a94a48..98a7d84 100644 --- a/internal/router/basic-handler.go +++ b/internal/router/basic-handler.go @@ -34,6 +34,14 @@ func (r *BasicHandler) TemplatesToInject() []string { return []string{} } +func (r *BasicHandler) SitemapInfo(supplements *Supplements) []SitemapInfo { + return nil +} + +func (r *BasicHandler) ContentType() string { + return fiber.MIMETextHTMLCharsetUTF8 +} + func (r *BasicHandler) Render(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { if !r.IsTemplated() { panic("handler did not implement Render method (while being non-templated)") diff --git a/internal/router/handlers/lang-blog-title.go b/internal/router/handlers/lang-blog-title.go index 97de089..9eac084 100644 --- a/internal/router/handlers/lang-blog-title.go +++ b/internal/router/handlers/lang-blog-title.go @@ -46,6 +46,25 @@ func (r *BlogPageHandler) ToValidateLang() router.LangSetting { return router.InPath } +func (r *BlogPageHandler) SitemapInfo(supplements *router.Supplements) []router.SitemapInfo { + sitemapInfo := []router.SitemapInfo{} + + pages, err := supplements.B2Client.Scan("") + if err != nil { + return sitemapInfo + } + + for _, page := range pages { + sitemapInfo = append(sitemapInfo, router.SitemapInfo{ + Loc: "/" + page.Lang + "/blog/" + page.FileName, + LastModified: page.ModifiedTime, + Priority: 1.0, + }) + } + + return sitemapInfo +} + func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta map[string]string, err error) { metadata, _, err := supplements.B2Client.ReadFrontmatter(lang + "/" + c.Params("title") + ".md") if err != nil { diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go index e4c20c6..ffac77e 100644 --- a/internal/router/handlers/lang-blog.go +++ b/internal/router/handlers/lang-blog.go @@ -47,6 +47,14 @@ func (r *CatalogueHandler) ToValidateLang() router.LangSetting { return router.InPath } +func (r *CatalogueHandler) SitemapInfo(supplements *router.Supplements) []router.SitemapInfo { + sitemapInfo := []router.SitemapInfo{} + for _, lang := range supplements.AvailableLanguages { + sitemapInfo = append(sitemapInfo, router.SitemapInfo{Loc: "/" + lang.Name + "/blog", Priority: 0.3}) + } + return sitemapInfo +} + 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 == "" { diff --git a/internal/router/handlers/lang-map.go b/internal/router/handlers/lang-map.go index 7df6104..551b485 100644 --- a/internal/router/handlers/lang-map.go +++ b/internal/router/handlers/lang-map.go @@ -40,6 +40,14 @@ func (r *MapHandler) ToValidateLang() router.LangSetting { return router.InPath } +func (r *MapHandler) SitemapInfo(supplements *router.Supplements) []router.SitemapInfo { + sitemapInfo := []router.SitemapInfo{} + for _, lang := range supplements.AvailableLanguages { + sitemapInfo = append(sitemapInfo, router.SitemapInfo{Loc: "/" + lang.Name + "/map", Priority: 0.3}) + } + return sitemapInfo +} + func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { pages, err := supplements.B2Client.Scan(lang + "/") slices.SortFunc(pages, func(a *b2.BlogPage, b *b2.BlogPage) int { diff --git a/internal/router/handlers/lang.go b/internal/router/handlers/lang.go index 74166ae..5430773 100644 --- a/internal/router/handlers/lang.go +++ b/internal/router/handlers/lang.go @@ -41,6 +41,14 @@ func (r *HomeHandler) ToValidateLang() router.LangSetting { return router.InPath } +func (r *HomeHandler) SitemapInfo(supplements *router.Supplements) []router.SitemapInfo { + sitemapInfo := []router.SitemapInfo{} + for _, lang := range supplements.AvailableLanguages { + sitemapInfo = append(sitemapInfo, router.SitemapInfo{Loc: "/" + lang.Name, Priority: 0.7}) + } + return sitemapInfo +} + func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta map[string]string, err error) { return map[string]string{ "og:title": supplements.Localization[lang].HomePage.Header, diff --git a/internal/router/handlers/root.go b/internal/router/handlers/root.go index 9f15782..89b9c63 100644 --- a/internal/router/handlers/root.go +++ b/internal/router/handlers/root.go @@ -35,6 +35,10 @@ func (r *RootHandler) CacheDuration() time.Duration { return 24 * time.Hour } +func (r *RootHandler) SitemapInfo(_ *router.Supplements) []router.SitemapInfo { + return []router.SitemapInfo{{Loc: "", Priority: 0.7}} +} + func (r *RootHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta map[string]string, err error) { return map[string]string{ "og:title": "Saya Blog", diff --git a/internal/router/handlers/sitemap.xml.go b/internal/router/handlers/sitemap.xml.go new file mode 100644 index 0000000..bb1df93 --- /dev/null +++ b/internal/router/handlers/sitemap.xml.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "log/slog" + "time" + + "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/gofiber/fiber/v2" +) + +type SitemapXmlHandler struct { + router.BasicHandler +} + +func init() { + router.Routes = append(router.Routes, &SitemapXmlHandler{}) +} + +func (r *SitemapXmlHandler) Filter() (method string, path string) { + return "GET", "/sitemap.xml" +} + +func (r *SitemapXmlHandler) IsTemplated() bool { + return false +} + +func (r *SitemapXmlHandler) TemplatesToInject() []string { + return []string{"views/pages/sitemap.xml"} +} + +func (r *SitemapXmlHandler) ToCache() router.CacheSetting { + return router.ByUrlOnly +} + +func (r *SitemapXmlHandler) CacheDuration() time.Duration { + return time.Hour +} + +func (r *SitemapXmlHandler) ToValidateLang() router.LangSetting { + return router.NotRequired +} + +func (r *SitemapXmlHandler) ContentType() string { + return fiber.MIMEApplicationXMLCharsetUTF8 +} + +func (r *SitemapXmlHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { + urls := []router.SitemapInfo{} + for _, route := range router.Routes { + sitemapInfo := route.SitemapInfo(supplements) + if sitemapInfo == nil { + continue + } + + method, match := route.Filter() + lastMod, err := supplements.TemplateManager.GetLastModified(method + " " + match) + if err != nil { + slog.Warn("failed to get last modified for a route", + slog.String("error", err.Error()), + slog.String("method", method), + slog.String("match", match)) + lastMod = time.Now() + } + + for i := range sitemapInfo { + if sitemapInfo[i].LastModified.Before(lastMod) { + sitemapInfo[i].LastModified = lastMod + } + if sitemapInfo[i].ChangeFreq == "" { + sitemapInfo[i].ChangeFreq = "monthly" + } + urls = append(urls, sitemapInfo[i]) + } + } + + templateMap["URLs"] = urls + + return fiber.StatusOK, nil +} diff --git a/internal/router/router.go b/internal/router/router.go index f28add2..56313ff 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -52,6 +52,13 @@ var ( Routes = make([]Route, 0) ) +type SitemapInfo struct { + Loc string + LastModified time.Time + ChangeFreq string + Priority float32 +} + type Route interface { Filter() (method string, path string) IsTemplated() bool @@ -59,6 +66,8 @@ type Route interface { CacheDuration() time.Duration ToValidateLang() LangSetting TemplatesToInject() []string + SitemapInfo(supplements *Supplements) []SitemapInfo + ContentType() string Render(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) AddMeta(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (meta map[string]string, err error) RenderHeader(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) @@ -87,6 +96,7 @@ type Router struct { app *fiber.App templatedRoutes map[string]map[string]Route templatedPathMatcher *PathMatcher + canonicalEndpoint string } func NewRouter(cfg *config.Config) (*Router, error) { @@ -213,7 +223,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { templatedRoutes := make(map[string]map[string]Route) templatedPathMatcher := NewPathMatcher() - return &Router{supplements, app, templatedRoutes, templatedPathMatcher}, nil + return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint}, nil } func (r *Router) InitRoutes() (err error) { @@ -261,10 +271,11 @@ func (r *Router) InitRoutes() (err error) { } defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": trimmedPath, - "QueryString": queryString, + "L": r.supplements.Localization[lang], + "Lang": lang, + "Path": trimmedPath, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, } statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap) @@ -303,8 +314,8 @@ func (r *Router) InitRoutes() (err error) { go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(statusCode).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(statusCode).Send(content) }) } } @@ -327,7 +338,7 @@ func (r *Router) InitRoutes() (err error) { r.app.Static("/", "./static") - Routes = make([]Route, 0) + // Routes = make([]Route, 0) return nil } @@ -379,14 +390,15 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error { } if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(val) } valueMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "QueryString": queryString, + "L": r.supplements.Localization[lang], + "Lang": lang, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, } var err error @@ -401,8 +413,8 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error { } go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(content) } func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { @@ -447,17 +459,18 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { if route.ToCache() != Disabled { if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(val) } } var statusCode int defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": strings.Trim(path, "/"), - "QueryString": queryString, + "L": r.supplements.Localization[lang], + "Lang": lang, + "Path": strings.Trim(path, "/"), + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, } switch part { @@ -505,8 +518,8 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(statusCode).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(statusCode).Send(content) } func (r *Router) getAndValidateLang(c *fiber.Ctx, langSetting LangSetting, defaultLang ...string) (string, error) { diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index b23e2e8..9d14e15 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -4,8 +4,10 @@ import ( "bytes" "fmt" "html/template" + "os" "path/filepath" "strings" + "time" ) type TemplateManager struct { @@ -13,8 +15,9 @@ type TemplateManager struct { } type templateManagerRender struct { - Main string - Tmpl *template.Template + Main string + Tmpl *template.Template + LastModified time.Time } type TemplateManagerTemplates struct { @@ -53,10 +56,15 @@ func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager if err != nil { return nil, err } + modTime, err := setLastModified(tmplStruct.Files...) + if err != nil { + return nil, err + } templateMap[tmplStruct.Name] = templateManagerRender{ - Main: filepath.Base(tmplStruct.Files[0]), - Tmpl: tmpl, + Main: filepath.Base(tmplStruct.Files[0]), + Tmpl: tmpl, + LastModified: modTime, } } @@ -114,10 +122,40 @@ func (tm *TemplateManager) Add(name string, files ...string) error { return fmt.Errorf("failed to add template into manager: %w", err) } + modTime, err := setLastModified(files...) + if err != nil { + return fmt.Errorf("failed to add template into manager: %w", err) + } + tm.templates[name] = templateManagerRender{ - Main: filepath.Base(files[0]), - Tmpl: tmpl, + Main: filepath.Base(files[0]), + Tmpl: tmpl, + LastModified: modTime, } return nil } + +func (tm *TemplateManager) GetLastModified(name string) (time.Time, error) { + tmpl, exists := tm.templates[name] + if !exists { + return time.Time{}, fmt.Errorf("template %s not found", name) + } + + return tmpl.LastModified, nil +} + +func setLastModified(filenames ...string) (time.Time, error) { + lastModified := time.Time{} + for _, filename := range filenames { + stat, err := os.Stat(filename) + if err != nil { + return lastModified, fmt.Errorf("failed to stat file: path '%s': %w", filename, err) + } + modTime := stat.ModTime() + if lastModified.Before(modTime) { + lastModified = modTime + } + } + return lastModified, nil +} |