package frontend import ( "context" "fmt" "net/http" "os" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/yourselfhosted/gomark/parser" "github.com/yourselfhosted/gomark/parser/tokenizer" "github.com/yourselfhosted/gomark/renderer" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/server/profile" "github.com/usememos/memos/store" ) const ( // maxMetadataDescriptionLength is the maximum length of metadata description. maxMetadataDescriptionLength = 256 ) 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(ctx context.Context, 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", HTML5: true, Skipper: func(c echo.Context) bool { return util.HasPrefixes(c.Path(), "/api", "/memos.api.v2", "/robots.txt", "/sitemap.xml", "/m/:name") }, })) s.registerRoutes(e) s.registerFileRoutes(ctx, e) } func (s *FrontendService) registerRoutes(e *echo.Echo) { rawIndexHTML := getRawIndexHTML() e.GET("/m/:uid", func(c echo.Context) error { ctx := c.Request().Context() uid := c.Param("uid") memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ UID: &uid, }) 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).String()) indexHTML = strings.ReplaceAll(indexHTML, "", fmt.Sprintf("", memo.ID)) return c.HTML(http.StatusOK, indexHTML) }) } func (s *FrontendService) registerFileRoutes(ctx context.Context, e *echo.Echo) { workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) if err != nil { return } instanceURL := workspaceGeneralSetting.GetInstanceUrl() if instanceURL == "" { return } e.GET("/robots.txt", func(c echo.Context) error { 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() 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/%s", instanceURL, memo.UID))) } sitemap := fmt.Sprintf(`%s`, strings.Join(urlsets, "\n")) return c.XMLBlob(http.StatusOK, []byte(sitemap)) }) } func generateMemoMetadata(memo *store.Memo, creator *store.User) *Metadata { metadata := getDefaultMetadata() metadata.Title = fmt.Sprintf("%s(@%s) on Memos", creator.Nickname, creator.Username) if memo.Visibility == store.Public { tokens := tokenizer.Tokenize(memo.Content) nodes, _ := parser.Parse(tokens) description := renderer.NewStringRenderer().Render(nodes) if len(description) == 0 { description = memo.Content } if len(description) > maxMetadataDescriptionLength { description = description[:maxMetadataDescriptionLength] + "..." } metadata.Description = description } return metadata } func getRawIndexHTML() string { bytes, _ := os.ReadFile("dist/index.html") return string(bytes) } type Metadata struct { Title string Description string ImageURL string } func getDefaultMetadata() *Metadata { return &Metadata{ Title: "Memos", Description: "A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.", ImageURL: "/logo.webp", } } func (m *Metadata) String() string { metadataList := []string{ fmt.Sprintf(``, m.Description), fmt.Sprintf(``, m.Title), fmt.Sprintf(``, m.Description), fmt.Sprintf(``, m.ImageURL), ``, // Twitter related fields. fmt.Sprintf(``, m.Title), fmt.Sprintf(``, m.Description), fmt.Sprintf(``, m.ImageURL), ``, ``, } return strings.Join(metadataList, "\n") }