blogs.go (2831B)
1 package content 2 3 import ( 4 "errors" 5 "log/slog" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 "time" 11 12 "git.prime8.dev/moncheeta/prime8dev/internal/utils" 13 "gopkg.in/yaml.v3" 14 ) 15 16 type Blog struct { 17 ID string 18 Title string 19 Description string 20 Date time.Time 21 Markdown string 22 HTML string 23 URL string 24 Image string 25 Tags []string 26 } 27 28 type frontmatter struct { 29 ID string `yaml:"id"` 30 Title string `yaml:"title"` 31 Description string `yaml:"description"` 32 Date string `yaml:"date"` 33 URL string `yaml:"url"` 34 Image string `yaml:"image"` 35 Tags []string `yaml:"tags"` 36 } 37 38 var ( 39 blogsList = loadBlogsOnStartup() 40 blogsByID = mapBlogsByID(blogsList) 41 ) 42 43 func GetBlogByID(id string) (*Blog, error) { 44 id = strings.TrimSpace(id) 45 if id == "" { 46 return nil, nil 47 } 48 49 b, ok := blogsByID[id] 50 if !ok { 51 return nil, nil 52 } 53 54 copy := b 55 return ©, nil 56 } 57 58 func LoadBlogs() ([]Blog, error) { 59 copyBlogs := make([]Blog, len(blogsList)) 60 copy(copyBlogs, blogsList) 61 return copyBlogs, nil 62 } 63 64 func loadBlogsOnStartup() []Blog { 65 blogs, err := loadBlogsFromDisk() 66 if err != nil { 67 slog.Warn("failed to load blogs on startup", "error", err) 68 return []Blog{} 69 } 70 71 return blogs 72 } 73 74 func loadBlogsFromDisk() ([]Blog, error) { 75 paths, err := filepath.Glob("./content/blogs/*.md") 76 if err != nil { 77 return nil, err 78 } 79 80 blogs := make([]Blog, 0, len(paths)) 81 for _, p := range paths { 82 b, err := parseBlogFile(p) 83 if err != nil { 84 return nil, err 85 } 86 blogs = append(blogs, b) 87 } 88 89 sort.Slice(blogs, func(i, j int) bool { 90 return blogs[i].Date.After(blogs[j].Date) 91 }) 92 93 return blogs, nil 94 } 95 96 func mapBlogsByID(blogs []Blog) map[string]Blog { 97 byID := make(map[string]Blog, len(blogs)) 98 for _, b := range blogs { 99 byID[b.ID] = b 100 } 101 return byID 102 } 103 104 func parseBlogFile(path string) (Blog, error) { 105 raw, err := os.ReadFile(path) 106 if err != nil { 107 return Blog{}, err 108 } 109 110 parts := strings.SplitN(string(raw), "---", 3) 111 if len(parts) < 3 { 112 return Blog{}, errors.New("invalid blog frontmatter format") 113 } 114 115 var fm frontmatter 116 if err := yaml.Unmarshal([]byte(parts[1]), &fm); err != nil { 117 return Blog{}, err 118 } 119 120 date, err := time.Parse("2006-01-02", fm.Date) 121 if err != nil { 122 return Blog{}, err 123 } 124 125 markdown := strings.TrimSpace(parts[2]) 126 html := "" 127 if fm.URL == "" && markdown != "" { 128 rendered, err := utils.MarkdownToHTML(markdown) 129 if err != nil { 130 slog.Warn("failed to render blog markdown", "blog_id", fm.ID, "path", path, "error", err) 131 return Blog{}, err 132 } 133 html = rendered 134 } 135 136 return Blog{ 137 ID: fm.ID, 138 Title: fm.Title, 139 Description: fm.Description, 140 Date: date, 141 Markdown: markdown, 142 HTML: html, 143 URL: fm.URL, 144 Image: fm.Image, 145 Tags: fm.Tags, 146 }, nil 147 }