about summary refs log tree commit diff stats
path: root/internal/builder/posts.go
diff options
context:
space:
mode:
authorAlan Pearce2024-04-27 21:18:03 +0200
committerAlan Pearce2024-04-27 21:18:03 +0200
commit2a4c795d5a165f995e9f7dc84e07465b140f3770 (patch)
tree379ee13be7bf0e6f7db096222e782f86f8ea1caf /internal/builder/posts.go
parent9b4ca4783a186c345d99f613aeaf73e1bc112bfa (diff)
downloadwebsite-2a4c795d5a165f995e9f7dc84e07465b140f3770.tar.lz
website-2a4c795d5a165f995e9f7dc84e07465b140f3770.tar.zst
website-2a4c795d5a165f995e9f7dc84e07465b140f3770.zip
implement live-reloading dev server
Squashed commit of the following:

commit 02f077432202af4d633eb2cad81dfdaa6921317f
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 21:09:14 2024 +0200

    builder: only remove output directory if set and in dev mode

commit 47001e01c55fa6e74aafeda04ebc3e4e7c47eba0
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 21:03:37 2024 +0200

    implement live reload on dev server

commit 411ec969f61e4b73439f1c54ea29f75135ecc618
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 20:59:26 2024 +0200

    server: implement graceful shutdown

commit 5400132eb6eb1b638e0b3fd4265f51611c92d473
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 20:41:07 2024 +0200

    add some debug logs

commit 3c9b678197c044603950232d222f501ef74d7873
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 20:39:09 2024 +0200

    prefix log output with executable name

commit 300e24c179e390e9d3f5aeab4471c97f17f1fa64
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Sat Apr 27 20:29:42 2024 +0200

    don't panic inside internal packages, return error instead

commit fe2715d330402ad67fe866471bed89c7238ad2ec
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Fri Apr 26 01:18:29 2024 +0200

    config: use a table to configure CSP headers

commit d012553aaf78a436fa8871830b5d720a9e292d4b
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Thu Apr 25 17:13:39 2024 +0200

    dev: create basic dev server to build and serve from a temporary directory

commit a1d11d3e69650d9b43ca1b1d7b7ccc05a808d5c1
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Thu Apr 25 13:02:22 2024 +0200

    remove unused redirect_other_hostnames config option

commit fd67b19b5c7f76f0c3579e8a05ef20a618e90be7
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Thu Apr 25 12:58:53 2024 +0200

    server: make port a string, which is what go uses

commit c798e8e736c0649008cade337158399470a9099b
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Thu Apr 25 12:58:33 2024 +0200

    config: remove unused port variable

commit f94882b9001f3b0855e26b26b4a84b96e3deb22b
Author: Alan Pearce <alan@alanpearce.eu>
Date:   Thu Apr 25 12:49:10 2024 +0200

    re-organise module layout
Diffstat (limited to 'internal/builder/posts.go')
-rw-r--r--internal/builder/posts.go121
1 files changed, 121 insertions, 0 deletions
diff --git a/internal/builder/posts.go b/internal/builder/posts.go
new file mode 100644
index 0000000..223531b
--- /dev/null
+++ b/internal/builder/posts.go
@@ -0,0 +1,121 @@
+package builder
+
+import (
+	"bytes"
+	"log/slog"
+	"os"
+	"path"
+	"path/filepath"
+	"slices"
+	"strings"
+	"time"
+
+	"github.com/adrg/frontmatter"
+	mapset "github.com/deckarep/golang-set/v2"
+	"github.com/pkg/errors"
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/extension"
+	htmlrenderer "github.com/yuin/goldmark/renderer/html"
+)
+
+type PostMatter struct {
+	Date        time.Time `toml:"date"`
+	Description string    `toml:"description"`
+	Title       string    `toml:"title"`
+	Taxonomies  struct {
+		Tags []string `toml:"tags"`
+	} `toml:"taxonomies"`
+}
+
+type Post struct {
+	Input    string
+	Output   string
+	Basename string
+	URL      string
+	Content  string
+	PostMatter
+}
+
+type Tags mapset.Set[string]
+
+var markdown = goldmark.New(
+	goldmark.WithRendererOptions(
+		htmlrenderer.WithUnsafe(),
+	),
+	goldmark.WithExtensions(
+		extension.GFM,
+		extension.Footnote,
+		extension.Typographer,
+	),
+)
+
+func getPost(filename string) (*PostMatter, []byte, error) {
+	matter := PostMatter{}
+	content, err := os.Open(filename)
+	defer content.Close()
+	if err != nil {
+		return nil, nil, errors.WithMessagef(err, "could not open post %s", filename)
+	}
+	rest, err := frontmatter.MustParse(content, &matter)
+	if err != nil {
+		return nil, nil, errors.WithMessagef(err, "could not parse front matter of post %s", filename)
+	}
+
+	return &matter, rest, nil
+}
+
+func renderMarkdown(content []byte) (string, error) {
+	var buf bytes.Buffer
+	if err := markdown.Convert(content, &buf); err != nil {
+		return "", errors.WithMessage(err, "could not convert markdown content")
+	}
+	return buf.String(), nil
+}
+
+func readPosts(root string, inputDir string, outputDir string) ([]Post, Tags, error) {
+	tags := mapset.NewSet[string]()
+	posts := []Post{}
+	subdir := filepath.Join(root, inputDir)
+	files, err := os.ReadDir(subdir)
+	if err != nil {
+		return nil, nil, errors.WithMessagef(err, "could not read post directory %s", subdir)
+	}
+	outputReplacer := strings.NewReplacer(root, outputDir, ".md", "/index.html")
+	urlReplacer := strings.NewReplacer(root, "", ".md", "/")
+	for _, f := range files {
+		pathFromRoot := filepath.Join(subdir, f.Name())
+		if !f.IsDir() && path.Ext(pathFromRoot) == ".md" {
+			output := outputReplacer.Replace(pathFromRoot)
+			url := urlReplacer.Replace(pathFromRoot)
+			slog.Debug("reading post", "post", pathFromRoot)
+			matter, content, err := getPost(pathFromRoot)
+			if err != nil {
+				return nil, nil, err
+			}
+
+			for _, tag := range matter.Taxonomies.Tags {
+				tags.Add(strings.ToLower(tag))
+			}
+
+			slog.Debug("rendering markdown in post", "post", pathFromRoot)
+			html, err := renderMarkdown(content)
+			if err != nil {
+				return nil, nil, err
+			}
+			post := Post{
+				Input:      pathFromRoot,
+				Output:     output,
+				Basename:   filepath.Base(url),
+				URL:        url,
+				PostMatter: *matter,
+				Content:    html,
+			}
+
+			posts = append(posts, post)
+		}
+	}
+	slices.SortFunc(posts, func(a, b Post) int {
+		return b.Date.Compare(a.Date)
+	})
+	return posts, tags, nil
+}