Passt so
This commit is contained in:
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# --- Build step ---
|
||||||
|
FROM golang:1.22 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -o /blog ./cmd/blog
|
||||||
|
|
||||||
|
# --- Runtime + optional MySQL client libraries ---
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y default-mysql-client ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /blog /usr/local/bin/blog
|
||||||
|
COPY content/ /content
|
||||||
|
COPY internal/web/static/ /static
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV BLOG_CONTENT_DIR=/content
|
||||||
|
ENV BLOG_STATIC_DIR=/static
|
||||||
|
CMD ["blog"]
|
||||||
|
|
100
cmd/blog/main.go
Normal file
100
cmd/blog/main.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.send.nrw/sendnrw/b1tsblog/internal/article"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getenv(k, d string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func enabled(k string, def bool) bool {
|
||||||
|
b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k)))
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheControl(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tplList *template.Template
|
||||||
|
tplArticle *template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"now": time.Now, // jetzt‑Zeit bereitstellen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basislayout zuerst parsen
|
||||||
|
base := template.Must(
|
||||||
|
template.New("base").Funcs(funcs).
|
||||||
|
ParseFiles("internal/web/templates/base.html"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// LIST‑Seite: base + list.html
|
||||||
|
tplList = template.Must(base.Clone())
|
||||||
|
template.Must(tplList.Funcs(funcs).ParseFiles("internal/web/templates/list.html"))
|
||||||
|
|
||||||
|
// ARTICLE‑Instanz
|
||||||
|
tplArticle = template.Must(base.Clone())
|
||||||
|
template.Must(tplArticle.Funcs(funcs).ParseFiles("internal/web/templates/article.html"))
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
staticDir := "internal/web/static"
|
||||||
|
|
||||||
|
articles, err := article.LoadDir("content")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler für /
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tplList.ExecuteTemplate(w, "base", articles); err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/post/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slug := strings.TrimPrefix(r.URL.Path, "/post/")
|
||||||
|
for _, a := range articles {
|
||||||
|
if a.Slug == slug {
|
||||||
|
if err := tplArticle.ExecuteTemplate(w, "base", a); err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.Handle("/static/",
|
||||||
|
cacheControl(http.StripPrefix("/static/",
|
||||||
|
http.FileServer(http.Dir(staticDir)))))
|
||||||
|
|
||||||
|
http.ListenAndServe(":8080", mux)
|
||||||
|
}
|
5
content/2025/test.md
Normal file
5
content/2025/test.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- { "title": "Mein erster Markdown‑Post", "date": "2025‑05‑04", "slug": "mein-erster-markdown-post" } -->
|
||||||
|
|
||||||
|
# Hello Markdown 🌟
|
||||||
|
|
||||||
|
Dies ist *kursiv*, **fett** und ein [Link](https://example.com).
|
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.send.nrw/sendnrw/b1tsblog
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
97
internal/article/load.go
Normal file
97
internal/article/load.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// internal/article/load.go
|
||||||
|
package article
|
||||||
|
|
||||||
|
// internal/article/load.go (gekürzt)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
md "github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/html"
|
||||||
|
"github.com/gomarkdown/markdown/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadDir(root string) ([]Article, error) {
|
||||||
|
var out []Article
|
||||||
|
|
||||||
|
// gültige Extension‑Maske
|
||||||
|
exts := parser.CommonExtensions | parser.AutoHeadingIDs | parser.DefinitionLists
|
||||||
|
mdParser := parser.NewWithExtensions(exts)
|
||||||
|
mdRenderer := html.NewRenderer(html.RendererOptions{
|
||||||
|
Flags: html.CommonFlags | html.HrefTargetBlank,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(p)
|
||||||
|
if ext != ".html" && ext != ".md" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
r := bufio.NewReader(f)
|
||||||
|
headerLine, err := r.ReadBytes('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
}
|
||||||
|
clean := strings.TrimSpace(string(headerLine))
|
||||||
|
|
||||||
|
clean = strings.TrimPrefix(clean, "<!--")
|
||||||
|
clean = strings.TrimSpace(clean)
|
||||||
|
|
||||||
|
// alles nach dem ersten "-->" abschneiden (egal ob Leerzeichen davor)
|
||||||
|
if idx := strings.Index(clean, "-->"); idx != -1 {
|
||||||
|
clean = clean[:idx]
|
||||||
|
}
|
||||||
|
clean = strings.TrimSpace(clean)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(clean), &meta); err != nil {
|
||||||
|
return fmt.Errorf("Front‑Matter in %s: %w (%q)", p, err, clean)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody := bodyBytes
|
||||||
|
if ext == ".md" {
|
||||||
|
htmlBody = md.ToHTML(bodyBytes, mdParser, mdRenderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
date, _ := time.Parse("2006-01-02", meta.Date)
|
||||||
|
out = append(out, Article{
|
||||||
|
Title: meta.Title,
|
||||||
|
Slug: meta.Slug,
|
||||||
|
Date: date,
|
||||||
|
Body: template.HTML(htmlBody),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Date.After(out[j].Date) })
|
||||||
|
return out, err
|
||||||
|
}
|
14
internal/article/model.go
Normal file
14
internal/article/model.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// internal/article/model.go
|
||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Article struct {
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
|
Date time.Time
|
||||||
|
Body template.HTML // already trusted
|
||||||
|
}
|
20
internal/web/templates/article.html
Normal file
20
internal/web/templates/article.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{{ define "article" }}
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "title" }}{{ .Title }} – B1tsblog{{ end }}
|
||||||
|
|
||||||
|
{{ define "body" }}
|
||||||
|
<article class="article">
|
||||||
|
<header>
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="article-content">
|
||||||
|
{{ .Body }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/">← Alle Artikel</a></p>
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
15
internal/web/templates/base.html
Normal file
15
internal/web/templates/base.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html><html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{{ block "title" . }}B1tsblog{{ end }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/main.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<!-- Monospace‑Font für Code (optional) -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><h1><a href="/">B1tsblog</a></h1></header>
|
||||||
|
<main>{{ block "body" . }}{{ end }}</main>
|
||||||
|
<footer>© {{ now.Year }}</footer>
|
||||||
|
</body></html>
|
16
internal/web/templates/list.html
Normal file
16
internal/web/templates/list.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{{ define "list" }} {{/* ‑‑ ausführbarer Entry‑Point */}}
|
||||||
|
{{ template "base.html" . }} {{/* ruft das Layout auf */}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "title" }}Alle Artikel{{ end }}
|
||||||
|
|
||||||
|
{{ define "body" }}
|
||||||
|
<ul>
|
||||||
|
{{ range . }}
|
||||||
|
<li>
|
||||||
|
<a href="/post/{{ .Slug }}">{{ .Title }}</a>
|
||||||
|
— {{ .Date.Format "02.01.2006" }}
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
Reference in New Issue
Block a user