- commit
- e2630f9
- parent
- 87407ab
- author
- Eric Bower
- date
- 2025-01-17 15:52:39 -0500 EST
chore(prose): experimental ssg works
6 files changed,
+411,
-167
+105,
-3
1@@ -1,9 +1,19 @@
2 package main
3
4 import (
5+ "bufio"
6+ "context"
7+ "encoding/json"
8+ "log/slog"
9+ "sync"
10+ "time"
11+
12 "github.com/picosh/pico/db/postgres"
13+ "github.com/picosh/pico/filehandlers"
14 "github.com/picosh/pico/prose"
15+ "github.com/picosh/pico/shared"
16 "github.com/picosh/pico/shared/storage"
17+ "github.com/picosh/utils/pipe"
18 )
19
20 func bail(err error) {
21@@ -12,11 +22,63 @@ func bail(err error) {
22 }
23 }
24
25+func render(ssg *prose.SSG, ch chan string) {
26+ var pendingFlushes sync.Map
27+ tick := time.Tick(10 * time.Second)
28+ for {
29+ select {
30+ case userID := <-ch:
31+ ssg.Logger.Info("received request to generate blog", "userId", userID)
32+ pendingFlushes.Store(userID, userID)
33+ case <-tick:
34+ ssg.Logger.Info("flushing ssg requests")
35+ go func() {
36+ pendingFlushes.Range(func(key, value any) bool {
37+ pendingFlushes.Delete(key)
38+ user, err := ssg.DB.FindUser(value.(string))
39+ if err != nil {
40+ ssg.Logger.Error("cannot find user", "err", err)
41+ return true
42+ }
43+
44+ bucket, err := ssg.Storage.GetBucket(shared.GetAssetBucketName(user.ID))
45+ if err != nil {
46+ ssg.Logger.Error("cannot find bucket", "err", err)
47+ return true
48+ }
49+
50+ err = ssg.ProseBlog(user, bucket)
51+ if err != nil {
52+ ssg.Logger.Error("cannot generate blog", "err", err)
53+ }
54+ return true
55+ })
56+ }()
57+ }
58+ }
59+}
60+
61+func createSubProseDrain(ctx context.Context, logger *slog.Logger) *pipe.ReconnectReadWriteCloser {
62+ info := shared.NewPicoPipeClient()
63+ send := pipe.NewReconnectReadWriteCloser(
64+ ctx,
65+ logger,
66+ info,
67+ "sub to prose-drain",
68+ "sub prose-drain -k",
69+ 100,
70+ -1,
71+ )
72+ return send
73+}
74+
75 func main() {
76 cfg := prose.NewConfigSite()
77- picoDb := postgres.NewDB(cfg.DbURL, cfg.Logger)
78- st, err := storage.NewStorageFS(cfg.Logger, cfg.StorageDir)
79+ logger := cfg.Logger
80+ picoDb := postgres.NewDB(cfg.DbURL, logger)
81+ st, err := storage.NewStorageMinio(logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
82 bail(err)
83+
84 ssg := &prose.SSG{
85 Cfg: cfg,
86 DB: picoDb,
87@@ -25,5 +87,45 @@ func main() {
88 TmplDir: "./prose/html",
89 StaticDir: "./prose/public",
90 }
91- bail(ssg.Prose())
92+
93+ ctx := context.Background()
94+ drain := createSubProseDrain(ctx, cfg.Logger)
95+
96+ ch := make(chan string)
97+ go render(ssg, ch)
98+
99+ for {
100+ scanner := bufio.NewScanner(drain)
101+ for scanner.Scan() {
102+ var data filehandlers.SuccesHook
103+
104+ err := json.Unmarshal(scanner.Bytes(), &data)
105+ if err != nil {
106+ logger.Error("json unmarshal", "err", err)
107+ continue
108+ }
109+
110+ logger = logger.With(
111+ "userId", data.UserID,
112+ "filename", data.Filename,
113+ "action", data.Action,
114+ )
115+
116+ if data.Action == "delete" {
117+ bucket, err := ssg.Storage.GetBucket(shared.GetAssetBucketName(data.UserID))
118+ if err != nil {
119+ ssg.Logger.Error("cannot find bucket", "err", err)
120+ continue
121+ }
122+ err = st.DeleteObject(bucket, data.Filename)
123+ if err != nil {
124+ logger.Error("cannot delete object", "err", err)
125+ continue
126+ }
127+ ch <- data.UserID
128+ } else if data.Action == "create" || data.Action == "update" {
129+ ch <- data.UserID
130+ }
131+ }
132+ }
133 }
M
db/db.go
+3,
-0
1@@ -131,6 +131,9 @@ type Post struct {
2 MimeType string `json:"mime_type"`
3 Data PostData `json:"data"`
4 Tags []string `json:"tags"`
5+
6+ // computed
7+ IsVirtual bool
8 }
9
10 type Paginate[T any] struct {
+2,
-0
1@@ -397,6 +397,7 @@ var imgRegex = regexp.MustCompile("(.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)")
2 func (web *WebRouter) AssetRequest(w http.ResponseWriter, r *http.Request) {
3 fname := r.PathValue("fname")
4 if imgRegex.MatchString(fname) {
5+ fmt.Println("HIT")
6 web.ImageRequest(w, r)
7 return
8 }
9@@ -414,6 +415,7 @@ func (web *WebRouter) ImageRequest(w http.ResponseWriter, r *http.Request) {
10 if len(matches) >= 3 {
11 imgOpts = matches[2]
12 }
13+ fmt.Println("ZZZ", fname, imgOpts)
14
15 opts, err := storage.UriToImgProcessOpts(imgOpts)
16 if err != nil {
+1,
-1
1@@ -22,7 +22,7 @@
2 </div>
3
4 {{if .HasFilter}}
5- <a href={{.URL}}>clear filters</a>
6+ <a href="/">clear filters</a>
7 {{end}}
8
9 <div class="posts group mt-2">
+298,
-162
1@@ -53,22 +53,7 @@ func (ssg *SSG) tmpl(fpath string) string {
2 return filepath.Join(ssg.TmplDir, fpath)
3 }
4
5-func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error {
6- pager := &db.Pager{Num: 250, Page: 0}
7- var err error
8- var posts []*db.Post
9- var p *db.Paginate[*db.Post]
10- if tag == "" {
11- p, err = ssg.DB.FindPostsForUser(pager, user.ID, Space)
12- } else {
13- p, err = ssg.DB.FindUserPostsByTag(pager, tag, user.ID, Space)
14- }
15- posts = p.Data
16-
17- if err != nil {
18- return err
19- }
20-
21+func (ssg *SSG) blogPage(w io.Writer, user *db.User, blog *UserBlogData, tag string) error {
22 files := []string{
23 ssg.tmpl("blog.page.tmpl"),
24 ssg.tmpl("blog-default.partial.tmpl"),
25@@ -91,9 +76,8 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error {
26 Domain: getBlogDomain(user.Name, ssg.Cfg.Domain),
27 }
28 readmeTxt := &ReadmeTxt{}
29-
30- readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space)
31- if err == nil {
32+ readme := blog.Readme
33+ if readme != nil {
34 parsedText, err := shared.ParseText(readme.Text)
35 if err != nil {
36 return err
37@@ -126,17 +110,23 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error {
38 }
39 }
40
41- hasCSS := false
42- _, err = ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space)
43- if err == nil {
44- hasCSS = true
45- }
46+ hasCSS := blog.CSS != nil
47+ postCollection := []PostItemData{}
48+ for _, post := range blog.Posts {
49+ if tag != "" {
50+ parsed, err := shared.ParseText(post.Text)
51+ if err != nil {
52+ blog.Logger.Error("post parse text", "err", err)
53+ continue
54+ }
55+ if !slices.Contains(parsed.Tags, tag) {
56+ continue
57+ }
58+ }
59
60- postCollection := make([]PostItemData, 0, len(posts))
61- for _, post := range posts {
62 p := PostItemData{
63 URL: template.URL(
64- fmt.Sprintf("/%s.html", post.Slug),
65+ fmt.Sprintf("/%s", post.Slug),
66 ),
67 BlogURL: template.URL("/"),
68 Title: utils.FilenameToTitle(post.Filename, post.Title),
69@@ -167,23 +157,7 @@ func (ssg *SSG) blogPage(w io.Writer, user *db.User, tag string) error {
70 return ts.Execute(w, data)
71 }
72
73-func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
74- var err error
75- pager := &db.Pager{Num: 10, Page: 0}
76- var posts []*db.Post
77- var p *db.Paginate[*db.Post]
78- if tag == "" {
79- p, err = ssg.DB.FindPostsForUser(pager, user.ID, Space)
80- } else {
81- p, err = ssg.DB.FindUserPostsByTag(pager, tag, user.ID, Space)
82- }
83-
84- if err != nil {
85- return err
86- }
87-
88- posts = p.Data
89-
90+func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, blog *UserBlogData) error {
91 ts, err := template.ParseFiles(ssg.tmpl("rss.page.tmpl"))
92 if err != nil {
93 return err
94@@ -194,8 +168,8 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
95 Domain: getBlogDomain(user.Name, ssg.Cfg.Domain),
96 }
97
98- readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space)
99- if err == nil {
100+ readme := blog.Readme
101+ if readme != nil {
102 parsedText, err := shared.ParseText(readme.Text)
103 if err != nil {
104 return err
105@@ -225,7 +199,7 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
106 }
107
108 var feedItems []*feeds.Item
109- for _, post := range posts {
110+ for _, post := range blog.Posts {
111 if slices.Contains(ssg.Cfg.HiddenPosts, post.Filename) {
112 continue
113 }
114@@ -234,9 +208,9 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
115 return err
116 }
117
118- footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space)
119+ footer := blog.Footer
120 var footerHTML string
121- if err == nil {
122+ if footer != nil {
123 footerParsed, err := shared.ParseText(footer.Text)
124 if err != nil {
125 return err
126@@ -261,7 +235,7 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
127 Link: &feeds.Link{Href: realUrl},
128 Content: tpl.String(),
129 Updated: *post.UpdatedAt,
130- Created: *post.CreatedAt,
131+ Created: *post.PublishAt,
132 Description: post.Description,
133 }
134
135@@ -282,40 +256,31 @@ func (ssg *SSG) rssBlogPage(w io.Writer, user *db.User, tag string) error {
136 return err
137 }
138
139-func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, error) {
140+func (ssg *SSG) writePostPage(w io.Writer, user *db.User, post *db.Post, blog *UserBlogData) (*shared.ParsedText, error) {
141 blogName := getBlogName(user.Name)
142 favicon := ""
143 ogImage := ""
144 ogImageCard := ""
145- hasCSS := false
146 withStyles := true
147 domain := getBlogDomain(user.Name, ssg.Cfg.Domain)
148 var data PostPageData
149- aliases := []string{}
150
151- css, err := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space)
152- if err == nil {
153- if len(css.Text) > 0 {
154- hasCSS = true
155- }
156- }
157-
158- footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space)
159+ footer := blog.Footer
160 var footerHTML template.HTML
161- if err == nil {
162+ if footer != nil {
163 footerParsed, err := shared.ParseText(footer.Text)
164 if err != nil {
165- return aliases, err
166+ return nil, err
167 }
168 footerHTML = template.HTML(footerParsed.Html)
169 }
170
171 // we need the blog name from the readme unfortunately
172- readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space)
173- if err == nil {
174+ readme := blog.Readme
175+ if readme != nil {
176 readmeParsed, err := shared.ParseText(readme.Text)
177 if err != nil {
178- return aliases, err
179+ return nil, err
180 }
181 if readmeParsed.MetaData.Title != "" {
182 blogName = readmeParsed.MetaData.Title
183@@ -332,7 +297,7 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e
184 diff := ""
185 parsedText, err := shared.ParseText(post.Text)
186 if err != nil {
187- return aliases, err
188+ return nil, err
189 }
190
191 if parsedText.Image != "" {
192@@ -343,8 +308,6 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e
193 ogImageCard = parsedText.ImageCard
194 }
195
196- aliases = parsedText.Aliases
197-
198 unlisted := false
199 if post.Hidden || post.PublishAt.After(time.Now()) {
200 unlisted = true
201@@ -365,7 +328,7 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e
202 Username: user.Name,
203 BlogName: blogName,
204 Contents: template.HTML(parsedText.Html),
205- HasCSS: hasCSS,
206+ HasCSS: blog.CSS != nil,
207 CssURL: template.URL("/_styles.css"),
208 Tags: parsedText.Tags,
209 Image: template.URL(ogImage),
210@@ -385,10 +348,10 @@ func (ssg *SSG) postPage(w io.Writer, user *db.User, post *db.Post) ([]string, e
211 }
212 ts, err := template.ParseFiles(files...)
213 if err != nil {
214- return aliases, err
215+ return nil, err
216 }
217
218- return aliases, ts.Execute(w, data)
219+ return parsedText, ts.Execute(w, data)
220 }
221
222 func (ssg *SSG) discoverPage(w io.Writer) error {
223@@ -511,9 +474,9 @@ func (ssg *SSG) discoverRssPage(w io.Writer) error {
224 return err
225 }
226
227-func (ssg *SSG) upload(bucket sst.Bucket, fpath string, rdr io.Reader) error {
228- toSite := filepath.Join("prose-blog", fpath)
229- ssg.Logger.Info("uploading object", "bucket", bucket.Name, "object", toSite)
230+func (ssg *SSG) upload(logger *slog.Logger, bucket sst.Bucket, fpath string, rdr io.Reader) error {
231+ toSite := filepath.Join("prose", fpath)
232+ logger.Info("uploading object", "bucket", bucket.Name, "object", toSite)
233 buf := &bytes.Buffer{}
234 size, err := io.Copy(buf, rdr)
235 if err != nil {
236@@ -527,25 +490,18 @@ func (ssg *SSG) upload(bucket sst.Bucket, fpath string, rdr io.Reader) error {
237 return err
238 }
239
240-func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error {
241+func (ssg *SSG) notFoundPage(w io.Writer, user *db.User, blog *UserBlogData) error {
242 ogImage := ""
243 ogImageCard := ""
244 favicon := ""
245 contents := template.HTML("Oops! we can't seem to find this post.")
246 title := "Post not found"
247 desc := "Post not found"
248- hasCSS := false
249-
250- css, err := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space)
251- if err == nil {
252- if len(css.Text) > 0 {
253- hasCSS = true
254- }
255- }
256+ hasCSS := blog.CSS != nil
257
258- footer, err := ssg.DB.FindPostWithFilename("_footer.md", user.ID, Space)
259+ footer := blog.Footer
260 var footerHTML template.HTML
261- if err == nil {
262+ if footer != nil {
263 footerParsed, err := shared.ParseText(footer.Text)
264 if err != nil {
265 return err
266@@ -554,8 +510,8 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error {
267 }
268
269 // we need the blog name from the readme unfortunately
270- readme, err := ssg.DB.FindPostWithFilename("_readme.md", user.ID, Space)
271- if err == nil {
272+ readme := blog.Readme
273+ if readme != nil {
274 readmeParsed, err := shared.ParseText(readme.Text)
275 if err != nil {
276 return err
277@@ -565,11 +521,11 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error {
278 favicon = readmeParsed.Favicon
279 }
280
281- notFound, err := ssg.DB.FindPostWithFilename("_404.md", user.ID, Space)
282- if err == nil {
283+ notFound := blog.NotFound
284+ if notFound != nil {
285 notFoundParsed, err := shared.ParseText(notFound.Text)
286 if err != nil {
287- ssg.Logger.Error("could not parse markdown", "err", err.Error())
288+ blog.Logger.Error("could not parse markdown", "err", err.Error())
289 return err
290 }
291 if notFoundParsed.MetaData.Title != "" {
292@@ -616,10 +572,10 @@ func (ssg *SSG) notFoundPage(w io.Writer, user *db.User) error {
293 return ts.Execute(w, data)
294 }
295
296-func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error {
297+func (ssg *SSG) images(user *db.User, blog *UserBlogData) error {
298 imgBucket, err := ssg.Storage.GetBucket(shared.GetImgsBucketName(user.ID))
299 if err != nil {
300- ssg.Logger.Info("user does not have an images dir, skipping")
301+ blog.Logger.Info("user does not have an images dir, skipping")
302 return nil
303 }
304 imgs, err := ssg.Storage.ListObjects(imgBucket, "/", false)
305@@ -632,7 +588,7 @@ func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error {
306 if err != nil {
307 return err
308 }
309- err = ssg.upload(bucket, inf.Name(), rdr)
310+ err = ssg.upload(blog.Logger, blog.Bucket, inf.Name(), rdr)
311 if err != nil {
312 return err
313 }
314@@ -641,7 +597,7 @@ func (ssg *SSG) images(user *db.User, bucket sst.Bucket) error {
315 return nil
316 }
317
318-func (ssg *SSG) static(bucket sst.Bucket) error {
319+func (ssg *SSG) static(logger *slog.Logger, bucket sst.Bucket) error {
320 files, err := os.ReadDir(ssg.StaticDir)
321 if err != nil {
322 return err
323@@ -655,7 +611,7 @@ func (ssg *SSG) static(bucket sst.Bucket) error {
324 if err != nil {
325 return err
326 }
327- err = ssg.upload(bucket, file.Name(), fp)
328+ err = ssg.upload(logger, bucket, file.Name(), fp)
329 if err != nil {
330 return err
331 }
332@@ -690,12 +646,12 @@ func (ssg *SSG) Prose() error {
333 ssg.Logger.Info("generating _redirects file", "text", redirectsFile)
334 // create redirects file
335 redirects := strings.NewReader(redirectsFile)
336- err = ssg.upload(bucket, "_redirects", redirects)
337+ err = ssg.upload(ssg.Logger, bucket, "_redirects", redirects)
338 if err != nil {
339 return err
340 }
341
342- err = ssg.upload(bucket, "index.html", rdr)
343+ err = ssg.upload(ssg.Logger, bucket, "index.html", rdr)
344 if err != nil {
345 return err
346 }
347@@ -710,13 +666,13 @@ func (ssg *SSG) Prose() error {
348 }
349 }()
350
351- err = ssg.upload(bucket, "rss.atom", rdr)
352+ err = ssg.upload(ssg.Logger, bucket, "rss.atom", rdr)
353 if err != nil {
354 return err
355 }
356
357 ssg.Logger.Info("copying static folder for root", "dir", ssg.StaticDir)
358- err = ssg.static(bucket)
359+ err = ssg.static(ssg.Logger, bucket)
360 if err != nil {
361 return err
362 }
363@@ -746,87 +702,273 @@ func (ssg *SSG) Prose() error {
364 return nil
365 }
366
367+func (ssg *SSG) PostPage(user *db.User, blog *UserBlogData, post *db.Post) (pt *shared.ParsedText, err error) {
368+ // create post file
369+ rdr, wtr := io.Pipe()
370+ var parsed *shared.ParsedText
371+ go func() {
372+ parsed, err = ssg.writePostPage(wtr, user, post, blog)
373+ wtr.Close()
374+ if err != nil {
375+ blog.Logger.Error("post page", "err", err)
376+ }
377+ }()
378+
379+ fname := post.Slug + ".html"
380+ err = ssg.upload(blog.Logger, blog.Bucket, fname, rdr)
381+ if err != nil {
382+ return parsed, err
383+ }
384+ return parsed, nil
385+}
386+
387+func (ssg *SSG) NotFoundPage(logger *slog.Logger, user *db.User, blog *UserBlogData) error {
388+ // create 404 page
389+ logger.Info("generating 404 page")
390+ rdr, wtr := io.Pipe()
391+ go func() {
392+ err := ssg.notFoundPage(wtr, user, blog)
393+ wtr.Close()
394+ if err != nil {
395+ blog.Logger.Error("not found page", "err", err)
396+ }
397+ }()
398+
399+ err := ssg.upload(blog.Logger, blog.Bucket, "404.html", rdr)
400+ if err != nil {
401+ return err
402+ }
403+
404+ return nil
405+}
406+
407+func (ssg *SSG) findPost(username string, bucket sst.Bucket, filename string, modTime time.Time) (*db.Post, error) {
408+ updatedAt := modTime
409+ fp := filepath.Join("prose/", filename)
410+ logger := ssg.Logger.With("filename", fp)
411+ rdr, info, err := ssg.Storage.GetObject(bucket, fp)
412+ if err != nil {
413+ logger.Error("get object", "err", err)
414+ return nil, err
415+ }
416+ txtb, err := io.ReadAll(rdr)
417+ if err != nil {
418+ logger.Error("reader to string", "err", err)
419+ return nil, err
420+ }
421+ txt := string(txtb)
422+ parsed, err := shared.ParseText(txt)
423+ if err != nil {
424+ logger.Error("parse text", "err", err)
425+ return nil, err
426+ }
427+ if parsed.PublishAt == nil || parsed.PublishAt.IsZero() {
428+ ca := info.Metadata.Get("Date")
429+ if ca != "" {
430+ dt, err := time.Parse(time.RFC1123, ca)
431+ if err != nil {
432+ return nil, err
433+ }
434+ parsed.PublishAt = &dt
435+ }
436+ }
437+
438+ slug := utils.SanitizeFileExt(filename)
439+
440+ return &db.Post{
441+ IsVirtual: true,
442+ Slug: slug,
443+ Filename: filename,
444+ FileSize: len(txt),
445+ Text: txt,
446+ PublishAt: parsed.PublishAt,
447+ UpdatedAt: &updatedAt,
448+ Hidden: parsed.Hidden,
449+ Description: parsed.Description,
450+ Title: utils.FilenameToTitle(filename, parsed.Title),
451+ Username: username,
452+ }, nil
453+}
454+
455+func (ssg *SSG) findPostByName(userID, username string, bucket sst.Bucket, filename string, modTime time.Time) (*db.Post, error) {
456+ post, err := ssg.findPost(username, bucket, filename, modTime)
457+ if err == nil {
458+ return post, nil
459+ }
460+ return ssg.DB.FindPostWithFilename(filename, userID, Space)
461+}
462+
463+func (ssg *SSG) findPosts(blog *UserBlogData) ([]*db.Post, bool, error) {
464+ posts := []*db.Post{}
465+ blog.Logger.Info("finding posts")
466+ objs, _ := ssg.Storage.ListObjects(blog.Bucket, "prose/", true)
467+ if len(objs) > 0 {
468+ blog.Logger.Info("found posts in bucket, using them")
469+ }
470+ for _, obj := range objs {
471+ if obj.IsDir() {
472+ continue
473+ }
474+
475+ ext := filepath.Ext(obj.Name())
476+ if ext == ".md" {
477+ post, err := ssg.findPost(blog.User.Name, blog.Bucket, obj.Name(), obj.ModTime())
478+ if err != nil {
479+ blog.Logger.Error("find post", "err", err, "filename", obj.Name())
480+ continue
481+ }
482+ posts = append(posts, post)
483+ }
484+ }
485+
486+ // we found markdown files in the pgs site so the assumption is
487+ // the pgs site is now the source of truth and we can ignore the posts table
488+ if len(posts) > 0 {
489+ return posts, true, nil
490+ }
491+
492+ blog.Logger.Info("no posts found in bucket, using posts table")
493+ data, err := ssg.DB.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, blog.User.ID, Space)
494+ if err != nil {
495+ return nil, false, err
496+ }
497+ return data.Data, false, nil
498+}
499+
500+type UserBlogData struct {
501+ Bucket sst.Bucket
502+ User *db.User
503+ Posts []*db.Post
504+ Readme *db.Post
505+ Footer *db.Post
506+ CSS *db.Post
507+ NotFound *db.Post
508+ Logger *slog.Logger
509+}
510+
511 func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error {
512 // programmatically generate redirects file based on aliases
513 // and other routes that were in prose that need to be available
514 redirectsFile := "/rss /rss.atom 301\n"
515 logger := shared.LoggerWithUser(ssg.Logger, user)
516+ logger.Info("generating blog for user")
517
518- data, err := ssg.DB.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, user.ID, Space)
519+ _, err := ssg.DB.FindProjectByName(user.ID, "prose")
520 if err != nil {
521- return err
522+ _, err := ssg.DB.InsertProject(user.ID, "prose", "prose")
523+ if err != nil {
524+ return err
525+ }
526+ return ssg.ProseBlog(user, bucket)
527+ }
528+
529+ blog := &UserBlogData{
530+ User: user,
531+ Bucket: bucket,
532+ Logger: logger,
533 }
534
535- // don't generate a site with 0 posts
536- if data.Total == 0 {
537+ posts, isVirtual, err := ssg.findPosts(blog)
538+ if err != nil {
539+ // no posts found, bail on generating an empty blog
540+ // TODO: gen the index anyway?
541 return nil
542 }
543
544- for _, post := range data.Data {
545- if post.Slug == "" {
546- logger.Warn("post slug empty, skipping")
547- continue
548- }
549+ blog.Posts = posts
550
551- logger.Info("generating post", "slug", post.Slug)
552- fpath := fmt.Sprintf("%s.html", post.Slug)
553+ css, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_styles.css", time.Time{})
554+ if css != nil && !css.IsVirtual {
555+ stylerdr := strings.NewReader(css.Text)
556+ err = ssg.upload(blog.Logger, bucket, "_styles.css", stylerdr)
557+ if err != nil {
558+ return err
559+ }
560+ }
561+ blog.CSS = css
562
563- // create post file
564- rdr, wtr := io.Pipe()
565- go func() {
566- aliases, err := ssg.postPage(wtr, user, post)
567- wtr.Close()
568- if err != nil {
569- ssg.Logger.Error("post page", "err", err)
570- }
571- // add aliases to redirects file
572- for _, alias := range aliases {
573- redirectsFile += fmt.Sprintf("%s %s 200\n", alias, "/"+fpath)
574- }
575- }()
576+ readme, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_readme.md", time.Time{})
577+ if readme != nil && !readme.IsVirtual {
578+ rdr := strings.NewReader(readme.Text)
579+ err = ssg.upload(blog.Logger, bucket, "_readme.md", rdr)
580+ if err != nil {
581+ return err
582+ }
583+ }
584+ blog.Readme = readme
585
586- err = ssg.upload(bucket, fpath, rdr)
587+ footer, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_footer.md", time.Time{})
588+ if readme != nil && !readme.IsVirtual {
589+ rdr := strings.NewReader(footer.Text)
590+ err = ssg.upload(blog.Logger, bucket, "_footer.md", rdr)
591 if err != nil {
592 return err
593 }
594+ }
595+ blog.Footer = footer
596
597- // create raw post file
598- fpath = post.Slug + ".md"
599- mdRdr := strings.NewReader(post.Text)
600- err = ssg.upload(bucket, fpath, mdRdr)
601+ notFound, _ := ssg.findPostByName(user.ID, user.Name, bucket, "_404.md", time.Time{})
602+ if notFound != nil && !notFound.IsVirtual {
603+ rdr := strings.NewReader(notFound.Text)
604+ err = ssg.upload(blog.Logger, bucket, "_404.md", rdr)
605 if err != nil {
606 return err
607 }
608 }
609+ blog.NotFound = notFound
610
611- // create 404 page
612- logger.Info("generating 404 page")
613- rdr, wtr := io.Pipe()
614- go func() {
615- err = ssg.notFoundPage(wtr, user)
616- wtr.Close()
617+ tagMap := map[string]string{}
618+ for _, post := range posts {
619+ if post.Slug == "" {
620+ logger.Warn("post slug empty, skipping")
621+ continue
622+ }
623+
624+ logger.Info("generating post", "slug", post.Slug)
625+
626+ parsed, err := ssg.PostPage(user, blog, post)
627 if err != nil {
628- ssg.Logger.Error("not found page", "err", err)
629+ return err
630+ }
631+ // add aliases to redirects file
632+ for _, alias := range parsed.Aliases {
633+ redirectsFile += fmt.Sprintf("%s %s 301\n", alias, "/"+post.Slug)
634+ }
635+ for _, tag := range parsed.Tags {
636+ tagMap[tag] = tag
637 }
638- }()
639
640- err = ssg.upload(bucket, "404.html", rdr)
641+ // create raw post file
642+ // only generate md file if we dont already have it in our pgs site
643+ if !post.IsVirtual {
644+ fpath := post.Slug + ".md"
645+ mdRdr := strings.NewReader(post.Text)
646+ err = ssg.upload(blog.Logger, bucket, fpath, mdRdr)
647+ if err != nil {
648+ return err
649+ }
650+ }
651+ }
652+
653+ err = ssg.NotFoundPage(logger, user, blog)
654 if err != nil {
655 return err
656 }
657
658- tags, err := ssg.DB.FindTagsForUser(user.ID, Space)
659- tags = append(tags, "")
660+ tags := []string{""}
661+ for k := range tagMap {
662+ tags = append(tags, k)
663+ }
664
665 // create index files
666 for _, tag := range tags {
667 logger.Info("generating blog index page", "tag", tag)
668 rdr, wtr := io.Pipe()
669 go func() {
670- err = ssg.blogPage(wtr, user, tag)
671+ err = ssg.blogPage(wtr, user, blog, tag)
672 wtr.Close()
673 if err != nil {
674- ssg.Logger.Error("blog page", "err", err)
675+ blog.Logger.Error("blog page", "err", err)
676 }
677 }()
678
679@@ -834,24 +976,24 @@ func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error {
680 if tag != "" {
681 fpath = fmt.Sprintf("index-%s.html", tag)
682 }
683- err = ssg.upload(bucket, fpath, rdr)
684+ err = ssg.upload(blog.Logger, bucket, fpath, rdr)
685 if err != nil {
686 return err
687 }
688 }
689
690 logger.Info("generating blog rss page", "tag", "")
691- rdr, wtr = io.Pipe()
692+ rdr, wtr := io.Pipe()
693 go func() {
694- err = ssg.rssBlogPage(wtr, user, "")
695+ err = ssg.rssBlogPage(wtr, user, blog)
696 wtr.Close()
697 if err != nil {
698- ssg.Logger.Error("blog rss page", "err", err)
699+ blog.Logger.Error("blog rss page", "err", err)
700 }
701 }()
702
703 fpath := "rss.atom"
704- err = ssg.upload(bucket, fpath, rdr)
705+ err = ssg.upload(blog.Logger, bucket, fpath, rdr)
706 if err != nil {
707 return err
708 }
709@@ -859,31 +1001,25 @@ func (ssg *SSG) ProseBlog(user *db.User, bucket sst.Bucket) error {
710 logger.Info("generating _redirects file", "text", redirectsFile)
711 // create redirects file
712 redirects := strings.NewReader(redirectsFile)
713- err = ssg.upload(bucket, "_redirects", redirects)
714+ err = ssg.upload(blog.Logger, bucket, "_redirects", redirects)
715 if err != nil {
716 return err
717 }
718
719- post, _ := ssg.DB.FindPostWithFilename("_styles.css", user.ID, Space)
720- if post != nil {
721- stylerdr := strings.NewReader(post.Text)
722- err = ssg.upload(bucket, "_styles.css", stylerdr)
723- if err != nil {
724- return err
725- }
726- }
727-
728 logger.Info("copying static folder", "dir", ssg.StaticDir)
729- err = ssg.static(bucket)
730+ err = ssg.static(blog.Logger, bucket)
731 if err != nil {
732 return err
733 }
734
735- logger.Info("copying images")
736- err = ssg.images(user, bucket)
737- if err != nil {
738- return err
739+ if !isVirtual {
740+ logger.Info("copying images")
741+ err = ssg.images(user, blog)
742+ if err != nil {
743+ return err
744+ }
745 }
746
747+ logger.Info("success!")
748 return nil
749 }
1@@ -218,6 +218,7 @@ func ParseText(text string) (*ParsedText, error) {
2 Tags: []string{},
3 Aliases: []string{},
4 WithStyles: true,
5+ PublishAt: &time.Time{},
6 },
7 }
8 hili := highlighting.NewHighlighting(
9@@ -318,7 +319,7 @@ func ParseText(text string) (*ParsedText, error) {
10 }
11 parsed.MetaData.Favicon = favicon
12
13- var publishAt *time.Time = nil
14+ publishAt := &time.Time{}
15 date, err := toString(metaData["date"])
16 if err != nil {
17 return &parsed, fmt.Errorf("front-matter field (%s): %w", "date", err)