commit bfc7001
Artur Manuel
·
2026-05-10 00:56:54 +0000 UTC
parent 6f5b0fe
ssg: refactor ssg completely
8 files changed,
+238,
-77
+54,
-41
1@@ -5,12 +5,12 @@ import (
2 "fmt"
3 "os"
4 "path/filepath"
5- "strings"
6-
7- "github.com/BurntSushi/toml"
8- "github.com/urfave/cli/v3"
9
10 "codeberg.org/dani-institute/pages/pkg/ssg"
11+ "github.com/urfave/cli/v3"
12+ "github.com/yuin/goldmark"
13+ meta "github.com/yuin/goldmark-meta"
14+ "github.com/yuin/goldmark/parser"
15 )
16
17 var cmd = cli.Command{
18@@ -18,53 +18,66 @@ var cmd = cli.Command{
19 Version: "2026.5.0",
20 Flags: []cli.Flag{
21 &cli.StringFlag{
22- Name: "pages-dir",
23- Required: true,
24+ Name: "base-url",
25+ Value: "",
26 },
27 &cli.StringFlag{
28- Name: "output-dir",
29- Required: true,
30+ Name: "config",
31+ Value: "./config.toml",
32 },
33 &cli.StringFlag{
34- Name: "config",
35- Value: "config.toml",
36+ Name: "templates-dir",
37+ Value: "./templates",
38+ },
39+ &cli.StringFlag{
40+ Name: "pages-dir",
41+ Value: "./pages",
42+ },
43+ &cli.StringFlag{
44+ Name: "output-dir",
45+ Value: "./output",
46 },
47 },
48- Action: func(ctx context.Context, c *cli.Command) error {
49- pagesDir := c.String("pages-dir")
50- outputDir := c.String("output-dir")
51- configFile := c.String("config")
52- config := ssg.DefaultConfig()
53- configBytes, err := os.ReadFile(configFile)
54- if err != nil && os.IsExist(err) {
55- return err
56- }
57- if err := toml.Unmarshal(configBytes, &config); err != nil {
58- return err
59- }
60- if _, err := os.ReadDir(outputDir); os.IsNotExist(err) {
61- if err := os.MkdirAll(outputDir, 0o700); err != nil {
62- return err
63- }
64- }
65- pages, err := os.ReadDir(pagesDir)
66+ Action: mainAction,
67+}
68+
69+func mainAction(ctx context.Context, c *cli.Command) error {
70+ var config ssg.Config
71+ baseUrl := c.String("base-url")
72+ _ = baseUrl
73+ configFile := c.String("config")
74+ if err := ssg.LoadConfigFile(configFile, &config); err != nil {
75+ return err
76+ }
77+ pagesDir := c.String("pages-dir")
78+ pages, err := ssg.GetPages(pagesDir)
79+ if err != nil {
80+ return err
81+ }
82+ templatesDir := c.String("templates-dir")
83+ templates, err := ssg.GetTemplates(templatesDir)
84+ if err != nil {
85+ return err
86+ }
87+ outputDir := c.String("output-dir")
88+ if err := os.MkdirAll(outputDir, 0o700); os.IsNotExist(err) {
89+ return err
90+ }
91+ markdown := goldmark.New(
92+ goldmark.WithExtensions(meta.Meta),
93+ goldmark.WithParserOptions(parser.WithAutoHeadingID()),
94+ )
95+ for name, page := range pages {
96+ path := filepath.Join(outputDir, name) + ".html"
97+ f, err := os.Create(path)
98 if err != nil {
99 return err
100 }
101- for _, page := range pages {
102- name := page.Name()
103- pagePath := filepath.Join(pagesDir, name)
104- outputPath := filepath.Join(outputDir, strings.ReplaceAll(name, ".md", ".html"))
105- w, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
106- if err != nil {
107- return err
108- }
109- if err := ssg.BuildTemplate(name, pagePath, w, config); err != nil {
110- return err
111- }
112+ if err := ssg.BuildPage(f, name, page, markdown, templates, config, baseUrl); err != nil {
113+ return err
114 }
115- return nil
116- },
117+ }
118+ return nil
119 }
120
121 func main() {
+2,
-0
1@@ -1,4 +1,6 @@
2+base_url = "https://dani-institute.codeberg.page"
3 title = "The Dani Institute"
4+description = "This is where you can find information about the Dani Village."
5
6 [extra]
7 test = "hi"
M
go.mod
+2,
-0
1@@ -6,4 +6,6 @@ require (
2 github.com/BurntSushi/toml v1.6.0 // indirect
3 github.com/urfave/cli/v3 v3.8.0 // indirect
4 github.com/yuin/goldmark v1.8.2 // indirect
5+ github.com/yuin/goldmark-meta v1.1.0 // indirect
6+ gopkg.in/yaml.v2 v2.3.0 // indirect
7 )
M
go.sum
+5,
-0
1@@ -4,3 +4,8 @@ github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
2 github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
3 github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
4 github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
5+github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
6+github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
7+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
9+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+0,
-30
1@@ -1,30 +0,0 @@
2-package ssg
3-
4-import (
5- "bytes"
6- "io"
7- "text/template"
8-
9- "github.com/yuin/goldmark"
10- "github.com/yuin/goldmark/parser"
11- "github.com/yuin/goldmark/renderer/html"
12-)
13-
14-func BuildTemplate(name string, fn string, output io.Writer, config Config) error {
15- masterTemplate, err := template.New(name).ParseFiles(fn)
16- if err != nil {
17- return err
18- }
19- var b bytes.Buffer
20- if err := masterTemplate.ExecuteTemplate(&b, name, config); err != nil {
21- return err
22- }
23- gm := goldmark.New(
24- goldmark.WithParserOptions(parser.WithAutoHeadingID()),
25- goldmark.WithRendererOptions(html.WithXHTML()),
26- )
27- if err := gm.Convert(b.Bytes(), output); err != nil {
28- return err
29- }
30- return nil
31-}
+18,
-6
1@@ -1,13 +1,25 @@
2 package ssg
3
4+import (
5+ "os"
6+
7+ "github.com/BurntSushi/toml"
8+)
9+
10 type Config struct {
11- Title string `toml:"title"`
12- Extra map[string]any `toml:"extra"`
13+ BaseURL string `toml:"base_url"`
14+ Title string `toml:"title"`
15+ Description string `toml:"description"`
16+ Extra map[string]any `toml:"extra"`
17 }
18
19-func DefaultConfig() Config {
20- return Config{
21- Title: "Default title",
22- Extra: map[string]any{},
23+func LoadConfigFile(fn string, config *Config) error {
24+ out, err := os.ReadFile(fn)
25+ if err != nil {
26+ return err
27+ }
28+ if err := toml.Unmarshal(out, config); err != nil {
29+ return err
30 }
31+ return nil
32 }
+94,
-0
1@@ -0,0 +1,94 @@
2+package ssg
3+
4+import (
5+ "bytes"
6+ "io"
7+ "io/fs"
8+ "log/slog"
9+ "os"
10+ "path/filepath"
11+ "strings"
12+
13+ "github.com/yuin/goldmark"
14+ meta "github.com/yuin/goldmark-meta"
15+ "github.com/yuin/goldmark/parser"
16+)
17+
18+type PageError int
19+
20+const (
21+ _ PageError = iota
22+ NonExistentTemplate
23+)
24+
25+func (e PageError) Error() string {
26+ switch e {
27+ case NonExistentTemplate:
28+ return "given template does not exist"
29+ }
30+ panic("this should not be reached")
31+}
32+
33+func GetPages(dir string) (map[string]string, error) {
34+ res := make(map[string]string, 0)
35+ err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
36+ if err != nil {
37+ slog.Error("got err while reading path", "path", path, "err", err)
38+ return filepath.SkipDir
39+ }
40+ if !d.IsDir() && strings.HasSuffix(path, ".md") {
41+ name := strings.TrimSuffix(filepath.Base(path), ".md")
42+ content, err := os.ReadFile(path)
43+ if err != nil {
44+ slog.Error("got err while reading file", "file", path, "err", err)
45+ return filepath.SkipDir
46+ }
47+ res[name] = string(content)
48+ }
49+ return nil
50+ })
51+ if err != nil {
52+ return res, err
53+ }
54+ return res, nil
55+}
56+
57+func BuildPage(w io.Writer, name string, page string, markdown goldmark.Markdown, templates Templates, config Config, baseUrl string) error {
58+ var buff bytes.Buffer
59+ ctx := parser.NewContext()
60+ if err := goldmark.Convert([]byte(page), &buff, parser.WithContext(ctx)); err != nil {
61+ return err
62+ }
63+ frontmatter := meta.Get(ctx)
64+ templateName, ok := frontmatter["template"].(string)
65+ if !ok {
66+ if name == "index" {
67+ templateName = "index"
68+ } else {
69+ templateName = "page"
70+ }
71+ }
72+ if baseUrl == "" {
73+ baseUrl = config.BaseURL
74+ }
75+ title := frontmatter["title"].(string)
76+ if !ok {
77+ title = config.Title
78+ }
79+ description, ok := frontmatter["description"].(string)
80+ if !ok {
81+ description = config.Description
82+ }
83+ templateData := TemplateData{
84+ Title: title,
85+ Description: description,
86+ BaseURL: baseUrl,
87+ Params: frontmatter,
88+ Config: config,
89+ Content: buff.String(),
90+ }
91+ if err := BuildTemplate(w, name, templates[templateName], templateData); err != nil {
92+ return err
93+ }
94+ return nil
95+}
+63,
-0
1@@ -0,0 +1,63 @@
2+package ssg
3+
4+import (
5+ "html/template"
6+ "io"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+)
11+
12+type TemplateData struct {
13+ Title string
14+ Description string
15+ BaseURL string
16+ Params map[string]any
17+ Config Config
18+ Content string
19+}
20+
21+type Templates map[string]string
22+
23+func DefaultTemplateData() TemplateData {
24+ return TemplateData{
25+ Title: "",
26+ Description: "",
27+ BaseURL: "",
28+ Params: map[string]any{},
29+ Config: Config{},
30+ Content: "",
31+ }
32+}
33+
34+func BuildTemplate(w io.Writer, name string, t string, data TemplateData) error {
35+ master, err := template.New(name).Parse(t)
36+ if err != nil {
37+ return err
38+ }
39+ if err := master.ExecuteTemplate(w, name, data); err != nil {
40+ return err
41+ }
42+ return nil
43+}
44+
45+func GetTemplates(dir string) (Templates, error) {
46+ res := make(map[string]string, 0)
47+ entries, err := os.ReadDir(dir)
48+ if err != nil {
49+ return res, err
50+ }
51+ for _, entry := range entries {
52+ entryName := entry.Name()
53+ if !strings.HasSuffix(entryName, ".html") {
54+ continue
55+ }
56+ name := strings.TrimSuffix(filepath.Base(entryName), ".html")
57+ content, err := os.ReadFile(filepath.Join(dir, entryName))
58+ if err != nil {
59+ return res, err
60+ }
61+ res[name] = string(content)
62+ }
63+ return res, nil
64+}