Create go backend for site SSR and Dockerfile for dockerization
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
bin/
|
||||||
|
.git/
|
||||||
|
*.env
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal 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
83
blog.go
Normal 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
39
error_handling.go
Normal 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
10
go.mod
Normal 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
9
go.sum
Normal 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
83
main.go
Normal 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
21
markdown.go
Normal 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
71
static.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user