finale Anpassung
This commit is contained in:
50
Dockerfile
50
Dockerfile
@@ -1,19 +1,55 @@
|
||||
# --- Build step ---
|
||||
############################
|
||||
# 1) Go‑Build
|
||||
############################
|
||||
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 ---
|
||||
|
||||
############################
|
||||
# 2) Content‑Clone (Stage)
|
||||
############################
|
||||
FROM alpine/git AS content
|
||||
|
||||
# Parameterisierbar beim docker build --build-arg …
|
||||
ARG CONTENT_REPO=https://github.com/username/blog-content.git
|
||||
ARG CONTENT_REF=main
|
||||
|
||||
RUN git clone --depth 1 --branch ${CONTENT_REF} ${CONTENT_REPO} /src
|
||||
|
||||
# ─── Repack: bring alles in eine saubere Struktur ────────────────
|
||||
# • Markdown‑Posts: /src/articles/*.md → /out/content
|
||||
# • Bilder + CSS + JS: /src/web/static/**/* → /out/static
|
||||
# (Pfad‑Anpassung hier nach DEINEM Repository‑Layout)
|
||||
|
||||
RUN mkdir -p /out/content /out/static
|
||||
RUN find /src/articles -name '*.md' -exec cp {} /out/content/ \;
|
||||
RUN cp -r /src/web/static/* /out/static/
|
||||
|
||||
|
||||
############################
|
||||
# 3) Runtime‑Image
|
||||
############################
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y default-mysql-client ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# (optional) MySQL‑Client für später
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ─── Binärdatei ─────
|
||||
COPY --from=build /blog /usr/local/bin/blog
|
||||
COPY content/ /content
|
||||
COPY internal/web/static/ /static
|
||||
EXPOSE 8080
|
||||
|
||||
# ─── Content + Assets ───
|
||||
COPY --from=content /out/content /content
|
||||
COPY --from=content /out/static /static
|
||||
|
||||
ENV BLOG_CONTENT_DIR=/content
|
||||
ENV BLOG_STATIC_DIR=/static
|
||||
CMD ["blog"]
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["blog"]
|
||||
|
@@ -46,19 +46,22 @@ func main() {
|
||||
}
|
||||
|
||||
// Basislayout zuerst parsen
|
||||
base := template.Must(
|
||||
layout := template.Must(
|
||||
template.New("base").Funcs(funcs).
|
||||
ParseFiles("internal/web/templates/base.html"),
|
||||
)
|
||||
|
||||
// LIST‑Seite: base + list.html
|
||||
tplList = template.Must(base.Clone())
|
||||
tplList = template.Must(layout.Clone())
|
||||
template.Must(tplList.Funcs(funcs).ParseFiles("internal/web/templates/list.html"))
|
||||
|
||||
// ARTICLE‑Instanz
|
||||
tplArticle = template.Must(base.Clone())
|
||||
tplArticle = template.Must(layout.Clone())
|
||||
template.Must(tplArticle.Funcs(funcs).ParseFiles("internal/web/templates/article.html"))
|
||||
|
||||
tplPage := template.Must(layout.Clone())
|
||||
template.Must(tplPage.ParseFiles("internal/web/templates/page.html"))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
staticDir := "internal/web/static"
|
||||
@@ -68,6 +71,13 @@ func main() {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
staticPages, err := article.LoadStatic("pages")
|
||||
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 != "/" {
|
||||
@@ -92,6 +102,19 @@ func main() {
|
||||
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 Template‑Pool (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)))))
|
||||
|
5
content/2025/test - Kopie.md
Normal file
5
content/2025/test - Kopie.md
Normal file
@@ -0,0 +1,5 @@
|
||||
<!--{"title": "Mein erster Markdown‑Posty", "date": "2025-05-04", "slug": "mein-erster-markdown-posty", "cover": "/static/img/placeholder.png"}-->
|
||||
|
||||
# Hello Markdown 🌟
|
||||
|
||||
Dies ist *kursiv*, **fett** und ein [Link](https://example.com).
|
@@ -4,11 +4,10 @@ package article
|
||||
// internal/article/load.go (gekürzt)
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -18,85 +17,178 @@ import (
|
||||
|
||||
md "github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
// LoadDir liest alle *.html und *.md unter root/content,
|
||||
// parst Front‑Matter (JSON‑Kommentar in der 1. Zeile) und liefert []Article.
|
||||
func LoadDir(root string) ([]Article, error) {
|
||||
var out []Article
|
||||
seen := make(map[string]bool) // Duplikats‑Check
|
||||
|
||||
// gültige Extension‑Maske
|
||||
exts := parser.CommonExtensions | parser.AutoHeadingIDs | parser.DefinitionLists
|
||||
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
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
ext := filepath.Ext(p)
|
||||
if ext != ".html" && ext != ".md" {
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".md" && ext != ".html" {
|
||||
return nil // uninteressante Datei
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
r := bufio.NewReader(f)
|
||||
headerLine, err := r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
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 front‑matter", 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"` // NEW
|
||||
Cover string `json:"cover"`
|
||||
}
|
||||
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)
|
||||
if err := json.Unmarshal(headerJSON, &meta); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
// Fallback‑Slug 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
|
||||
|
||||
htmlBody := bodyBytes
|
||||
body := parts[1]
|
||||
if ext == ".md" {
|
||||
mdParser := parser.NewWithExtensions(exts)
|
||||
htmlBody = md.ToHTML(bodyBytes, mdParser, mdRenderer)
|
||||
body = md.ToHTML(body, nil, mdRenderer) // frischer Parser pro Call
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", meta.Date)
|
||||
if err != nil {
|
||||
fmt.Println("Time", err, date)
|
||||
return fmt.Errorf("%s: parse date: %w", path, err)
|
||||
}
|
||||
|
||||
out = append(out, Article{
|
||||
Title: meta.Title,
|
||||
Slug: meta.Slug,
|
||||
Date: date,
|
||||
Cover: meta.Cover, // NEW
|
||||
Body: template.HTML(htmlBody),
|
||||
Cover: meta.Cover,
|
||||
Body: template.HTML(body),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Date.After(out[j].Date) })
|
||||
return out, err
|
||||
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
|
||||
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 front‑matter found")
|
||||
}
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
// LoadStatic liest alle .md/.html‑Dateien 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)
|
||||
}
|
||||
|
||||
// Front‑Matter (erste Zeile) herauslösen
|
||||
parts := bytes.SplitN(raw, []byte("\n"), 2)
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("%s: missing front‑matter", 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"`
|
||||
}
|
||||
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,
|
||||
Body: template.HTML(body),
|
||||
}
|
||||
}
|
||||
return pages, nil
|
||||
}
|
||||
|
@@ -1,133 +1,151 @@
|
||||
/* ---------- Farbpalette (Feel‑free to tune) ---------- */
|
||||
/* ---------- Farbpalette ---------- */
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg‑alt: #161b22;
|
||||
--text: #c9d1d9;
|
||||
--text‑muted: #8b949e;
|
||||
--accent: #3b82f6; /* Blau */
|
||||
--accent‑light:#60a5fa;
|
||||
--code‑bg: #1e242c;
|
||||
--code‑border: #30363d;
|
||||
--radius: 0.6rem;
|
||||
--gap: 1.5rem;
|
||||
font‑size: 16px;
|
||||
font‑family: system‑ui, "Segoe UI", Roboto, sans‑serif;
|
||||
color‑scheme: dark;
|
||||
/* Light theme */
|
||||
--bg: #f5f7fa;
|
||||
--bg-alt: #ffffff;
|
||||
--card-bg: #ffffff;
|
||||
--text: #1f2933;
|
||||
--text-muted: #64748b;
|
||||
--accent: #2563eb; /* Indigo‑600 */
|
||||
--accent-light: #3b82f6; /* Indigo‑500 */
|
||||
--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, "Segoe UI", Roboto, sans-serif;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---------- Globale Elemente ---------- */
|
||||
* { box‑sizing: border‑box; }
|
||||
/* Dark mode (optional) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg-alt: #161b22;
|
||||
--card-bg: #161b22;
|
||||
--text: #c9d1d9;
|
||||
--text-muted: #8b949e;
|
||||
--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.6;
|
||||
line-height: 1.65;
|
||||
}
|
||||
a {
|
||||
color: var(--accent);
|
||||
text‑decoration: none;
|
||||
text-decoration: none;
|
||||
transition: color .15s;
|
||||
}
|
||||
a:hover { text‑decoration: underline; }
|
||||
a:hover { color: var(--accent-light); }
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
/* Container in der Mitte */
|
||||
.wrapper {
|
||||
max‑width: 768px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--gap);
|
||||
padding: var(--gap) calc(var(--gap) / 1.5);
|
||||
}
|
||||
|
||||
/* ---------- Kopf & Fuß ---------- */
|
||||
header, footer {
|
||||
display: flex;
|
||||
align‑items: center;
|
||||
justify‑content: space-between;
|
||||
background: var(--bg‑alt);
|
||||
padding: 1rem var(--gap);
|
||||
border‑bottom: 1px solid var(--code‑border);
|
||||
background: var(--bg-alt);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font‑size: 1.4rem;
|
||||
letter‑spacing: 0.5px;
|
||||
header {
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 1rem calc(var(--gap) / 1.2);
|
||||
}
|
||||
header a { color: var(--text); }
|
||||
|
||||
header h1 { margin: 0; font-size: 1.4rem; }
|
||||
footer {
|
||||
border‑top: 1px solid var(--code‑border);
|
||||
font‑size: 0.9rem;
|
||||
color: var(--text‑muted);
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--text-muted);
|
||||
font-size: .9rem;
|
||||
margin-top: var(--gap);
|
||||
}
|
||||
|
||||
/* ---------- Artikelliste ---------- */
|
||||
.post‑list li {
|
||||
/* ---------- Karten‑Grid ---------- */
|
||||
.post-list {
|
||||
display: grid;
|
||||
gap: var(--gap);
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 1rem 0;
|
||||
border‑bottom: 1px solid var(--code‑border);
|
||||
}
|
||||
.post‑list li:last‑child { border‑bottom: none; }
|
||||
.post‑list time { color: var(--text‑muted); font‑size: 0.9rem; }
|
||||
.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; }
|
||||
|
||||
/* ---------- Einzelartikel ---------- */
|
||||
article h1 { margin‑top: 0; font‑size: 2rem; }
|
||||
article time { color: var(--text‑muted); font‑size: 0.9rem; }
|
||||
article img, article video {
|
||||
max‑width: 100%; height: auto; border‑radius: var(--radius);
|
||||
/* ---------- Artikel ---------- */
|
||||
.hero {
|
||||
width: 100%; height: 320px; object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
article pre, article code {
|
||||
font‑family: "Fira Code", Consolas, monospace;
|
||||
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;
|
||||
border‑radius: var(--radius);
|
||||
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 {
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
border‑left: 4px solid var(--accent‑light);
|
||||
background: var(--bg‑alt);
|
||||
color: var(--text‑muted);
|
||||
border-left: 4px solid var(--accent);
|
||||
padding: .5rem 1rem; margin: 1rem 0;
|
||||
background: var(--code-bg);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
/* ---------- Karten‑Layout für Artikelliste ---------- */
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-alt);
|
||||
transition: transform .15s ease, border-color .15s ease;
|
||||
.main-nav ul {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--accent);
|
||||
.main-nav a {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.card img {
|
||||
width: 160px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
.main-nav a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 .4rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.card time { color: var(--text-muted); font-size: .9rem; }
|
||||
|
||||
/* ---------- Hero‑Bild im Artikel ---------- */
|
||||
.hero {
|
||||
width: 100%;
|
||||
max-height: 340px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max‑width: 600px) {
|
||||
:root { font‑size: 15px; }
|
||||
header, footer { flex‑direction: column; gap: 0.5rem; }
|
||||
@media (max-width: 640px) {
|
||||
:root { font-size: 15px; }
|
||||
.hero { height: 200px; }
|
||||
header { flex-direction: column; gap: .5rem; }
|
||||
}
|
||||
|
@@ -9,12 +9,18 @@
|
||||
<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">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/">B1tsblog</a></h1>
|
||||
<nav>
|
||||
<!-- Platz für spätere Links -->
|
||||
<nav class="main-nav">
|
||||
<ul>
|
||||
<li><a href="/">Startseite</a></li>
|
||||
<li><a href="/page/impressum">Impressum</a></li>
|
||||
<!-- später: <li><a href="/page/datenschutz">Datenschutz</a></li> -->
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -23,7 +29,7 @@
|
||||
</main>
|
||||
|
||||
<footer class="wrapper">
|
||||
© {{ now.Year }} · Powered by Go
|
||||
© {{ now.Year }} · B1tK1ll3r
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,7 +1,5 @@
|
||||
{{ define "title" }}Alle Artikel{{ end }}
|
||||
|
||||
{{ define "body" }}
|
||||
<ul class="post-list" style="list-style:none; padding:0; display:grid; gap:var(--gap);">
|
||||
<ul class="post-list">
|
||||
{{ range . }}
|
||||
<li>
|
||||
<a class="card" href="/post/{{ .Slug }}">
|
||||
@@ -10,7 +8,7 @@
|
||||
{{ else }}
|
||||
<img src="/static/img/placeholder.png" alt="">
|
||||
{{ end }}
|
||||
<div>
|
||||
<div class="card-content">
|
||||
<h2>{{ .Title }}</h2>
|
||||
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>
|
||||
</div>
|
||||
@@ -19,5 +17,3 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ define "list" }}{{ template "layout" . }}{{ end }}
|
||||
|
11
internal/web/templates/page.html
Normal file
11
internal/web/templates/page.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{ define "title" }}{{ .Title }} – B1tsblog{{ end }}
|
||||
|
||||
{{ define "body" }}
|
||||
<article>
|
||||
<h1>{{ .Title }}</h1>
|
||||
<div class="article-content">{{ .Body }}</div>
|
||||
<p><a href="/">← Zurück</a></p>
|
||||
</article>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page" }}{{ template "layout" . }}{{ end }}
|
6
pages/impressum.md
Normal file
6
pages/impressum.md
Normal file
@@ -0,0 +1,6 @@
|
||||
<!--{"title": "Impressum", "slug": "impressum"}-->
|
||||
Max Mustermann
|
||||
Musterstraße 1
|
||||
12345 Musterstadt
|
||||
|
||||
E‑Mail: max@example.com
|
Reference in New Issue
Block a user