A vibe coded tangled fork which supports pijul.
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 }
80 var posts []Post
81 for _, entry := range entries {
82 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
83 continue
84 }
85
86 data, err := fs.ReadFile(fsys, entry.Name())
87 if err != nil {
88 return nil, err
89 }
90
91 var meta PostMeta
92 rest, err := frontmatter.Parse(bytes.NewReader(data), &meta)
93 if err != nil {
94 return nil, err
95 }
96
97 if meta.Draft && !includeDrafts {
98 continue
99 }
100
101 htmlStr := rctx.RenderMarkdownWith(string(rest), markup.NewMarkdownWith("", textension.Dashes))
102
103 posts = append(posts, Post{
104 Meta: meta,
105 Body: template.HTML(htmlStr),
106 })
107 }
108
109 slices.SortFunc(posts, func(a, b Post) int {
110 return cmp.Compare(b.Meta.Date, a.Meta.Date)
111 })
112
113 return posts, nil
114}
115
116func AtomFeed(posts []Post, baseURL string) (string, error) {
117 feed := &feeds.Feed{
118 Title: "the tangled blog",
119 Link: &feeds.Link{Href: baseURL},
120 Author: &feeds.Author{Name: "Tangled"},
121 Created: time.Now(),
122 }
123
124 for _, p := range posts {
125 postURL := strings.TrimRight(baseURL, "/") + "/" + p.Meta.Slug
126
127 var authorName strings.Builder
128 for i, a := range p.Meta.Authors {
129 if i > 0 {
130 authorName.WriteString(" & ")
131 }
132 authorName.WriteString(a.Name)
133 }
134
135 feed.Items = append(feed.Items, &feeds.Item{
136 Title: p.Meta.Title,
137 Link: &feeds.Link{Href: postURL},
138 Description: p.Meta.Subtitle,
139 Author: &feeds.Author{Name: authorName.String()},
140 Created: p.ParsedDate(),
141 })
142 }
143
144 return feed.ToAtom()
145}
146
147// RenderIndex renders the blog index page to w.
148func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error {
149 tpl, err := p.ParseWith(os.DirFS(templatesDir), "index.html")
150 if err != nil {
151 return err
152 }
153 var featured []Post
154 for _, post := range posts {
155 if post.Meta.Image != "" {
156 featured = append(featured, post)
157 if len(featured) == 3 {
158 break
159 }
160 }
161 }
162 return tpl.ExecuteTemplate(w, "layouts/base", indexParams{Posts: posts, Featured: featured})
163}
164
165// RenderPost renders a single blog post page to w.
166func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error {
167 tpl, err := p.ParseWith(os.DirFS(templatesDir), "post.html")
168 if err != nil {
169 return err
170 }
171 return tpl.ExecuteTemplate(w, "layouts/base", postParams{Post: post})
172}