Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221package blog
import ( "bytes" "cmp" "embed" "html/template" "io" "io/fs" "os" "slices" "strings" "time"
"github.com/adrg/frontmatter" "github.com/gorilla/feeds"
"tangled.org/core/appview/pages" "tangled.org/core/appview/pages/markup" textension "tangled.org/core/appview/pages/markup/extension")
//go:embed postsvar PostsFS embed.FS
type Author struct { Name string `yaml:"name"` Email string `yaml:"email"` Handle string `yaml:"handle"`}
type PostMeta struct { Slug string `yaml:"slug"` Title string `yaml:"title"` Subtitle string `yaml:"subtitle"` Date string `yaml:"date"` Authors []Author `yaml:"authors"` Image string `yaml:"image"` Draft bool `yaml:"draft"`}
type Post struct { Meta PostMeta Body template.HTML}
func (p Post) ParsedDate() time.Time { t, _ := time.Parse("2006-01-02", p.Meta.Date) return t}
type indexParams struct { LoggedInUser any Posts []Post Featured []Post}
type postParams struct { LoggedInUser any Post Post}
// Posts parses and returns all non-draft posts sorted newest-first.func Posts(postsDir string) ([]Post, error) { return parsePosts(postsDir, false)}
// AllPosts parses and returns all posts including drafts, sorted newest-first.func AllPosts(postsDir string) ([]Post, error) { return parsePosts(postsDir, true)}
func parsePosts(postsDir string, includeDrafts bool) ([]Post, error) { fsys := os.DirFS(postsDir)
entries, err := fs.ReadDir(fsys, ".") if err != nil { return nil, err }
rctx := &markup.RenderContext{ RendererType: markup.RendererTypeDefault, } var posts []Post for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { continue }
data, err := fs.ReadFile(fsys, entry.Name()) if err != nil { return nil, err }
var meta PostMeta rest, err := frontmatter.Parse(bytes.NewReader(data), &meta) if err != nil { return nil, err }
if meta.Draft && !includeDrafts { continue }
htmlStr := rctx.RenderMarkdownWith(string(rest), markup.NewMarkdownWith("", textension.Dashes))
posts = append(posts, Post{ Meta: meta, Body: template.HTML(htmlStr), }) }
slices.SortFunc(posts, func(a, b Post) int { return cmp.Compare(b.Meta.Date, a.Meta.Date) })
return posts, nil}
func AtomFeed(posts []Post, baseURL string) (string, error) { feed := &feeds.Feed{ Title: "The Tangled Blog", Link: &feeds.Link{Href: baseURL}, Author: &feeds.Author{Name: "Tangled"}, Created: time.Now(), }
for _, p := range posts { postURL := strings.TrimRight(baseURL, "/") + "/" + p.Meta.Slug
var authorName strings.Builder for i, a := range p.Meta.Authors { if i > 0 { authorName.WriteString(" & ") } authorName.WriteString(a.Name) }
feed.Items = append(feed.Items, &feeds.Item{ Title: p.Meta.Title, Link: &feeds.Link{Href: postURL}, Description: p.Meta.Subtitle, Author: &feeds.Author{Name: authorName.String()}, Created: p.ParsedDate(), }) }
return feed.ToAtom()}
// parseLayout builds a template set from the appview's fragments together with// the blog's own layouts, fragments, and the given page template (relative to// templatesDir). The result is ready to ExecuteTemplate with "layouts/blogbase".func parseLayout(p *pages.Pages, templatesDir, page string) (*template.Template, error) { fragmentPaths, err := p.FragmentPaths() if err != nil { return nil, err }
tpl, err := template.New("layouts/blogbase"). Funcs(p.FuncMap()). ParseFS(p.EmbedFS(), fragmentPaths...) if err != nil { return nil, err }
extraFS := os.DirFS(templatesDir) err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, ".html") { return nil } // only the requested page, plus shared layouts and fragments if path != page && !strings.Contains(path, "fragments/") && !strings.Contains(path, "layouts/") { return nil } data, err := fs.ReadFile(extraFS, path) if err != nil { return err } if _, err = tpl.New(path).Parse(string(data)); err != nil { return err } return nil }) if err != nil { return nil, err }
return tpl, nil}
// RenderIndex renders the blog index page to w.func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error { tpl, err := parseLayout(p, templatesDir, "index.html") if err != nil { return err } var featured []Post for _, post := range posts { if post.Meta.Image != "" { featured = append(featured, post) if len(featured) == 3 { break } } } return tpl.ExecuteTemplate(w, "layouts/blogbase", indexParams{Posts: posts, Featured: featured})}
// RenderPost renders a single blog post page to w.func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error { tpl, err := parseLayout(p, templatesDir, "post.html") if err != nil { return err } return tpl.ExecuteTemplate(w, "layouts/blogbase", postParams{Post: post})}