package frontend import ( "fmt" "html/template" "net/http" "os" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" v1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/plugin/gomark/parser" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/gomark/renderer" "github.com/usememos/memos/server/profile" "github.com/usememos/memos/store" ) type FrontendService struct { Profile *profile.Profile Store *store.Store } func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendService { return &FrontendService{ Profile: profile, Store: store, } } func (s *FrontendService) Serve(e *echo.Echo) { // Use echo static middleware to serve the built dist folder. // refer: https://github.com/labstack/echo/blob/master/middleware/static.go e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ Root: "dist", Skipper: defaultAPIRequestSkipper, HTML5: true, })) s.registerRoutes(e) } func (s *FrontendService) registerRoutes(e *echo.Echo) { rawIndexHTMLBytes, _ := os.ReadFile("dist/index.html") rawIndexHTML := string(rawIndexHTMLBytes) e.GET("/robots.txt", func(c echo.Context) error { ctx := c.Request().Context() instanceURLSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ Name: v1.SystemSettingInstanceURLName.String(), }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get instance URL system setting").SetInternal(err) } if instanceURLSetting == nil { return echo.NewHTTPError(http.StatusInternalServerError, "Instance URL system setting is not set") } instanceURL := instanceURLSetting.Value if instanceURL == "" { return echo.NewHTTPError(http.StatusInternalServerError, "Instance URL system setting is not set") } robotsTxt := fmt.Sprintf(`User-agent: * Allow: / Host: %s Sitemap: %s/sitemap.xml`, instanceURL, instanceURL) return c.String(http.StatusOK, robotsTxt) }) e.GET("/sitemap.xml", func(c echo.Context) error { ctx := c.Request().Context() instanceURLSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ Name: v1.SystemSettingInstanceURLName.String(), }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get instance URL system setting").SetInternal(err) } if instanceURLSetting == nil { return echo.NewHTTPError(http.StatusInternalServerError, "Instance URL system setting is not set") } instanceURL := instanceURLSetting.Value if instanceURL == "" { return echo.NewHTTPError(http.StatusInternalServerError, "Instance URL system setting is not set") } urlsets := []string{} // Append memo list. memoList, err := s.Store.ListMemos(ctx, &store.FindMemo{ VisibilityList: []store.Visibility{store.Public}, }) if err != nil { return err } for _, memo := range memoList { urlsets = append(urlsets, fmt.Sprintf(`%s`, fmt.Sprintf("%s/m/%d", instanceURL, memo.ID))) } // Append user list. userList, err := s.Store.ListUsers(ctx, &store.FindUser{}) if err != nil { return err } for _, user := range userList { urlsets = append(urlsets, fmt.Sprintf(`%s`, fmt.Sprintf("%s/u/%s", instanceURL, user.Username))) } sitemap := fmt.Sprintf(`%s`, strings.Join(urlsets, "\n")) return c.XMLBlob(http.StatusOK, []byte(sitemap)) }) e.GET("/m/:memoID", func(c echo.Context) error { ctx := c.Request().Context() memoID, err := util.ConvertStringToInt32(c.Param("memoID")) if err != nil { // Redirect to `index.html` if any error occurs. return c.HTML(http.StatusOK, rawIndexHTML) } memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ ID: &memoID, }) if err != nil { return c.HTML(http.StatusOK, rawIndexHTML) } if memo == nil { return c.HTML(http.StatusOK, rawIndexHTML) } creator, err := s.Store.GetUser(ctx, &store.FindUser{ ID: &memo.CreatorID, }) if err != nil { return c.HTML(http.StatusOK, rawIndexHTML) } // Inject memo metadata into `index.html`. indexHTML := strings.ReplaceAll(rawIndexHTML, "", generateMemoMetadata(memo, creator)) return c.HTML(http.StatusOK, indexHTML) }) } func generateMemoMetadata(memo *store.Memo, creator *store.User) string { description := "" if memo.Visibility == store.Private { description = "This memo is private." } else if memo.Visibility == store.Protected { description = "This memo is protected." } else { tokens := tokenizer.Tokenize(memo.Content) nodes, _ := parser.Parse(tokens) description = renderer.NewStringRenderer().Render(nodes) if len(description) == 0 { description = memo.Content } if len(description) > 200 { description = description[:200] + "..." } } metadataList := []string{ fmt.Sprintf(``, template.HTMLEscapeString(description)), fmt.Sprintf(``, template.HTMLEscapeString(fmt.Sprintf("%s(@%s) on Memos", creator.Nickname, creator.Username))), fmt.Sprintf(``, template.HTMLEscapeString(description)), fmt.Sprintf(``, "https://www.usememos.com/logo.png"), ``, // Twitter related metadata. fmt.Sprintf(``, template.HTMLEscapeString(fmt.Sprintf("%s(@%s) on Memos", creator.Nickname, creator.Username))), fmt.Sprintf(``, template.HTMLEscapeString(description)), fmt.Sprintf(``, "https://www.usememos.com/logo.png"), ``, } return strings.Join(metadataList, "\n") } func defaultAPIRequestSkipper(c echo.Context) bool { path := c.Request().URL.Path return util.HasPrefixes(path, "/api", "/memos.api.v2") }