Compare commits

...

77 Commits

Author SHA1 Message Date
a38c883450 Update to Go-1.25.3
All checks were successful
release-tag / release-image (push) Successful in 2m56s
2025-10-21 07:22:05 +02:00
2f01625cff Try to fix loading issue on some templates.
All checks were successful
release-tag / release-image (push) Successful in 2m39s
2025-05-18 22:29:10 +02:00
a78eab43d9 added store handler
All checks were successful
release-tag / release-image (push) Successful in 2m25s
2025-05-18 22:05:44 +02:00
7ef6b551ac Bugfix Einlesen der Artikel vor Erstellen der Ressourcen
All checks were successful
release-tag / release-image (push) Successful in 2m24s
2025-05-11 17:10:56 +02:00
dce90ca88b bugfix
All checks were successful
release-tag / release-image (push) Successful in 2m20s
2025-05-11 16:56:08 +02:00
7493ca278e fix
All checks were successful
release-tag / release-image (push) Successful in 2m21s
2025-05-11 16:39:14 +02:00
445db0ec10 fix
All checks were successful
release-tag / release-image (push) Successful in 2m21s
2025-05-11 16:29:16 +02:00
87493fe40c removeall fix
All checks were successful
release-tag / release-image (push) Successful in 2m17s
2025-05-11 16:22:48 +02:00
ca22dd8596 Test für Git AutoClone
All checks were successful
release-tag / release-image (push) Successful in 2m14s
2025-05-11 16:16:52 +02:00
4c41bb8838 Anpassung Datenschutzerklärung + ReleaseCandidate
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-05-11 12:02:29 +02:00
98de9b1b98 Potentieller Persistenz-Fix
All checks were successful
release-tag / release-image (push) Successful in 2m10s
2025-05-11 11:49:10 +02:00
621db424f0 Fix für Boot-Loop
All checks were successful
release-tag / release-image (push) Successful in 2m12s
2025-05-11 11:34:48 +02:00
7e21b10a6c Persistenz implementiert + Anpassungen
All checks were successful
release-tag / release-image (push) Successful in 2m7s
2025-05-11 11:30:52 +02:00
8cd66c9f59 Anpassungen Counter-Design
All checks were successful
release-tag / release-image (push) Successful in 2m5s
2025-05-11 11:17:45 +02:00
96894516a7 Fix
All checks were successful
release-tag / release-image (push) Successful in 2m9s
2025-05-11 11:11:50 +02:00
2e31bfdc77 Card-Fix für Counter und Berechnungs-Fix
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-05-11 11:07:25 +02:00
08ef9c7132 Implementierung eines anonymen Counters - Nicht persistent
All checks were successful
release-tag / release-image (push) Successful in 2m9s
2025-05-11 10:57:36 +02:00
847d860144 Patch
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-05-10 22:26:03 +02:00
8b7f1127f8 Fix
All checks were successful
release-tag / release-image (push) Successful in 2m14s
2025-05-10 22:19:11 +02:00
278d3aa48f Invictus 2955 - Das Jahr der Idris-P?
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-05-10 22:10:09 +02:00
9dca6a57df Bugfix
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-05-10 19:17:30 +02:00
12ce629b31 Anpassung für dynamische Content-Gits
Some checks failed
release-tag / release-image (push) Failing after 43s
2025-05-10 19:13:29 +02:00
d7726b57e4 Windows Server 2025 Domänen-Controller legt Netzwerkprofil auf Öffentlich fest
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-10 14:54:13 +02:00
c25c60ab27 README.md aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-08 06:33:34 +00:00
7ee0c9e9f7 TLS-Zertifikat mit SHA3 auf Windows Server 2016 einspielen
All checks were successful
release-tag / release-image (push) Successful in 1m54s
2025-05-08 06:45:56 +02:00
aba08de902 Source-Patch
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-07 22:28:23 +02:00
4764d1536c Content-Patch
All checks were successful
release-tag / release-image (push) Successful in 1m48s
2025-05-07 22:18:02 +02:00
3357061c79 Warum personenbezogene Daten nie in die Betreffzeile einer E‑Mail gehören
All checks were successful
release-tag / release-image (push) Successful in 1m50s
2025-05-07 21:27:53 +02:00
a83e360317 Update Datenschutz und Impressum Telefonnummer
All checks were successful
release-tag / release-image (push) Successful in 1m55s
2025-05-07 18:07:49 +02:00
7b598a04be Patch
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-05-06 23:34:50 +02:00
8554e13ebf Patch KI-Nutzung und Umsetzung AI-Act
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-05-06 23:25:28 +02:00
f6806c5ad0 minor fix
All checks were successful
release-tag / release-image (push) Successful in 1m56s
2025-05-06 22:17:37 +02:00
c0f985982e Anpassung Datenschutz und Nav-Links
All checks were successful
release-tag / release-image (push) Successful in 1m53s
2025-05-06 22:10:58 +02:00
17424d399d Warum meldet der Client, dass die Default Domain Policy nicht gelesen werden kann
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-05-06 19:58:20 +02:00
2e5f9b6fa5 Update Entfernen der Footer-Bilder auf grund von Design 2025-05-06 16:54:42 +02:00
cd869b610e Anpassung base html Klasse footer
All checks were successful
release-tag / release-image (push) Successful in 1m55s
2025-05-06 15:58:43 +02:00
7873cbe132 Anpassungen
All checks were successful
release-tag / release-image (push) Successful in 1m55s
2025-05-06 15:54:10 +02:00
8d77eb94e8 Contentpatch 20250506
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-06 14:42:51 +02:00
0fb3924382 Logo entfernt + Links design angepasst
All checks were successful
release-tag / release-image (push) Successful in 1m56s
2025-05-06 12:16:18 +02:00
b2dc65e024 Header für Logo angepast
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-06 11:27:29 +02:00
1b40a248d2 Base HTML auf Logo + Alt-Text angepasst
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-05-06 11:20:55 +02:00
d912799a9e Logo Anpassungen
All checks were successful
release-tag / release-image (push) Successful in 1m51s
2025-05-06 11:16:53 +02:00
477539928c Anpassung Logo + Content
All checks were successful
release-tag / release-image (push) Successful in 1m50s
2025-05-06 11:11:28 +02:00
d4840c936a added staging workflow
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-05-06 10:39:27 +02:00
f534182c20 Open Source als Innovationsmotor: Warum Unternehmen auf offene Software setzen
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-05-05 22:58:04 +02:00
16d2548976 Neue Blogserie: Open Source im Unternehmen
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-05-05 22:37:31 +02:00
6017bd2cd3 CSS für BITV angepasst
All checks were successful
release-tag / release-image (push) Successful in 1m58s
2025-05-05 21:46:31 +02:00
e266c9c78f Alte Ressourcen entfernt, Favicon gesetzt
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-05-05 21:37:29 +02:00
229f78a7ee Navigation "zurück" angepasst + Webp-Ressourcen angepasst 2025-05-05 21:34:02 +02:00
c88cf623f0 Meta Description Bugfix #2
All checks were successful
release-tag / release-image (push) Successful in 2m6s
2025-05-05 21:21:57 +02:00
b1536ad724 Meta Description Bugfix #1
All checks were successful
release-tag / release-image (push) Successful in 2m9s
2025-05-05 21:08:06 +02:00
38fa612a18 Test Meta Description
All checks were successful
release-tag / release-image (push) Successful in 2m22s
2025-05-05 20:58:17 +02:00
10172d1fdc optimized for webp
All checks were successful
release-tag / release-image (push) Successful in 2m7s
2025-05-05 17:09:53 +02:00
eeb2709330 README.md aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-05-05 11:48:11 +00:00
0773b2a8e1 README.md aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m10s
2025-05-05 09:33:21 +00:00
fb26c8af92 README.md aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-05-05 07:23:49 +00:00
c54b4e652a Neue Sortierung und Welcome + Datenschutz
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-05-04 22:58:46 +02:00
73c068c84f MySQL auf Linux installieren + Footer
All checks were successful
release-tag / release-image (push) Successful in 2m10s
2025-05-04 22:36:11 +02:00
962d3fdd02 PHPMyAdmin mit Serverauswahl im Homelab mittels Docker bereitstellen
All checks were successful
release-tag / release-image (push) Successful in 2m5s
Windows Fehler 0x80072F8F - Installation optionaler Features schlägt fehl
2025-05-04 22:15:32 +02:00
a179c7e51a In Ubuntu den Port 53 - DNS - selber nutzen
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-05-04 21:53:51 +02:00
8ca50097fd CSS für Code-Element aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m4s
2025-05-04 21:44:15 +02:00
f746cea41a CSS für Code-Elemet aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m1s
2025-05-04 21:06:18 +02:00
7e85585d6c Ein eigenes Docker-Image erstellen - so geht's
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-05-04 21:00:06 +02:00
89f7d21483 Der eigene DNS-Server im Homelab
All checks were successful
release-tag / release-image (push) Successful in 2m14s
2025-05-04 20:36:00 +02:00
b5a0c1d64e Content-Patch
All checks were successful
release-tag / release-image (push) Successful in 2m3s
2025-05-04 20:19:43 +02:00
e4b060b753 Fonts auf lokal geändert
All checks were successful
release-tag / release-image (push) Successful in 1m59s
2025-05-04 20:01:26 +02:00
2cdc782395 Content-Patch
All checks were successful
release-tag / release-image (push) Successful in 2m5s
2025-05-04 18:45:46 +02:00
b83eff82a0 Testfix for loop restart #3
All checks were successful
release-tag / release-image (push) Successful in 1m58s
2025-05-04 18:39:54 +02:00
1d09537ff0 Testfix for loop restart #2
All checks were successful
release-tag / release-image (push) Successful in 1m50s
2025-05-04 18:19:20 +02:00
efac316b4c Testfix for loop restart #1
All checks were successful
release-tag / release-image (push) Successful in 1m54s
2025-05-04 18:13:27 +02:00
128aa11273 Fix GoVersion to 1.23.1
All checks were successful
release-tag / release-image (push) Successful in 2m3s
2025-05-04 18:04:14 +02:00
dfe7d03445 Fix Dockerfile src
Some checks failed
release-tag / release-image (push) Failing after 1m18s
2025-05-04 18:01:07 +02:00
b0eea7ae62 Workflow hinzugefügt
Some checks failed
release-tag / release-image (push) Failing after 47s
2025-05-04 17:58:21 +02:00
8f7a0b359c Anpassung auf dynamisches GIT 2025-05-04 17:56:46 +02:00
a1471fc310 finale Anpassung 2025-05-04 17:10:38 +02:00
eb2d05f082 Läuft 2025-05-04 14:16:29 +02:00
92d272b36e Passt so 2025-05-04 13:40:31 +02:00
20 changed files with 1196 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
name: release-tag
on:
push:
branches:
- 'main'
jobs:
release-image:
runs-on: ubuntu-fast
env:
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
DOCKER_LATEST: latest
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with: # replace it with your local IP
config-inline: |
[registry."${{ vars.DOCKER_REGISTRY }}"]
http = true
insecure = true
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
build-args: |
CONTENT_REPO=https://git.send.nrw/b1tsblog/blogcontent.git
CONTENT_REF=main
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
- name: Build and push StarCitizen
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
build-args: |
CONTENT_REPO=https://git.send.nrw/b1tsblog/sccontent.git
CONTENT_REF=main
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:sc-${{ env.DOCKER_LATEST }}

View File

@@ -0,0 +1,64 @@
name: release-tag
on:
push:
branches:
- 'staging'
jobs:
release-image:
runs-on: ubuntu-fast
env:
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
DOCKER_LATEST: staging
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with: # replace it with your local IP
config-inline: |
[registry."${{ vars.DOCKER_REGISTRY }}"]
http = true
insecure = true
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
- name: Build and push StarCitizen
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
build-args: |
CONTENT_REPO=https://git.send.nrw/b1tsblog/sccontent.git
CONTENT_REF=main
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:sc-${{ env.DOCKER_LATEST }}

70
Dockerfile Normal file
View File

@@ -0,0 +1,70 @@
############################
# 1) GoBuild
############################
FROM golang:1.25 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /blog ./cmd/blog
############################
# 2) ContentClone (Stage)
############################
FROM alpine/git AS content
# Parameterisierbar beim docker build --build-arg …
ARG CONTENT_REPO=https://git.send.nrw/b1tsblog/blogcontent.git
ARG CONTENT_REF=main
RUN git clone --depth 1 --branch ${CONTENT_REF} ${CONTENT_REPO} /src
# ─── Repack: bring alles in eine saubere Struktur ────────────────
# • MarkdownPosts: /articles/*.md → /out/content
# • Bilder + CSS + JS: /static/**/* → /out/static
# • statische Seiten: /pages/* → /out/pages
# (PfadAnpassung hier nach DEINEM RepositoryLayout)
RUN mkdir -p /out/content /out/static /out/pages /out/templates/
RUN find /src/articles -name '*.md' -exec cp {} /out/content/ \;
RUN cp -r /src/static/* /out/static/
RUN cp -r /src/pages/* /out/pages/
RUN cp -r /src/templates/* /out/templates/
############################
# 3) RuntimeImage
############################
FROM debian:bookworm-slim
# (optional) MySQLClient für später
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# ─── Binärdatei ─────
COPY --from=build /blog /usr/local/bin/blog
# ─── Content + Assets ───
RUN mkdir -p /content /static /pages /app /templates /ticks
COPY . /app
COPY --from=content /out/content /content
COPY --from=content /out/static /static
COPY --from=content /out/pages /pages
COPY --from=content /out/templates /templates
ENV BLOG_CONTENT_DIR=/content
ENV BLOG_STATIC_DIR=/static
ENV BLOG_PAGES_DIR=/pages
ENV BLOG_TEMPLATES_DIR=/templates
ENV BLOG_TICKS_DIR=/ticks
ENV GIT_ENABLE=false
ENV GIT_REPO=null
ENV GIT_BRANCH=main
ENV GIT_DIR=/git-temp
ENV GIT_INTERVAL=10
EXPOSE 8080
CMD ["blog"]

41
Dockerfile_Slim Normal file
View File

@@ -0,0 +1,41 @@
############################
# 1) GoBuild
############################
FROM golang:1.23.1 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /blog ./cmd/blog
############################
# 3) RuntimeImage
############################
FROM debian:bookworm-slim
# (optional) MySQLClient für später
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# ─── Binärdatei ─────
COPY --from=build /blog /usr/local/bin/blog
# ─── Content + Assets ───
RUN mkdir -p /content /static /pages /app /templates /ticks /git-temp
COPY . /app
ENV BLOG_CONTENT_DIR=/content
ENV BLOG_STATIC_DIR=/static
ENV BLOG_PAGES_DIR=/pages
ENV BLOG_TEMPLATES_DIR=/templates
ENV BLOG_TICKS_DIR=/ticks
ENV GIT_ENABLE=false
ENV GIT_REPO=null
ENV GIT_BRANCH=main
ENV GIT_DIR=/git-temp
ENV GIT_INTERVAL=10
EXPOSE 8080
CMD ["blog"]

View File

@@ -1,2 +1,25 @@
# b1tsblog
## B1tsBlog
- Anpassung Datenschutzerklärung
- Windows Server 2025 Domänen-Controller legt Netzwerkprofil auf Öffentlich fest
- TLS-Zertifikat mit SHA3 auf Windows Server 2016 einspielen
- Warum personenbezogene Daten nie in die Betreffzeile einer E-Mail gehören
- Warum meldet der Client, dass die Default Domain Policy nicht gelesen werden kann
- Open Source als Innovationsmotor: Warum Unternehmen auf offene Software setzen
- Neue Blogserie: Open Source im Unternehmen
- Optimiert für schnelleres Laden mit webp-Inhalten
- docker-swarm-mit-abweichendem-port-einrichten
- Content-Update 2025-05-05
- PHPMyAdmin mit Serverauswahl im Homelab mittels Docker bereitstellen
- Windows Fehler 0x80072F8F - Installation optionaler Features schlägt fehl
- In Ubuntu den Port 53 - DNS - selber nutzen
- Ein eigenes Docker-Image erstellen - so geht's
- Der eigene DNS-Server im Homelab
- Content-Update 2025-05-04
## B1ts Star Citizen Blog
- Anpassung Datenschutzerklärung
- Invictus 2955 - Das Jahr der Idris-P?

424
cmd/blog/main.go Normal file
View File

@@ -0,0 +1,424 @@
package main
import (
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"git.send.nrw/sendnrw/b1tsblog/internal/article"
)
var TickCatalog []TickEntry
func SaveTickCatalog(filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(TickCatalog)
}
func LoadTickCatalog(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
return json.NewDecoder(file).Decode(&TickCatalog)
}
func IncTick(xSlug string) error {
for a, b := range TickCatalog {
if b.Slug == xSlug {
TickCatalog[a].Count = TickCatalog[a].Count + 1
return nil
}
}
newEntry := TickEntry{Slug: xSlug, Count: 1}
TickCatalog = append(TickCatalog, newEntry)
return fmt.Errorf("")
}
func getTick(xSlug string) string {
for _, b := range TickCatalog {
if b.Slug == xSlug {
var n int64 = b.Count
return strconv.FormatInt(n, 10)
}
}
return "0"
}
type TickEntry struct {
Slug string `json:"slug"`
Count int64 `json:"count"`
}
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
)
var Xarticles []article.Article
func main() {
// Signal-Kanal einrichten
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
// Goroutine, die auf Signale wartet
go func() {
<-stop
fmt.Println("Stop Sign...")
prepareExit()
os.Exit(0)
}()
// --- Verzeichnisse konfigurierbar machen -------------------------
contentDir := getenv("BLOG_CONTENT_DIR", "/content")
staticDir := getenv("BLOG_STATIC_DIR", "/app/internal/web/static")
pagesDir := getenv("BLOG_PAGES_DIR", "/pages")
templatesDir := getenv("BLOG_TEMPLATES_DIR", "/templates")
storeEnabled := enabled("STORE_ENABLE", false)
ticksDir := getenv("BLOG_TICKS_DIR", "/ticks")
gitEnable := enabled("GIT_ENABLE", false)
gitRepo := getenv("GIT_REPO", "null")
gitBranch := getenv("GIT_BRANCH", "main")
gitDir := getenv("GIT_DIR", "/git-temp")
gitInterval := getenv("GIT_INTERVAL", "10")
TickCatalog = nil
if err := LoadTickCatalog(ticksDir + "/ticks.json"); err != nil {
fmt.Println("Fehler beim Laden:", err)
}
fmt.Println("Geladener Katalog:", TickCatalog)
cloneRepo(gitRepo, gitBranch, gitDir)
funcs := template.FuncMap{
"now": time.Now, // jetztZeit bereitstellen
}
// Basislayout zuerst parsen
layout := template.Must(template.New("base").Funcs(funcs).ParseFiles(templatesDir + "/base.html"))
var tplStore *template.Template
if storeEnabled {
tplStore = template.Must(template.New("store").Funcs(funcs).ParseFiles(templatesDir + "/store.html"))
}
// LISTSeite: base + list.html
tplList = template.Must(layout.Clone())
template.Must(tplList.Funcs(funcs).ParseFiles(templatesDir + "/list.html"))
// ARTICLEInstanz
tplArticle = template.Must(layout.Clone())
template.Must(tplArticle.Funcs(funcs).ParseFiles(templatesDir + "/article.html"))
tplPage := template.Must(layout.Clone())
template.Must(tplPage.ParseFiles(templatesDir + "/page.html"))
mux := http.NewServeMux()
articles, err := article.LoadDir(contentDir)
if err != nil {
fmt.Println(err)
}
Xarticles = articles
staticPages, err := article.LoadStatic(pagesDir)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(staticPages)
}
// Handler für /
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
/* */
for a, b := range Xarticles {
Xarticles[a].Counter = getTick(b.Slug)
}
/* */
if err := tplList.ExecuteTemplate(w, "layout", article.ListPage{
Title: "Startseite",
Description: "Alle Artikel im Überblick",
Articles: Xarticles,
}); 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 Xarticles {
if a.Slug == slug {
IncTick(slug)
t := getTick(slug)
a.Counter = t
if err := tplArticle.ExecuteTemplate(w, "layout", a); err != nil {
http.Error(w, err.Error(), 500)
}
return
}
}
http.NotFound(w, r)
})
mux.HandleFunc("/page/", func(w http.ResponseWriter, r *http.Request) {
slug := strings.TrimPrefix(r.URL.Path, "/page/")
p, ok := staticPages[slug]
if !ok {
http.NotFound(w, r)
return
}
// "layout" kommt aus deinem TemplatePool (list/article nutzen es ja auch)
if err := tplPage.ExecuteTemplate(w, "page", p); err != nil {
http.Error(w, err.Error(), 500)
}
})
mux.Handle("/static/", cacheControl(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))))
mux.HandleFunc("/store", func(w http.ResponseWriter, r *http.Request) {
if storeEnabled {
if err := tplStore.ExecuteTemplate(w, "store", nil); err != nil {
http.Error(w, err.Error(), 500)
}
} else {
http.NotFound(w, r)
}
})
if gitEnable {
xMinute, _ := strconv.Atoi(gitInterval)
xDuration := time.Duration(xMinute) * time.Minute
go startAutoClone(gitRepo, gitBranch, gitDir, xDuration)
}
StopServer(http.ListenAndServe(":8080", mux))
}
func prepareExit() {
fmt.Println("~", "Running exit tasks...")
if err := SaveTickCatalog(getenv("BLOG_TICKS_DIR", "/ticks") + "/ticks.json"); err != nil {
fmt.Println("Fehler beim Speichern:", err)
}
fmt.Println("Geladener Katalog:", TickCatalog)
fmt.Println("~", "Exit completed.")
}
func StopServer(e error) {
fmt.Println("~", "Stopping server...")
prepareExit()
fmt.Println("~", "Server stopped!")
}
func cloneRepo(repoURL, branch, dir string) {
fmt.Printf("Starte Klonvorgang für Branch '%s'...\n", branch)
// Verzeichnis löschen
if err := os.RemoveAll(dir); err != nil {
fmt.Println("Fehler beim Löschen des Verzeichnisses:", err)
return
}
// Git-Clone mit Branch
cmd := exec.Command("git", "clone", "--branch", branch, "--single-branch", repoURL, dir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Fehler beim Klonen:", err)
} else {
fmt.Println("Repo erfolgreich geklont.")
}
contentDir := getenv("BLOG_CONTENT_DIR", "/content")
err := os.RemoveAll("/content")
if err != nil {
fmt.Println(err, "/content")
}
err = os.RemoveAll("/static")
if err != nil {
fmt.Println(err, "/static")
}
err = os.RemoveAll("/pages")
if err != nil {
fmt.Println(err, "/pages")
}
err = os.RemoveAll("/templates")
if err != nil {
fmt.Println(err, "/templates")
}
if err := os.MkdirAll("/content", 0755); err != nil {
fmt.Println("Fehler beim Erstellen des Zielordners:", err)
return
}
if err := os.MkdirAll("/static", 0755); err != nil {
fmt.Println("Fehler beim Erstellen des Zielordners:", err)
return
}
if err := os.MkdirAll("/pages", 0755); err != nil {
fmt.Println("Fehler beim Erstellen des Zielordners:", err)
return
}
if err := os.MkdirAll("/templates", 0755); err != nil {
fmt.Println("Fehler beim Erstellen des Zielordners:", err)
return
}
if err := copyDirContents("/git-temp/articles", "/content"); err != nil {
fmt.Println("Fehler beim Kopieren:", err)
} else {
fmt.Println("Kopieren abgeschlossen.")
}
if err := copyDirContents("/git-temp/static", "/static"); err != nil {
fmt.Println("Fehler beim Kopieren:", err)
} else {
fmt.Println("Kopieren abgeschlossen.")
}
if err := copyDirContents("/git-temp/pages", "/pages"); err != nil {
fmt.Println("Fehler beim Kopieren:", err)
} else {
fmt.Println("Kopieren abgeschlossen.")
}
if err := copyDirContents("/git-temp/templates", "/templates"); err != nil {
fmt.Println("Fehler beim Kopieren:", err)
} else {
fmt.Println("Kopieren abgeschlossen.")
}
articles, err := article.LoadDir(contentDir)
if err != nil {
fmt.Println(err)
}
Xarticles = articles
}
func copyDirContents(srcDir, destDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return fmt.Errorf("Fehler beim Lesen von %s: %w", srcDir, err)
}
for _, entry := range entries {
srcPath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name())
info, err := entry.Info()
if err != nil {
return err
}
if info.IsDir() {
if err := os.MkdirAll(destPath, info.Mode()); err != nil {
return fmt.Errorf("Fehler beim Erstellen von Ordner %s: %w", destPath, err)
}
// rekursiv kopieren
if err := copyDirContents(srcPath, destPath); err != nil {
return err
}
} else {
if err := copyFile(srcPath, destPath, info); err != nil {
return err
}
}
}
return nil
}
func copyFile(src, dest string, info os.FileInfo) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return os.Chmod(dest, info.Mode())
}
func startAutoClone(repoURL, branch, dir string, interval time.Duration) {
go cloneRepo(repoURL, branch, dir) // sofortiger Start
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
go cloneRepo(repoURL, branch, dir)
}
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module git.send.nrw/sendnrw/b1tsblog
go 1.25.3
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=

199
internal/article/load.go Normal file
View File

@@ -0,0 +1,199 @@
// internal/article/load.go
package article
// internal/article/load.go (gekürzt)
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"time"
md "github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
)
// LoadDir liest alle *.html und *.md unter root/content,
// parst FrontMatter (JSONKommentar in der 1. Zeile) und liefert []Article.
func LoadDir(root string) ([]Article, error) {
var out []Article
seen := make(map[string]bool) // DuplikatsCheck
mdRenderer := html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
})
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if ext != ".md" && ext != ".html" {
return nil // uninteressante Datei
}
raw, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
parts := bytes.SplitN(raw, []byte("\n"), 2)
if len(parts) < 2 {
return fmt.Errorf("%s: missing frontmatter", path)
}
headerJSON, err := extractJSONHeader(parts[0])
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
var meta struct {
Title string `json:"title"`
Date string `json:"date"`
Slug string `json:"slug"`
Cover string `json:"cover"`
Description string `json:"description"`
}
if err := json.Unmarshal(headerJSON, &meta); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
// FallbackSlug aus Dateinamen
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(filepath.Base(path), ext)
}
if seen[meta.Slug] {
return fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
}
seen[meta.Slug] = true
body := parts[1]
if ext == ".md" {
body = md.ToHTML(body, nil, mdRenderer) // frischer Parser pro Call
}
date, err := time.Parse("2006-01-02", meta.Date)
if err != nil {
return fmt.Errorf("%s: parse date: %w", path, err)
}
out = append(out, Article{
Title: meta.Title,
Slug: meta.Slug,
Date: date,
Cover: meta.Cover,
Description: meta.Description,
Body: template.HTML(body),
})
return nil
})
if err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool { return out[i].Date.After(out[j].Date) })
return out, nil
}
// ---------- NEU ----------
type StaticPage struct {
Title string
Slug string
Description string
Body template.HTML
}
// helper.go einmal zentral verwenden
func extractJSONHeader(line []byte) ([]byte, error) {
s := strings.TrimSpace(string(line))
s = strings.TrimPrefix(s, "<!--")
// alles bis zum ersten "-->" abschneiden
if idx := strings.Index(s, "-->"); idx != -1 {
s = s[:idx]
}
s = strings.TrimSpace(s)
// optional: falls doch kein JSON, besserer Fehler
if !strings.HasPrefix(s, "{") {
return nil, fmt.Errorf("no JSON frontmatter found")
}
return []byte(s), nil
}
// LoadStatic liest alle .md/.htmlDateien unter dir und liefert sie als Map[slug]StaticPage.
func LoadStatic(dir string) (map[string]StaticPage, error) {
pages := make(map[string]StaticPage)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
ext := filepath.Ext(e.Name())
if ext != ".md" && ext != ".html" {
continue // unbekanntes Format
}
path := filepath.Join(dir, e.Name())
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %s: %w", path, err)
}
// FrontMatter (erste Zeile) herauslösen
parts := bytes.SplitN(raw, []byte("\n"), 2)
if len(parts) < 2 {
return nil, fmt.Errorf("%s: missing frontmatter", path)
}
headerJSON, err := extractJSONHeader(parts[0])
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
var meta struct {
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
}
if err := json.Unmarshal(headerJSON, &meta); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
// Fallback: Slug aus Dateinamen ableiten, falls im Header leer
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(e.Name(), ext)
}
if _, dup := pages[meta.Slug]; dup {
return nil, fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
}
body := parts[1]
if ext == ".md" {
body = md.ToHTML(body, nil, nil) // eigener Parser je Aufruf
}
pages[meta.Slug] = StaticPage{
Title: meta.Title,
Slug: meta.Slug,
Description: meta.Description,
Body: template.HTML(body),
}
}
return pages, nil
}

23
internal/article/model.go Normal file
View File

@@ -0,0 +1,23 @@
// internal/article/model.go
package article
import (
"html/template"
"time"
)
type Article struct {
Title string
Slug string
Date time.Time
Cover string
Body template.HTML
Description string
Counter string
}
type ListPage struct {
Title string
Description string
Articles []Article
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,182 @@
/* ---------- Local WebFonts ---------- */
@font-face {
font-family: "Fira Code";
src: url("/static/fonts/FiraCode-VariableFont.woff2") format("woff2");
font-weight: 400 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/static/fonts/Inter-VariableFont.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* ---------- Farbpalette ---------- */
:root {
/* Light theme */
--bg: #f5f7fa;
--bg-alt: #ffffff;
--card-bg: #ffffff;
--text: #000000;
--text-muted: #1f2933;
--accent: #2563eb; /* Indigo600 */
--accent-light: #3b82f6; /* Indigo500 */
--code-bg: #f1f5f9;
--code-border: #e2e8f0;
--radius: 0.75rem;
--gap: 2rem;
--shadow: 0 4px 16px rgba(0,0,0,.08);
font-size: 16px;
font-family: "Inter", system-ui, sans-serif;
color-scheme: light;
}
/* Dark mode (optional) */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1117;
--bg-alt: #161b22;
--card-bg: #161b22;
--text: #ffffff;
--text-muted: #e4e8ec;
--accent: #3b82f6;
--accent-light:#60a5fa;
--code-bg: #1e242c;
--code-border: #30363d;
--shadow: 0 4px 16px rgba(0,0,0,.32);
}
}
/* ---------- Grundlayout ---------- */
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
line-height: 1.65;
}
a {
color: var(--accent);
text-decoration: underline dotted;
transition: color .15s;
}
a:hover { color: var(--accent-light); }
a.no-underline {
text-decoration: none;
}
/* Container in der Mitte */
.wrapper {
max-width: 1200px;
margin: 0 auto;
padding: var(--gap) calc(var(--gap) / 1.5);
}
img.footer {
max-width: 150px;
max-height: 60px; /* oder ein anderer fixer Wert, z. B. 500px */
width: auto;
height: auto;
}
/* ---------- Kopf & Fuß ---------- */
header, footer {
background: var(--bg-alt);
box-shadow: var(--shadow);
}
header {
position: sticky; top: 0; z-index: 10;
display: flex; justify-content: space-between; align-items: center;
padding: 10px calc(var(--gap) / 1.2);
}
header h1 { margin: 0; font-size: 1.4rem; }
footer {
text-align: center;
padding: 2rem 1rem;
color: var(--text-muted);
font-size: .9rem;
margin-top: var(--gap);
}
/* ---------- KartenGrid ---------- */
.post-list {
display: grid;
gap: var(--gap);
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
list-style: none;
padding: 0;
margin: 0;
}
.card {
display: flex;
flex-direction: column;
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: transform .2s ease, box-shadow .2s ease;
}
.card:hover { transform: translateY(-6px); box-shadow: 0 6px 24px rgba(0,0,0,.1); }
.card img {
width: 100%; height: 180px; object-fit: cover;
}
.card-content { padding: 1rem 1.25rem 1.5rem; }
.card h2 { margin: .25rem 0 .5rem; font-size: 1.25rem; line-height: 1.3; }
.card time { color: var(--text-muted); font-size: .85rem; }
/* ---------- Artikel ---------- */
.hero {
width: 100%; height: 320px; object-fit: cover;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
article h1 { font-size: 2.2rem; margin: 1.2rem 0 .3rem; }
article time { color: var(--text-muted); font-size: .9rem; }
article img:not(.hero), article video {
max-width: 100%; height: auto; border-radius: var(--radius);
box-shadow: var(--shadow);
margin: 1rem 0;
}
article pre {
background: var(--code-bg);
border: 1px solid var(--code-border);
padding: 1rem 1.2rem;
border-radius: var(--radius);
overflow: auto;
font-family: "Fira Code", Consolas, monospace;
}
article blockquote {
border-left: 4px solid var(--accent);
padding: .5rem 1rem; margin: 1rem 0;
background: var(--code-bg);
color: var(--text-muted);
}
code {
background: var(--code-bg);
color: var(--text-muted);
}
.main-nav ul {
display: flex;
gap: 1.25rem;
list-style: none;
margin: 0;
padding: 0;
}
.main-nav a {
font-weight: 600;
color: var(--text);
}
.main-nav a:hover {
color: var(--accent);
}
/* ---------- Responsive ---------- */
@media (max-width: 640px) {
:root { font-size: 15px; }
.hero { height: 200px; }
header { flex-direction: column; gap: .5rem; }
}

View File

@@ -0,0 +1,20 @@
{{ define "title" }}{{ .Title }}  B1tsblog{{ end }}
{{ define "body" }}
<article>
{{ if .Cover }}
<img class="hero" src="{{ .Cover }}" alt="">
{{ end }}
<p><a class="no-underline" href="/">Zurück zur Übersicht</a></p>
<h1>{{ .Title }}</h1>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>
<div class="article-content">
{{ .Body }}
</div>
<p><a class="no-underline" href="/">Zurück zur Übersicht</a></p>
</article>
{{ end }}
{{ define "article" }}{{ template "layout" . }}{{ end }}

View File

@@ -0,0 +1,39 @@
{{ define "layout" }}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
{{ if .Description }}
<meta name="description" content="{{ .Description }}">
{{ end }}
<title>{{ block "title" . }}B1tsblog{{ end }}</title>
<link rel="icon" type="image/vnd.icon" href="/static/img/favicon.ico">
<link rel="stylesheet" href="/static/main.css">
</head>
<body>
<header>
<h1><a href="/" class="no-underline">B1tsblog</a></h1>
<nav class="main-nav">
<ul>
<li><a class="no-underline" href="/">Start</a></li>
<li><a class="no-underline" href="/page/welcome">Hallo</a></li>
<li><a class="no-underline" href="/page/ai">KI</a></li>
<li><a class="no-underline" href="/page/datenschutzerklaerung">Datenschutz</a></li>
<li><a class="no-underline" href="/page/impressum">Impressum</a></li>
</ul>
</nav>
</header>
<main class="wrapper">
{{ template "body" . }}
</main>
<footer class="wrapper">
© {{ now.Year }} · Jan Bergner / B1tsBlog
<hr>
Ich verzichte auf Cookies, affiliate Links, Tracking und die Einbindung von Drittanbieter-Diensten.
</footer>
</body>
</html>
{{ end }}

View File

@@ -0,0 +1,19 @@
{{ define "body" }}
<ul class="post-list">
{{ range .Articles }}
<li>
<a class="card no-underline" href="/post/{{ .Slug }}">
{{ if .Cover }}
<img src="{{ .Cover }}" alt="">
{{ else }}
<img src="/static/img/placeholder.webp" alt="">
{{ end }}
<div class="card-content">
<h2>{{ .Title }}</h2>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time> ({{ .Counter }})
</div>
</a>
</li>
{{ end }}
</ul>
{{ end }}

View File

@@ -0,0 +1,12 @@
{{ define "title" }}{{ .Title }}  B1tsblog{{ end }}
{{ define "body" }}
<article>
<p><a class="no-underline" href="/">Zurück</a></p>
<h1>{{ .Title }}</h1>
<div class="article-content">{{ .Body }}</div>
<p><a class="no-underline" href="/">Zurück</a></p>
</article>
{{ end }}
{{ define "page" }}{{ template "layout" . }}{{ end }}

6
pages/impressum.md Normal file
View File

@@ -0,0 +1,6 @@
<!--{"title": "Impressum", "slug": "impressum"}-->
Max Mustermann
Musterstraße1
12345 Musterstadt
EMail: max@example.com