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" -}} + + + + + + + burning.moe + + {{- block "css" . }} + {{- end }} + + +
+ {{- block "content" . }} + {{ end -}} +
+ +{{- block "js" . }} +{{ end -}} +
+ note: please do not set fire to cute girls +
+
+ powered by + debian + and + go, + proxied by + caddy + - + source +
+ + +{{ end -}} diff --git a/templates/home.page.tmpl b/templates/home.page.tmpl new file mode 100644 index 0000000..6ecbe41 --- /dev/null +++ b/templates/home.page.tmpl @@ -0,0 +1,16 @@ +{{- template "base" . -}} + +{{- define "content" }} + + + + + +
+ + about + +{{ end -}}