This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / blog / blog.go
3.9 kB 174 lines
1package blog 2 3import ( 4 "bytes" 5 "cmp" 6 "html/template" 7 "io" 8 "io/fs" 9 "os" 10 "slices" 11 "strings" 12 "time" 13 14 "github.com/adrg/frontmatter" 15 "github.com/gorilla/feeds" 16 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/pages/markup" 19 textension "tangled.org/core/appview/pages/markup/extension" 20) 21 22type Author struct { 23 Name string `yaml:"name"` 24 Email string `yaml:"email"` 25 Handle string `yaml:"handle"` 26} 27 28type PostMeta struct { 29 Slug string `yaml:"slug"` 30 Title string `yaml:"title"` 31 Subtitle string `yaml:"subtitle"` 32 Date string `yaml:"date"` 33 Authors []Author `yaml:"authors"` 34 Image string `yaml:"image"` 35 Draft bool `yaml:"draft"` 36} 37 38type Post struct { 39 Meta PostMeta 40 Body template.HTML 41} 42 43func (p Post) ParsedDate() time.Time { 44 t, _ := time.Parse("2006-01-02", p.Meta.Date) 45 return t 46} 47 48type indexParams struct { 49 LoggedInUser any 50 Posts []Post 51 Featured []Post 52} 53 54type postParams struct { 55 LoggedInUser any 56 Post Post 57} 58 59// Posts parses and returns all non-draft posts sorted newest-first. 60func Posts(postsDir string) ([]Post, error) { 61 return parsePosts(postsDir, false) 62} 63 64// AllPosts parses and returns all posts including drafts, sorted newest-first. 65func AllPosts(postsDir string) ([]Post, error) { 66 return parsePosts(postsDir, true) 67} 68 69func parsePosts(postsDir string, includeDrafts bool) ([]Post, error) { 70 fsys := os.DirFS(postsDir) 71 72 entries, err := fs.ReadDir(fsys, ".") 73 if err != nil { 74 return nil, err 75 } 76 77 rctx := &markup.RenderContext{ 78 RendererType: markup.RendererTypeDefault, 79 Sanitizer: markup.NewSanitizer(), 80 } 81 var posts []Post 82 for _, entry := range entries { 83 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { 84 continue 85 } 86 87 data, err := fs.ReadFile(fsys, entry.Name()) 88 if err != nil { 89 return nil, err 90 } 91 92 var meta PostMeta 93 rest, err := frontmatter.Parse(bytes.NewReader(data), &meta) 94 if err != nil { 95 return nil, err 96 } 97 98 if meta.Draft && !includeDrafts { 99 continue 100 } 101 102 htmlStr := rctx.RenderMarkdownWith(string(rest), markup.NewMarkdownWith("", textension.Dashes)) 103 sanitized := rctx.SanitizeDefault(htmlStr) 104 105 posts = append(posts, Post{ 106 Meta: meta, 107 Body: template.HTML(sanitized), 108 }) 109 } 110 111 slices.SortFunc(posts, func(a, b Post) int { 112 return cmp.Compare(b.Meta.Date, a.Meta.Date) 113 }) 114 115 return posts, nil 116} 117 118func AtomFeed(posts []Post, baseURL string) (string, error) { 119 feed := &feeds.Feed{ 120 Title: "the tangled blog", 121 Link: &feeds.Link{Href: baseURL}, 122 Author: &feeds.Author{Name: "Tangled"}, 123 Created: time.Now(), 124 } 125 126 for _, p := range posts { 127 postURL := strings.TrimRight(baseURL, "/") + "/" + p.Meta.Slug 128 129 var authorName string 130 for i, a := range p.Meta.Authors { 131 if i > 0 { 132 authorName += " & " 133 } 134 authorName += a.Name 135 } 136 137 feed.Items = append(feed.Items, &feeds.Item{ 138 Title: p.Meta.Title, 139 Link: &feeds.Link{Href: postURL}, 140 Description: p.Meta.Subtitle, 141 Author: &feeds.Author{Name: authorName}, 142 Created: p.ParsedDate(), 143 }) 144 } 145 146 return feed.ToAtom() 147} 148 149// RenderIndex renders the blog index page to w. 150func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error { 151 tpl, err := p.ParseWith(os.DirFS(templatesDir), "index.html") 152 if err != nil { 153 return err 154 } 155 var featured []Post 156 for _, post := range posts { 157 if post.Meta.Image != "" { 158 featured = append(featured, post) 159 if len(featured) == 3 { 160 break 161 } 162 } 163 } 164 return tpl.ExecuteTemplate(w, "layouts/base", indexParams{Posts: posts, Featured: featured}) 165} 166 167// RenderPost renders a single blog post page to w. 168func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error { 169 tpl, err := p.ParseWith(os.DirFS(templatesDir), "post.html") 170 if err != nil { 171 return err 172 } 173 return tpl.ExecuteTemplate(w, "layouts/base", postParams{Post: post}) 174}