Compare commits

...

4 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
10 changed files with 211 additions and 3 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "themes/gallery"]
path = themes/gallery
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
+31 -2
View File
@@ -1,8 +1,37 @@
FROM hugomods/hugo:exts AS builder
WORKDIR /src
COPY . .
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
COPY --from=builder /src/public /usr/share/nginx/html
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"]
+24
View File
@@ -278,6 +278,30 @@ section.galleries .card img:hover {
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 {
+4
View File
@@ -7,3 +7,7 @@ resources:
params:
cover: true
---
{{< video src="test.mp4" >}}
{{< video src="test2.mp4" >}}
+3
View File
@@ -5,6 +5,9 @@ services:
- default
- reverse-proxy_shared
restart: unless-stopped
environment:
AUTH_USER: hochzeit
AUTH_PASS: changeme
networks:
reverse-proxy_shared:
+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;"
+1 -1
View File
@@ -1,6 +1,6 @@
baseURL = "https://plassonke.de/"
title = "Hochzeit"
theme = "gallery"
theme = ["hugo-video", "gallery"]
defaultContentLanguage = "de"
disableKinds = ["taxonomy"]
enableRobotsTXT = true
+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