prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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 &copy, 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 }