Compare commits

...

9 Commits

Author SHA1 Message Date
y.vierkoetter 544d6c866c added video support 2026-06-22 23:36:20 +02:00
y.vierkoetter 2d93109831 fixed docker 2026-06-20 00:35:55 +02:00
yvierkoetter ba9d4afaa6 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	docker-compose.yml
2026-06-19 17:11:11 +02:00
yvierkoetter c10bfaa87c Add nginx basic auth via docker-compose environment variables
Credentials set via AUTH_USER / AUTH_PASS in docker-compose.yml.
Entrypoint generates htpasswd at container start using openssl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:10:39 +02:00
y.vierkoetter fa5e52f185 small improvements 2026-06-19 03:02:33 +02:00
y.vierkoetter 5b031095fa changed some text 2026-06-19 00:29:49 +02:00
y.vierkoetter 501855ee87 small changes 2026-06-19 00:00:48 +02:00
yvierkoetter 8cf7b70fad Merge remote-tracking branch 'origin/main' 2026-06-18 23:52:40 +02:00
yvierkoetter 3faa7c6ee0 Add Dockerfile and docker-compose for production deployment
Multi-stage build: hugomods/hugo:exts → nginx:alpine.
Runs on port 8080.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:51:52 +02:00
16 changed files with 286 additions and 9 deletions
+4
View File
@@ -0,0 +1,4 @@
public/
resources/
.hugo_build.lock
.idea/
+3
View File
@@ -1,3 +1,6 @@
[submodule "themes/gallery"] [submodule "themes/gallery"]
path = themes/gallery path = themes/gallery
url = https://github.com/nicokaiser/hugo-theme-gallery.git url = https://github.com/nicokaiser/hugo-theme-gallery.git
[submodule "themes/hugo-video"]
path = themes/hugo-video
url = https://github.com/martignoni/hugo-video.git
+7
View File
@@ -0,0 +1,7 @@
# rsync -arzP --exclude-from='.rsyncignore' ./ netadmin@4koetter.com:~/plassonke.de
.git/
public/
resources/
.hugo_build.lock
.idea/
+37
View File
@@ -0,0 +1,37 @@
FROM hugomods/hugo:exts AS builder
WORKDIR /src
COPY archetypes/ archetypes/
COPY themes/ themes/
COPY layouts/ layouts/
COPY static/ static/
COPY assets/ assets/
COPY content/ content/
COPY hugo.toml .
RUN hugo --gc --minify
FROM alpine:3.20 AS video-posters
RUN apk add --no-cache python3 ffmpeg
WORKDIR /src
COPY --from=builder /src/public /src/public
COPY scripts/add-video-posters.py /add-video-posters.py
RUN python3 /add-video-posters.py /src/public
FROM nginx:alpine
RUN apk add --no-cache openssl
COPY --from=video-posters /src/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
+38
View File
@@ -199,6 +199,20 @@ body > header {
justify-content: flex-end; justify-content: flex-end;
} }
/* ── Album card hover ─────────────────────────────────── */
section.galleries .card figure {
overflow: hidden;
}
section.galleries .card img {
transition: transform 0.6s ease-in-out;
}
section.galleries .card img:hover {
transform: scale(1.06);
}
/* ── Album card text ──────────────────────────────────── */ /* ── Album card text ──────────────────────────────────── */
.card > div > h2 { .card > div > h2 {
@@ -264,6 +278,30 @@ body > header {
opacity: 0.75; opacity: 0.75;
} }
/* ── Videos ────────────────────────────────── */
.prose {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
border-top: 1px solid var(--border);
margin-left: 0;
margin-right: 0;
padding: 0;
padding-top: 4rem;
gap: 0.5rem;
max-width: unset !important;
}
@media only screen and (max-width: 500px) {
.prose {
grid-template-columns: 1fr;
}
}
.prose video {
background: #000;
}
/* ── Gallery back link ────────────────────────────────── */ /* ── Gallery back link ────────────────────────────────── */
.gallery-back { .gallery-back {
+16 -4
View File
@@ -1,11 +1,23 @@
--- ---
title: "Herzlich Willkommen" title: "Lina & Marlin"
params: params:
tagline: "Schön, dass ihr da seid." tagline: "Alles Liebe zu eurer Hochzeit"
albums_label: "Alben" albums_label: "Alben"
closing: "20. Juni 2026" closing: "20. Juni 2026"
--- ---
Hier findet ihr die Erinnerungen an unseren besonderen Tag — festgehalten in Bildern, die für immer bleiben. Liebe Lina, lieber Marlin,
Schreibt euch diesen Moment ins Gedächtnis: die Musik, das Lachen, die Umarmungen. Wir freuen uns, ihn mit euch geteilt zu haben. vielleicht habt ihr euren Fehler bereits gefunden?
Die Domain `z.app` als Geschenk zu verbieten ist nicht genug, wenn `plassonke.de` noch verfügbar ist.
Deshalb bekommt ihr diese jetzt als Teil eures Geschenkes.
Da ihr dieses Geschenk erst im Nachhinein bekommt, können wir uns Linas große Freude über die erste eigene Domain wohl nur vorstellen.
Da es leider kein `z.app` für Marlin gibt, kannst du diese Freude aber teilen.
Wenn ihr schon keinen Doppelnamen nehmt, kann man stattdessen vielleicht `zapp.plassonke.de` nutzen.
Dann ist auch für Marlin etwas dabei.
Falls die Freude über die Domain irgendwann doch etwas abebbt,
könnt ihr euch hoffentlich mit den Bildern an einen tollen Tag zurückerinnern.
Wir wünschen euch alles Gute für eure gemeinsame Zukunft!
+5 -1
View File
@@ -1,5 +1,9 @@
--- ---
title: "Empfang" title: "Empfang"
date: 2026-06-20 date: 2026-06-10
weight: 2 weight: 2
resources:
- src: IMG_1574.png
params:
cover: true
--- ---
+5 -1
View File
@@ -1,5 +1,9 @@
--- ---
title: "Feier" title: "Feier"
date: 2026-06-20 date: 2026-06-10
weight: 4 weight: 4
resources:
- src: IMG_1625.png
params:
cover: true
--- ---
+4
View File
@@ -7,3 +7,7 @@ resources:
params: params:
cover: true cover: true
--- ---
{{< video src="test.mp4" >}}
{{< video src="test2.mp4" >}}
+5 -1
View File
@@ -1,5 +1,9 @@
--- ---
title: "Zeremonie" title: "Zeremonie"
date: 2026-06-20 date: 2026-06-10
weight: 3 weight: 3
resources:
- src: IMG_1474.png
params:
cover: true
--- ---
+14
View File
@@ -0,0 +1,14 @@
services:
web:
build: .
networks:
- default
- reverse-proxy_shared
restart: unless-stopped
environment:
AUTH_USER: hochzeit
AUTH_PASS: changeme
networks:
reverse-proxy_shared:
external: true
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -e
echo "${AUTH_USER}:$(openssl passwd -apr1 "${AUTH_PASS}")" > /etc/nginx/.htpasswd
exec nginx -g "daemon off;"
+3 -2
View File
@@ -1,6 +1,6 @@
baseURL = "https://plassonke.de/" baseURL = "https://plassonke.de/"
title = "Hochzeit" title = "Hochzeit"
theme = "gallery" theme = ["hugo-video", "gallery"]
defaultContentLanguage = "de" defaultContentLanguage = "de"
disableKinds = ["taxonomy"] disableKinds = ["taxonomy"]
enableRobotsTXT = true enableRobotsTXT = true
@@ -10,7 +10,8 @@ timeout = "120s"
[params] [params]
defaultTheme = "light" defaultTheme = "light"
description = "Hochzeit" description = "Fotos von Linas und Marlins Hochzeit"
images = ["content/hero.png"]
title = "Hochzeit" title = "Hochzeit"
[params.author] [params.author]
name = "Yannis Vierkoetter" name = "Yannis Vierkoetter"
+12
View File
@@ -0,0 +1,12 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
auth_basic "Hochzeit";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
try_files $uri $uri/ =404;
}
}
+128
View File
@@ -0,0 +1,128 @@
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
from urllib.parse import unquote
VIDEO_TAG_RE = re.compile(
r"<video(?P<attrs>[^>]*)>(?P<body>.*?)</video>",
re.IGNORECASE | re.DOTALL,
)
SOURCE_RE = re.compile(
r'<source[^>]+src=["\'](?P<src>[^"\']+)["\'][^>]*>',
re.IGNORECASE,
)
def extract_src(video_attrs: str, video_body: str) -> str | None:
"""Extract video path from either <video src=""> or nested <source src="">."""
video_src = re.search(r'src=["\']([^"\']+)["\']', video_attrs, re.IGNORECASE)
if video_src:
return video_src.group(1)
source_src = SOURCE_RE.search(video_body)
if source_src:
return source_src.group("src")
return None
def make_poster(video_path: Path, poster_path: Path) -> None:
"""Extract a poster frame from a video using ffmpeg."""
poster_path.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
"ffmpeg",
"-y",
"-ss",
"00:00:01",
"-i",
str(video_path),
"-frames:v",
"1",
"-q:v",
"3",
str(poster_path),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def process_file(html_file: Path, root: Path) -> None:
"""Replace video preload mode and add poster attributes."""
content = html_file.read_text(encoding="utf-8")
changed = False
def replace_video(match: re.Match[str]) -> str:
nonlocal changed
attrs = match.group("attrs")
body = match.group("body")
if 'preload="auto"' not in attrs and "preload='auto'" not in attrs:
return match.group(0)
src = extract_src(attrs, body)
if not src:
return match.group(0)
src_clean = unquote(src.split("#")[0].split("?")[0])
if src_clean.startswith(("http://", "https://", "//")):
return match.group(0)
video_path = (html_file.parent / src_clean).resolve()
try:
video_path.relative_to(root.resolve())
except ValueError:
return match.group(0)
if not video_path.exists():
print(f"Missing video: {video_path}")
return match.group(0)
poster_path = video_path.with_suffix(".jpg")
poster_src = src_clean.rsplit(".", 1)[0] + ".jpg"
if not poster_path.exists():
make_poster(video_path, poster_path)
attrs_new = attrs
attrs_new = attrs_new.replace('preload="auto"', 'preload="none"')
attrs_new = attrs_new.replace("preload='auto'", 'preload="none"')
if "poster=" not in attrs_new.lower():
attrs_new += f' poster="{poster_src}"'
changed = True
return f"<video{attrs_new}>{body}</video>"
new_content = VIDEO_TAG_RE.sub(replace_video, content)
if changed:
html_file.write_text(new_content, encoding="utf-8")
print(f"Updated: {html_file}")
def main() -> None:
"""Process all HTML files below the given root directory."""
root = Path(sys.argv[1]).resolve()
for html_file in root.rglob("*.html"):
process_file(html_file, root)
if __name__ == "__main__":
main()
+1
Submodule themes/hugo-video added at fb3686a015