Create go backend for site SSR and Dockerfile for dockerization

This commit is contained in:
2026-03-10 18:44:07 -05:00
parent 92600c3159
commit b47820eeea
9 changed files with 336 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
bin/
.git/
*.env

17
Dockerfile Normal file
View File

@@ -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"]

83
blog.go Normal file
View File

@@ -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)
}

39
error_handling.go Normal file
View File

@@ -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)
}

10
go.mod Normal file
View File

@@ -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
)

9
go.sum Normal file
View File

@@ -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=

83
main.go Normal file
View File

@@ -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)
}
}

21
markdown.go Normal file
View File

@@ -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)
}

71
static.go Normal file
View File

@@ -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)
}