commit bfc7001

Artur Manuel  ·  2026-05-10 00:56:54 +0000 UTC
parent 6f5b0fe
ssg: refactor ssg completely
8 files changed,  +238, -77
M go.mod
M go.sum
+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+}