diff --git a/cmd/scripts/prose-ssg/main.go b/cmd/scripts/prose-ssg/main.go index 23a9a297..35530e90 100644 --- a/cmd/scripts/prose-ssg/main.go +++ b/cmd/scripts/prose-ssg/main.go @@ -1,9 +1,19 @@ package main import ( + "bufio" + "context" + "encoding/json" + "log/slog" + "sync" + "time" + "github.com/picosh/pico/db/postgres" + "github.com/picosh/pico/filehandlers" "github.com/picosh/pico/prose" + "github.com/picosh/pico/shared" "github.com/picosh/pico/shared/storage" + "github.com/picosh/utils/pipe" ) func bail(err error) { @@ -12,11 +22,63 @@ func bail(err error) { } } +func render(ssg *prose.SSG, ch chan string) { + var pendingFlushes sync.Map + tick := time.Tick(10 * time.Second) + for { + select { + case userID := <-ch: + ssg.Logger.Info("received request to generate blog", "userId", userID) + pendingFlushes.Store(userID, userID) + case <-tick: + ssg.Logger.Info("flushing ssg requests") + go func() { + pendingFlushes.Range(func(key, value any) bool { + pendingFlushes.Delete(key) + user, err := ssg.DB.FindUser(value.(string)) + if err != nil { + ssg.Logger.Error("cannot find user", "err", err) + return true + } + + bucket, err := ssg.Storage.GetBucket(shared.GetAssetBucketName(user.ID)) + if err != nil { + ssg.Logger.Error("cannot find bucket", "err", err) + return true + } + + err = ssg.ProseBlog(user, bucket) + if err != nil { + ssg.Logger.Error("cannot generate blog", "err", err) + } + return true + }) + }() + } + } +} + +func createSubProseDrain(ctx context.Context, logger *slog.Logger) *pipe.ReconnectReadWriteCloser { + info := shared.NewPicoPipeClient() + send := pipe.NewReconnectReadWriteCloser( + ctx, + logger, + info, + "sub to prose-drain", + "sub prose-drain -k", + 100, + -1, + ) + return send +} + func main() { cfg := prose.NewConfigSite() - picoDb := postgres.NewDB(cfg.DbURL, cfg.Logger) - st, err := storage.NewStorageFS(cfg.Logger, cfg.StorageDir) + logger := cfg.Logger + picoDb := postgres.NewDB(cfg.DbURL, logger) + st, err := storage.NewStorageMinio(logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass) bail(err) + ssg := &prose.SSG{ Cfg: cfg, DB: picoDb, @@ -25,5 +87,45 @@ func main() { TmplDir: "./prose/html", StaticDir: "./prose/public", } - bail(ssg.Prose()) + + ctx := context.Background() + drain := createSubProseDrain(ctx, cfg.Logger) + + ch := make(chan string) + go render(ssg, ch) + + for { + scanner := bufio.NewScanner(drain) + for scanner.Scan() { + var data filehandlers.SuccesHook + + err := json.Unmarshal(scanner.Bytes(), &data) + if err != nil { + logger.Error("json unmarshal", "err", err) + continue + } + + logger = logger.With( + "userId", data.UserID, + "filename", data.Filename, + "action", data.Action, + ) + + if data.Action == "delete" { + bucket, err := ssg.Storage.GetBucket(shared.GetAssetBucketName(data.UserID)) + if err != nil { + ssg.Logger.Error("cannot find bucket", "err", err) + continue + } + err = st.DeleteObject(bucket, data.Filename) + if err != nil { + logger.Error("cannot delete object", "err", err) + continue + } + ch <- data.UserID + } else if data.Action == "create" || data.Action == "update" { + ch <- data.UserID + } + } + } } diff --git a/db/db.go b/db/db.go index f3055989..2671f69c 100644 --- a/db/db.go +++ b/db/db.go @@ -131,6 +131,9 @@ type Post struct { MimeType string `json:"mime_type"` Data PostData `json:"data"` Tags []string `json:"tags"` + + // computed + IsVirtual bool } type Paginate[T any] struct { diff --git a/pgs/web.go b/pgs/web.go index a91261e0..cb665d40 100644 --- a/pgs/web.go +++ b/pgs/web.go @@ -397,6 +397,7 @@ var imgRegex = regexp.MustCompile("(.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)") func (web *WebRouter) AssetRequest(w http.ResponseWriter, r *http.Request) { fname := r.PathValue("fname") if imgRegex.MatchString(fname) { + fmt.Println("HIT") web.ImageRequest(w, r) return } @@ -414,6 +415,7 @@ func (web *WebRouter) ImageRequest(w http.ResponseWriter, r *http.Request) { if len(matches) >= 3 { imgOpts = matches[2] } + fmt.Println("ZZZ", fname, imgOpts) opts, err := storage.UriToImgProcessOpts(imgOpts) if err != nil { diff --git a/prose/html/blog-aside.partial.tmpl b/prose/html/blog-aside.partial.tmpl index 3e3b71e6..28e49357 100644 --- a/prose/html/blog-aside.partial.tmpl +++ b/prose/html/blog-aside.partial.tmpl @@ -22,7 +22,7 @@ {{if .HasFilter}} - clear filters + clear filters {{end}}
diff --git a/prose/ssg.go b/prose/ssg.go index c9aa171d..020a5e46 100644 --- a/prose/ssg.go +++ b/prose/ssg.go @@ -53,22 +53,7 @@ func (ssg *SSG) tmpl(fpath string) string { return filepath.Join(ssg.TmplDir, fpath) } -func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error { - pager := &db.Pager{Num: 250, Page: 0} - var err error - var posts []*db.Post - var p *db.Paginate[*db.Post] - if tag == "" { - p, err = ssg.DB.FindPostsForUser(pager, user.ID, Space) - } else { - p, err = ssg.DB.FindUserPostsByTag(pager, tag, user.ID, Space) - } - posts = p.Data - - if err != nil { - return err - } - +func (ssg *SSG) blogPage(w io.Writer, user *db.User, blog *UserBlogData, tag string) error { files := []string{ ssg.tmpl("blog.page.tmpl"), ssg.tmpl("blog-default.partial.tmpl"), @@ -91,9 +76,8 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error { Domain: getBlogDomain(user.Name, ssg.Cfg.Domain), } readmeTxt := &ReadmeTxt{} - - readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space) - if err == nil { + readme := blog.Readme + if readme != nil { parsedText, err := shared.ParseText(readme.Text) if err != nil { return err @@ -126,17 +110,23 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error { } } - hasCSS := false - _, err = ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space) - if err == nil { - hasCSS = true - } + hasCSS := blog.CSS != nil + postCollection := []PostItemData{} + for _, post := range blog.Posts { + if tag != "" { + parsed, err := shared.ParseText(post.Text) + if err != nil { + blog.Logger.Error("post parse text", "err", err) + continue + } + if !slices.Contains(parsed.Tags, tag) { + continue + } + } - postCollection := make([]PostItemData, 0, len(posts)) - for _, post := range posts { p := PostItemData{ URL: template.URL( - fmt.Sprintf("/%s.html", post.Slug), + fmt.Sprintf("/%s", post.Slug), ), BlogURL: template.URL("/"), Title: utils.FilenameToTitle(post.Filename, post.Title), @@ -167,23 +157,7 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error { return ts.Execute(w, data) } -func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { - var err error - pager := &db.Pager{Num: 10, Page: 0} - var posts []*db.Post - var p *db.Paginate[*db.Post] - if tag == "" { - p, err = ssg.DB.FindPostsForUser(pager, user.ID, Space) - } else { - p, err = ssg.DB.FindUserPostsByTag(pager, tag, user.ID, Space) - } - - if err != nil { - return err - } - - posts = p.Data - +func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, blog *UserBlogData) error { ts, err := template.ParseFiles(ssg.tmpl("rss.page.tmpl")) if err != nil { return err @@ -194,8 +168,8 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { Domain: getBlogDomain(user.Name, ssg.Cfg.Domain), } - readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space) - if err == nil { + readme := blog.Readme + if readme != nil { parsedText, err := shared.ParseText(readme.Text) if err != nil { return err @@ -225,7 +199,7 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { } var feedItems []*feeds.Item - for _, post := range posts { + for _, post := range blog.Posts { if slices.Contains(ssg.Cfg.HiddenPosts, post.Filename) { continue } @@ -234,9 +208,9 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { return err } - footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space) + footer := blog.Footer var footerHTML string - if err == nil { + if footer != nil { footerParsed, err := shared.ParseText(footer.Text) if err != nil { return err @@ -261,7 +235,7 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { Link: &feeds.Link{Href: realUrl}, Content: tpl.String(), Updated: *post.UpdatedAt, - Created: *post.CreatedAt, + Created: *post.PublishAt, Description: post.Description, } @@ -282,40 +256,31 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error { return err } -func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, error) { +func (ssg *SSG) writePostPage(w io.Writer, user *db.User, post *db.Post, blog *UserBlogData) (*shared.ParsedText, error) { blogName := getBlogName(user.Name) favicon := "" ogImage := "" ogImageCard := "" - hasCSS := false withStyles := true domain := getBlogDomain(user.Name, ssg.Cfg.Domain) var data PostPageData - aliases := []string{} - css, err := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space) - if err == nil { - if len(css.Text) > 0 { - hasCSS = true - } - } - - footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space) + footer := blog.Footer var footerHTML template.HTML - if err == nil { + if footer != nil { footerParsed, err := shared.ParseText(footer.Text) if err != nil { - return aliases, err + return nil, err } footerHTML = template.HTML(footerParsed.Html) } // we need the blog name from the readme unfortunately - readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space) - if err == nil { + readme := blog.Readme + if readme != nil { readmeParsed, err := shared.ParseText(readme.Text) if err != nil { - return aliases, err + return nil, err } if readmeParsed.MetaData.Title != "" { blogName = readmeParsed.MetaData.Title @@ -332,7 +297,7 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e diff := "" parsedText, err := shared.ParseText(post.Text) if err != nil { - return aliases, err + return nil, err } if parsedText.Image != "" { @@ -343,8 +308,6 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e ogImageCard = parsedText.ImageCard } - aliases = parsedText.Aliases - unlisted := false if post.Hidden || post.PublishAt.After(time.Now()) { unlisted = true @@ -365,7 +328,7 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e Username: user.Name, BlogName: blogName, Contents: template.HTML(parsedText.Html), - HasCSS: hasCSS, + HasCSS: blog.CSS != nil, CssURL: template.URL("/_styles.css"), Tags: parsedText.Tags, Image: template.URL(ogImage), @@ -385,10 +348,10 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e } ts, err := template.ParseFiles(files...) if err != nil { - return aliases, err + return nil, err } - return aliases, ts.Execute(w, data) + return parsedText, ts.Execute(w, data) } func (ssg *SSG) discoverPage(w io.Writer) error { @@ -511,9 +474,9 @@ func (ssg *SSG) discoverRssPage(w io.Writer) error { return err } -func (ssg *SSG) upload(bucket sst.Bucket, fpath string, rdr io.Reader) error { - toSite := filepath.Join("prose-blog", fpath) - ssg.Logger.Info("uploading object", "bucket", bucket.Name, "object", toSite) +func (ssg *SSG) upload(logger *slog.Logger, bucket sst.Bucket, fpath string, rdr io.Reader) error { + toSite := filepath.Join("prose", fpath) + logger.Info("uploading object", "bucket", bucket.Name, "object", toSite) buf := &bytes.Buffer{} size, err := io.Copy(buf, rdr) if err != nil { @@ -527,25 +490,18 @@ func (ssg *SSG) upload(bucket sst.Bucket, fpath string, rdr io.Reader) error { return err } -func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error { +func (ssg *SSG) notFoundPage(w io.Writer, user *db.User, blog *UserBlogData) error { ogImage := "" ogImageCard := "" favicon := "" contents := template.HTML("Oops! we can't seem to find this post.") title := "Post not found" desc := "Post not found" - hasCSS := false - - css, err := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space) - if err == nil { - if len(css.Text) > 0 { - hasCSS = true - } - } + hasCSS := blog.CSS != nil - footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space) + footer := blog.Footer var footerHTML template.HTML - if err == nil { + if footer != nil { footerParsed, err := shared.ParseText(footer.Text) if err != nil { return err @@ -554,8 +510,8 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error { } // we need the blog name from the readme unfortunately - readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space) - if err == nil { + readme := blog.Readme + if readme != nil { readmeParsed, err := shared.ParseText(readme.Text) if err != nil { return err @@ -565,11 +521,11 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error { favicon = readmeParsed.Favicon } - notFound, err := ssg.DB.FindPostWithFilename("_404.md", user.ID, Space) - if err == nil { + notFound := blog.NotFound + if notFound != nil { notFoundParsed, err := shared.ParseText(notFound.Text) if err != nil { - ssg.Logger.Error("could not parse markdown", "err", err.Error()) + blog.Logger.Error("could not parse markdown", "err", err.Error()) return err } if notFoundParsed.MetaData.Title != "" { @@ -616,10 +572,10 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error { return ts.Execute(w, data) } -func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error { +func (ssg *SSG) images(user *db.User, blog *UserBlogData) error { imgBucket, err := ssg.Storage.GetBucket(shared.GetImgsBucketName(user.ID)) if err != nil { - ssg.Logger.Info("user does not have an images dir, skipping") + blog.Logger.Info("user does not have an images dir, skipping") return nil } imgs, err := ssg.Storage.ListObjects(imgBucket, "/", false) @@ -632,7 +588,7 @@ func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error { if err != nil { return err } - err = ssg.upload(bucket, inf.Name(), rdr) + err = ssg.upload(blog.Logger, blog.Bucket, inf.Name(), rdr) if err != nil { return err } @@ -641,7 +597,7 @@ func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error { return nil } -func (ssg *SSG) static(bucket sst.Bucket) error { +func (ssg *SSG) static(logger *slog.Logger, bucket sst.Bucket) error { files, err := os.ReadDir(ssg.StaticDir) if err != nil { return err @@ -655,7 +611,7 @@ func (ssg *SSG) static(bucket sst.Bucket) error { if err != nil { return err } - err = ssg.upload(bucket, file.Name(), fp) + err = ssg.upload(logger, bucket, file.Name(), fp) if err != nil { return err } @@ -690,12 +646,12 @@ func (ssg *SSG) Prose() error { ssg.Logger.Info("generating _redirects file", "text", redirectsFile) // create redirects file redirects := strings.NewReader(redirectsFile) - err = ssg.upload(bucket, "_redirects", redirects) + err = ssg.upload(ssg.Logger, bucket, "_redirects", redirects) if err != nil { return err } - err = ssg.upload(bucket, "index.html", rdr) + err = ssg.upload(ssg.Logger, bucket, "index.html", rdr) if err != nil { return err } @@ -710,13 +666,13 @@ func (ssg *SSG) Prose() error { } }() - err = ssg.upload(bucket, "rss.atom", rdr) + err = ssg.upload(ssg.Logger, bucket, "rss.atom", rdr) if err != nil { return err } ssg.Logger.Info("copying static folder for root", "dir", ssg.StaticDir) - err = ssg.static(bucket) + err = ssg.static(ssg.Logger, bucket) if err != nil { return err } @@ -746,87 +702,273 @@ func (ssg *SSG) Prose() error { return nil } +func (ssg *SSG) PostPage(user *db.User, blog *UserBlogData, post *db.Post) (pt *shared.ParsedText, err error) { + // create post file + rdr, wtr := io.Pipe() + var parsed *shared.ParsedText + go func() { + parsed, err = ssg.writePostPage(wtr, user, post, blog) + wtr.Close() + if err != nil { + blog.Logger.Error("post page", "err", err) + } + }() + + fname := post.Slug + ".html" + err = ssg.upload(blog.Logger, blog.Bucket, fname, rdr) + if err != nil { + return parsed, err + } + return parsed, nil +} + +func (ssg *SSG) NotFoundPage(logger *slog.Logger, user *db.User, blog *UserBlogData) error { + // create 404 page + logger.Info("generating 404 page") + rdr, wtr := io.Pipe() + go func() { + err := ssg.notFoundPage(wtr, user, blog) + wtr.Close() + if err != nil { + blog.Logger.Error("not found page", "err", err) + } + }() + + err := ssg.upload(blog.Logger, blog.Bucket, "404.html", rdr) + if err != nil { + return err + } + + return nil +} + +func (ssg *SSG) findPost(username string, bucket sst.Bucket, filename string, modTime time.Time) (*db.Post, error) { + updatedAt := modTime + fp := filepath.Join("prose/", filename) + logger := ssg.Logger.With("filename", fp) + rdr, info, err := ssg.Storage.GetObject(bucket, fp) + if err != nil { + logger.Error("get object", "err", err) + return nil, err + } + txtb, err := io.ReadAll(rdr) + if err != nil { + logger.Error("reader to string", "err", err) + return nil, err + } + txt := string(txtb) + parsed, err := shared.ParseText(txt) + if err != nil { + logger.Error("parse text", "err", err) + return nil, err + } + if parsed.PublishAt == nil || parsed.PublishAt.IsZero() { + ca := info.Metadata.Get("Date") + if ca != "" { + dt, err := time.Parse(time.RFC1123, ca) + if err != nil { + return nil, err + } + parsed.PublishAt = &dt + } + } + + slug := utils.SanitizeFileExt(filename) + + return &db.Post{ + IsVirtual: true, + Slug: slug, + Filename: filename, + FileSize: len(txt), + Text: txt, + PublishAt: parsed.PublishAt, + UpdatedAt: &updatedAt, + Hidden: parsed.Hidden, + Description: parsed.Description, + Title: utils.FilenameToTitle(filename, parsed.Title), + Username: username, + }, nil +} + +func (ssg *SSG) findPostByName(userID, username string, bucket sst.Bucket, filename string, modTime time.Time) (*db.Post, error) { + post, err := ssg.findPost(username, bucket, filename, modTime) + if err == nil { + return post, nil + } + return ssg.DB.FindPostWithFilename(filename, userID, Space) +} + +func (ssg *SSG) findPosts(blog *UserBlogData) ([]*db.Post, bool, error) { + posts := []*db.Post{} + blog.Logger.Info("finding posts") + objs, _ := ssg.Storage.ListObjects(blog.Bucket, "prose/", true) + if len(objs) > 0 { + blog.Logger.Info("found posts in bucket, using them") + } + for _, obj := range objs { + if obj.IsDir() { + continue + } + + ext := filepath.Ext(obj.Name()) + if ext == ".md" { + post, err := ssg.findPost(blog.User.Name, blog.Bucket, obj.Name(), obj.ModTime()) + if err != nil { + blog.Logger.Error("find post", "err", err, "filename", obj.Name()) + continue + } + posts = append(posts, post) + } + } + + // we found markdown files in the pgs site so the assumption is + // the pgs site is now the source of truth and we can ignore the posts table + if len(posts) > 0 { + return posts, true, nil + } + + blog.Logger.Info("no posts found in bucket, using posts table") + data, err := ssg.DB.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, blog.User.ID, Space) + if err != nil { + return nil, false, err + } + return data.Data, false, nil +} + +type UserBlogData struct { + Bucket sst.Bucket + User *db.User + Posts []*db.Post + Readme *db.Post + Footer *db.Post + CSS *db.Post + NotFound *db.Post + Logger *slog.Logger +} + func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error { // programmatically generate redirects file based on aliases // and other routes that were in prose that need to be available redirectsFile := "/rss /rss.atom 301\n" logger := shared.LoggerWithUser(ssg.Logger, user) + logger.Info("generating blog for user") - data, err := ssg.DB.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, user.ID, Space) + _, err := ssg.DB.FindProjectByName(user.ID, "prose") if err != nil { - return err + _, err := ssg.DB.InsertProject(user.ID, "prose", "prose") + if err != nil { + return err + } + return ssg.ProseBlog(user, bucket) + } + + blog := &UserBlogData{ + User: user, + Bucket: bucket, + Logger: logger, } - // don't generate a site with 0 posts - if data.Total == 0 { + posts, isVirtual, err := ssg.findPosts(blog) + if err != nil { + // no posts found, bail on generating an empty blog + // TODO: gen the index anyway? return nil } - for _, post := range data.Data { - if post.Slug == "" { - logger.Warn("post slug empty, skipping") - continue - } + blog.Posts = posts - logger.Info("generating post", "slug", post.Slug) - fpath := fmt.Sprintf("%s.html", post.Slug) + css, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_styles.css", time.Time{}) + if css != nil && !css.IsVirtual { + stylerdr := strings.NewReader(css.Text) + err = ssg.upload(blog.Logger, bucket, "_styles.css", stylerdr) + if err != nil { + return err + } + } + blog.CSS = css - // create post file - rdr, wtr := io.Pipe() - go func() { - aliases, err := ssg.postPage(wtr, user, post) - wtr.Close() - if err != nil { - ssg.Logger.Error("post page", "err", err) - } - // add aliases to redirects file - for _, alias := range aliases { - redirectsFile += fmt.Sprintf("%s %s 200\n", alias, "/"+fpath) - } - }() + readme, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_readme.md", time.Time{}) + if readme != nil && !readme.IsVirtual { + rdr := strings.NewReader(readme.Text) + err = ssg.upload(blog.Logger, bucket, "_readme.md", rdr) + if err != nil { + return err + } + } + blog.Readme = readme - err = ssg.upload(bucket, fpath, rdr) + footer, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_footer.md", time.Time{}) + if readme != nil && !readme.IsVirtual { + rdr := strings.NewReader(footer.Text) + err = ssg.upload(blog.Logger, bucket, "_footer.md", rdr) if err != nil { return err } + } + blog.Footer = footer - // create raw post file - fpath = post.Slug + ".md" - mdRdr := strings.NewReader(post.Text) - err = ssg.upload(bucket, fpath, mdRdr) + notFound, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_404.md", time.Time{}) + if notFound != nil && !notFound.IsVirtual { + rdr := strings.NewReader(notFound.Text) + err = ssg.upload(blog.Logger, bucket, "_404.md", rdr) if err != nil { return err } } + blog.NotFound = notFound - // create 404 page - logger.Info("generating 404 page") - rdr, wtr := io.Pipe() - go func() { - err = ssg.notFoundPage(wtr, user) - wtr.Close() + tagMap := map[string]string{} + for _, post := range posts { + if post.Slug == "" { + logger.Warn("post slug empty, skipping") + continue + } + + logger.Info("generating post", "slug", post.Slug) + + parsed, err := ssg.PostPage(user, blog, post) if err != nil { - ssg.Logger.Error("not found page", "err", err) + return err + } + // add aliases to redirects file + for _, alias := range parsed.Aliases { + redirectsFile += fmt.Sprintf("%s %s 301\n", alias, "/"+post.Slug) + } + for _, tag := range parsed.Tags { + tagMap[tag] = tag } - }() - err = ssg.upload(bucket, "404.html", rdr) + // create raw post file + // only generate md file if we dont already have it in our pgs site + if !post.IsVirtual { + fpath := post.Slug + ".md" + mdRdr := strings.NewReader(post.Text) + err = ssg.upload(blog.Logger, bucket, fpath, mdRdr) + if err != nil { + return err + } + } + } + + err = ssg.NotFoundPage(logger, user, blog) if err != nil { return err } - tags, err := ssg.DB.FindTagsForUser(user.ID, Space) - tags = append(tags, "") + tags := []string{""} + for k := range tagMap { + tags = append(tags, k) + } // create index files for _, tag := range tags { logger.Info("generating blog index page", "tag", tag) rdr, wtr := io.Pipe() go func() { - err = ssg.blogPage(wtr, user, tag) + err = ssg.blogPage(wtr, user, blog, tag) wtr.Close() if err != nil { - ssg.Logger.Error("blog page", "err", err) + blog.Logger.Error("blog page", "err", err) } }() @@ -834,24 +976,24 @@ func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error { if tag != "" { fpath = fmt.Sprintf("index-%s.html", tag) } - err = ssg.upload(bucket, fpath, rdr) + err = ssg.upload(blog.Logger, bucket, fpath, rdr) if err != nil { return err } } logger.Info("generating blog rss page", "tag", "") - rdr, wtr = io.Pipe() + rdr, wtr := io.Pipe() go func() { - err = ssg.rssBlogPage(wtr, user, "") + err = ssg.rssBlogPage(wtr, user, blog) wtr.Close() if err != nil { - ssg.Logger.Error("blog rss page", "err", err) + blog.Logger.Error("blog rss page", "err", err) } }() fpath := "rss.atom" - err = ssg.upload(bucket, fpath, rdr) + err = ssg.upload(blog.Logger, bucket, fpath, rdr) if err != nil { return err } @@ -859,31 +1001,25 @@ func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error { logger.Info("generating _redirects file", "text", redirectsFile) // create redirects file redirects := strings.NewReader(redirectsFile) - err = ssg.upload(bucket, "_redirects", redirects) + err = ssg.upload(blog.Logger, bucket, "_redirects", redirects) if err != nil { return err } - post, _ := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space) - if post != nil { - stylerdr := strings.NewReader(post.Text) - err = ssg.upload(bucket, "_styles.css", stylerdr) - if err != nil { - return err - } - } - logger.Info("copying static folder", "dir", ssg.StaticDir) - err = ssg.static(bucket) + err = ssg.static(blog.Logger, bucket) if err != nil { return err } - logger.Info("copying images") - err = ssg.images(user, bucket) - if err != nil { - return err + if !isVirtual { + logger.Info("copying images") + err = ssg.images(user, blog) + if err != nil { + return err + } } + logger.Info("success!") return nil } diff --git a/shared/mdparser.go b/shared/mdparser.go index f1eaee2f..b7f49943 100644 --- a/shared/mdparser.go +++ b/shared/mdparser.go @@ -218,6 +218,7 @@ func ParseText(text string) (*ParsedText, error) { Tags: []string{}, Aliases: []string{}, WithStyles: true, + PublishAt: &time.Time{}, }, } hili := highlighting.NewHighlighting( @@ -318,7 +319,7 @@ func ParseText(text string) (*ParsedText, error) { } parsed.MetaData.Favicon = favicon - var publishAt *time.Time = nil + publishAt := &time.Time{} date, err := toString(metaData["date"]) if err != nil { return &parsed, fmt.Errorf("front-matter field (%s): %w", "date", err)