commit bda7a38

Artur Manuel  ·  2026-05-20 08:06:00 +0000 UTC
parents 3a816a8, 5a34032
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
A go.mod
A go.sum
+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
+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+![bigpoo](bigpoo.png) 
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+}
+31, -0
 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>