From 8668f3a947fe5a7f40464a5d46329adde188132b Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 18 May 2025 20:37:58 +0800 Subject: [PATCH] Add RSS/Sitemap handling. --- server/api/resource.go | 15 +++++ server/dao/resource.go | 10 ++++ server/middleware/frontend_middleware.go | 76 +++++++++--------------- server/utils/article_to_description.go | 55 +++++++++++++++++ server/utils/rss.go | 49 +++++++++++++++ server/utils/site_map.go | 39 ++++++++++++ 6 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 server/utils/article_to_description.go create mode 100644 server/utils/rss.go create mode 100644 server/utils/site_map.go diff --git a/server/api/resource.go b/server/api/resource.go index 9940554..31d12b8 100644 --- a/server/api/resource.go +++ b/server/api/resource.go @@ -2,14 +2,26 @@ package api import ( "encoding/json" + "github.com/gofiber/fiber/v3/log" "net/url" + "nysoure/server/dao" "nysoure/server/model" "nysoure/server/service" + "nysoure/server/utils" "strconv" "github.com/gofiber/fiber/v3" ) +func updateSiteMapAndRss(baseURL string) { + resources, err := dao.GetAllResources() + if err != nil { + log.Error("Error getting resources: ", err) + } + utils.GenerateSiteMap(baseURL, resources) + utils.GenerateRss(baseURL, resources) +} + func handleCreateResource(c fiber.Ctx) error { var params service.ResourceCreateParams body := c.Body() @@ -25,6 +37,7 @@ func handleCreateResource(c fiber.Ctx) error { if err != nil { return err } + updateSiteMapAndRss(c.BaseURL()) return c.Status(fiber.StatusOK).JSON(model.Response[uint]{ Success: true, Data: id, @@ -69,6 +82,7 @@ func handleDeleteResource(c fiber.Ctx) error { if err != nil { return err } + updateSiteMapAndRss(c.BaseURL()) return c.Status(fiber.StatusOK).JSON(model.Response[any]{ Success: true, Data: nil, @@ -211,6 +225,7 @@ func handleUpdateResource(c fiber.Ctx) error { if err != nil { return err } + updateSiteMapAndRss(c.BaseURL()) return c.Status(fiber.StatusOK).JSON(model.Response[any]{ Success: true, Data: nil, diff --git a/server/dao/resource.go b/server/dao/resource.go index 6a68cc6..393c681 100644 --- a/server/dao/resource.go +++ b/server/dao/resource.go @@ -233,3 +233,13 @@ func GetResourcesByUsername(username string, page, pageSize int) ([]model.Resour return resources, int(totalPages), nil } + +// GetAllResources retrieves all resources from the database without all related data. +// It is used to generate a sitemap and rss feed. +func GetAllResources() ([]model.Resource, error) { + var resources []model.Resource + if err := db.Find(&resources).Error; err != nil { + return nil, err + } + return resources, nil +} diff --git a/server/middleware/frontend_middleware.go b/server/middleware/frontend_middleware.go index 3809d47..61e1049 100644 --- a/server/middleware/frontend_middleware.go +++ b/server/middleware/frontend_middleware.go @@ -4,15 +4,13 @@ import ( "fmt" "nysoure/server/config" "nysoure/server/service" + "nysoure/server/utils" "os" + "path/filepath" "strconv" "strings" "github.com/gofiber/fiber/v3" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - "github.com/k3a/html2text" ) func FrontendMiddleware(c fiber.Ctx) error { @@ -23,6 +21,14 @@ func FrontendMiddleware(c fiber.Ctx) error { path := c.Path() file := "static" + path + if path == "/robots.txt" { + return handleRobotsTxt(c) + } else if path == "/sitemap.xml" { + return handleSiteMap(c) + } else if path == "/rss.xml" { + return handleRss(c) + } + if _, err := os.Stat(file); path == "/" || os.IsNotExist(err) { return serveIndexHtml(c) } else { @@ -30,6 +36,23 @@ func FrontendMiddleware(c fiber.Ctx) error { } } +func handleRobotsTxt(c fiber.Ctx) error { + c.Set("Content-Type", "text/plain; charset=utf-8") + c.Set("Cache-Control", "no-cache") + c.Set("X-Robots-Tag", "noindex") + return c.SendString("User-agent: *\nDisallow: /api/\nDisallow: /admin/\n") +} + +func handleSiteMap(c fiber.Ctx) error { + path := filepath.Join(utils.GetStoragePath(), utils.SiteMapFileName) + return c.SendFile(path) +} + +func handleRss(c fiber.Ctx) error { + path := filepath.Join(utils.GetStoragePath(), utils.RssFileName) + return c.SendFile(path) +} + func serveIndexHtml(c fiber.Ctx) error { data, err := os.ReadFile("static/index.html") if err != nil { @@ -58,7 +81,7 @@ func serveIndexHtml(c fiber.Ctx) error { preview = fmt.Sprintf("%s/api/image/%d", serverBaseURL, r.Images[0].ID) } title = r.Title - description = getResourceDescription(r.Article) + description = utils.ArticleToDescription(r.Article, 200) } } } else if strings.HasPrefix(path, "/user/") { @@ -82,46 +105,3 @@ func serveIndexHtml(c fiber.Ctx) error { c.Set("Content-Type", "text/html; charset=utf-8") return c.SendString(content) } - -func mergeSpaces(str string) string { - // Replace multiple spaces with a single space - builder := strings.Builder{} - for i, r := range str { - if r == '\t' || r == '\r' { - continue - } - if r == ' ' || r == '\n' { - if i > 0 && str[i-1] != ' ' && str[i-1] != '\n' { - builder.WriteRune(' ') - } - } else { - builder.WriteRune(r) - } - } - return builder.String() -} - -func getResourceDescription(article string) string { - htmlContent := mdToHTML([]byte(article)) - plain := html2text.HTML2Text(string(htmlContent)) - plain = strings.TrimSpace(plain) - plain = mergeSpaces(plain) - if len([]rune(plain)) > 200 { - plain = string([]rune(plain)[:197]) + "..." - } - return plain -} - -func mdToHTML(md []byte) []byte { - // create Markdown parser with extensions - extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock | parser.MathJax - p := parser.NewWithExtensions(extensions) - doc := p.Parse(md) - - // create HTML renderer with extensions - htmlFlags := html.CommonFlags | html.HrefTargetBlank - opts := html.RendererOptions{Flags: htmlFlags} - renderer := html.NewRenderer(opts) - - return markdown.Render(doc, renderer) -} diff --git a/server/utils/article_to_description.go b/server/utils/article_to_description.go new file mode 100644 index 0000000..d4059d3 --- /dev/null +++ b/server/utils/article_to_description.go @@ -0,0 +1,55 @@ +package utils + +import ( + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/k3a/html2text" + "strings" +) + +func ArticleToDescription(article string, maxLength int) string { + if maxLength < 3 { + maxLength = 3 + } + htmlContent := mdToHTML([]byte(article)) + plain := html2text.HTML2Text(string(htmlContent)) + plain = strings.TrimSpace(plain) + plain = mergeSpaces(plain) + if len([]rune(plain)) > maxLength { + plain = string([]rune(plain)[:(maxLength-3)]) + "..." + } + return plain +} + +func mergeSpaces(str string) string { + // Replace multiple spaces with a single space + builder := strings.Builder{} + for i, r := range str { + if r == '\t' || r == '\r' { + continue + } + if r == ' ' || r == '\n' { + if i > 0 && str[i-1] != ' ' && str[i-1] != '\n' { + builder.WriteRune(' ') + } + } else { + builder.WriteRune(r) + } + } + return builder.String() +} + +func mdToHTML(md []byte) []byte { + // create Markdown parser with extensions + extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock | parser.MathJax + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +} diff --git a/server/utils/rss.go b/server/utils/rss.go new file mode 100644 index 0000000..f903e81 --- /dev/null +++ b/server/utils/rss.go @@ -0,0 +1,49 @@ +package utils + +import ( + "github.com/gofiber/fiber/v3/log" + "net/url" + "nysoure/server/model" + "os" + "path/filepath" + "strconv" + "strings" +) + +const ( + RssFileName = "rss.xml" +) + +func GenerateRss(baseURL string, resources []model.Resource) { + path := filepath.Join(GetStoragePath(), RssFileName) + builder := strings.Builder{} + builder.WriteString(``) + builder.WriteRune('\n') + builder.WriteString(``) + builder.WriteRune('\n') + builder.WriteString(``) + builder.WriteRune('\n') + for _, resource := range resources { + builder.WriteString(" \n") + builder.WriteString(" ") + builder.WriteString(url.PathEscape(resource.Title)) + builder.WriteString("\n") + builder.WriteString(" ") + builder.WriteString(baseURL + "/resources/" + strconv.Itoa(int(resource.ID))) + builder.WriteString("\n") + builder.WriteString(" ") + builder.WriteString(url.PathEscape(ArticleToDescription(resource.Article, 255))) + builder.WriteString("\n") + builder.WriteString(" ") + builder.WriteString(resource.UpdatedAt.Format("Mon, 02 Jan 2006 15:04:05 MST")) + builder.WriteString("\n") + builder.WriteString(" \n") + } + builder.WriteString(``) + builder.WriteRune('\n') + builder.WriteString(``) + data := builder.String() + if err := os.WriteFile(path, []byte(data), 0644); err != nil { + log.Error("failed to write RSS file", err) + } +} diff --git a/server/utils/site_map.go b/server/utils/site_map.go new file mode 100644 index 0000000..1032d61 --- /dev/null +++ b/server/utils/site_map.go @@ -0,0 +1,39 @@ +package utils + +import ( + "fmt" + "github.com/gofiber/fiber/v3/log" + "nysoure/server/model" + "os" + "path/filepath" + "strings" +) + +const ( + SiteMapFileName = "sitemap.xml" +) + +func GenerateSiteMap(baseURL string, resources []model.Resource) { + path := filepath.Join(GetStoragePath(), SiteMapFileName) + builder := strings.Builder{} + builder.WriteString(``) + builder.WriteRune('\n') + builder.WriteString(``) + builder.WriteRune('\n') + for _, resource := range resources { + builder.WriteString(" \n") + builder.WriteString(" ") + builder.WriteString(fmt.Sprintf("%s/resources/%d", baseURL, resource.ID)) + builder.WriteString("\n") + builder.WriteString(" ") + builder.WriteString(resource.UpdatedAt.Format("2006-01-02")) + builder.WriteString("\n") + builder.WriteString(" \n") + } + builder.WriteString(``) + builder.WriteRune('\n') + data := builder.String() + if err := os.WriteFile(path, []byte(data), 0644); err != nil { + log.Error("failed to write site map file", err) + } +}