From b47820eeea9c9aa29a99e0ee1a17af93e6188bfd Mon Sep 17 00:00:00 2001 From: CALiC0 Date: Tue, 10 Mar 2026 18:44:07 -0500 Subject: [PATCH] Create go backend for site SSR and Dockerfile for dockerization --- .dockerignore | 3 ++ Dockerfile | 17 ++++++++++ blog.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++ error_handling.go | 39 ++++++++++++++++++++++ go.mod | 10 ++++++ go.sum | 9 +++++ main.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++ markdown.go | 21 ++++++++++++ static.go | 71 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 336 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 blog.go create mode 100644 error_handling.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 markdown.go create mode 100644 static.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..53033a1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +bin/ +.git/ +*.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d3b315f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.26-alpine AS base + +WORKDIR /build + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY *.go . + +RUN go build -o calic0site + +EXPOSE 8080 + +COPY config /config + +CMD ["/build/calic0site", "-config=/config", "-content=/content", "-debug"] \ No newline at end of file diff --git a/blog.go b/blog.go new file mode 100644 index 0000000..a482957 --- /dev/null +++ b/blog.go @@ -0,0 +1,83 @@ +package main + +import ( + "bufio" + "errors" + "html/template" + "net/http" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/adrg/frontmatter" +) + +type Post struct { + markdownData []byte + Title string `toml:"title"` + PostDate time.Time `toml:"post_date"` + Summary string `toml:"summary"` + Body template.HTML + FileName string +} + +func postHandler(w http.ResponseWriter, r *http.Request) { + + if _, err := os.Stat(filepath.Join(contentDir, "posts", r.PathValue("post")+".md")); err == nil { + + } else if errors.Is(err, os.ErrNotExist) { + sendError(w, http.StatusNotFound, "That post could not be found.") + return + } else { + sendError(w, http.StatusInternalServerError, "There was a problem statting the file for that post: "+err.Error()) + return + } + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/post.gohtml"))) + f, err := os.Open(filepath.Join("posts", r.PathValue("post")+".md")) + if err != nil { + sendError(w, http.StatusInternalServerError, "There was a problem opening the file for that post: "+err.Error()) + return + } + var post Post + rest, err := frontmatter.Parse(bufio.NewReader(f), &post) + if err != nil { + sendError(w, http.StatusInternalServerError, "There was a problem reading the frontmatter for that post: "+err.Error()) + return + } + post.FileName = r.PathValue("post") + post.Body = template.HTML(mdToHTML(rest)) + var data PageData = PageData{ + Header: config.HeaderData, + Post: post, + } + tmpl.ExecuteTemplate(w, "layout", data) +} +func blogHandler(w http.ResponseWriter, r *http.Request) { + var data PageData = PageData{ + Header: config.HeaderData, + Posts: make([]Post, 0), + } + files, _ := os.ReadDir(filepath.Join(contentDir, "posts")) + for _, file := range files { + if strings.HasSuffix(file.Name(), ".md") { + f, err := os.Open(filepath.Join(contentDir, "posts", file.Name())) + if err != nil { + continue + } + var post Post + rest, err := frontmatter.Parse(bufio.NewReader(f), &post) + if err != nil { + continue + } + post.Body = template.HTML(mdToHTML(rest)) + post.FileName = strings.TrimSuffix(file.Name(), ".md") + data.Posts = append(data.Posts, post) + } + } + slices.Reverse(data.Posts) + + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/blog.gohtml"))) + tmpl.ExecuteTemplate(w, "layout", data) +} diff --git a/error_handling.go b/error_handling.go new file mode 100644 index 0000000..33297cb --- /dev/null +++ b/error_handling.go @@ -0,0 +1,39 @@ +package main + +import ( + "html/template" + "net/http" + "path/filepath" + "slices" +) + +var nonDebugErrors []int = []int{ + http.StatusNotFound, + http.StatusBadRequest, + http.StatusTeapot, + http.StatusUnauthorized, +} + +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + sendError(w, http.StatusNotFound, "That page could not be found.") +} + +func sendError(w http.ResponseWriter, code int, message string) { + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/error.gohtml"))) + var data PageData + if *debug || slices.Contains(nonDebugErrors, code) { + data = PageData{ + Header: config.HeaderData, + StatusCode: code, + StatusMessage: message, + } + } else { + data = PageData{ + Header: config.HeaderData, + StatusCode: 0, + StatusMessage: "An unknown error has occurred.", + } + } + w.WriteHeader(code) + tmpl.ExecuteTemplate(w, "layout", data) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6597b46 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.calico.tel/calic0/calic0.me + +go 1.24.4 + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/adrg/frontmatter v0.2.0 // indirect + github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0a3eb4 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= +github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a02d1ed --- /dev/null +++ b/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "flag" + "html/template" + "net/http" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +type HeaderData struct { + MarqueeItems []MarqueeItem `toml:"marquee_items"` + Links Links `toml:"links"` +} +type MarqueeItem struct { + Text string `toml:"text"` + URL template.URL `toml:"url"` +} +type Links struct { + TopLevel []Link `toml:"top_level"` + Categories []LinkCategory `toml:"categories"` +} +type Link struct { + Name string `toml:"name"` + URL template.URL `toml:"url"` +} +type LinkCategory struct { + Name string `toml:"name"` + MainLink string `toml:"main_link"` + Links []Link `toml:"links"` +} + +type Config struct { + HeaderData HeaderData `toml:"header_data"` +} + +type PageData struct { + Header HeaderData + Body template.HTML + Posts []Post + Post Post + StatusCode int + StatusMessage string +} + +var debug *bool +var configDir string +var contentDir string + +var config Config + +func main() { + debug = flag.Bool("debug", false, "Run in debug mode") + var httpAddress string + flag.StringVar(&httpAddress, "address", ":8080", "HTTP address to listen on") + flag.StringVar(&configDir, "config", "", "The directory containing the server config") + flag.StringVar(&contentDir, "content", "", "The directory containing content") + flag.Parse() + + _, err := toml.DecodeFile(filepath.Join(configDir, "config.toml"), &config) + if err != nil { + panic(err) + } + + mux := &http.ServeMux{} + + mux.HandleFunc("/", notFoundHandler) + mux.HandleFunc("/{$}", homeHandler) + mux.HandleFunc("/page/{page}", staticPageHandler) + mux.HandleFunc("/page/{category}/{page}", staticCategoryHandler) + mux.HandleFunc("/blog/{post}", postHandler) + mux.HandleFunc("/blog", blogHandler) + fs := http.FileServer(http.Dir(filepath.Join(contentDir, "static"))) + mux.Handle("/static/", http.StripPrefix("/static/", fs)) + mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, filepath.Join(contentDir, "/static/favicon.ico")) + }) + err = http.ListenAndServe(httpAddress, mux) + if err != nil { + panic(err) + } +} diff --git a/markdown.go b/markdown.go new file mode 100644 index 0000000..0170214 --- /dev/null +++ b/markdown.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func mdToHTML(md []byte) []byte { + // create markdown parser with extensions + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +} diff --git a/static.go b/static.go new file mode 100644 index 0000000..9521666 --- /dev/null +++ b/static.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "fmt" + "html/template" + "net/http" + "os" + "path/filepath" +) + +func homeHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/page.gohtml"))) + contents, err := os.ReadFile(filepath.Join(contentDir, "pages/home.md")) + if err != nil { + sendError(w, http.StatusInternalServerError, "There was a problem reading the data for that page") + return + } + contents_html := mdToHTML(contents) + data := PageData{ + Header: config.HeaderData, + Body: template.HTML(contents_html), + } + tmpl.ExecuteTemplate(w, "layout", data) +} +func staticPageHandler(w http.ResponseWriter, r *http.Request) { + if _, err := os.Stat(filepath.Join(contentDir, "pages", r.PathValue("page")+".md")); err == nil { + fmt.Println("Static Page Handler!", r.PathValue("page")) + + } else if errors.Is(err, os.ErrNotExist) { + sendError(w, http.StatusNotFound, "That page could not be found.") + return + } else { + sendError(w, http.StatusInternalServerError, "There was a problem statting the file for that page: "+err.Error()) + return + } + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/page.gohtml"))) + contents, err := os.ReadFile(filepath.Join(contentDir, "pages", r.PathValue("page")+".md")) + if err != nil { + sendError(w, http.StatusInternalServerError, "There was a problem reading the file for that page: "+err.Error()) + return + } + contents_html := mdToHTML(contents) + data := PageData{ + Header: config.HeaderData, + Body: template.HTML(contents_html), + } + tmpl.ExecuteTemplate(w, "layout", data) +} +func staticCategoryHandler(w http.ResponseWriter, r *http.Request) { + if _, err := os.Stat(filepath.Join(contentDir, "pages", r.PathValue("category"), r.PathValue("page")+".md")); err == nil { + } else if errors.Is(err, os.ErrNotExist) { + sendError(w, http.StatusNotFound, "That page could not be found.") + return + } else { + sendError(w, http.StatusInternalServerError, "There was a problem statting the file for that page: "+err.Error()) + return + } + tmpl := template.Must(template.ParseFiles(filepath.Join(contentDir, "templates/layout.gohtml"), filepath.Join(contentDir, "templates/page.gohtml"))) + contents, err := os.ReadFile(filepath.Join(contentDir, "pages", r.PathValue("category"), r.PathValue("page")+".md")) + if err != nil { + sendError(w, http.StatusInternalServerError, "There was a problem reading the file for that page: "+err.Error()) + return + } + contents_html := mdToHTML(contents) + data := PageData{ + Header: config.HeaderData, + Body: template.HTML(contents_html), + } + tmpl.ExecuteTemplate(w, "layout", data) +}