Compare commits

...

18 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
y.vierkoetter e94bd7eb29 Merge remote-tracking branch 'origin/main' 2026-06-18 23:50:14 +02:00
y.vierkoetter 4db8a7f103 added hero.png 2026-06-18 23:49:11 +02:00
yvierkoetter 42c5b4f345 Add robots.txt blocking all crawlers and AI training bots
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:47:43 +02:00
yvierkoetter c409ecc888 Set favicon to vase rings SVG
Includes dark mode variant (warm ivory fill via prefers-color-scheme).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:45:41 +02:00
yvierkoetter b85c765015 Set up wedding album structure
Albums: Standesamt, Empfang, Zeremonie, Feier (weight-ordered).
Removed cats placeholder album.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:36:11 +02:00
yvierkoetter c767765986 Remove placeholder cat images
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:33:18 +02:00
yvierkoetter b1a4697769 Gallery pages: title in header, remove hgroup, add back link
- Album title moved into the header bar (centered, small-caps)
- hgroup removed from page body via single.html and list.html overrides
- Back-to-home link added at the bottom of each gallery page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:31:19 +02:00
yvierkoetter 7caadb048a Style album card text to match site aesthetic
Lighter title weight, small-caps uppercase photo count with letter-spacing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:11:00 +02:00
yvierkoetter 6a445bf1b4 Remove unused Cormorant Garant font
Font was only applied to the hero h1 which is now replaced by SVG.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:07:50 +02:00
41 changed files with 683 additions and 4655 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"]
+114 -4
View File
@@ -175,6 +175,58 @@ section.hero {
opacity: 0.45; opacity: 0.45;
} }
/* ── Gallery page header title ────────────────────────── */
body > header {
justify-content: space-between;
margin-bottom: 0;
}
.header-title {
flex: 1;
text-align: center;
font-weight: 500;
font-size: 1.5rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-1);
margin: 0;
}
.header-right {
width: 3rem;
display: flex;
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 ──────────────────────────────────── */
.card > div > h2 {
font-weight: 400;
font-size: 0.9375rem;
letter-spacing: 0.06em;
}
.card > div > p {
font-size: 0.6875rem;
letter-spacing: 0.15em;
text-transform: uppercase;
}
/* ── Albums divider ───────────────────────────────────── */ /* ── Albums divider ───────────────────────────────────── */
.home-divider { .home-divider {
@@ -226,6 +278,56 @@ section.hero {
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 {
display: flex;
justify-content: center;
padding: 3rem 1.5rem 2rem;
border-top: 1px solid var(--border);
margin-top: 3rem;
}
.gallery-back-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-2);
text-decoration: none;
transition: color 0.2s;
}
.gallery-back-link:hover {
color: var(--text-1);
}
/* ── Keyframes ────────────────────────────────────────── */ /* ── Keyframes ────────────────────────────────────────── */
@keyframes hero-fade-up { @keyframes hero-fade-up {
@@ -240,11 +342,19 @@ section.hero {
} }
@keyframes hero-fade-in { @keyframes hero-fade-in {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes hero-bounce { @keyframes hero-bounce {
0%, 100% { transform: translateY(0); } 0%, 100% {
50% { transform: translateY(7px); } transform: translateY(0);
}
50% {
transform: translateY(7px);
}
} }
+1 -1
View File
@@ -14,7 +14,7 @@
// Background at 55%, text at 80% — both slower than normal, image slowest // Background at 55%, text at 80% — both slower than normal, image slowest
if (heroBg) heroBg.style.transform = 'translateY(' + (s * 0.25) + 'px)'; if (heroBg) heroBg.style.transform = 'translateY(' + (s * 0.25) + 'px)';
if (heroFg) heroFg.style.transform = 'translateY(' + (s * 0.15) + 'px)'; if (heroFg) heroFg.style.transform = 'translateY(' + (s * 0.05) + 'px)';
// Scroll arrow fades out over the first 30% of hero height // Scroll arrow fades out over the first 30% of hero height
if (heroScroll) { if (heroScroll) {
+1 -1
View File
@@ -3,7 +3,7 @@
"paths": { "paths": {
"*": [ "*": [
"*", "*",
"../themes/gallery/assets/*" "..\\themes\\gallery\\assets\\*"
] ]
} }
} }
+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!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

-4
View File
@@ -1,4 +0,0 @@
---
title: "Cats"
date: 2026-06-18
---
+9
View File
@@ -0,0 +1,9 @@
---
title: "Empfang"
date: 2026-06-10
weight: 2
resources:
- src: IMG_1574.png
params:
cover: true
---
+9
View File
@@ -0,0 +1,9 @@
---
title: "Feier"
date: 2026-06-10
weight: 4
resources:
- src: IMG_1625.png
params:
cover: true
---
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 MiB

+13
View File
@@ -0,0 +1,13 @@
---
title: "Standesamt"
date: 2026-02-14
weight: 1
resources:
- src: IMG_1570.png
params:
cover: true
---
{{< video src="test.mp4" >}}
{{< video src="test2.mp4" >}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

+6 -1
View File
@@ -1,4 +1,9 @@
--- ---
title: "Zeremonie" title: "Zeremonie"
date: 2026-06-18 date: 2026-06-10
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"
+7
View File
@@ -0,0 +1,7 @@
{{ define "main" }}
<section class="galleries">
{{ range where .Pages "Params.private" "ne" true }}
{{ partial "album-card.html" . }}
{{ end }}
</section>
{{ end }}
+17
View File
@@ -0,0 +1,17 @@
{{ define "main" }}
{{ partial "gallery.html" . }}
{{ partial "related.html" . }}
{{ with .Content }}
<section class="prose">
{{ . }}
</section>
{{ end }}
<div class="gallery-back">
<a href="{{ .Site.Home.RelPermalink }}" class="gallery-back-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 12H5M5 12l7 7M5 12l7-7"/>
</svg>
Zurück zur Startseite
</a>
</div>
{{ end }}
-30
View File
@@ -1,30 +0,0 @@
<style>
@font-face {
font-family: 'Cormorant Garant';
src: url('/fonts/CormorantGarant-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Cormorant Garant';
src: url('/fonts/CormorantGarant-LightItalic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Cormorant Garant';
src: url('/fonts/CormorantGarant-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Cormorant Garant';
src: url('/fonts/CormorantGarant-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
</style>
+7 -6
View File
@@ -7,13 +7,15 @@
</svg> </svg>
</a> </a>
{{ else }} {{ else }}
<a class="btn" href="{{ .Site.Home.RelPermalink }}"> <a class="btn btn-square" href="{{ .Site.Home.RelPermalink }}" title="{{ .Site.Title }}">
{{ .Site.Title }} <svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path clip-rule="evenodd" fill-rule="evenodd" d="M11.03 3.97a.75.75 0 0 1 0 1.06l-6.22 6.22H21a.75.75 0 0 1 0 1.5H4.81l6.22 6.22a.75.75 0 1 1-1.06 1.06l-7.5-7.5a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 0 1 1.06 0Z"></path>
</svg>
</a> </a>
{{ end }} {{ end }}
<h1 class="header-title">{{ .Title }}</h1>
<div class="header-right">
{{ if site.Menus.main }} {{ if site.Menus.main }}
<ul>
<li>
<button class="btn btn-square" id="menu-toggle" aria-expanded="false" type="button" title="{{ T "menu" }}"> <button class="btn btn-square" id="menu-toggle" aria-expanded="false" type="button" title="{{ T "menu" }}">
<svg class="icon-menu" width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <svg class="icon-menu" width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path clip-rule="evenodd" fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z"></path> <path clip-rule="evenodd" fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z"></path>
@@ -22,9 +24,8 @@
<path clip-rule="evenodd" fill-rule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"></path> <path clip-rule="evenodd" fill-rule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"></path>
</svg> </svg>
</button> </button>
</li>
</ul>
{{ end }} {{ end }}
</div>
</header> </header>
{{ partial "menu.html" . }} {{ partial "menu.html" . }}
{{ end }} {{ end }}
+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()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.3 KiB

+35
View File
@@ -0,0 +1,35 @@
User-agent: *
Disallow: /
User-agent: GPTBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: FacebookBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: cohere-ai
Disallow: /
User-agent: Amazonbot
Disallow: /
+1
Submodule themes/hugo-video added at fb3686a015
+75
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 257 KiB