Home

blog @main - refs - log -
-
https://git.jolheiser.com/blog.git
My nonexistent blog
tree log patch
wip Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAmXRTB4ACgkQuFOtpdp7 v3pfXg//ftDf+cwFxMFmazovPkyArIxj1Yp7kZdZtW8YKrq3hNOf86v1l+7uMRt1 jf/cm8Oa51xj9sf6HX84soD2k81llOwJRK4R0PJcADyQ9KtHeXMgVONPVhjS+y5i IEmedx4u/mNaq5ORMGSdpieOguAfBhva6NuFnAvydC98mYJ5+xhkrh6G11VUyxW8 d/GVMSaeoc12E9H6pR/pC+IQOfk/aUHTNh3Lv7+52gZijX5iICEGxZZZh9JcAUT5 OlvK+/4aQ7Urxka2mq4gXTP28YjiNnn6dU0s2oyaSlGhlW6YFHVXkHW8uRt6fAze 0UF3zyAhfCJuIx7twa9F2CqgHZAeGRFW5YkiwpCpeoNEdoPZXKADam2IEKMU3PVL c76R32YDczmhZM14VvUtmh/1yWYlcNYkGZNNTv2QzcTcF1kyzmUrWMwtjpMu071m MKiFFpddbbi9G9G7hbdD6DDN2CgPKjc7iocTYqyCrFhHRRwFcTBWGEqqDZd673Ni 24Xnxzc+kK5KHipuQ9cyj9fpeeRNpkT/gaMk3xsDg5TaCUSlG0qk3VhHhlBqQxyv 9Dp6MejY46gP0zQAmOAmPsk8GUYeniqmedgpZ8UnB+c0dUJR0E6XykHr5DOzjREf 3NQuxRxFz6lXs5AAuXA9Px3eP6bFigwBxWW8ZAbGs1lA2ccMRTU= =fMbW -----END PGP SIGNATURE-----
jolheiser <john.olheiser@gmail.com>
9 months ago
12 changed files, 567 additions(+), 17 deletions(-)
I .gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c6aa2a4851f3a5271fdf9b86ce78e6e8ce1cbcef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/blog*
+!blog.go
+!blog_test.go
+
+_example/out
I _example/articles/introduction.md
diff --git a/_example/articles/introduction.md b/_example/articles/introduction.md
new file mode 100644
index 0000000000000000000000000000000000000000..ac53dbc5a9a19fa49dab18849b89e6f1fa8b26ee
--- /dev/null
+++ b/_example/articles/introduction.md
@@ -0,0 +1,36 @@
+---
+title: "Introduction"
+summary: "An introduction of `blog`"
+time: 2024-02-17
+authors:
+  - name: "jolheiser"
+    email: "john+blog@jolheiser.com"
+---
+
+# Hello and welcome to blog!
+
+`blog` is a simple static blog generator!
+
+<small>Truly cutting edge, I know. Never seen one of these before, have you?</small>
+
+```go
+package main
+
+import "fmt"
+
+func main() {
+  fmt.Println("Hello, blog!")
+}
+````
+
+Things it can do:
+
+- [x] Render code
+- [ ] Fill the void
+
+## Why was blog created?
+
+~~Because I needed a specific thing that other generators couldn't give me. Blog is up to 1000 times faster than \<hot other blog generator\>!!!~~
+
+Because I wanted to, like many other projects in this space. Blog is designed to be simple (to me) with enough flexibility for someone else to use without
+cursing at me (too much).
I _example/templates/article.tmpl
diff --git a/_example/templates/article.tmpl b/_example/templates/article.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..63c40605857d22e08af8ee9b7ad86b377094dd97
--- /dev/null
+++ b/_example/templates/article.tmpl
@@ -0,0 +1,4 @@
+{{template "head" .article.Title}}
+<body>
+<main>{{.article.Content}}</main>
+</body>
I _example/templates/base.tmpl
diff --git a/_example/templates/base.tmpl b/_example/templates/base.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..ea524bba9a9adc390eb0fa0525bdcc34ee7cac80
--- /dev/null
+++ b/_example/templates/base.tmpl
@@ -0,0 +1,12 @@
+{{define "head"}}
+<!DOCTYPE html>
+<head>
+<title>{{.}}</title>
+<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sakura.css/css/sakura.css" type="text/css"> -->
+<link
+  rel="stylesheet"
+  href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css"
+/>
+<link rel="stylesheet" href="chroma.css"/>
+</head>
+{{end}}
I _example/templates/index.tmpl
diff --git a/_example/templates/index.tmpl b/_example/templates/index.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..8f02204de131292ae68007925ca62f4a782d2d87
--- /dev/null
+++ b/_example/templates/index.tmpl
@@ -0,0 +1,8 @@
+{{template "head" .author.Name}}
+<body>
+<main>
+{{range $article := .articles}}
+<a href="{{$article.Filename}}.html">{{$article.Title}}</a>
+{{end}}
+</main>
+</body>
M blog.go -> blog.go
diff --git a/blog.go b/blog.go
index 5f29fbbd4e2e070101295601d62d5c76e0a1b995..644353a354057efe605a8a87576faa2ecb9dc9c9 100644
--- a/blog.go
+++ b/blog.go
@@ -1,15 +1,22 @@
 package blog
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"html/template"
+	"io"
 	"io/fs"
 	"net/url"
 	"os"
+	"path/filepath"
+	"sort"
+	"strings"
 	"time"
 
 	"github.com/bmatcuk/doublestar/v4"
+	"github.com/pelletier/go-toml/v2"
+	"gopkg.in/yaml.v3"
 )
 
 // Blog is a collection of [Article]
@@ -17,23 +24,31 @@ type Blog struct {
 	indexTemplate   *template.Template
 	articleTemplate *template.Template
 	Articles        []Article
+	Author          Author
 }
 
 // Article is a blog post/article
 type Article struct {
+	"io/fs"
 
+	Content  template.HTML
+	ArticleMeta
 package blog
+	"net/url"
+
+// ArticleMeta is the metadata of an [Article]
+type ArticleMeta struct {
 	Title    string
 	Subtitle string
 	Summary  string
-	Content  string
 	Time     time.Time
-
+	"io/fs"
 	"io/fs"
 	Tags     []string
+	Category string
 }
 
-
+	"io/fs"
 	"os"
 type Author struct {
 	Name  string
@@ -45,39 +60,86 @@
 // Link is a link name and URL
 type Link struct {
 	Name string
-import (
+	URL  LinkURL
+package blog
 	"net/url"
+
+	"net/url"
 package blog
 	"net/url"
+
 
+	"net/url"
 import (
-	"os"
+	"net/url"
 	"errors"
 	if err != nil {
-		return nil, fmt.Errorf("could not parse templates in %q: %w", dir, err)
+		return err
 	}
+	*l = LinkURL(*u)
+	return nil
+}
+
+// NewBlog constructs a new blog from articles in articleDir and templates in templateDir
+func NewBlog(articleDir, templateDir string, author Author) (*Blog, error) {
+	tmpl, err := parseTemplates(os.DirFS(templateDir))
 	"errors"
+package blog
+		return nil, fmt.Errorf("could not parse templates in %q: %w", templateDir, err)
 	"errors"
+import (
+	indexTmpl := tmpl.Lookup("index.tmpl")
+	if indexTmpl == nil {
+	"os"
 	"errors"
+	"os"
 	"fmt"
-	"errors"
+	"os"
 	"html/template"
+		}
 	}
-	"errors"
+	articleTmpl := tmpl.Lookup("article.tmpl")
+	if articleTmpl == nil {
+		articleTmpl = tmpl.Lookup("article")
+		if articleTmpl == nil {
+			return nil, errors.New("`article` template is required but was not found")
+	"os"
 	"io/fs"
 	"errors"
-	"net/url"
+import (
+	articles, err := parseArticles(os.DirFS(articleDir))
 	"errors"
-	"os"
+package blog
+		return nil, fmt.Errorf("could not parse articles in %q: %w", articleDir, err)
 	}
 	return &Blog{
+	"time"
 	"fmt"
 package blog
+	"html/template"
+		Articles:        articles,
+		Author:          author,
 	"fmt"
+import (
+}
 
-	"fmt"
+// Index renders the blog index to w
+func (b *Blog) Index(w io.Writer) error {
+	return b.indexTemplate.Execute(w, map[string]any{
+		"articles": b.Articles,
+	"github.com/bmatcuk/doublestar/v4"
 import (
 package blog
+type Blog struct {
+}
+
+// Article renders an article to w
+func (b *Blog) Article(w io.Writer, a Article) error {
+	return b.articleTemplate.Execute(w, map[string]any{
+		"article": a,
+		"author":  b.Author,
+	})
+package blog
 	"net/url"
 
 func parseTemplates(fs fs.FS) (*template.Template, error) {
@@ -84,26 +145,135 @@ 	matches, err := doublestar.Glob(fs, "**/*.{tmpl,gohtml}")
 	if err != nil {
 		return nil, fmt.Errorf("could not glob templates: %w", err)
 	}
+	tmpl, err := template.New("").ParseFS(fs, matches...)
+	if err != nil {
 	"fmt"
+	"net/url"
+	}
+	return tmpl, nil
+}
+
+func parseArticles(fs fs.FS) ([]Article, error) {
+	matches, err := doublestar.Glob(fs, "**/*.md")
+	if err != nil {
+		return nil, fmt.Errorf("could not glob articles: %w", err)
+	}
+	articles := make([]Article, 0, len(matches))
+	for _, match := range matches {
+		if err := func() error {
+			fi, err := fs.Open(match)
+			if err != nil {
+				return err
+			}
+)
 	"io/fs"
+
+			content, err := io.ReadAll(fi)
+)
 	"errors"
 package blog
+	Content  string
+			}
+
+			article, err := parseArticle(string(content))
+			if err != nil {
+)
 	"fmt"
+			}
+			article.Filename = strings.TrimSuffix(filepath.Base(match), filepath.Ext(match))
+			articles = append(articles, article)
+			return nil
+		}(); err != nil {
+			return nil, err
+		}
+	}
+	sort.SliceStable(articles, func(i, j int) bool {
+		return articles[i].Time.After(articles[j].Time)
+	})
+	return articles, nil
+package blog
 	"net/url"
+
+func parseArticle(content string) (Article, error) {
+	lines := strings.Split(content, "\n")
+
+	start, end := -1, -1
+	var isSep func(string) bool
+	var decoder func([]byte, any) error
+
+package blog
 	}
+		if strings.TrimSpace(line) == "" {
+type Blog struct {
 	"fmt"
 	"os"
+	"io/fs"
 package blog
+		return nil, errors.New("`index` template is required but was not found")
+			end = idx
+type Blog struct {
 	"net/url"
+		}
 
+		if isTOMLSeparator(line) {
+			start = idx
+			isSep = isTOMLSeparator
+			decoder = toml.Unmarshal
+			continue
+		}
+
+		if isYAMLSeparator(line) {
+			start = idx
+			isSep = isYAMLSeparator
+			decoder = yaml.Unmarshal
+			continue
+		}
+	}
+
+	var meta ArticleMeta
+	body := content
+	if start != -1 && end != -1 {
+		body = strings.Join(lines[end+1:], "\n")
+package blog
 func parseArticles(fs fs.FS) ([]Article, error) {
+package blog
 	matches, err := doublestar.Glob(fs, "**/*.md")
+		}
 	"errors"
+import (
+
 package blog
+		return nil, fmt.Errorf("could not glob articles: %w", err)
+package blog
 	"html/template"
-
+import (
+	if err != nil {
+		return Article{}, fmt.Errorf("could not convert article: %w", err)
 	}
+
+package blog
 	"html/template"
+	"fmt"
+		Content:     template.HTML(buf.String()),
+		ArticleMeta: meta,
+	"fmt"
 import (
+}
+
+func isTOMLSeparator(line string) bool {
+	for _, char := range line {
+		if char != '+' {
+			return false
+		}
 	}
+	return true
+}
+
+func isYAMLSeparator(line string) bool {
+	for _, char := range line {
+		if char != '-' {
+			return false
+		}
+	}
+	return true
 }
I blog_test.go
diff --git a/blog_test.go b/blog_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..821a659ae04f26a6c4bb1157f8c4835e58de07b2
--- /dev/null
+++ b/blog_test.go
@@ -0,0 +1,72 @@
+package blog
+
+import (
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/alecthomas/assert"
+)
+
+func TestParseArticle(t *testing.T) {
+	var (
+		raw = `+++
+title = "Honk"
+subtitle = "Bonk"
+summary = """This
+is
+a
+summary"""
+time = 2024-02-16
+tags = ["bocchi", "rocks"]
+		
+[author]
+name = "Bocchi"
+job = "Guitarist"
+email = "bocchi@rock.s"
+
+[[author.links]]
+name = "website"
+url = "https://example.com/bocchi"
++++
+
+# Hello world
+
+Beep boop`
+		meta = ArticleMeta{
+			Title:    "Honk",
+			Subtitle: "Bonk",
+			Summary: `This
+is
+a
+summary`,
+			Time: func() time.Time {
+				t, _ := time.ParseInLocation("2006-01-02", "2024-02-16", time.Local)
+				return t
+			}(),
+			Author: Author{
+				Name:  "Bocchi",
+				Job:   "Guitarist",
+				Email: "bocchi@rock.s",
+				Links: []Link{
+					{
+						Name: "website",
+						URL: func() LinkURL {
+							u, _ := url.Parse("https://example.com/bocchi")
+							return LinkURL(*u)
+						}(),
+					},
+				},
+			},
+			Tags: []string{"bocchi", "rocks"},
+		}
+		body = `<h1 id="hello-world">Hello world</h1>
+<p>Beep boop</p>
+`
+	)
+
+	article, err := parseArticle(raw)
+	assert.NoError(t, err)
+	assert.Equal(t, meta, article.ArticleMeta)
+	assert.Equal(t, body, article.Content)
+}
I cmd/blog/main.go
diff --git a/cmd/blog/main.go b/cmd/blog/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..d5d835e2eab4153e10fc157dd07aec9cbb7507d4
--- /dev/null
+++ b/cmd/blog/main.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/pelletier/go-toml/v2"
+	"go.jolheiser.com/blog"
+	"gopkg.in/yaml.v3"
+)
+
+type args struct {
+	ArticleDir  string      `yaml:"article-dir" toml:"article-dir"`
+	TemplateDir string      `yaml:"template-dir" toml:"template-dir"`
+	OutDir      string      `yaml:"out-dir" toml:"out-dir"`
+	Author      blog.Author `yaml:"author" toml:"author"`
+}
+
+func main() {
+	if err := maine(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+func maine() error {
+	var args args
+
+	fs := flag.NewFlagSet("blog", flag.ExitOnError)
+
+	configFlag := fs.String("config", "", "Configuration, TOML or YAML")
+	fs.StringVar(configFlag, "c", *configFlag, "--config")
+	articlesFlag := fs.String("articles", "", "Path to articles")
+	fs.StringVar(articlesFlag, "a", *articlesFlag, "--articles")
+	templatesFlag := fs.String("templates", "", "Path to templates")
+	fs.StringVar(templatesFlag, "t", *templatesFlag, "--templates")
+	outFlag := fs.String("out", "", "Path to output")
+	fs.StringVar(outFlag, "o", *outFlag, "--out")
+
+	if err := fs.Parse(os.Args[1:]); err != nil {
+		return err
+	}
+
+	if *configFlag != "" {
+		data, err := os.ReadFile(*configFlag)
+		if err != nil {
+			return err
+		}
+		switch ext := filepath.Ext(*configFlag); ext {
+		case ".yaml", ".yml":
+			if err := yaml.Unmarshal(data, &args); err != nil {
+				return err
+			}
+		case ".toml":
+			if err := toml.Unmarshal(data, &args); err != nil {
+				return err
+			}
+		default:
+			return fmt.Errorf("could not determine config type %q", ext)
+		}
+	}
+
+	if *articlesFlag != "" {
+		args.ArticleDir = *articlesFlag
+	}
+	if args.ArticleDir == "" {
+		args.ArticleDir = "articles"
+	}
+	if *templatesFlag != "" {
+		args.TemplateDir = *templatesFlag
+	}
+	if args.TemplateDir == "" {
+		args.TemplateDir = "templates"
+	}
+	if *outFlag != "" {
+		args.OutDir = *outFlag
+	}
+	if args.OutDir == "" {
+		args.OutDir = "out"
+	}
+
+	if err := os.MkdirAll(args.OutDir, os.ModePerm); err != nil {
+		return err
+	}
+
+	blog, err := blog.NewBlog(args.ArticleDir, args.TemplateDir, args.Author)
+	if err != nil {
+		return err
+	}
+
+	indexFile, err := os.Create(filepath.Join(args.OutDir, "index.html"))
+	if err != nil {
+		return err
+	}
+	defer indexFile.Close()
+	if err := blog.Index(indexFile); err != nil {
+		return err
+	}
+
+	for _, article := range blog.Articles {
+		if err := func() error {
+			articleFile, err := os.Create(filepath.Join(args.OutDir, article.Filename+".html"))
+			if err != nil {
+				return err
+			}
+			defer articleFile.Close()
+			return blog.Article(articleFile, article)
+		}(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
I cmd/chromacss/main.go
diff --git a/cmd/chromacss/main.go b/cmd/chromacss/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..edf8cd5022a0e2574837c673ee0f869f4389a39f
--- /dev/null
+++ b/cmd/chromacss/main.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"github.com/alecthomas/chroma/v2/formatters/html"
+	"github.com/alecthomas/chroma/v2/styles"
+)
+
+func main() {
+	if err := maine(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+func maine() error {
+	fs := flag.NewFlagSet("chromacss", flag.ExitOnError)
+	lightFlag := fs.String("light", "catppuccin-latte", "Light theme")
+	fs.StringVar(lightFlag, "l", *lightFlag, "--light")
+	darkFlag := fs.String("dark", "catppuccin-mocha", "Dark theme")
+	fs.StringVar(darkFlag, "d", *darkFlag, "--dark")
+	outFlag := fs.String("out", "", "Output (default: stdout)")
+	fs.StringVar(outFlag, "o", *outFlag, "--out")
+	if err := fs.Parse(os.Args[1:]); err != nil {
+		return err
+	}
+
+	out := os.Stdout
+	if *outFlag != "" {
+		fi, err := os.Create(*outFlag)
+		if err != nil {
+			return err
+		}
+		defer fi.Close()
+		out = fi
+	}
+
+	formatter := html.New(html.WithClasses(true), html.WithAllClasses(true))
+
+	lightStyle := *lightFlag
+	darkStyle := *darkFlag
+
+	if lightStyle == "" {
+		lightStyle = "catppuccin-latte"
+		if darkStyle != "" {
+			lightStyle = darkStyle
+			darkStyle = ""
+		}
+	}
+
+	styles.Fallback = styles.Get("catppuccin-latte")
+	style := styles.Get(lightStyle)
+	if err := formatter.WriteCSS(out, style); err != nil {
+		return err
+	}
+
+	if darkStyle != "" {
+		out.WriteString("@media (prefers-color-scheme: dark) {")
+		styles.Fallback = styles.Get("catpuccin-mocha")
+		style = styles.Get(darkStyle)
+		if err := formatter.WriteCSS(out, style); err != nil {
+			return err
+		}
+		out.WriteString("}")
+	}
+
+	return nil
+}
M go.mod -> go.mod
diff --git a/go.mod b/go.mod
index ebf428e62bc7377fd661ad412ab8c6c312f3c820..8c1aa017c7924a7b8ed3b4ceab8cde26ece9de51 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,28 @@
 go 1.21.6
 
 require (
+module go.jolheiser.com/blog
 	github.com/alecthomas/chroma/v2 v2.12.0 // indirect
+module go.jolheiser.com/blog
 	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+	github.com/alecthomas/chroma/v2 v2.12.0 // indirect
+module go.jolheiser.com/blog
 	github.com/dlclark/regexp2 v1.10.0 // indirect
+module go.jolheiser.com/blog
 	github.com/peterbourgon/ff/v3 v3.4.0 // indirect
+	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+	github.com/dlclark/regexp2 v1.10.0 // indirect
+	github.com/gorilla/feeds v1.1.2 // indirect
+	github.com/jolheiser/goldmark-meta v0.0.2 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+	github.com/peterbourgon/ff/v3 v3.4.0 // indirect
+	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/yuin/goldmark v1.7.0 // indirect
 	github.com/yuin/goldmark-emoji v1.0.2 // indirect
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
 	github.com/yuin/goldmark-meta v1.1.0 // indirect
+	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
M go.sum -> go.sum
diff --git a/go.sum b/go.sum
index caa7d4f6c0a660af8270928e7bdf133b7c732019..62a81853e4095cb8c64512a1f9d8ff9e25a8fc83 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,16 @@
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o=
+github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY=
 github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
 github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
 github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
+github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
+github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
+github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
+github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -12,12 +20,33 @@ github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
 github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
+github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
+github.com/jolheiser/goldmark-meta v0.0.2 h1:3qnvGnp1n7Cdu1L9LpuZrM6dfmbMAS22+hNJnxGPn6s=
+github.com/jolheiser/goldmark-meta v0.0.2/go.mod h1:x5vZW1+kBEhR4+AKHwnNsD+nQaYdgmXoiFgG3fc6ZFc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
 github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
 github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
+github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/yuin/goldmark v1.3.7 h1:NSaHgaeJFCtWXCBkBKXw0rhgMuJ0VoE9FB5mWldcrQ4=
 github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.15 h1:CFa84T0goNn/UIXYS+dmjjVxMyTAvpOmzld40N/nfK0=
@@ -30,7 +59,14 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
 github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
 github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
M goldmark.go -> goldmark.go
diff --git a/goldmark.go b/goldmark.go
index 99f519f9fdc3e2bc2d605a469bc28c8d51a4eba7..404e8a5ff1189ddd0c7569bb22b4a44217dca46b 100644
--- a/goldmark.go
+++ b/goldmark.go
@@ -1,13 +1,14 @@
 package blog
 
 import (
-	"github.com/alecthomas/chroma/v2/formatters/html"
+	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
 	"github.com/yuin/goldmark"
 	emoji "github.com/yuin/goldmark-emoji"
 	highlighting "github.com/yuin/goldmark-highlighting/v2"
 	meta "github.com/yuin/goldmark-meta"
 	"github.com/yuin/goldmark/extension"
 	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/renderer/html"
 )
 
 // Markdown is the default markdown converter
@@ -16,19 +17,22 @@ var Markdown = goldmark.New(
 	goldmark.WithParserOptions(
 		parser.WithAutoHeadingID(),
 	),
+	goldmark.WithRendererOptions(
+		html.WithUnsafe(),
+	),
 	goldmark.WithExtensions(
 		extension.GFM,
 		meta.Meta,
 		emoji.Emoji,
 		highlighting.NewHighlighting(
 			highlighting.WithFormatOptions(
-
+import (
 	"github.com/alecthomas/chroma/v2/formatters/html"
-
+import (
 	"github.com/yuin/goldmark"
-
+import (
 	emoji "github.com/yuin/goldmark-emoji"
-
+import (
 	highlighting "github.com/yuin/goldmark-highlighting/v2"
 			),
 		),