diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e660fd9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+bin/
diff --git a/cmd/web/main.go b/cmd/web/main.go
new file mode 100644
index 0000000..83d466b
--- /dev/null
+++ b/cmd/web/main.go
@@ -0,0 +1,31 @@
+// Main entry point for the web app. Does all
+// the setup, then runs the http server.
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "git.burning.moe/celediel/burning.moe/internal/config"
+ "git.burning.moe/celediel/burning.moe/internal/handlers"
+ "git.burning.moe/celediel/burning.moe/internal/render"
+)
+
+func main() {
+ // Initialise app and config
+ app := config.Initialise()
+
+ // Initialise handlers and renderer
+ handlers.Initialise(&app)
+ render.Initialise(&app)
+
+ // Initialise the webserver
+ srv := &http.Server{
+ Addr: fmt.Sprintf(":%d", app.ListenPort),
+ Handler: routes(&app),
+ }
+
+ // and finally, start the server
+ app.Logger.Printf("Listening on port %d", app.ListenPort)
+ srv.ListenAndServe()
+}
diff --git a/cmd/web/routes.go b/cmd/web/routes.go
new file mode 100644
index 0000000..bee268f
--- /dev/null
+++ b/cmd/web/routes.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "net/http"
+
+ "git.burning.moe/celediel/burning.moe/internal/config"
+ "git.burning.moe/celediel/burning.moe/internal/handlers"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+)
+
+// routes handles all of the HTTP setup. Middleware is enabled,
+// static fileserver is setup, and handlers are ... handled
+func routes(app *config.AppConfig) http.Handler {
+ mux := chi.NewRouter()
+
+ // Import some middleware
+ mux.Use(middleware.Recoverer)
+
+ // Setup static file server
+ app.Logger.Debug("Setting up /static file server")
+ mux.Handle("/static/*", http.StripPrefix("/static", http.FileServer(http.Dir("./static"))))
+
+ // Setup routes for handlers
+ for _, handler := range handlers.Handlers {
+ app.Logger.Info("Setting up handler for " + handler.Handles)
+ mux.Get(handler.Handles, handler.Handler)
+ }
+
+ return mux
+}
diff --git a/go.mod b/go.mod
index 5429eca..1d8401f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,28 @@
module git.burning.moe/celediel/burning.moe
go 1.21.5
+
+require (
+ github.com/go-chi/chi/v5 v5.0.11
+ github.com/ilyakaznacheev/cleanenv v1.5.0
+ github.com/magefile/mage v1.15.0
+)
+
+require (
+ github.com/BurntSushi/toml v1.3.2 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/lipgloss v0.9.1 // indirect
+ github.com/charmbracelet/log v0.3.1 // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f909bfe
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,43 @@
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
+github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
+github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
+github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
+github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
+github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
+github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
+github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
+olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..bab954f
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,83 @@
+package config
+
+import (
+ "math"
+ "os"
+
+ "git.burning.moe/celediel/burning.moe/internal/models"
+ "github.com/charmbracelet/log"
+ "github.com/ilyakaznacheev/cleanenv"
+)
+
+// AppConfig contains data to be accessed across the app.
+type AppConfig struct {
+ ListenPort uint16
+ TemplateCache models.TemplateCache
+ UseCache bool
+ Logger *log.Logger
+ LogLevel log.Level
+}
+
+// defaluts contains default settings that are used if no environmental variables are set
+var defaults = &AppConfig{
+ ListenPort: 9001,
+ UseCache: true,
+ LogLevel: log.InfoLevel,
+}
+
+// ConfigDatabase contains data to be loaded from environmental variables
+type ConfigDatabase struct {
+ Port uint16 `env:"PORT" env-default:"9001" env-description:"server port"`
+ LogLevel string `env:"LOGLEVEL" env-default:"warn" env-description:"Logging level. Default: warn, Possible values: debug info warn error fatal none"`
+ UseCache bool `env:"CACHE" env-default:"true" env-description:"Use template cache"`
+}
+
+// Initialises the app wide AppConfig, loads values from environment, and set up the Logger
+func Initialise() AppConfig {
+ app := *defaults
+ app.Logger = log.New(os.Stderr)
+
+ // load values from config
+ if cfg, err := loadConfig(); err == nil {
+ app.ListenPort = cfg.Port
+ app.UseCache = cfg.UseCache
+ app.LogLevel = logLevelFromString(cfg.LogLevel)
+ } else {
+ app.Logger.Print("Failed loading config from environment", "err", err)
+ }
+
+ app.Logger.SetLevel(app.LogLevel)
+ app.Logger.Debug("Loaded config from environment:", "port", app.ListenPort, "useCache", app.UseCache, "log_level", app.LogLevel)
+
+ return app
+}
+
+// loadConfig utilises cleanenv to load config values from the environment
+func loadConfig() (ConfigDatabase, error) {
+ var cfg ConfigDatabase
+ if err := cleanenv.ReadEnv(&cfg); err != nil {
+ return ConfigDatabase{}, err
+ } else {
+ return cfg, nil
+ }
+}
+
+// logLevelFromString turns a string like "warn" into a log.Level like log.WarnLevel
+func logLevelFromString(level string) log.Level {
+ switch level {
+ case "debug":
+ return log.DebugLevel
+ case "info":
+ return log.InfoLevel
+ case "warn":
+ return log.WarnLevel
+ case "error":
+ return log.ErrorLevel
+ case "fatal":
+ return log.FatalLevel
+ case "none":
+ return math.MaxInt32
+ default:
+ return defaults.LogLevel
+ }
+}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
new file mode 100644
index 0000000..8edca53
--- /dev/null
+++ b/internal/handlers/handlers.go
@@ -0,0 +1,42 @@
+package handlers
+
+import (
+ "net/http"
+
+ "git.burning.moe/celediel/burning.moe/internal/config"
+ "git.burning.moe/celediel/burning.moe/internal/models"
+ "git.burning.moe/celediel/burning.moe/internal/render"
+)
+
+// Handler holds data required for handlers.
+type Handler struct {
+ Handles string
+ Handler func(w http.ResponseWriter, r *http.Request)
+}
+
+var app *config.AppConfig
+
+// The actual handlers
+var Handlers = []Handler{
+ // /about
+ {
+ Handles: "/about",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ app.Logger.Info("Got request for about page.")
+ render.RenderTemplate(w, "about.page", &models.TemplateData{})
+ },
+ },
+ // / comes last
+ {
+ Handles: "/",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ app.Logger.Info("Got request for homepage.")
+ render.RenderTemplate(w, "home.page", &models.TemplateData{})
+ },
+ },
+}
+
+// Initialise the handlers package.
+func Initialise(a *config.AppConfig) {
+ app = a
+}
diff --git a/internal/models/templatecache.go b/internal/models/templatecache.go
new file mode 100644
index 0000000..89163a9
--- /dev/null
+++ b/internal/models/templatecache.go
@@ -0,0 +1,18 @@
+package models
+
+import (
+ "html/template"
+ "time"
+)
+
+// TemplateCache holds the template cache as map of TemplateCacheItem
+type TemplateCache struct {
+ Cache map[string]TemplateCacheItem
+}
+
+// TemplateCacheItem holds a pointer to a generated
+// template, and the time it was generated at.
+type TemplateCacheItem struct {
+ Template *template.Template
+ GeneratedAt time.Time
+}
diff --git a/internal/models/templatedata.go b/internal/models/templatedata.go
new file mode 100644
index 0000000..7ea43b2
--- /dev/null
+++ b/internal/models/templatedata.go
@@ -0,0 +1,14 @@
+package models
+
+// TemplateData holds data sent from handlers to templates.
+type TemplateData struct {
+ StringMap map[string]string
+ IntMap map[string]int
+ FloatMap map[string]float32
+ Data map[string]interface{}
+ CSRFToken string
+ Flash string
+ Warning string
+ Error string
+}
+
diff --git a/internal/render/render.go b/internal/render/render.go
new file mode 100644
index 0000000..0096d96
--- /dev/null
+++ b/internal/render/render.go
@@ -0,0 +1,111 @@
+package render
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "net/http"
+ "path/filepath"
+ "time"
+
+ "git.burning.moe/celediel/burning.moe/internal/config"
+ "git.burning.moe/celediel/burning.moe/internal/models"
+)
+
+const (
+ templatesDir string = "./templates/"
+ layoutGlob string = "*.layout.tmpl"
+ pageGlob string = "*.page.tmpl"
+)
+
+var app *config.AppConfig
+
+// Initialise the render package.
+func Initialise(a *config.AppConfig) {
+ var err error
+ app = a
+ app.TemplateCache, err = GenerateNewTemplateCache()
+ if err != nil {
+ app.Logger.Fatal("Error generating template cache, bailing out!")
+ }
+}
+
+// GenerateNewTemplateCache generates a new template cache.
+func GenerateNewTemplateCache() (models.TemplateCache, error) {
+ // start with an empty map
+ cache := models.TemplateCache{}
+ cache.Cache = map[string]models.TemplateCacheItem{}
+
+ // Generate a list of pages based on globs
+ pages, err := filepath.Glob(templatesDir + pageGlob)
+
+ // a nice try catch would be pretty cool right about here
+ if err != nil {
+ return cache, err
+ }
+
+ // Iterate each page, parsing the file and adding it to the cache
+ for _, page := range pages {
+ name := filepath.Base(page)
+ app.Logger.Info("Generating template " + name)
+
+ templateSet, err := template.New(name).ParseFiles(page)
+ if err != nil {
+ return cache, err
+ }
+
+ // Glob and parse any layouts found
+ layouts, err := filepath.Glob(templatesDir + layoutGlob)
+ if err != nil {
+ return cache, err
+ }
+
+ if len(layouts) > 0 {
+ templateSet, err = templateSet.ParseGlob(templatesDir + layoutGlob)
+ if err != nil {
+ return cache, err
+ }
+ }
+ cache.Cache[name] = models.TemplateCacheItem{
+ Template: templateSet,
+ GeneratedAt: time.Now(),
+ }
+ }
+
+ // All was good, so return the cache, and no error
+ return cache, nil
+}
+
+// RenderTemplate renders requested template (t), pulling from cache.
+func RenderTemplate(w http.ResponseWriter, t string, data *models.TemplateData) {
+ filename := t + ".tmpl"
+ var cache models.TemplateCache
+ if app.UseCache {
+ cache = app.TemplateCache
+ } else {
+ var err error
+ cache, err = GenerateNewTemplateCache()
+ if err != nil {
+ app.Logger.Fatal("Error generating template cache, bailing out!")
+ }
+ }
+
+ // Get templates from cache
+ template, ok := cache.Cache[filename]
+ if !ok {
+ app.Logger.Fatal(fmt.Sprintf("Couldn't get %s from template cache, bailing out!", filename))
+ }
+
+ // Execute templates in a new buffer
+ buf := new(bytes.Buffer)
+ err := template.Template.Execute(buf, data)
+
+ if err != nil {
+ app.Logger.Fatal("Error executing template %s! Goodbye!", "err", err)
+ }
+
+ _, err = buf.WriteTo(w)
+ if err != nil {
+ app.Logger.Error("Error writing template %s!\n", "err", err)
+ }
+}
diff --git a/magefile.go b/magefile.go
new file mode 100644
index 0000000..5bf9480
--- /dev/null
+++ b/magefile.go
@@ -0,0 +1,38 @@
+//go:build mage
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/magefile/mage/sh"
+)
+
+var (
+ binaryName string = "burningmoe"
+ buildDir string = "bin"
+ cmd string = fmt.Sprintf(".%[1]ccmd%[1]cweb", os.PathSeparator)
+ output string = fmt.Sprintf(".%[1]c%[2]s%[1]c%[3]s", os.PathSeparator, buildDir, binaryName)
+)
+
+func Build() error {
+ fmt.Println("Building...")
+ return sh.Run("go", "build", "-o", output, cmd)
+}
+
+func Run() error {
+ fmt.Println("Running...")
+ return sh.Run("go", "run", cmd)
+}
+
+func RunBinary() error {
+ Build()
+ fmt.Println("Running binary...")
+ return sh.Run(output)
+}
+
+func Clean() error {
+ fmt.Println("Cleaning...")
+ return os.Remove(output)
+}
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..9f8499b
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,71 @@
+@import url('https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&display=swap');
+
+body {
+ background: #FFF;
+ color: #121212;
+ text-align: center;
+ font-family: 'Gloria Hallelujah', cursive;
+}
+
+a {
+ color: #333;
+ text-decoration: none;
+ -o-transition: .5s;
+ -ms-transition: .5s;
+ -moz-transition: .5s;
+ -webkit-transition: .5s;
+ /* ...and now for the proper property */
+ transition: .5s;
+}
+
+a:hover {
+ color: #999;
+}
+
+a.back {
+ color: #111;
+}
+
+#stuff {
+ margin: 55px 150px 25px 150px;
+}
+
+#words {
+ font-size: 1.0em;
+ margin-top: -10px;
+}
+
+#bigwords {
+ font-size: 1.3em;
+}
+
+#leftfooter {
+ position: fixed;
+ bottom: 10px;
+ left: 10px;
+ font-size: 0.75em;
+ text-align: left;
+}
+
+#rightfooter {
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ font-size: 0.75em;
+ text-align: right;
+}
+
+#header {
+ margin: 0px;
+ margin-top: -20px;
+}
+
+h1 {
+ font-size: 1.7em;
+ font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif;
+}
+
+h2 {
+ padding-top: 0px;
+ font-size: 1.4em;
+}
\ No newline at end of file
diff --git a/static/img/burningmoe.jpg b/static/img/burningmoe.jpg
new file mode 100644
index 0000000..65b755f
Binary files /dev/null and b/static/img/burningmoe.jpg differ
diff --git a/static/img/moemoe.png b/static/img/moemoe.png
new file mode 100644
index 0000000..6b955b0
Binary files /dev/null and b/static/img/moemoe.png differ
diff --git a/templates/about.page.tmpl b/templates/about.page.tmpl
new file mode 100644
index 0000000..779a70e
--- /dev/null
+++ b/templates/about.page.tmpl
@@ -0,0 +1,53 @@
+{{- template "base" . -}}
+
+{{- define "content" }}
+
+ celediel
+
+ she/her, 1989, queer anarchist, self-taught aspiring developer
+
+ links
+
+
+
+
+ matrix
+
+
+
+
+ lemmy
+
+
+
+
+ self-hosted git
+
+
+
+
+ github
+
+
+
+ hosted apps
+
+
+ self-hosted git
+
+
+
+ wastebin
+
+
+
+ gist
+
+
+ back
+
+{{ end -}}
+
+{{- define "js" }}
+
+{{ end -}}
diff --git a/templates/base.layout.tmpl b/templates/base.layout.tmpl
new file mode 100644
index 0000000..ae55de0
--- /dev/null
+++ b/templates/base.layout.tmpl
@@ -0,0 +1,37 @@
+{{- define "base" -}}
+
+
+