commit bda7a38
Merge pull request 'meta: create a webpage' (#1) from wip_ssg into main Reviewed-on: https://codeberg.org/dani-institute/pages/pulls/1
17 files changed,
+515,
-0
+7,
-0
1@@ -0,0 +1,7 @@
2+[*.go]
3+indent_style = tab
4+indent_size = 8
5+
6+[Makefile]
7+indent_style = tab
8+indent_size = 8
+20,
-0
1@@ -0,0 +1,20 @@
2+**
3+!/cmd
4+!/cmd/**
5+!/pkg
6+!/pkg/**
7+!/ssg
8+!/ssg/**
9+!/templates
10+!/templates/**
11+!/pages
12+!/pages/**
13+!/assets
14+!/assets/**
15+!/.gitignore
16+!/.editorconfig
17+!/Makefile
18+!/README.md
19+!/LICENSE
20+!/go.sum
21+!/go.mod
A
Makefile
+15,
-0
1@@ -0,0 +1,15 @@
2+.POSIX:
3+
4+GO ?= go
5+
6+all:
7+ $(GO) build ./cmd/generate_pages
8+
9+clean:
10+ rm generate_pages
11+
12+lint:
13+ $(GO) fmt ./...
14+ $(GO) vet ./...
15+ $(GO) fix ./...
16+ staticcheck ./...
+0,
-0
+99,
-0
1@@ -0,0 +1,99 @@
2+package main
3+
4+import (
5+ "context"
6+ "log/slog"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+
11+ "codeberg.org/dani-institute/pages/pkg/ssg"
12+ "github.com/urfave/cli/v3"
13+ "github.com/yuin/goldmark"
14+ meta "github.com/yuin/goldmark-meta"
15+ "github.com/yuin/goldmark/parser"
16+)
17+
18+var cmd = cli.Command{
19+ Name: "generate_pages",
20+ Version: "2026.5.0",
21+ Flags: []cli.Flag{
22+ &cli.StringFlag{
23+ Name: "base-url",
24+ Value: "",
25+ },
26+ &cli.StringFlag{
27+ Name: "config",
28+ Value: "./config.toml",
29+ },
30+ &cli.StringFlag{
31+ Name: "templates-dir",
32+ Value: "./templates",
33+ },
34+ &cli.StringFlag{
35+ Name: "pages-dir",
36+ Value: "./pages",
37+ },
38+ &cli.StringFlag{
39+ Name: "output-dir",
40+ Value: "./output",
41+ },
42+ },
43+ Action: mainAction,
44+}
45+
46+func mainAction(ctx context.Context, c *cli.Command) error {
47+ slog.Info("generating pages")
48+ var config ssg.Config
49+ baseUrl := c.String("base-url")
50+ _ = baseUrl
51+ configFile := c.String("config")
52+ if err := ssg.LoadConfigFile(configFile, &config); err != nil {
53+ return err
54+ }
55+ pagesDir := c.String("pages-dir")
56+ pages, err := ssg.GetPages(pagesDir)
57+ if err != nil {
58+ return err
59+ }
60+ templatesDir := c.String("templates-dir")
61+ templates, err := ssg.GetTemplates(templatesDir)
62+ if err != nil {
63+ return err
64+ }
65+ sidebar, err := ssg.GetSidebar(pagesDir)
66+ if err != nil {
67+ return err
68+ }
69+ outputDir := c.String("output-dir")
70+ if err := os.MkdirAll(outputDir, 0o700); os.IsNotExist(err) {
71+ return err
72+ }
73+ if err := ssg.FromAssets("./assets", outputDir); err != nil {
74+ return err
75+ }
76+ markdown := goldmark.New(
77+ goldmark.WithExtensions(meta.Meta),
78+ goldmark.WithParserOptions(parser.WithAutoHeadingID()),
79+ )
80+ for path, page := range pages {
81+ name := strings.TrimSuffix(filepath.Base(path), ".md")
82+ slog.Info("building page", "page", name)
83+ outputPath := filepath.Join(outputDir, name) + ".html"
84+ f, err := os.Create(outputPath)
85+ if err != nil {
86+ return err
87+ }
88+ if err := ssg.BuildPage(f, name, path, page, markdown, templates, config, baseUrl, sidebar); err != nil {
89+ return err
90+ }
91+ }
92+ return nil
93+}
94+
95+func main() {
96+ if err := cmd.Run(context.Background(), os.Args); err != nil {
97+ slog.Error("got err while generating pages", "err", err)
98+ os.Exit(1)
99+ }
100+}
+6,
-0
1@@ -0,0 +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"
A
go.mod
+11,
-0
1@@ -0,0 +1,11 @@
2+module codeberg.org/dani-institute/pages
3+
4+go 1.26.2
5+
6+require (
7+ github.com/BurntSushi/toml v1.6.0 // indirect
8+ github.com/urfave/cli/v3 v3.8.0 // indirect
9+ github.com/yuin/goldmark v1.8.2 // indirect
10+ github.com/yuin/goldmark-meta v1.1.0 // indirect
11+ gopkg.in/yaml.v2 v2.3.0 // indirect
12+)
A
go.sum
+11,
-0
1@@ -0,0 +1,11 @@
2+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
3+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
4+github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
5+github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
6+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
7+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
8+github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
9+github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
10+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
12+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+12,
-0
1@@ -0,0 +1,12 @@
2+----
3+title: the dani museum
4+----
5+
6+# Hi hiii welcome to the dani museum!! (HEAVILY W.I.P) :3
7+
8+Check this shit out this peaks ---> [kid named shit](https://www.youtube.com/watch?v=u87oNgyIPvY)
9+*(TUFF!!)*
10+
11+
12+
13+(*This is bigpoo, say hi to bigpoo 🥺)*
+6,
-0
1@@ -0,0 +1,6 @@
2+---
3+title: Test
4+description: Test
5+---
6+
7+# {{ .Extra.test }} :)
+36,
-0
1@@ -0,0 +1,36 @@
2+package ssg
3+
4+import (
5+ "io"
6+ "io/fs"
7+ "log/slog"
8+ "os"
9+ "path/filepath"
10+)
11+
12+// i couldn't come up with a better name for the function without it being too verbose but bwaaa who cares ig ;w;
13+func FromAssets(assetsDir string, outputDir string) error {
14+ err := filepath.WalkDir(assetsDir, func(path string, d fs.DirEntry, err error) error {
15+ if err != nil {
16+ slog.Error("got err while reading path", "path", path, "err", err) // those who error :skull:
17+ return filepath.SkipDir
18+ }
19+ if d.IsDir() {
20+ return nil
21+ }
22+ destPath := filepath.Join(outputDir, filepath.Base(path))
23+ src, err := os.Open(path)
24+ if err != nil {
25+ return err
26+ }
27+ defer src.Close()
28+ dst, err := os.Create(destPath)
29+ if err != nil {
30+ return err
31+ }
32+ defer dst.Close()
33+ _, err = io.Copy(dst, src)
34+ return err
35+ })
36+ return err
37+}
+25,
-0
1@@ -0,0 +1,25 @@
2+package ssg
3+
4+import (
5+ "os"
6+
7+ "github.com/BurntSushi/toml"
8+)
9+
10+type Config struct {
11+ BaseURL string `toml:"base_url"`
12+ Title string `toml:"title"`
13+ Description string `toml:"description"`
14+ Extra map[string]any `toml:"extra"`
15+}
16+
17+func LoadConfigFile(fn string, config *Config) error {
18+ out, err := os.ReadFile(fn)
19+ if err != nil {
20+ return err
21+ }
22+ if err := toml.Unmarshal(out, config); err != nil {
23+ return err
24+ }
25+ return nil
26+}
+103,
-0
1@@ -0,0 +1,103 @@
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+ "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+ content, err := os.ReadFile(path)
42+ if err != nil {
43+ slog.Error("got err while reading file", "file", path, "err", err)
44+ return filepath.SkipDir
45+ }
46+ res[path] = string(content)
47+ }
48+ return nil
49+ })
50+ if err != nil {
51+ return res, err
52+ }
53+ return res, nil
54+}
55+
56+func BuildPage(w io.Writer, name string, path string, page string, markdown goldmark.Markdown, templates Templates, config Config, baseUrl string, sidebar []SidebarItem) error {
57+ var buff bytes.Buffer
58+ ctx := parser.NewContext()
59+ if err := markdown.Convert([]byte(page), &buff, parser.WithContext(ctx)); err != nil {
60+ return err
61+ }
62+ frontmatter := meta.Get(ctx)
63+ var templateName string
64+ switch v := frontmatter["template"]; v {
65+ case nil:
66+ if name == "index" {
67+ templateName = "index"
68+ } else {
69+ templateName = "page"
70+ }
71+ default:
72+ templateName = v.(string)
73+ }
74+ if baseUrl == "" {
75+ baseUrl = config.BaseURL
76+ }
77+ var title string
78+ switch v := frontmatter["title"]; v {
79+ case nil:
80+ title = config.Title
81+ default:
82+ title = v.(string)
83+ }
84+ var description string
85+ switch v := frontmatter["description"]; v {
86+ case nil:
87+ description = config.Description
88+ default:
89+ description = v.(string)
90+ }
91+ templateData := TemplateData{
92+ Title: title,
93+ Description: description,
94+ BaseURL: baseUrl,
95+ Params: frontmatter,
96+ Config: config,
97+ Content: buff.String(),
98+ Sidebar: sidebar,
99+ }
100+ if err := BuildTemplate(w, name, templates[templateName], templateData); err != nil {
101+ return err
102+ }
103+ return nil
104+}
1@@ -0,0 +1,31 @@
2+package ssg
3+
4+import (
5+ "os"
6+ "strings"
7+)
8+
9+func GetSidebar(pagesDir string) ([]SidebarItem, error) {
10+ entries, err := os.ReadDir(pagesDir)
11+ if err != nil {
12+ return nil, err
13+ }
14+ var nav []SidebarItem
15+ for _, entry := range entries {
16+ name := entry.Name()
17+ if entry.IsDir() {
18+ nav = append(nav, SidebarItem{
19+ Name: name,
20+ Path: "/" + name,
21+ })
22+ } else if strings.HasSuffix(name, ".md") {
23+ stem := strings.TrimSuffix(name, ".md")
24+ }
25+ nav = append(nav, SidebarItem{
26+ Name: stem,
27+ Path: stem + ".html",
28+ })
29+ }
30+ }
31+ return nav, nil
32+}
+73,
-0
1@@ -0,0 +1,73 @@
2+package ssg
3+
4+import (
5+ "io"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "text/template"
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+ Sidebar []SidebarItem
20+}
21+
22+type SidebarItem struct {
23+ Name string
24+ Path string
25+}
26+
27+type Templates map[string]string
28+
29+func DefaultTemplateData() TemplateData {
30+ return TemplateData{
31+ Title: "",
32+ Description: "",
33+ BaseURL: "",
34+ Params: map[string]any{},
35+ Config: Config{},
36+ Content: "",
37+ }
38+}
39+
40+func BuildTemplate(w io.Writer, name string, t string, data TemplateData) error {
41+ master, err := template.New(name).Parse(t)
42+ if err != nil {
43+ return err
44+ }
45+ _, err = master.New("content").Parse(data.Content)
46+ if err != nil {
47+ return err
48+ }
49+ if err := master.ExecuteTemplate(w, name, data); err != nil {
50+ return err
51+ }
52+ return nil
53+}
54+
55+func GetTemplates(dir string) (Templates, error) {
56+ res := make(map[string]string, 0)
57+ entries, err := os.ReadDir(dir)
58+ if err != nil {
59+ return res, err
60+ }
61+ for _, entry := range entries {
62+ entryName := entry.Name()
63+ if !strings.HasSuffix(entryName, ".html") {
64+ continue
65+ }
66+ name := strings.TrimSuffix(filepath.Base(entryName), ".html")
67+ content, err := os.ReadFile(filepath.Join(dir, entryName))
68+ if err != nil {
69+ return res, err
70+ }
71+ res[name] = string(content)
72+ }
73+ return res, nil
74+}
+30,
-0
1@@ -0,0 +1,30 @@
2+<!DOCTYPE html>
3+<html lang="en">
4+ <head>
5+ <meta charset="UTF-8">
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7+ <title>{{ .Title }}</title>
8+ <meta name="description" content="{{ .Description }}">
9+ <style>
10+ td { padding: 0.5em 1em; }
11+ </style>
12+ </head>
13+ <body>
14+ <p>{{ .Title }} | words words words words words</p>
15+ <hr>
16+ <table>
17+ <tr>
18+ <td valign="top">
19+ {{ range .Sidebar }}
20+ <a href="{{ .Path }}">› {{ .Name }}</a><br>
21+ {{ end }}
22+ </td>
23+ <td valign="top">
24+ <main>
25+ {{ .Content }}
26+ </main>
27+ </td>
28+ </tr>
29+ </table>
30+ </body>
31+</html>
+30,
-0
1@@ -0,0 +1,30 @@
2+<!DOCTYPE html>
3+<html lang="en">
4+ <head>
5+ <meta charset="UTF-8">
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7+ <title>{{ .Title }}</title>
8+ <meta name="description" content="{{ .Description }}">
9+ <style>
10+ td { padding: 0.5em 1em; }
11+ </style>
12+ </head>
13+ <body>
14+ <p>{{ .Title }} | words words words words words</p>
15+ <hr>
16+ <table>
17+ <tr>
18+ <td valign="top">
19+ {{ range .Sidebar }}
20+ <a href="{{ .Path }}">› {{ .Name }}</a><br>
21+ {{ end }}
22+ </td>
23+ <td valign="top">
24+ <main>
25+ {{ .Content }}
26+ </main>
27+ </td>
28+ </tr>
29+ </table>
30+ </body>
31+</html>