This commit is contained in:
2025-05-04 13:40:31 +02:00
parent afd8edaa6c
commit 92d272b36e
10 changed files with 293 additions and 0 deletions

19
Dockerfile Normal file
View 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
View 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, // jetztZeit bereitstellen
}
// Basislayout zuerst parsen
base := template.Must(
template.New("base").Funcs(funcs).
ParseFiles("internal/web/templates/base.html"),
)
// LISTSeite: base + list.html
tplList = template.Must(base.Clone())
template.Must(tplList.Funcs(funcs).ParseFiles("internal/web/templates/list.html"))
// ARTICLEInstanz
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
View File

@@ -0,0 +1,5 @@
<!-- { "title": "Mein erster MarkdownPost", "date": "20250504", "slug": "mein-erster-markdown-post" } -->
# Hello Markdown 🌟
Dies ist *kursiv*, **fett** und ein [Link](https://example.com).

5
go.mod Normal file
View 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
View 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
View 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 ExtensionMaske
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("FrontMatter 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
View 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
}

View 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 }}

View 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">
<!-- MonospaceFont 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>

View File

@@ -0,0 +1,16 @@
{{ define "list" }} {{/* ausführbarer EntryPoint */}}
{{ 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 }}