frontend.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package frontend
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "strings"
  8. "github.com/labstack/echo/v4"
  9. "github.com/labstack/echo/v4/middleware"
  10. apiv1 "github.com/usememos/memos/api/v1"
  11. "github.com/usememos/memos/internal/util"
  12. "github.com/usememos/memos/plugin/gomark/parser"
  13. "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
  14. "github.com/usememos/memos/plugin/gomark/renderer"
  15. "github.com/usememos/memos/server/profile"
  16. "github.com/usememos/memos/store"
  17. )
  18. const (
  19. // maxMetadataDescriptionLength is the maximum length of metadata description.
  20. maxMetadataDescriptionLength = 256
  21. )
  22. type FrontendService struct {
  23. Profile *profile.Profile
  24. Store *store.Store
  25. }
  26. func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendService {
  27. return &FrontendService{
  28. Profile: profile,
  29. Store: store,
  30. }
  31. }
  32. func (s *FrontendService) Serve(ctx context.Context, e *echo.Echo) {
  33. // Use echo static middleware to serve the built dist folder.
  34. // refer: https://github.com/labstack/echo/blob/master/middleware/static.go
  35. e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
  36. Root: "dist",
  37. HTML5: true,
  38. Skipper: func(c echo.Context) bool {
  39. return util.HasPrefixes(c.Path(), "/api", "/memos.api.v2", "/robots.txt", "/sitemap.xml", "/m/:memoID")
  40. },
  41. }))
  42. s.registerRoutes(e)
  43. s.registerFileRoutes(ctx, e)
  44. }
  45. func (s *FrontendService) registerRoutes(e *echo.Echo) {
  46. rawIndexHTML := getRawIndexHTML()
  47. e.GET("/m/:memoID", func(c echo.Context) error {
  48. ctx := c.Request().Context()
  49. memoID, err := util.ConvertStringToInt32(c.Param("memoID"))
  50. if err != nil {
  51. // Redirect to `index.html` if any error occurs.
  52. return c.HTML(http.StatusOK, rawIndexHTML)
  53. }
  54. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  55. ID: &memoID,
  56. })
  57. if err != nil {
  58. return c.HTML(http.StatusOK, rawIndexHTML)
  59. }
  60. if memo == nil {
  61. return c.HTML(http.StatusOK, rawIndexHTML)
  62. }
  63. creator, err := s.Store.GetUser(ctx, &store.FindUser{
  64. ID: &memo.CreatorID,
  65. })
  66. if err != nil {
  67. return c.HTML(http.StatusOK, rawIndexHTML)
  68. }
  69. // Inject memo metadata into `index.html`.
  70. indexHTML := strings.ReplaceAll(rawIndexHTML, "<!-- memos.metadata.head -->", generateMemoMetadata(memo, creator).String())
  71. indexHTML = strings.ReplaceAll(indexHTML, "<!-- memos.metadata.body -->", fmt.Sprintf("<!-- memos.memo.%d -->", memo.ID))
  72. return c.HTML(http.StatusOK, indexHTML)
  73. })
  74. }
  75. func (s *FrontendService) registerFileRoutes(ctx context.Context, e *echo.Echo) {
  76. instanceURLSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
  77. Name: apiv1.SystemSettingInstanceURLName.String(),
  78. })
  79. if err != nil || instanceURLSetting == nil {
  80. return
  81. }
  82. instanceURL := instanceURLSetting.Value
  83. if instanceURL == "" {
  84. return
  85. }
  86. e.GET("/robots.txt", func(c echo.Context) error {
  87. robotsTxt := fmt.Sprintf(`User-agent: *
  88. Allow: /
  89. Host: %s
  90. Sitemap: %s/sitemap.xml`, instanceURL, instanceURL)
  91. return c.String(http.StatusOK, robotsTxt)
  92. })
  93. e.GET("/sitemap.xml", func(c echo.Context) error {
  94. ctx := c.Request().Context()
  95. urlsets := []string{}
  96. // Append memo list.
  97. memoList, err := s.Store.ListMemos(ctx, &store.FindMemo{
  98. VisibilityList: []store.Visibility{store.Public},
  99. })
  100. if err != nil {
  101. return err
  102. }
  103. for _, memo := range memoList {
  104. urlsets = append(urlsets, fmt.Sprintf(`<url><loc>%s</loc></url>`, fmt.Sprintf("%s/m/%d", instanceURL, memo.ID)))
  105. }
  106. sitemap := fmt.Sprintf(`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">%s</urlset>`, strings.Join(urlsets, "\n"))
  107. return c.XMLBlob(http.StatusOK, []byte(sitemap))
  108. })
  109. }
  110. func generateMemoMetadata(memo *store.Memo, creator *store.User) *Metadata {
  111. metadata := getDefaultMetadata()
  112. metadata.Title = fmt.Sprintf("%s(@%s) on Memos", creator.Nickname, creator.Username)
  113. if memo.Visibility == store.Public {
  114. tokens := tokenizer.Tokenize(memo.Content)
  115. nodes, _ := parser.Parse(tokens)
  116. description := renderer.NewStringRenderer().Render(nodes)
  117. if len(description) == 0 {
  118. description = memo.Content
  119. }
  120. if len(description) > maxMetadataDescriptionLength {
  121. description = description[:maxMetadataDescriptionLength] + "..."
  122. }
  123. metadata.Description = description
  124. }
  125. return metadata
  126. }
  127. func getRawIndexHTML() string {
  128. bytes, _ := os.ReadFile("dist/index.html")
  129. return string(bytes)
  130. }
  131. type Metadata struct {
  132. Title string
  133. Description string
  134. ImageURL string
  135. }
  136. func getDefaultMetadata() *Metadata {
  137. return &Metadata{
  138. Title: "Memos",
  139. Description: "A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.",
  140. ImageURL: "/logo.webp",
  141. }
  142. }
  143. func (m *Metadata) String() string {
  144. metadataList := []string{
  145. fmt.Sprintf(`<meta name="description" content="%s" />`, m.Description),
  146. fmt.Sprintf(`<meta property="og:title" content="%s" />`, m.Title),
  147. fmt.Sprintf(`<meta property="og:description" content="%s" />`, m.Description),
  148. fmt.Sprintf(`<meta property="og:image" content="%s" />`, m.ImageURL),
  149. `<meta property="og:type" content="website" />`,
  150. // Twitter related fields.
  151. fmt.Sprintf(`<meta property="twitter:title" content="%s" />`, m.Title),
  152. fmt.Sprintf(`<meta property="twitter:description" content="%s" />`, m.Description),
  153. fmt.Sprintf(`<meta property="twitter:image" content="%s" />`, m.ImageURL),
  154. `<meta name="twitter:card" content="summary" />`,
  155. `<meta name="twitter:creator" content="memos" />`,
  156. }
  157. return strings.Join(metadataList, "\n")
  158. }