diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9593caf
--- /dev/null
+++ b/Dockerfile
@@ -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"]
+
\ No newline at end of file
diff --git a/cmd/blog/main.go b/cmd/blog/main.go
new file mode 100644
index 0000000..0167ed5
--- /dev/null
+++ b/cmd/blog/main.go
@@ -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)
+}
diff --git a/content/2025/test.md b/content/2025/test.md
new file mode 100644
index 0000000..6d23997
--- /dev/null
+++ b/content/2025/test.md
@@ -0,0 +1,5 @@
+
+
+# Hello Markdown 🌟
+
+Dies ist *kursiv*, **fett** und ein [Link](https://example.com).
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..7eb502e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module git.send.nrw/sendnrw/b1tsblog
+
+go 1.23.1
+
+require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..148941c
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/internal/article/load.go b/internal/article/load.go
new file mode 100644
index 0000000..f4a24d5
--- /dev/null
+++ b/internal/article/load.go
@@ -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, "" 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
+}
diff --git a/internal/article/model.go b/internal/article/model.go
new file mode 100644
index 0000000..b11a434
--- /dev/null
+++ b/internal/article/model.go
@@ -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
+}
diff --git a/internal/web/templates/article.html b/internal/web/templates/article.html
new file mode 100644
index 0000000..b0834e2
--- /dev/null
+++ b/internal/web/templates/article.html
@@ -0,0 +1,20 @@
+{{ define "article" }}
+ {{ template "base.html" . }}
+{{ end }}
+
+{{ define "title" }}{{ .Title }} – B1tsblog{{ end }}
+
+{{ define "body" }}
+{{ .Title }}
+
+