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/uFOtpdp7v3oFAmXWxIoACgkQuFOtpdp7 v3okNhAAk+lCQ+Qotg3zgadBbvYkHErqQXQrwx+eXWH7WeGg3B0Qj67+fN2W8fx7 aoGhB0JltJPpMT+lCAM7vW4gmAi918/PC6s35lZ9QsABBSlxVKoxQat7ZLOoFFey BsDCpDdsPcXQ2/Qgr1Vop+gNmTGUMfm5yBfs+E9vHaKQcwq5+I5/dZloe75d+nn1 EPRQm8tseGqtO41ZqpZqFGf+qoh23IXTkrrxoC2P3BOxtJJwQXKEi9n4Aoh8rr/E ZQ1P1pL00VSu9QhrWUq9FsPB90uU2aP518/htiD1lJlFNB9Rv+5TJGTha3Jali4g StR3Zy+DWU+1BlKmGNP0hSvcm3+yvIisV/Mco+3bc1xMJlDTe9gYQtLIXDNQhWBG z1BVXT/ITKeHbclZdQpqCwtdLxvvHMeHXlN9KDaihvWXOAIFTd6M9jsdhgNqE2g0 AWfKqzcoCLMsBtc3JRUIypPnwksgdmGK/5zIf2sMw9vJvZd57PdxF3Cv2hjVvfCh I1s4nEATpK1GqPZgmS6nqq9hmHEklqw4idXqQqI9bb2EIGc+YW8R+ukVBN66jaGK HxOZ5ighJlM80DszW9/HlLnk0RMi404zhdxKf+imklhANl6r2cJ7zbvJIgEjCwq5 cw9L1KYTRnHT9XtiNOoKlqkMnU1D+hY9SWSlW0JWd/geV+UMS4A= =Tt3l -----END PGP SIGNATURE-----
jolheiser <john.olheiser@gmail.com>
1 year ago
18 changed files, 523 additions(+), 668 deletions(-)
.gitignore_example/README.mdarticles/introduction.md_example/config.yaml_example/templates/article.tmpl_example/templates/base.tmpl_example/templates/index.tmplblog.goblog_test.gocmd/blog/main.gocmd/chromacss/main.gogo.modgo.sumgoldmark.gomain.gotemplates.gotemplates.templtemplates_templ.go
M .gitignore.gitignore
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
diff --git a/.gitignore b/.gitignore
index c6aa2a4851f3a5271fdf9b86ce78e6e8ce1cbcef..89f9ac04aac6c8ee66e158853e7d0439b3ec782d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1 @@
-/blog*
-!blog.go
-!blog_test.go
-
-_example/out
+out/
D _example/README.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
diff --git a/_example/README.md b/_example/README.md
deleted file mode 100644
index 9a7224f619191edd2bcdf5d9f5acc90976f2e60d..0000000000000000000000000000000000000000
--- a/_example/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Example blog structure
-
-The `articles` dir contains a list of articles, namely this one.
-
-The `templates` dir contains a handful of [go templates](https://pkg.go.dev/html/template) that make up how to render the blog.
-- `article.tmpl` is a template for an individual article (this page).
-- `index.tmpl` is the main blog overview.
-- `base.tmpl` is a collection of [defined blocks](https://pkg.go.dev/text/template#hdr-Nested_template_definitions)
-that are then re-used in `index.tmpl` and `article.tmpl` to create a cohesive look-and-feel.
-
-Finally, `config.yaml` (which can also be TOML if desired) allows you to not have to continuously re-type all the flags to define things like your 
-`--article-dir`, `--template-dir`, or `--out`.
-
M _example/articles/introduction.mdarticles/introduction.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
diff --git a/_example/articles/introduction.md b/articles/introduction.md
rename from _example/articles/introduction.md
rename to articles/introduction.md
index 67956be729182b0ac63aa2360d99554f7152f141..221258b975335736ddb331fa0f3d20d269d75495 100644
--- a/_example/articles/introduction.md
+++ b/articles/introduction.md
@@ -1,12 +1,9 @@
----
-title: "Introduction"
-summary: "An introduction of `blog`"
-time: 2024-02-17
-category: Miscellaneous
-authors:
-  - name: "jolheiser"
-    email: "john+blog@jolheiser.com"
----
++++
+title = "Introduction"
+summary = "An introduction of `blog`"
+date = 2024-02-17
+category = "Miscellaneous"
++++
 
 # Hello and welcome to blog!
 
@@ -64,3 +61,4 @@ that are then re-used in `index.tmpl` and `article.tmpl` to create a cohesive look-and-feel.
 
 Finally, `config.yaml` (which can also be TOML if desired) allows you to not have to continuously re-type all the flags to define things like your 
 `--article-dir`, `--template-dir`, or `--out`.
+
D _example/config.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
diff --git a/_example/config.yaml b/_example/config.yaml
deleted file mode 100644
index 914646cc3ba1a16edd9b65861c517ff51634acc7..0000000000000000000000000000000000000000
--- a/_example/config.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-article-dir: articles
-template-dir: templates
-out-dir: out
-author:
-  name: jolheiser
-  email: john+blog@jolheiser.com
-  links:
-    - name: GitHub
-      url: "https://github.com/jolheiser"
D _example/templates/article.tmpl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
diff --git a/_example/templates/article.tmpl b/_example/templates/article.tmpl
deleted file mode 100644
index db4e9ecb5eab85d4a3d0f5bbc553b3b5f6659c02..0000000000000000000000000000000000000000
--- a/_example/templates/article.tmpl
+++ /dev/null
@@ -1,8 +0,0 @@
-{{template "head" .Article.Title}}
-<body>
-<header>
- <a href="index.html">{{.Author.Name}}</a>
-</header>
-<hr/>
-<main>{{.Article.Content}}</main>
-</body>
D _example/templates/base.tmpl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
diff --git a/_example/templates/base.tmpl b/_example/templates/base.tmpl
deleted file mode 100644
index 7a05f6c9c322f6db80c4885b595f3a2c944552ee..0000000000000000000000000000000000000000
--- a/_example/templates/base.tmpl
+++ /dev/null
@@ -1,8 +0,0 @@
-{{define "head"}}
-<!DOCTYPE html>
-<head>
-<title>{{.}}</title>
-<link id="style" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css"/>
-<link rel="stylesheet" href="chroma.css"/>
-</head>
-{{end}}
D _example/templates/index.tmpl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/_example/templates/index.tmpl b/_example/templates/index.tmpl
deleted file mode 100644
index 20ddba201b1aab26c30e6d8bc357fddfaaed7f0b..0000000000000000000000000000000000000000
--- a/_example/templates/index.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{{template "head" (printf "%s's Blog" .Author.Name)}}
-<body>
-<header>
-	<h1>{{.Author.Name}}</h1>
-	<p>I really like to hack on Gitea and I use Catppuccin (btw)</p>
-</header>
-<main>
-{{range $category, $articles := .ArticlesByCategory}}
-	<h3>{{$category}}</h3>
-	<ul>
-	{{range $article := $articles}}
-		<li><a href="{{$article.Filename}}.html">{{$article.Title}}</a> ({{$article.Time.Format "01/02/2006"}}) - {{$article.Summary}}</li>
-	{{end}}
-	</ul>
-{{end}}
-</main>
-</body>
D blog.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
diff --git a/blog.go b/blog.go
deleted file mode 100644
index 21f761408aedd5cedc2f77052c46a8941dca5727..0000000000000000000000000000000000000000
--- a/blog.go
+++ /dev/null
@@ -1,244 +0,0 @@
-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]
-type Blog struct {
-	indexTemplate   *template.Template
-	articleTemplate *template.Template
-	Articles        []Article
-	Author          Author
-}
-
-// Article is a blog post/article
-type Article struct {
-	Filename string
-	Content  template.HTML
-	ArticleMeta
-}
-
-// ArticleMeta is the metadata of an [Article]
-// The meta is parsed from frontmatter in an article
-type ArticleMeta struct {
-	Title    string
-	Subtitle string
-	Summary  string
-	Time     time.Time
-	Author   Author
-	Tags     []string
-	Category string
-}
-
-// Author is an author of a blog/post
-type Author struct {
-	Name  string
-	Job   string
-	Email string
-	Links []Link
-}
-
-// Link is a link name and URL
-type Link struct {
-	Name string
-	URL  LinkURL
-}
-
-// LinkURL is a URL
-type LinkURL url.URL
-
-func (l *LinkURL) UnmarshalText(data []byte) error {
-	u, err := url.Parse(string(data))
-	if err != nil {
-		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))
-	if err != nil {
-		return nil, fmt.Errorf("could not parse templates in %q: %w", templateDir, err)
-	}
-	indexTmpl := tmpl.Lookup("index.tmpl")
-	if indexTmpl == nil {
-		indexTmpl = tmpl.Lookup("index")
-		if indexTmpl == nil {
-			return nil, errors.New("`index` template is required but was not found")
-		}
-	}
-	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")
-		}
-	}
-	articles, err := parseArticles(os.DirFS(articleDir))
-	if err != nil {
-		return nil, fmt.Errorf("could not parse articles in %q: %w", articleDir, err)
-	}
-	return &Blog{
-		indexTemplate:   indexTmpl,
-		articleTemplate: articleTmpl,
-		Articles:        articles,
-		Author:          author,
-	}, nil
-}
-
-// Index renders the blog index to w
-func (b *Blog) Index(w io.Writer) error {
-	byCat := make(map[string][]Article)
-	for _, article := range b.Articles {
-		byCat[article.Category] = append(byCat[article.Category], article)
-	}
-	return b.indexTemplate.Execute(w, map[string]any{
-		"Articles":           b.Articles,
-		"ArticlesByCategory": byCat,
-		"Author":             b.Author,
-	})
-}
-
-// 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,
-	})
-}
-
-func parseTemplates(fs fs.FS) (*template.Template, error) {
-	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 {
-		return nil, fmt.Errorf("could not parse templates: %w", err)
-	}
-	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
-			}
-			defer fi.Close()
-
-			content, err := io.ReadAll(fi)
-			if err != nil {
-				return err
-			}
-
-			article, err := parseArticle(string(content))
-			if err != nil {
-				return err
-			}
-			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
-}
-
-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
-
-	for idx, line := range lines {
-		if strings.TrimSpace(line) == "" {
-			continue
-		}
-		if isSep != nil && isSep(line) {
-			end = idx
-			break
-		}
-
-		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")
-		if err := decoder([]byte(strings.Join(lines[start+1:end], "\n")), &meta); err != nil {
-			return Article{}, fmt.Errorf("could not parse frontmatter: %w", err)
-		}
-	}
-
-	var buf bytes.Buffer
-	err := Markdown.Convert([]byte(body), &buf)
-	if err != nil {
-		return Article{}, fmt.Errorf("could not convert article: %w", err)
-	}
-
-	return Article{
-		Content:     template.HTML(buf.String()),
-		ArticleMeta: meta,
-	}, nil
-}
-
-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
-}
D blog_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
diff --git a/blog_test.go b/blog_test.go
deleted file mode 100644
index 821a659ae04f26a6c4bb1157f8c4835e58de07b2..0000000000000000000000000000000000000000
--- a/blog_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-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)
-}
D cmd/blog/main.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
diff --git a/cmd/blog/main.go b/cmd/blog/main.go
deleted file mode 100644
index d5d835e2eab4153e10fc157dd07aec9cbb7507d4..0000000000000000000000000000000000000000
--- a/cmd/blog/main.go
+++ /dev/null
@@ -1,116 +0,0 @@
-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
-}
D cmd/chromacss/main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
diff --git a/cmd/chromacss/main.go b/cmd/chromacss/main.go
deleted file mode 100644
index e2b1eefe7aa3763e530c534812bee0f951a768b8..0000000000000000000000000000000000000000
--- a/cmd/chromacss/main.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"os"
-
-	"github.com/alecthomas/chroma/v2/formatters/html"
-	"github.com/alecthomas/chroma/v2/styles"
-	"go.jolheiser.com/blog"
-)
-
-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(blog.ChromaOpts...)
-
-	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.modgo.mod
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
diff --git a/go.mod b/go.mod
index 8c1aa017c7924a7b8ed3b4ceab8cde26ece9de51..3d226dae1f71f357af8b8c8cbd8f38889bcba113 100644
--- a/go.mod
+++ b/go.mod
@@ -3,24 +3,18 @@
 go 1.21.6
 
 require (
-	github.com/BurntSushi/toml v1.3.2 // indirect
-	github.com/alecthomas/assert v1.0.0 // indirect
-	github.com/alecthomas/chroma/v2 v2.12.0 // indirect
-	github.com/alecthomas/colour v0.1.0 // indirect
-	github.com/alecthomas/repr v0.2.0 // indirect
-	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+	github.com/BurntSushi/toml v1.3.2
+	github.com/a-h/templ v0.2.543
+	github.com/alecthomas/chroma/v2 v2.12.0
+	github.com/yuin/goldmark v1.7.0
+	github.com/yuin/goldmark-emoji v1.0.2
+	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
+	github.com/yuin/goldmark-meta v1.1.0
+)
+
+require (
 	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
+	github.com/kr/pretty v0.3.1 // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
M go.sumgo.sum
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
diff --git a/go.sum b/go.sum
index 62a81853e4095cb8c64512a1f9d8ff9e25a8fc83..c4b1bc6fb037c92e5fe68ac1efae39f51b36f823 100644
--- a/go.sum
+++ b/go.sum
@@ -1,53 +1,37 @@
 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/a-h/templ v0.2.543 h1:8YyLvyUtf0/IE2nIwZ62Z/m2o2NqwhnMynzOL78Lzbk=
+github.com/a-h/templ v0.2.543/go.mod h1:jP908DQCwI08IrnTalhzSEH9WJqG/Q94+EODQcJGFUA=
+github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
+github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 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=
 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
 github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@@ -57,14 +41,9 @@ 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 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=
D goldmark.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
diff --git a/goldmark.go b/goldmark.go
deleted file mode 100644
index 195e16337b248d03814a8d54db027cad16c6ec19..0000000000000000000000000000000000000000
--- a/goldmark.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package blog
-
-import (
-	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
-// Set over this variable to alter how the other functions convert
-var Markdown = goldmark.New(
-	goldmark.WithParserOptions(
-		parser.WithAutoHeadingID(),
-	),
-	goldmark.WithRendererOptions(
-		html.WithUnsafe(),
-	),
-	goldmark.WithExtensions(
-		extension.GFM,
-		meta.Meta,
-		emoji.Emoji,
-		highlighting.NewHighlighting(
-			highlighting.WithFormatOptions(
-				ChromaOpts...,
-			),
-		),
-	),
-)
-
-var ChromaOpts = []chromahtml.Option{
-	chromahtml.WithClasses(true),
-	chromahtml.WithAllClasses(true),
-	chromahtml.WithLineNumbers(true),
-}
I main.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
diff --git a/main.go b/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..f0c5c9ee09a29fe144c863df27be02a3a1bf7a3e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,103 @@
+//go:generate templ generate
+//go:generate go run .
+package main
+
+import (
+	"context"
+	"embed"
+	"flag"
+	"fmt"
+	iofs "io/fs"
+	"os"
+	"path/filepath"
+
+	"github.com/alecthomas/chroma/v2/styles"
+)
+
+//go:embed articles/*
+var articleFS embed.FS
+
+func maine() error {
+	fs := flag.NewFlagSet("blog", flag.ExitOnError)
+	outFlag := fs.String("out", "out", "Output directory")
+	fs.StringVar(outFlag, "o", *outFlag, "--out")
+	if err := fs.Parse(os.Args[1:]); err != nil {
+		return err
+	}
+
+	files, err := articleFS.ReadDir("articles")
+	if err != nil {
+		return err
+	}
+
+	articles := make(Articles)
+	for _, file := range files {
+		if filepath.Ext(file.Name()) != ".md" {
+			continue
+		}
+		content, err := iofs.ReadFile(articleFS, fmt.Sprintf("articles/%s", file.Name()))
+		if err != nil {
+			return err
+		}
+		article, err := Parse(string(content))
+		if err != nil {
+			return err
+		}
+		articles[article.Category] = append(articles[article.Category], article)
+		if err := writeArticle(*outFlag, article); err != nil {
+			return err
+		}
+	}
+
+	if err := writeCSS(*outFlag); err != nil {
+		return err
+	}
+
+	fi, err := os.Create(filepath.Join(*outFlag, "index.html"))
+	if err != nil {
+		return err
+	}
+	defer fi.Close()
+
+	return IndexTemplate(articles).Render(context.Background(), fi)
+}
+
+func writeArticle(out string, article Article) error {
+	dest := filepath.Join(out, article.Slug(), "index.html")
+	if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
+		return err
+	}
+
+	fi, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer fi.Close()
+
+	return ArticleTemplate(article).Render(context.Background(), fi)
+}
+
+func writeCSS(out string) error {
+	fi, err := os.Create(filepath.Join(out, "chroma.css"))
+	if err != nil {
+		return err
+	}
+	defer fi.Close()
+
+	if err := CSS.WriteCSS(fi, styles.Get("catpuccin-latte")); err != nil {
+		return err
+	}
+	fi.WriteString("@media (prefers-color-scheme: dark) {")
+	if err := CSS.WriteCSS(fi, styles.Get("catppuccin-mocha")); err != nil {
+		return err
+	}
+	fi.WriteString("}")
+	return nil
+}
+
+func main() {
+	if err := maine(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
I templates.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
diff --git a/templates.go b/templates.go
new file mode 100644
index 0000000000000000000000000000000000000000..8af412b693c9a8150dd0e116a5d93f31a59570d1
--- /dev/null
+++ b/templates.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/BurntSushi/toml"
+	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"
+)
+
+// Articles by Category
+type Articles map[string][]Article
+
+type Article struct {
+	Content string
+	Meta
+}
+
+func (a Article) Slug() string {
+	slug := strings.NewReplacer(" ", "-").Replace(strings.ToLower(a.Meta.Title))
+	return fmt.Sprintf("%s/%s/", a.Meta.Date.Format("2006"), slug)
+}
+
+type Meta struct {
+	Title    string
+	Summary  string
+	Date     time.Time
+	Category string
+}
+
+type Author struct {
+	Name  string
+	Email string
+}
+
+func Parse(content string) (Article, error) {
+	lines := strings.Split(content, "\n")
+
+	start, end := -1, -1
+	for idx, line := range lines {
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+
+		if isTOMLSeparator(line) {
+			if start == -1 {
+				start = idx
+				continue
+			}
+			end = idx
+			break
+		}
+	}
+
+	var meta Meta
+	body := content
+	if start != -1 && end != -1 {
+		body = strings.Join(lines[end+1:], "\n")
+		if err := toml.Unmarshal([]byte(strings.Join(lines[start+1:end], "\n")), &meta); err != nil {
+			return Article{}, fmt.Errorf("could not parse frontmatter: %w", err)
+		}
+	}
+
+	var buf bytes.Buffer
+	err := Markdown.Convert([]byte(body), &buf)
+	if err != nil {
+		return Article{}, fmt.Errorf("could not convert article: %w", err)
+	}
+
+	return Article{
+		Content: buf.String(),
+		Meta:    meta,
+	}, nil
+}
+
+func isTOMLSeparator(line string) bool {
+	for _, char := range line {
+		if char != '+' {
+			return false
+		}
+	}
+	return true
+}
+
+var (
+	opts = []chromahtml.Option{
+		chromahtml.WithClasses(true),
+		chromahtml.WithAllClasses(true),
+		chromahtml.WithLineNumbers(true),
+	}
+	Markdown = goldmark.New(
+		goldmark.WithParserOptions(
+			parser.WithAutoHeadingID(),
+		),
+		goldmark.WithRendererOptions(
+			html.WithUnsafe(),
+		),
+		goldmark.WithExtensions(
+			extension.GFM,
+			meta.Meta,
+			emoji.Emoji,
+			highlighting.NewHighlighting(
+				highlighting.WithFormatOptions(
+					opts...,
+				),
+			),
+		),
+	)
+	CSS = chromahtml.New(opts...)
+)
I templates.templ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
diff --git a/templates.templ b/templates.templ
new file mode 100644
index 0000000000000000000000000000000000000000..daee1e81f364b2c7d571ffe38b7e3eb24d24def0
--- /dev/null
+++ b/templates.templ
@@ -0,0 +1,45 @@
+package main
+
+templ baseTemplate(title, description string) {
+	<!DOCTYPE html>
+	<html>
+		<head>
+			<meta charset="UTF-8"/>
+			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+			<title>{ title }</title>
+			<meta property="og:title" content={ title }/>
+			<meta property="og:description" content={ description }/>
+			<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css"/>
+			<link rel="stylesheet" href="/chroma.css"/>
+		</head>
+		<body>
+			{ children... }
+		</body>
+	</html>
+}
+
+templ IndexTemplate(articles Articles) {
+	@baseTemplate("jolheiser's blog", "Hahaha yes.....YES!") {
+		<main>
+			for category, articles := range articles {
+				<h2>{ category }</h2>
+				<ul>
+					for _, article := range articles {
+						<li><a href={ templ.SafeURL(article.Slug()) }>{ article.Title }</a></li>
+					}
+				</ul>
+			}
+		</main>
+	}
+}
+
+templ ArticleTemplate(article Article) {
+	@baseTemplate(article.Title, article.Summary) {
+		<header>
+			<h1>{ article.Title }</h1>
+			<h2>{ article.Date.Format("01/02/2006") }</h2>
+		</header>
+		<main>@templ.Raw(article.Content)</main>
+	}
+}
+
I templates_templ.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
diff --git a/templates_templ.go b/templates_templ.go
new file mode 100644
index 0000000000000000000000000000000000000000..eb1a5286ff88f24036184a633dcb98ac78aa66d5
--- /dev/null
+++ b/templates_templ.go
@@ -0,0 +1,219 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: 0.2.480
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+func baseTemplate(title, description string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var1 == nil {
+			templ_7745c5c3_Var1 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var2 string = title
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><meta property=\"og:title\" content=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(title))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><meta property=\"og:description\" content=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(description))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css\"><link rel=\"stylesheet\" href=\"/chroma.css\"></head><body>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func IndexTemplate(articles Articles) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var3 == nil {
+			templ_7745c5c3_Var3 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Var4 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+			if !templ_7745c5c3_IsBuffer {
+				templ_7745c5c3_Buffer = templ.GetBuffer()
+				defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			for category, articles := range articles {
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				var templ_7745c5c3_Var5 string = category
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2><ul>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				for _, article := range articles {
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a href=\"")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var6 templ.SafeURL = templ.SafeURL(article.Slug())
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var7 string = article.Title
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</main>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			if !templ_7745c5c3_IsBuffer {
+				_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+			}
+			return templ_7745c5c3_Err
+		})
+		templ_7745c5c3_Err = baseTemplate("jolheiser's blog", "Hahaha yes.....YES!").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func ArticleTemplate(article Article) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var8 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var8 == nil {
+			templ_7745c5c3_Var8 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Var9 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+			if !templ_7745c5c3_IsBuffer {
+				templ_7745c5c3_Buffer = templ.GetBuffer()
+				defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<header><h1>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var10 string = article.Title
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><h2>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var11 string = article.Date.Format("01/02/2006")
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2></header><main>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templ.Raw(article.Content).Render(ctx, templ_7745c5c3_Buffer)
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</main>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			if !templ_7745c5c3_IsBuffer {
+				_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+			}
+			return templ_7745c5c3_Err
+		})
+		templ_7745c5c3_Err = baseTemplate(article.Title, article.Summary).Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}