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