feat: reposition desfoto as an independent brand with four Arbeitsbereiche

This commit is contained in:
desfoto automation
2026-09-19 19:14:42 +02:00
parent 96247da074
commit 68613788b8
86 changed files with 1287 additions and 756 deletions

View File

@@ -90,15 +90,21 @@ if [ "$live" = 1 ] && [ "$release_sha" != "unknown" ] && git -C "$root" rev-pars
fi
printf '\n== routes\n'
for route in / /shootings/ /businessfotografie/ /portrait-und-model/ /familien-und-paare/ \
/minishootings/ /video/ /social-media/ /projekte/ /ueber/ /kontakt/ /impressum/ /datenschutz/; do
for route in / /fotografie/ /businessfotografie/ /portrait-und-model/ /familie/ \
/minishootings/ /video/ /musik-und-buehne/ /social-media/ /projekte/ /ueber/ /kontakt/ /impressum/ /datenschutz/; do
check_status "$base$route" 200
done
# The retired photo URL must keep working: it is the umbrella page now.
check_redirect "$base/fotografie" "$base/shootings/"
check_redirect "$base/fotografie/" "$base/shootings/"
check_redirect "$base/fotografie.html" "$base/shootings/"
check_redirect "$base/shootings" "$base/fotografie/"
check_redirect "$base/shootings/" "$base/fotografie/"
check_redirect "$base/shootings.html" "$base/fotografie/"
check_redirect "$base/fotografie.html" "$base/fotografie/"
# Familien & Paare was renamed: the remaining family work stays with desfoto.
check_redirect "$base/familien-und-paare" "$base/familie/"
check_redirect "$base/familien-und-paare/" "$base/familie/"
check_redirect "$base/familien-und-paare.html" "$base/familie/"
printf '\n== canonical host and redirects\n'
if [ "$live" = 1 ]; then
@@ -117,13 +123,19 @@ esac
fi
printf '\n== content\n'
check_contains "$base/" 'href="/shootings/"'
check_contains "$base/" 'href="/fotografie/"'
check_contains "$base/" 'href="/musik-und-buehne/"'
check_contains "$base/" 'href="/video/"'
check_contains "$base/" "Mobiles Studio"
check_contains "$base/" "Studio Neumünster"
check_not_contains "$base/" 'href="/fotografie/'
check_contains "$base/shootings/" "Autarke Blitzanlage"
check_contains "$base/shootings/" "Minishootings"
check_contains "$base/" "desfoto ist ein Angebot von Denny Schulz, Neumünster."
check_not_contains "$base/" 'href="/shootings'
check_not_contains "$base/" "Paarshooting"
check_contains "$base/fotografie/" "Autarke Blitzanlage"
check_contains "$base/fotografie/" "Minishootings"
check_contains "$base/familie/" "Familienshooting"
check_contains "$base/musik-und-buehne/" "Thjódrörir"
check_contains "$base/musik-und-buehne/" "Sagenbringer"
check_contains "$base/impressum/" "§ 5 DDG"
check_contains "$base/impressum/" "DE462149560"
check_contains "$base/datenschutz/" "keine Zugriffsprotokolle"
@@ -131,8 +143,8 @@ check_contains "$base/datenschutz/" "youtube-nocookie.com"
check_contains "$base/video/" "NWWFTf7l8g0"
check_contains "$base/" "https://www.dennyschulz.de/"
check_contains "$base/" "https://dennyapp.de/"
check_contains "$base/sitemap.xml" "<loc>https://desfoto.de/shootings/</loc>"
check_not_contains "$base/sitemap.xml" "<loc>https://desfoto.de/fotografie/</loc>"
check_contains "$base/sitemap.xml" "<loc>https://desfoto.de/fotografie/</loc>"
check_not_contains "$base/sitemap.xml" "<loc>https://desfoto.de/shootings/</loc>"
printf '\n== error handling and metadata\n'
check_status "$base/diese-seite-gibt-es-nicht/" 404
@@ -143,20 +155,31 @@ check_status "$base/robots.txt" 200
printf '\n== assets\n'
check_status "$base/assets/img/logo-160.png" 200
check_status "$base/assets/img/favicon-32.png" 200
check_status "$base/assets/img/og-shootings.jpg" 200
check_status "$base/assets/img/og-fotografie.jpg" 200
check_status "$base/assets/site.css" 200
check_status "$base/assets/fonts/fraunces-latin.woff2" 200
# The portfolio expansion ships the operator's own public work...
check_status "$base/assets/img/music-dreadlocks-1200.webp" 200
check_status "$base/assets/img/business-hotel-window-1200.webp" 200
check_status "$base/assets/img/couple-park-1200.webp" 200
check_status "$base/assets/img/free-portrait-light-1200.webp" 200
check_status "$base/assets/img/editorial-leap-1200.webp" 200
# ...and the wedding pictures showing the guests stay removed.
check_status "$base/assets/img/group-feier-1200.webp" 404
check_status "$base/assets/img/group-outdoor-1200.webp" 404
check_status "$base/assets/img/group-posiert-1200.webp" 404
# Couple imagery and the studio shot with the old business card ship nowhere.
check_status "$base/assets/img/couple-park-1200.webp" 404
check_status "$base/assets/img/portrait-couple-1200.webp" 404
check_status "$base/assets/img/studio-setup-1200.webp" 404
# Brautpaar-Bilder sind komplett aus dem Bestand: Fotografie- und Eventbilder
# mit Paaren gehören zu dennyschulz.de, nicht zu desfoto.
check_status "$base/assets/img/portrait-natural-1200.webp" 404
check_status "$base/assets/img/event-motion-1200.webp" 404
check_status "$base/assets/img/event-dance-1200.webp" 404
check_contains "$base/minishootings/" "free-modern-portrait-1200.webp"
check_contains "$base/social-media/" "event-speaker-1200.webp"
check_contains "$base/projekte/" "music-dreadlocks-1200.webp"
check_contains "$base/familien-und-paare/" "couple-park-1200.webp"
check_contains "$base/familie/" "free-portrait-light-1200.webp"
printf '\n== privacy promises and headers\n'
check_header "$base/" "Strict-Transport-Security"

View File

@@ -12,11 +12,17 @@ hotlink its assets. Linking out to it from the footer is an explicit operator
request and is the only permitted reference (see "Editing rules").
## Structure
- `/` — hero, two pillars (**Shootings** = photo, **Videoprojekte** = video), portfolio
gallery, music video, mobiles Studio, process, facts.
- `/shootings/` — the umbrella for all photographic work: Businessfotografie,
Portrait & Model, Familien & Paare, Minishootings, plus the mobiles Studio.
`/fotografie` and `/fotografie/` are 301-redirected here by `nginx.conf`.
- `/` — hero, the four **Arbeitsbereiche** (Fotografie, Video, Musik & Bühne, Projekte),
the bookable services ("Was du bei mir buchen kannst"), the portfolio gallery
("Was ich außerdem mache"), music video, mobiles Studio, process, facts.
- `/fotografie/` — the umbrella for all photographic work: Businessfotografie,
Portrait & Model, Familie, Minishootings, plus the mobiles Studio.
`/shootings`, `/shootings/` and `/shootings.html` are 301-redirected here by
`nginx.conf`, and the old `/fotografie.html` is canonicalised to `/fotografie/`.
- `/familie/` — family photography (no couples: Paare and Engagement stay on
`dennyschulz.de`). `/familien-und-paare`, `/familien-und-paare/` and
`/familien-und-paare.html` are 301-redirected here.
- `/musik-und-buehne/` — concerts, band portraits and the Thjódrörir music video.
- `/video/`, `/social-media/`, `/projekte/` (music + free work + pro bono), `/ueber/`.
## Commands
@@ -64,8 +70,19 @@ request and is the only permitted reference (see "Editing rules").
the content stays hidden forever. `.reveal--mask` therefore clips its children.
- Never nest `<a>` inside `<a>` (or any other invalid nesting). The parser closes the
outer anchor and the surrounding grid falls apart — that is what happened to the
two homepage pillars. Cards with their own links use the stretched-link pattern
(`.pillar__go::after`); `tests/test_site.py` guards this for every page.
two homepage pillars the redesign replaced. Cards with their own links use the
stretched-link pattern (`.pillar__go::after`, `.link-arrow`) or keep the link in
the card foot; `tests/test_site.py` guards this for every page.
- **Brand separation:** `desfoto` is the brand, `Denny Schulz Fotografie` is only the
legal entity. The legal name may appear in the Impressum, the Datenschutz page and
the JSON-LD `legalName` — nowhere else. The footer carries exactly one ownership
sentence (`SITE["ownership"]`). Couples, engagements and weddings are advertised on
`dennyschulz.de` only; desfoto shows single-person portraits, family and business
work, and no couple imagery may be added back to `src/images.json`.
- `/fotografie/` is the umbrella term for the photographic work ("Shooting" is not a
section label any more). Renaming a route means updating `pages.ROUTES`,
`nginx.conf` (301 from the old URL) and `scripts/build-site.py:ROUTE_OG`;
the build prunes the retired page from `site/` automatically.
- Two-space indentation, `from __future__ import annotations`, type hints on public
functions, no new third-party Python packages (Pillow is the only dependency).

View File

@@ -64,13 +64,22 @@ server {
location = /impressum.html { return 301 /impressum/; }
location = /datenschutz.html { return 301 /datenschutz/; }
location = /kontakt.html { return 301 /kontakt/; }
# /fotografie.html used to redirect to /shootings/; it resolves to its own
# canonical page now instead of turning into a 404.
location = /fotografie.html { return 301 /fotografie/; }
# Shootings is the umbrella term for the photographic work; the old
# /fotografie/ URL stays reachable and points to the new page.
# Fotografie is the umbrella term for the photographic work; the old
# /shootings/ URL stays reachable and points to the new page.
# (/fotografie/index.html is already canonicalised to /fotografie/ above.)
location = /fotografie { return 301 /shootings/; }
location = /fotografie/ { return 301 /shootings/; }
location = /fotografie.html { return 301 /shootings/; }
location = /shootings { return 301 /fotografie/; }
location = /shootings/ { return 301 /fotografie/; }
location = /shootings.html { return 301 /fotografie/; }
# Familie keeps the photographic range that stays with desfoto; the old
# combined Familien-und-Paare URL points here.
location = /familien-und-paare { return 301 /familie/; }
location = /familien-und-paare/ { return 301 /familie/; }
location = /familien-und-paare.html { return 301 /familie/; }
location ^~ /.well-known/ {
allow all;

View File

@@ -48,15 +48,16 @@ ACCENT = (192, 67, 28)
# OpenGraph image per route, built from the page's own key visual.
ROUTE_OG = {
"/": ("music-vocal-blue", "og-desfoto.jpg", "Bilder und Filme, die nicht beliebig aussehen."),
"/shootings/": ("business-portrait-studio", "og-shootings.jpg", "Shootings aus Neumünster"),
"/fotografie/": ("business-portrait-studio", "og-fotografie.jpg", "Fotografie aus Neumünster"),
"/businessfotografie/": ("business-portrait-studio", "og-businessfotografie.jpg", "Businessfotografie"),
"/portrait-und-model/": ("editorial-colour", "og-portrait.jpg", "Portrait & Model"),
"/familien-und-paare/": ("people-joy", "og-familien.jpg", "Familien & Paare"),
"/minishootings/": ("portrait-natural", "og-minishootings.jpg", "Minishootings"),
"/familie/": ("free-portrait-light", "og-familie.jpg", "Familienfotografie"),
"/minishootings/": ("free-modern-portrait", "og-minishootings.jpg", "Minishootings"),
"/video/": ("music-guitar-silhouette", "og-video.jpg", "Videoprojekte"),
"/social-media/": ("event-motion", "og-social-media.jpg", "Social-Media-Inhalte"),
"/musik-und-buehne/": ("music-vocal-blue", "og-musik-und-buehne.jpg", "Musik & Bühne"),
"/social-media/": ("event-speaker", "og-social-media.jpg", "Social-Media-Inhalte"),
"/projekte/": ("free-mountain-view", "og-projekte.jpg", "Eigene Projekte"),
"/ueber/": ("studio-setup", "og-ueber.jpg", "Über desfoto"),
"/ueber/": ("gear-cases", "og-ueber.jpg", "Über desfoto"),
"/kontakt/": ("event-venue", "og-kontakt.jpg", "Kontakt"),
"/impressum/": ("gear-cases", "og-impressum.jpg", "Impressum"),
"/datenschutz/": ("event-venue", "og-datenschutz.jpg", "Datenschutz"),
@@ -563,6 +564,24 @@ def link_check(routes: list[tuple[str, str]]) -> list[str]:
return problems
def prune_stale_pages(routes: list[tuple]) -> None:
"""Remove built pages that no longer belong to the route table.
The build writes into the existing tree, so a renamed route would otherwise
leave its old HTML (and its retired image srcsets) behind and the link check
would keep failing on files nobody serves any more.
"""
expected = {SITE / route[1] for route in routes} | {SITE / "404.html"}
for html in sorted(SITE.rglob("*.html")):
if html in expected:
continue
print(f" [pages] removed stale {html.relative_to(SITE)}")
html.unlink()
parent = html.parent
if parent != SITE and not any(parent.iterdir()):
parent.rmdir()
def normalize_permissions() -> None:
"""Make the build output readable for the serving nginx worker.
@@ -587,6 +606,7 @@ def main() -> int:
build_brand_assets(table)
print("== pages ==")
routes = build_html(table)
prune_stale_pages(routes)
build_meta_files(routes)
build_script()
print("== checks ==")

View File

@@ -42,7 +42,7 @@
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familien-und-paare/">Familien &amp; Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familie/">Familie</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/musik-und-buehne/">Musik &amp; Bühne</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
@@ -69,23 +69,29 @@
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
<span class="brand__text"><span>desfoto</span><small>Fotografie &amp; Video</small></span>
</span>
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
<p class="footer__claim">desfoto ist ein Angebot von Denny Schulz, Neumünster.</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -101,7 +107,7 @@
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
</div>
</div>
<div class="footer__bottom">
@@ -110,7 +116,7 @@
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© Denny Schulz · Denny Schulz Fotografie · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© Denny Schulz · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -195,6 +195,11 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
.grid--2{grid-template-columns:repeat(auto-fit,minmax(19rem,1fr))}
.grid--3{grid-template-columns:repeat(auto-fit,minmax(16.5rem,1fr))}
.grid--4{grid-template-columns:repeat(auto-fit,minmax(13.5rem,1fr))}
/* Vier Arbeitsbereiche: vier Spalten auf großen, zwei auf mittleren und eine
auf kleinen Schirmen. auto-fit würde bei mittlerer Breite 3+1 Karten setzen. */
.grid--areas{grid-template-columns:1fr}
@media (min-width:40rem){.grid--areas{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:72rem){.grid--areas{grid-template-columns:repeat(4,minmax(0,1fr))}}
.card{display:flex;flex-direction:column;gap:1rem;height:100%}
.card__title{display:flex;align-items:baseline;gap:.6rem}
.card__num{font-size:.72rem;font-weight:600;letter-spacing:.16em;color:var(--accent)}

File diff suppressed because one or more lines are too long

View File

@@ -43,7 +43,7 @@
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familien-und-paare/">Familien &amp; Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familie/">Familie</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/musik-und-buehne/">Musik &amp; Bühne</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
@@ -56,7 +56,6 @@
<span class="kicker">Rechtliches</span>
<h1 class="balance">Datenschutzerklärung</h1>
<p class="page-head__lead">Informationen zur Verarbeitung personenbezogener Daten · Stand September 2026</p>
</div>
</section><section class="section"><div class="shell"><div class="prose">
<p>Diese Datenschutzerklärung informiert dich darüber, welche personenbezogenen Daten beim Besuch
@@ -185,23 +184,29 @@
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
<span class="brand__text"><span>desfoto</span><small>Fotografie &amp; Video</small></span>
</span>
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
<p class="footer__claim">desfoto ist ein Angebot von Denny Schulz, Neumünster.</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -217,7 +222,7 @@
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
</div>
</div>
<div class="footer__bottom">
@@ -226,7 +231,7 @@
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© Denny Schulz · Denny Schulz Fotografie · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© Denny Schulz · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>

138
site/familie/index.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

138
site/fotografie/index.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -43,7 +43,7 @@
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familien-und-paare/">Familien &amp; Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familie/">Familie</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/musik-und-buehne/">Musik &amp; Bühne</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
@@ -56,7 +56,6 @@
<span class="kicker">Rechtliches</span>
<h1 class="balance">Impressum</h1>
<p class="page-head__lead">Angaben gemäß § 5 DDG · Stand September 2026</p>
</div>
</section><section class="section"><div class="shell"><div class="prose">
<h2>Diensteanbieter</h2>
@@ -110,23 +109,29 @@
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
<span class="brand__text"><span>desfoto</span><small>Fotografie &amp; Video</small></span>
</span>
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
<p class="footer__claim">desfoto ist ein Angebot von Denny Schulz, Neumünster.</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -142,7 +147,7 @@
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
</div>
</div>
<div class="footer__bottom">
@@ -151,7 +156,7 @@
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© Denny Schulz · Denny Schulz Fotografie · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© Denny Schulz · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,7 @@
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familien-und-paare/">Familien &amp; Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait &amp; Model</a><a href="/familie/">Familie</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/musik-und-buehne/">Musik &amp; Bühne</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
@@ -57,7 +57,6 @@
<span class="kicker">Kontakt</span>
<h1 class="balance">Erzähl mir, was du vorhast</h1>
<p class="page-head__lead">Kurze Nachricht genügt: worum geht es, wann soll es stattfinden und was hast du mit den Bildern oder dem Video vor. Du bekommst eine ehrliche Einschätzung und einen Preis.</p>
</div>
</section><section class="section"><div class="shell"><div class="contact-grid"><div class="stack"><h2 class="balance">Direkter Draht</h2><p class="lead">Am schnellsten geht es per E-Mail oder Telefon. Beides landet direkt bei mir, nicht in einem Ticketsystem.</p><div class="dl"><div><dt>E-Mail</dt><dd><a href="mailto:info@dennyschulz.de">info@dennyschulz.de</a></dd></div><div><dt>Telefon</dt><dd><a href="tel:+491754083133">+49 175 4083133</a></dd></div><div><dt>Region</dt><dd>Neumünster · Schleswig-Holstein</dd></div><div><dt>Antwortzeit</dt><dd>Antwort in der Regel innerhalb von 24 Stunden</dd></div></div><div class="note">Termine vergebe ich nach Absprache — auch abends und am Wochenende, wenn es anders nicht passt.</div></div><div class="contact-card"><h2 class="balance" style="font-size:clamp(1.3rem,2vw,1.7rem);margin-bottom:1.2rem">Anfrage vorbereiten</h2><form class="form" data-contact-form novalidate action="mailto:info@dennyschulz.de" method="post" enctype="text/plain">
<div class="field-row">
@@ -76,9 +75,10 @@
<select id="cf-topic" name="topic">
<option>Businessfotografie</option>
<option>Portrait &amp; Model</option>
<option>Familien &amp; Paare</option>
<option>Familie</option>
<option>Minishooting</option>
<option>Videoprojekt</option>
<option>Video</option>
<option>Musik &amp; Bühne</option>
<option>Social-Media-Inhalte</option>
<option>Pro-bono-Anfrage</option>
<option>Etwas anderes</option>
@@ -127,23 +127,29 @@
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
<span class="brand__text"><span>desfoto</span><small>Fotografie &amp; Video</small></span>
</span>
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
<p class="footer__claim">desfoto ist ein Angebot von Denny Schulz, Neumünster.</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -159,7 +165,7 @@
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
</div>
</div>
<div class="footer__bottom">
@@ -168,7 +174,7 @@
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© Denny Schulz · Denny Schulz Fotografie · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© Denny Schulz · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,7 @@
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/" aria-current="page">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/" aria-current="page">Portrait &amp; Model</a><a href="/familien-und-paare/">Familien &amp; Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/" aria-current="page">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/" aria-current="page">Portrait &amp; Model</a><a href="/familie/">Familie</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/musik-und-buehne/">Musik &amp; Bühne</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
@@ -67,7 +67,7 @@
<h3>Draußen, wo du bist</h3>
<p class="card__text">Available light, Stadt, Wald, Wasser oder dein eigener Ort.
Lebendiger, weniger kontrolliert — und oft genau richtig für Portraits, die sich echt anfühlen.</p>
</div></div></div></section><section class="section section--paper2"><div class="shell"><div class="section-head"><div class="section-head__text stack"><span class="kicker">Beispiele</span><h2 class="balance">Portraits und Editorial</h2></div></div><div class="grid grid--3"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-pose-1200.webp" srcset="/assets/img/editorial-pose-480.webp 480w, /assets/img/editorial-pose-768.webp 768w, /assets/img/editorial-pose-1200.webp 1200w, /assets/img/editorial-pose-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2000" decoding="async" loading="lazy" alt="Editorial-Portrait vor schwarzem Hintergrund" style="object-position:50% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-back-1200.webp" srcset="/assets/img/editorial-back-480.webp 480w, /assets/img/editorial-back-768.webp 768w, /assets/img/editorial-back-1200.webp 1200w, /assets/img/editorial-back-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Detailaufnahme eines Rückens mit Sporttop" style="object-position:50% 40%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-black-1200.webp" srcset="/assets/img/editorial-black-480.webp 480w, /assets/img/editorial-black-768.webp 768w, /assets/img/editorial-black-1200.webp 1200w, /assets/img/editorial-black-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Model im Sporttop vor schwarzem Hintergrund" style="object-position:50% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-earring-1200.webp" srcset="/assets/img/editorial-earring-480.webp 480w, /assets/img/editorial-earring-768.webp 768w, /assets/img/editorial-earring-1200.webp 1200w, /assets/img/editorial-earring-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Portrait im Profil mit Ohrring" style="object-position:45% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-studio-side-1200.webp" srcset="/assets/img/editorial-studio-side-480.webp 480w, /assets/img/editorial-studio-side-768.webp 768w, /assets/img/editorial-studio-side-1200.webp 1200w, /assets/img/editorial-studio-side-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2399" decoding="async" loading="lazy" alt="Marta im seitlichen Studiolicht vor dunklem Hintergrund" style="object-position:50% 30%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-balance-1200.webp" srcset="/assets/img/editorial-balance-480.webp 480w, /assets/img/editorial-balance-768.webp 768w, /assets/img/editorial-balance-1200.webp 1200w, /assets/img/editorial-balance-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2000" decoding="async" loading="lazy" alt="Marta balanciert sitzend mit angehobenem Bein" style="object-position:50% 40%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-leap-1200.webp" srcset="/assets/img/editorial-leap-480.webp 480w, /assets/img/editorial-leap-768.webp 768w, /assets/img/editorial-leap-1200.webp 1200w, /assets/img/editorial-leap-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2399" decoding="async" loading="lazy" alt="Marta springt mit angewinkelten Beinen" style="object-position:50% 35%"><img class="frame frame--4x5 frame--hover" src="/assets/img/portrait-natural-1200.webp" srcset="/assets/img/portrait-natural-480.webp 480w, /assets/img/portrait-natural-768.webp 768w, /assets/img/portrait-natural-1200.webp 1200w, /assets/img/portrait-natural-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="1067" decoding="async" loading="lazy" alt="Natürliches Portrait im Freien" style="object-position:50% 30%"><img class="frame frame--4x5 frame--hover" src="/assets/img/people-kiss-1200.webp" srcset="/assets/img/people-kiss-480.webp 480w, /assets/img/people-kiss-768.webp 768w, /assets/img/people-kiss-1200.webp 1200w, /assets/img/people-kiss-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="1068" decoding="async" loading="lazy" alt="Paar küsst sich vor hellem Hintergrund" style="object-position:50% 32%"></div></div></section><section class="section section--ink">
</div></div></div></section><section class="section section--paper2"><div class="shell"><div class="section-head"><div class="section-head__text stack"><span class="kicker">Beispiele</span><h2 class="balance">Portraits und Editorial</h2></div></div><div class="grid grid--3"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-pose-1200.webp" srcset="/assets/img/editorial-pose-480.webp 480w, /assets/img/editorial-pose-768.webp 768w, /assets/img/editorial-pose-1200.webp 1200w, /assets/img/editorial-pose-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2000" decoding="async" loading="lazy" alt="Editorial-Portrait vor schwarzem Hintergrund" style="object-position:50% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-back-1200.webp" srcset="/assets/img/editorial-back-480.webp 480w, /assets/img/editorial-back-768.webp 768w, /assets/img/editorial-back-1200.webp 1200w, /assets/img/editorial-back-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Detailaufnahme eines Rückens mit Sporttop" style="object-position:50% 40%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-black-1200.webp" srcset="/assets/img/editorial-black-480.webp 480w, /assets/img/editorial-black-768.webp 768w, /assets/img/editorial-black-1200.webp 1200w, /assets/img/editorial-black-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Model im Sporttop vor schwarzem Hintergrund" style="object-position:50% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-earring-1200.webp" srcset="/assets/img/editorial-earring-480.webp 480w, /assets/img/editorial-earring-768.webp 768w, /assets/img/editorial-earring-1200.webp 1200w, /assets/img/editorial-earring-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2398" decoding="async" loading="lazy" alt="Portrait im Profil mit Ohrring" style="object-position:45% 28%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-studio-side-1200.webp" srcset="/assets/img/editorial-studio-side-480.webp 480w, /assets/img/editorial-studio-side-768.webp 768w, /assets/img/editorial-studio-side-1200.webp 1200w, /assets/img/editorial-studio-side-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2399" decoding="async" loading="lazy" alt="Marta im seitlichen Studiolicht vor dunklem Hintergrund" style="object-position:50% 30%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-balance-1200.webp" srcset="/assets/img/editorial-balance-480.webp 480w, /assets/img/editorial-balance-768.webp 768w, /assets/img/editorial-balance-1200.webp 1200w, /assets/img/editorial-balance-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2000" decoding="async" loading="lazy" alt="Marta balanciert sitzend mit angehobenem Bein" style="object-position:50% 40%"><img class="frame frame--4x5 frame--hover" src="/assets/img/editorial-leap-1200.webp" srcset="/assets/img/editorial-leap-480.webp 480w, /assets/img/editorial-leap-768.webp 768w, /assets/img/editorial-leap-1200.webp 1200w, /assets/img/editorial-leap-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2399" decoding="async" loading="lazy" alt="Marta springt mit angewinkelten Beinen" style="object-position:50% 35%"><img class="frame frame--4x5 frame--hover" src="/assets/img/free-portrait-light-1200.webp" srcset="/assets/img/free-portrait-light-480.webp 480w, /assets/img/free-portrait-light-768.webp 768w, /assets/img/free-portrait-light-1200.webp 1200w, /assets/img/free-portrait-light-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2400" decoding="async" loading="lazy" alt="Frauenportrait im weichen Seitenlicht vor dunklem Hintergrund" style="object-position:50% 30%"><img class="frame frame--4x5 frame--hover" src="/assets/img/free-modern-portrait-1200.webp" srcset="/assets/img/free-modern-portrait-480.webp 480w, /assets/img/free-modern-portrait-768.webp 768w, /assets/img/free-modern-portrait-1200.webp 1200w, /assets/img/free-modern-portrait-1600.webp 1600w" sizes="(min-width: 62rem) 30vw, 92vw" width="1600" height="2000" decoding="async" loading="lazy" alt="Portrait einer Frau mit Sonnenbrille" style="object-position:50% 30%"></div></div></section><section class="section section--ink">
<div class="shell">
<div class="section-head">
<div class="section-head__text stack">
@@ -90,23 +90,29 @@
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
<span class="brand__text"><span>desfoto</span><small>Fotografie &amp; Video</small></span>
</span>
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
<p class="footer__claim">desfoto ist ein Angebot von Denny Schulz, Neumünster.</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -122,7 +128,7 @@
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
</div>
</div>
<div class="footer__bottom">
@@ -131,7 +137,7 @@
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© Denny Schulz · Denny Schulz Fotografie · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© Denny Schulz · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"name": "desfoto",
"short_name": "desfoto",
"description": "desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster: Shootings für Business, Portrait, Model und Familien, Minishootings, das mobile Studio mit autarker Blitzanlage sowie Videoprojekte und Social-Media-Inhalte.",
"description": "desfoto steht für Fotografie, Video und mediale Produktionen aus Neumünster: Fotografie für Menschen und Unternehmen, Video für Unternehmen, Musik und Events, Musik- und Bühnenarbeiten sowie eigene Projekte.",
"start_url": "/",
"display": "standalone",
"background_color": "#F6F2EA",

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://desfoto.de/</loc><lastmod>2026-09-19</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>https://desfoto.de/shootings/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/fotografie/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/businessfotografie/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/portrait-und-model/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/familien-und-paare/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/familie/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/minishootings/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/video/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/musik-und-buehne/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/social-media/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://desfoto.de/projekte/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://desfoto.de/ueber/</loc><lastmod>2026-09-19</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>

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

View File

@@ -13,19 +13,22 @@ SITE = {
"url": "https://desfoto.de",
"claim": "Fotografie & Video",
"tagline": "Fotografie und Video aus Neumünster",
# The only sentence that ties the brand back to its owner; it belongs in the
# footer and nowhere else (the legal entity itself lives in the Impressum).
"ownership": "desfoto ist ein Angebot von Denny Schulz, Neumünster.",
"locale": "de_DE",
"language": "de",
"theme_color": "#17140F",
"default_description": (
"desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster: "
"Shootings für Business, Portrait, Model und Familien, Minishootings, das mobile "
"Studio mit autarker Blitzanlage sowie Videoprojekte und Social-Media-Inhalte."
"desfoto steht für Fotografie, Video und mediale Produktionen aus Neumünster: "
"Fotografie für Menschen und Unternehmen, Video für Unternehmen, Musik und Events, "
"Musik- und Bühnenarbeiten sowie eigene Projekte."
),
"keywords": (
"Fotograf Neumünster, Videograf Neumünster, Shootings, Businessfotografie, "
"Fotograf Neumünster, Videograf Neumünster, Fotografie, Businessfotografie, "
"Businessportrait, Model-Shooting, Familienfotograf, Minishooting, mobiles Studio, "
"Blitzanlage, Musikvideo, Konzertfotografie, Social Media Inhalte, "
"Schleswig-Holstein"
"Blitzanlage, Musikvideo, Konzertfotografie, Bandfotografie, Musik & Bühne, "
"Social Media Inhalte, Schleswig-Holstein"
),
}
@@ -68,61 +71,72 @@ VIDEO = {
NAV = [
{
"label": "Shootings",
"href": "/shootings/",
"label": "Fotografie",
"href": "/fotografie/",
"children": [
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
("Familien & Paare", "/familien-und-paare/"),
("Familie", "/familie/"),
("Minishootings", "/minishootings/"),
],
},
{"label": "Video", "href": "/video/"},
{"label": "Musik & Bühne", "href": "/musik-und-buehne/"},
{"label": "Projekte", "href": "/projekte/"},
{"label": "Über", "href": "/ueber/"},
]
# Die zwei Säulen der Arbeit: Foto (Shootings) und Video bleiben getrennt,
# damit beide Bereiche für sich verständlich bleiben.
PILLARS = [
# Die vier Arbeitsbereiche der Marke. Die Startseite erklärt desfoto darüber —
# erst der Bereich, dann die einzelnen buchbaren Leistungen darunter.
AREAS = [
{
"slug": "foto",
"label": "Foto",
"href": "/shootings/",
"title": "Shootings",
"lead": "Der Überbegriff für alle fotografischen Arbeiten.",
"slug": "fotografie",
"title": "Fotografie",
"scope": "Menschen, Unternehmen, Produkte, Veranstaltungen.",
"text": (
"Businessfotografie, Portrait & Model, Familien & Paare und Minishootings — "
"ein Shooting, ein Ablauf, ein Ansprechpartner. Du wählst nur, was du "
"wirklich brauchst."
"Portrait, Business, Familie und Minishootings — im Studio in Neumünster "
"oder mit dem mobilen Studio an deinem Ort."
),
"points": [
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
("Familien & Paare", "/familien-und-paare/"),
("Minishootings", "/minishootings/"),
],
"image": "business-portrait-studio",
"fit": "50% 30%",
"href": "/fotografie/",
"image": "business-portrait-sit",
"fit": "50% 35%",
},
{
"slug": "video",
"label": "Video",
"href": "/video/",
"title": "Videoprojekte",
"lead": "Bewegtbild von der Idee bis zum fertigen Schnitt.",
"title": "Video",
"scope": "Unternehmen, Musik, Events, Social Content.",
"text": (
"Musikvideo, Imagefilm, Eventfilm und Formate für Social Media — "
"gedreht, geschnitten und ausgeliefert in dem Format, in dem du es "
"veröffentlichst."
"Musikvideo, Imagefilm, Recruiting und Eventnachbericht — von der Idee "
"über den Dreh bis zum fertigen Export."
),
"points": [
("Videoprojekte", "/video/"),
("Social Media", "/social-media/"),
],
"image": "studio-lens",
"href": "/video/",
"image": "movement-light",
"fit": "50% 45%",
},
{
"slug": "musik-und-buehne",
"title": "Musik & Bühne",
"scope": "Bands, Künstler, Konzerte, Musikvideo.",
"text": (
"Live, Bandportraits und Musikvideo — als Musiker kenne ich beide Seiten "
"der Bühne."
),
"href": "/musik-und-buehne/",
"image": "music-drums",
"fit": "50% 45%",
},
{
"slug": "projekte",
"title": "Projekte",
"scope": "Freie Arbeiten, Kooperationen, besondere Produktionen.",
"text": (
"Landschaft, Tierwelt, Menschen und pro bono — die Arbeiten, die neben "
"den Aufträgen entstehen."
),
"href": "/projekte/",
"image": "free-mountain-view",
"fit": "50% 50%",
},
]
# Was in einem Shooting steckt — die gemeinsamen Eckpunkte aller Shootings.
@@ -158,7 +172,7 @@ STUDIO = {
{
"title": "Studio Neumünster",
"text": (
"Ein eigenes Studio in Neumünster steht für Shootings bereit:"
"Ein eigenes Studio in Neumünster steht für Fototermine bereit:"
" kontrolliertes Licht, ruhiger Hintergrund und Platz für"
" Konzeptarbeit. Gut für Portraits, Businessbilder und alles, was"
" eine wiederholbare Optik braucht — unabhängig vom Wetter."
@@ -200,7 +214,7 @@ STUDIO = {
"text": "Vom Büro in Neumünster bis zum Betrieb in Kiel, Lübeck oder Hamburg.",
},
],
"image": "studio-setup",
"image": "studio-lens",
}
# Netzwerk: eigene Projekte des Betreibers (ausdrücklicher Wunsch des Betreibers).
@@ -209,7 +223,7 @@ NETWORK = [
"label": "dennyschulz.de",
"href": "https://www.dennyschulz.de/",
"title": "dennyschulz.de",
"text": "Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.",
"text": "Hochzeiten und Paare — die Arbeit unter eigenem Namen, mit eigenem Schwerpunkt.",
},
{
"label": "dennyapp.de",
@@ -244,14 +258,14 @@ SERVICES = [
"fit": "50% 30%",
},
{
"slug": "familien-und-paare",
"href": "/familien-und-paare/",
"title": "Familien & Paare",
"slug": "familie",
"href": "/familie/",
"title": "Familie",
"short": (
"Familienshootings und Paarportraits, die nicht gestellt wirken. "
"Zeit lassen, mitgehen, im richtigen Moment auslösen."
"Familienshootings, die nicht gestellt wirken. Zeit lassen, mitgehen, "
"im richtigen Moment auslösen."
),
"image": "people-joy",
"image": "free-portrait-light",
"fit": "50% 30%",
},
{
@@ -262,7 +276,7 @@ SERVICES = [
"Die kurze Session für alle, die schnell ein gutes Bild brauchen: "
"klarer Ablauf, kleine Auswahl, kurze Lieferzeit."
),
"image": "portrait-natural",
"image": "free-modern-portrait",
"fit": "50% 30%",
},
{
@@ -284,39 +298,39 @@ SERVICES = [
"Foto- und Videocontent im Format deiner Kanäle — wiederkehrend, planbar "
"und über die Monate hinweg einheitlich."
),
"image": "event-motion",
"image": "event-speaker",
"fit": "50% 45%",
},
]
# Fotografie-Übersicht: Shootings als Überbegriff über den einzelnen Leistungen.
# Fotografie-Übersicht: die drei Themen, unter denen die einzelnen Angebote liegen.
PHOTO_OVERVIEW = [
{
"title": "Shootings",
"title": "Menschen",
"text": (
"Ein Überbegriff, vier Wege: Businessfotografie für Teams und Marken, "
"Portrait & Model für die persönliche Bildsprache, Familien & Paare für "
"die Menschen, die zu dir gehören, und Minishootings für alle, die schnell "
"ein gutes Bild brauchen."
"Portrait, Bewerbungsbilder, Familie und Model — im Studio in Neumünster, "
"an einem Ort deiner Wahl oder dort, wo dein Alltag stattfindet."
),
"image": "editorial-pose",
"fit": "50% 30%",
},
{
"title": "Studio & mobil",
"title": "Unternehmen & Marken",
"text": (
"Shootings im eigenen Studio in Neumünster oder mit dem mobilen Studio "
"inklusive autarker Blitzanlage, Hintergründen und Lichtformern — "
"aufgebaut an deinem Ort, statt umgekehrt."
"Team- und Standortbilder, Produkte und Kampagnenmotive — abgestimmt auf "
"die Flächen, auf denen sie später stehen."
),
"image": "business-portrait-sit",
"fit": "50% 30%",
},
{
"title": "Anlässe",
"title": "Veranstaltungen",
"text": (
"Feiern, Events, Vereinsleben und Premieren. Dokumentarisch begleitet, "
"ohne den Ablauf auszubremsen."
),
"image": "event-speaker",
"fit": "50% 40%",
},
]
@@ -329,10 +343,10 @@ WORK_GALLERY = [
("free-dog-husky", "Ein Hund auf einem sonnigen Waldweg", "g-w2", "1x1", "50% 45%"),
("editorial-colour", "Model-Shooting im Studio", "g-w3", "3x2", "50% 30%"),
("music-dreadlocks", "Musiker mit Dreadlocks vor einem Mikrofon", "g-w3", "3x2", "50% 35%"),
("couple-backlight-kiss", "Paar küsst sich im Gegenlicht", "g-w3", "4x5", "50% 45%"),
("free-woman-coast", "Eine Frau mit Hut steht an einer felsigen Küste", "g-w3", "4x5", "50% 40%"),
("free-fog-forest", "Nebel über einem Nadelwald", "g-w3", "3x2", "50% 45%"),
("free-cat", "Nahaufnahme einer getigerten Katze", "g-w2", "1x1", "50% 42%"),
("couple-sun-silhouette", "Silhouetten eines Paares vor der Sonne", "g-w3", "3x2", "50% 50%"),
("portrait-marina-sunset", "Silhouette einer Frau vor orangefarbenem Abendhimmel", "g-w3", "3x2", "50% 50%"),
]
PROCESS = [
@@ -387,11 +401,12 @@ FACTS = [
]
FOCUS_ITEMS = [
("Shootings", "/shootings/"),
("Fotografie", "/fotografie/"),
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
("Familien & Paare", "/familien-und-paare/"),
("Familie", "/familie/"),
("Minishootings", "/minishootings/"),
("Videoprojekte", "/video/"),
("Video", "/video/"),
("Musik & Bühne", "/musik-und-buehne/"),
("Social Media", "/social-media/"),
]

View File

@@ -9,12 +9,6 @@
"alt": "Kameraobjektiv im Studiolicht",
"focus": "50% 50%"
},
{
"slug": "studio-setup",
"raw": "Studio/1777027120196_c501de64_dennyschulz_-05467.jpg",
"alt": "Studioequipment mit Logo von Denny Schulz Fotografie",
"focus": "50% 50%"
},
{
"slug": "gear-cases",
"raw": "1778949186414_b1648d20_dennyschulz_-05400-2.jpg",
@@ -75,36 +69,6 @@
"alt": "Fashion-Portrait in kräftigen Farben",
"focus": "50% 30%"
},
{
"slug": "portrait-natural",
"raw": "1784293240857_deae4788_dennyschulz_-07255.jpg",
"alt": "Natürliches Portrait im Freien",
"focus": "50% 30%"
},
{
"slug": "portrait-couple",
"raw": "1784293231735_a8ccbd3e_dennyschulz_-08620.jpg",
"alt": "Paarportrait im Grünen",
"focus": "50% 30%"
},
{
"slug": "people-joy",
"raw": "1776343134779_e893c351_dennyschulz_-03745114107.jpg",
"alt": "Paar lacht im Abendlicht",
"focus": "50% 30%"
},
{
"slug": "people-kiss",
"raw": "1776343151721_e3f65ef1_dennyschulz_-04483.jpg",
"alt": "Paar küsst sich vor hellem Hintergrund",
"focus": "50% 30%"
},
{
"slug": "event-motion",
"raw": "1776670469051_dc2993ae_dennyschulz_-04663.jpg",
"alt": "Bewegung auf einer Veranstaltung",
"focus": "50% 45%"
},
{
"slug": "event-speaker",
"raw": "1784293225986_a37d21f4_dennyschulz_-07139.jpg",
@@ -117,12 +81,6 @@
"alt": "Veranstaltungsort mit Gästen",
"focus": "50% 45%"
},
{
"slug": "event-dance",
"raw": "1784293226489_fa1b9420_dennyschulz_-09046.jpg",
"alt": "Tanzfläche bei einer Feier",
"focus": "50% 40%"
},
{
"slug": "movement-light",
"raw": "1788850842323_a5ae410e_280826_shooting-69.jpg",
@@ -353,41 +311,6 @@
"alt": "Marta springt mit angewinkelten Beinen",
"focus": "50% 35%"
},
{
"slug": "couple-park",
"source": "legacy",
"raw": "dennyschulz-5142.jpg",
"alt": "Sara und Carlos stehen Arm in Arm im Grünen",
"focus": "50% 30%"
},
{
"slug": "couple-backlight-kiss",
"source": "legacy",
"raw": "dennyschulz-5191.jpg",
"alt": "Sara und Carlos küssen sich im Gegenlicht",
"focus": "50% 45%"
},
{
"slug": "couple-forest-walk",
"source": "legacy",
"raw": "dennyschulz-4415.jpg",
"alt": "Sara und Carlos gehen Hand in Hand durch den Wald",
"focus": "50% 35%"
},
{
"slug": "couple-sun-silhouette",
"source": "legacy",
"raw": "dennyschulz-2076.jpg",
"alt": "Die Silhouetten des Paars rahmen die tief stehende Sonne",
"focus": "50% 50%"
},
{
"slug": "couple-laugh-walk",
"source": "legacy",
"raw": "dennyschulz-0368.jpg",
"alt": "Tamara und Tobi lachen auf einem Waldweg",
"focus": "50% 35%"
},
{
"slug": "portrait-marina-snow",
"source": "legacy",

View File

@@ -199,23 +199,29 @@ def footer_html() -> str:
{BRAND_MARK}
<span class="brand__text"><span>desfoto</span><small>{e(SITE['claim'])}</small></span>
</span>
<p class="footer__claim">{e(SITE['tagline'])}. Ein Angebot von {e(LEGAL['business'])}.</p>
<p class="footer__claim">{e(SITE['ownership'])}</p>
</div>
<div>
<h2>Shootings</h2>
<h2>Fotografie</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/fotografie/">Überblick Fotografie</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
<a href="/familie/">Familie</a>
<a href="/minishootings/">Minishootings</a>
</div>
</div>
<div>
<h2>Video &amp; mehr</h2>
<h2>Video &amp; Bühne</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/musik-und-buehne/">Musik &amp; Bühne</a>
<a href="/social-media/">Social Media</a>
</div>
</div>
<div>
<h2>Mehr</h2>
<div class="footer__links">
<a href="/projekte/">Projekte</a>
<a href="/ueber/">Über desfoto</a>
</div>
@@ -240,7 +246,7 @@ def footer_html() -> str:
<a href="/datenschutz/">Datenschutz</a>
<a href="/kontakt/">Kontakt</a>
</nav>
<span>© {e(LEGAL['owner'])} · {e(LEGAL['business'])} · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
<span>© {e(LEGAL['owner'])} · Alle Rechte an den gezeigten Bildern und Filmen vorbehalten.</span>
</div>
</div>
</footer>
@@ -296,17 +302,20 @@ def page_head(
f'<div class="breadcrumb"><a href="/">Start</a><span>/</span><span>{e(breadcrumb[0])}</span></div>'
)
actions_html = f'<div class="page-head__actions">{actions}</div>' if actions else ""
# Bedingte Zeilen tragen ihren Zeilenumbruch selbst. Sonst bleibt bei leerem
# Wert eine Zeile aus Einrückung stehen — trailing whitespace, das das
# Release-Gateway (`git diff --check`) ablehnt.
crumb_line = f" {crumb}\n" if crumb else ""
actions_line = f" {actions_html}\n" if actions_html else ""
return f"""<section class="page-head">
<div class="page-head__bg">
{picture(image, "100vw", css_class="", priority=True, loading="eager", alt="", fit=fit)}
</div>
<div class="shell page-head__inner">
{crumb}
<span class="kicker">{e(kicker)}</span>
{crumb_line} <span class="kicker">{e(kicker)}</span>
<h1 class="balance">{e(title)}</h1>
<p class="page-head__lead">{lead}</p>
{actions_html}
</div>
{actions_line} </div>
</section>"""

View File

@@ -7,12 +7,12 @@ canonical paths are returned by the ROUTES table at the bottom of the module.
from __future__ import annotations
from content import (
AREAS,
CONTACT,
FACTS,
LEGAL,
NETWORK,
PHOTO_OVERVIEW,
PILLARS,
PROCESS,
SERVICES,
SHOOTING_STEPS,
@@ -81,6 +81,9 @@ def video_block(*, dark: bool = True, with_meta: bool = True) -> str:
if with_meta
else ""
)
# Ohne Meta-Zeile darf die Einrückung nicht stehenbleiben: eine Zeile aus zwei
# Leerzeichen ist trailing whitespace und `git diff --check` lehnt sie ab.
meta_line = f" {meta}\n" if meta else ""
return f"""<div class="video-shell">
<button class="video-facade" type="button" data-video-id="{e(VIDEO['id'])}"
aria-label="Video starten: {e(VIDEO['title'])} (lädt YouTube nach Klick)">
@@ -90,8 +93,7 @@ def video_block(*, dark: bool = True, with_meta: bool = True) -> str:
<span class="video-facade__label">Video starten</span>
</span>
</button>
{meta}
</div>
{meta_line}</div>
<p class="form__hint" style="margin-top:.8rem">Das Video wird erst nach deinem Klick von YouTube geladen (youtube-nocookie.com). Mehr dazu in der <a href="/datenschutz/">Datenschutzerklärung</a>.</p>"""
@@ -146,39 +148,27 @@ def caption_gallery(
return f'<div class="gallery" data-stagger="{stagger}">{figures}</div>'
def pillar_cards() -> str:
"""The two big entry cards: Shootings (photo) and Videoprojekte (video).
def area_cards() -> str:
"""The four Arbeitsbereiche of the brand.
The body carries its own links, so the card must not be one great ``<a>``:
Fotografie, Video, Musik & Bühne and Projekte replace the old flat service
list: the area comes first, the bookable services sit underneath it. Each
card carries its own link, so the card itself must not be one great ``<a>``:
nested anchors are invalid HTML and the parser would break the grid apart.
``.pillar__go`` stretches over the card and keeps the whole surface clickable.
"""
cards = []
for pillar in PILLARS:
points = "".join(
f'<a href="{e(href)}">{e(label)}</a>' for label, href in pillar["points"]
)
for area in AREAS:
cards.append(
'<article class="pillar spot reveal">'
'<span class="pillar__media">'
+ picture(
pillar["image"],
"(min-width: 62rem) 46vw, 92vw",
css_class="",
fit=pillar.get("fit"),
alt="",
)
+ "</span>"
'<div class="pillar__body">'
f'<span class="pillar__label">{e(pillar["label"])}</span>'
f'<h3 class="pillar__title">{e(pillar["title"])}</h3>'
f'<p class="pillar__text">{e(pillar["lead"])} {e(pillar["text"])}</p>'
f'<div class="pillar__points">{points}</div>'
f'<a class="pillar__go" href="{e(pillar["href"])}">'
f'{e(pillar["title"])} ansehen {ICONS["arrow"]}</a>'
"</div></article>"
'<article class="card reveal">'
f'<div>{picture(area["image"], "(min-width: 72rem) 22vw, (min-width: 40rem) 44vw, 92vw", css_class="frame frame--16x10", fit=area.get("fit"))}</div>'
f'<h3 class="card__title">{e(area["title"])}</h3>'
f'<p class="eyebrow">{e(area["scope"])}</p>'
f'<p class="card__text">{e(area["text"])}</p>'
f'<div class="card__foot"><a class="link-arrow" href="{e(area["href"])}">'
f'{e(area["title"])} ansehen {ICONS["arrow"]}</a></div>'
"</article>"
)
return '<div class="pillars" data-stagger="0.12">' + "".join(cards) + "</div>"
return '<div class="grid grid--areas" data-stagger="0.08">' + "".join(cards) + "</div>"
def marquee(items: list[str]) -> str:
@@ -211,7 +201,6 @@ def studio_section(*, variant: str = "paper2", tight: bool = False) -> str:
"(min-width: 62rem) 46vw, 92vw",
css_class="frame frame--4x5",
fit="50% 45%",
alt="Studioequipment mit Logo von Denny Schulz Fotografie",
)
+ '<span class="studio__badge"><strong>Autarke Blitzanlage</strong>'
"<span>Hintergründe, Blitze, Lichtformer</span></span>"
@@ -250,9 +239,10 @@ def contact_form() -> str:
<select id="cf-topic" name="topic">
<option>Businessfotografie</option>
<option>Portrait &amp; Model</option>
<option>Familien &amp; Paare</option>
<option>Familie</option>
<option>Minishooting</option>
<option>Videoprojekt</option>
<option>Video</option>
<option>Musik &amp; Bühne</option>
<option>Social-Media-Inhalte</option>
<option>Pro-bono-Anfrage</option>
<option>Etwas anderes</option>
@@ -331,19 +321,19 @@ def home() -> str:
<span class="kicker hero__kicker">{e(SITE['tagline'])}</span>
<h1 class="hero__title">{words('Bilder von Menschen, Marken und Musik.')}</h1>
<p class="hero__sub">
desfoto ist die fotografische und filmische Arbeit von Denny Schulz aus Neumünster:
Shootings für Business, Portrait und Familie, dazu Konzerte, eigene Projekte,
Video und Inhalte für Social Media.
desfoto ist Fotografie, Video und mediale Produktion aus Neumünster:
Bilder für Menschen und Unternehmen, Bewegtbild für Marken, Musik und Events,
Bühnenarbeiten und eigene Projekte.
</p>
<div class="btn-row hero__actions">
<a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a>
<a class="btn btn--outline-light" href="/projekte/">Arbeiten ansehen</a>
</div>
<div class="hero__facts">
<div class="hero__fact"><strong>Shootings</strong>Business, Portrait, Familie und Minishootings unter einem Dach.</div>
<div class="hero__fact"><strong>Studio &amp; vor Ort</strong>Eigenes Studio in Neumünster oder mobiles Studio inklusive autarker Blitzanlage bei dir.</div>
<div class="hero__fact"><strong>Foto &amp; Video</strong>Beides aus einer Hand, damit Optik und Ton zusammenpassen.</div>
<div class="hero__fact"><strong>Musik &amp; Bühne</strong>Konzerte, Bands und Musikvideos aus eigener Erfahrung.</div>
<div class="hero__fact"><strong>Klarer Ablauf</strong>Umfang, Termin und Preis sind vor der Buchung geklärt.</div>
</div>
</div>
<span class="scroll-cue" aria-hidden="true">Scroll<i></i></span>
@@ -361,22 +351,29 @@ def home() -> str:
</div>""",
)
pillars = section(
kicker="Zwei Bereiche",
title="Shootings und Videoprojekte",
lead="Foto und Video bleiben getrennt, damit beide Bereiche für sich verständlich sind — und kommen trotzdem von derselben Person.",
content=pillar_cards(),
areas = section(
kicker="Arbeitsbereiche",
title="Fotografie, Video, Musik & Bühne und Projekte",
lead="Vier Bereiche, in denen ich arbeite. Was du davon buchen kannst, steht direkt darunter — die freien Arbeiten laufen daneben weiter.",
content=area_cards(),
)
bookable = section(
variant="paper2",
kicker="Buchen",
title="Was du bei mir buchen kannst",
lead="Auftragsarbeiten mit klarem Umfang: Businessfotografie, Portrait & Model, Familie, Minishootings, Video und Social-Media-Inhalte.",
content=service_cards(),
)
works = section(
variant="paper2",
kicker="Portfolio",
title="Musik, Menschen und freie Arbeiten",
lead="Konzerte und Bands, Portraits und Business, dazu eigene Strecken aus Landschaft und Tierwelt. Das ist die Bandbreite meiner Arbeit.",
title="Was ich außerdem mache",
lead="Freie Arbeiten, Musik & Bühne und pro bono — Projekte, die nicht jede Woche auf einer Rechnung stehen. Hier muss kein Bild ein Angebot verkaufen.",
content=work_gallery()
+ '<div class="btn-row" style="margin-top:2rem">'
'<a class="btn btn--ink" href="/projekte/">Alle Projekte</a>'
'<a class="btn btn--ghost" href="/shootings/">Shootings ansehen</a></div>',
'<a class="btn btn--ghost" href="/musik-und-buehne/">Musik &amp; Bühne</a></div>',
)
video_sec = f"""<section class="section section--ink">
@@ -422,7 +419,6 @@ def home() -> str:
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": SITE["brand"],
"alternateName": LEGAL["business"],
"description": SITE["default_description"],
"url": SITE["url"] + "/",
"image": SITE["url"] + "/assets/img/og-desfoto.jpg",
@@ -442,19 +438,23 @@ def home() -> str:
],
body=hero
+ statement
+ pillars
+ areas
+ marquee(
[
"Fotografie",
"Video",
"Musik & Bühne",
"Projekte",
"Businessfotografie",
"Portrait & Model",
"Familien & Paare",
"Familie",
"Minishootings",
"Mobiles Studio",
"Videoprojekte",
"Musikvideo",
"Social Media",
]
)
+ bookable
+ works
+ video_sec
+ studio
@@ -464,15 +464,15 @@ def home() -> str:
)
def shootings() -> str:
path = "/shootings/"
def fotografie() -> str:
path = "/fotografie/"
overview = "".join(
'<article class="card reveal"><div>'
+ picture(
item["image"],
"(min-width: 62rem) 30vw, 92vw",
css_class="frame frame--4x5",
fit="50% 30%",
fit=item.get("fit"),
alt=item["title"],
)
+ f'</div><h3>{e(item["title"])}</h3><p class="card__text">{e(item["text"])}</p></article>'
@@ -480,35 +480,35 @@ def shootings() -> str:
)
body = (
page_head(
kicker="Shootings",
title="Shootings — von Business bis Familie",
lead="Alles, was ich fotografisch mache, läuft unter dem Begriff Shooting: vier Schwerpunkte, ein Ablauf, ein Ansprechpartner.",
kicker="Fotografie",
title="Fotografie — Menschen, Unternehmen, Produkte, Veranstaltungen",
lead="Alles, was ich fotografisch mache: drei Themen, vier Schwerpunkte, ein Ablauf und ein Ansprechpartner — im Studio in Neumünster oder mit dem mobilen Studio bei dir.",
image="editorial-earring",
breadcrumb=("Shootings", path),
breadcrumb=("Fotografie", path),
fit="45% 30%",
actions='<a class="btn btn--solid" href="/kontakt/">Shooting anfragen</a>'
actions='<a class="btn btn--solid" href="/kontakt/">Fototermin anfragen</a>'
'<a class="btn btn--outline-light" href="/projekte/">Arbeiten ansehen</a>',
)
+ section(
kicker="Überblick",
title="Ein Überbegriff, vier Wege",
title="Drei Themen, ein Blick",
lead="Du musst dich nicht für eine Schublade entscheiden — erzähl einfach, worum es geht.",
content=f'<div class="grid grid--3" data-stagger="0.09">{overview}</div>',
)
+ section(
variant="paper2",
kicker="Schwerpunkte",
title="Die vier Shootings im Detail",
title="Die vier buchbaren Schwerpunkte",
content=service_cards(
["businessfotografie", "portrait-und-model", "familien-und-paare", "minishootings"]
["businessfotografie", "portrait-und-model", "familie", "minishootings"]
),
tight=True,
)
+ studio_section(variant="")
+ section(
kicker="Ablauf",
title="So läuft ein Shooting",
lead="Bei jedem Shooting derselbe Weg — vom ersten Satz bis zur fertigen Datei.",
title="So läuft ein Termin",
lead="Bei jedem Auftrag derselbe Weg — vom ersten Satz bis zur fertigen Datei.",
content='<div class="steps">'
+ "".join(
f'<div class="step reveal"><h3>{e(step["title"])}</h3><p>{e(step["text"])}</p></div>'
@@ -519,16 +519,16 @@ def shootings() -> str:
+ section(
variant="paper2",
kicker="Beispiele",
title="Aus Shootings und Aufträgen",
title="Aus Aufträgen und eigener Arbeit",
content=media_grid(
[
("business-portrait-dark", "Portrait eines Mannes vor dunklem Hintergrund", "4x5", "50% 28%"),
("editorial-colour", "Fashion-Portrait in kräftigen Farben", "4x5", "50% 30%"),
("portrait-couple", "Paarportrait im Grünen", "4x5", "50% 30%"),
("editorial-balance", "Editorial-Pose mit ausbalancierter Haltung", "4x5", "50% 40%"),
("portrait-marina-sunset", "Silhouette einer Frau vor orangefarbenem Abendhimmel", "4x5", "50% 40%"),
("business-portrait-stool", "Sitzendes Portrait im weichen Studiolicht", "3x2", "50% 35%"),
("business-hotel-window", "Drei Frauen in Bademänteln und Handtuchturbanen am Fenster", "3x2", "50% 40%"),
("portrait-natural", "Natürliches Portrait im Freien", "3x2", "50% 32%"),
("couple-backlight-kiss", "Sara und Carlos küssen sich im Gegenlicht", "1x1", "50% 45%"),
("portrait-marina-snow", "Marina mit roten Zöpfen auf einem verschneiten Waldweg", "3x2", "50% 30%"),
("free-woman-coast", "Eine Frau mit Hut steht an einer felsigen Küste", "1x1", "50% 40%"),
("editorial-leap", "Marta springt mit angewinkelten Beinen", "1x1", "50% 35%"),
("free-dog-husky", "Ein Hund steht auf einem sonnigen Waldweg", "1x1", "50% 45%"),
]
@@ -558,22 +558,22 @@ def shootings() -> str:
),
)
+ cta_band(
title="Welches Shooting passt zu dir?",
title="Welcher Bereich passt zu dir?",
text="Schreib kurz, worum es geht und wann es stattfinden soll. Du bekommst Ablauf, Umfang und Preis.",
secondary=("Minishootings ansehen", "/minishootings/"),
)
)
return page(
title="Shootings in Neumünster · desfoto",
description="Shootings aus Neumünster: Businessfotografie, Portrait & Model, Familien & Paare und Minishootings — mit mobilem Studio inklusive autarker Blitzanlage.",
title="Fotografie in Neumünster · desfoto",
description="Fotografie aus Neumünster: Businessfotografie, Portrait & Model, Familie und Minishootings — mit mobilem Studio inklusive autarker Blitzanlage.",
path=path,
json_ld=[
service_ld(
"Shootings",
"Fotografie",
path,
"Fotografische Shootings für Business, Portrait & Model, Familien und Paare.",
"Fotografie für Menschen, Unternehmen, Produkte und Veranstaltungen.",
),
breadcrumb_ld("Shootings", path),
breadcrumb_ld("Fotografie", path),
],
body=body,
)
@@ -655,7 +655,7 @@ def businessfotografie() -> str:
("business-hotel-window", "Drei Frauen in Bademänteln und Handtuchturbanen am Fenster", "4x5", "50% 40%"),
("business-hotel-tea", "Drei Frauen mit Teetassen und Handtuchturbanen", "4x5", "50% 45%"),
("gear-cases", "Kamerakoffer und Objektive vor einem Studiorack", "3x2", "50% 50%"),
("studio-setup", "Studioequipment mit Logo von Denny Schulz Fotografie", "3x2", "50% 45%"),
("business-portrait-sit", "Sitzendes Portrait mit Kameratasche im Studio", "3x2", "50% 35%"),
("business-hotel-orange", "Frauen im Bademantel halten Orangenscheiben vor ihre Gesichter", "3x2", "50% 50%"),
("business-hotel-laugh", "Drei Frauen lachen mit Orangenscheiben in den Händen", "3x2", "50% 45%"),
],
@@ -765,8 +765,8 @@ def portrait_und_model() -> str:
("editorial-studio-side", "50% 30%"),
("editorial-balance", "50% 40%"),
("editorial-leap", "50% 35%"),
("portrait-natural", "50% 30%"),
("people-kiss", "50% 32%"),
("free-portrait-light", "50% 30%"),
("free-modern-portrait", "50% 30%"),
]
)
+ "</div>",
@@ -789,21 +789,21 @@ def portrait_und_model() -> str:
)
def familien_und_paare() -> str:
path = "/familien-und-paare/"
def familie() -> str:
path = "/familie/"
body = (
page_head(
kicker="Familien & Paare",
kicker="Familie",
title="Wachsendes festhalten, ohne Pose",
lead="Familienshootings und Paarportraits, die nicht gestellt wirken. Zeit lassen, mitgehen und im richtigen Moment auslösen.",
image="people-joy",
breadcrumb=("Familien & Paare", path),
fit="50% 30%",
lead="Familienshootings, die nicht gestellt wirken. Zeit lassen, mitgehen und im richtigen Moment auslösen.",
image="free-portrait-light",
breadcrumb=("Familie", path),
fit="50% 28%",
actions='<a class="btn btn--solid" href="/kontakt/">Termin anfragen</a>',
)
+ section(
kicker="Anlässe",
title="Für welche Momente mich Familien und Paare buchen",
title="Für welche Momente mich Familien buchen",
content='<div class="grid grid--3">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
@@ -812,18 +812,6 @@ def familien_und_paare() -> str:
"Familienshooting",
"Alle zusammen, ohne dass es nach Fototermin aussieht. Auch mit mehreren Generationen.",
),
(
"Paarshooting",
"Zwei Menschen, die sich nah sind. Vor der Hochzeit, zum Jahrestag oder einfach so.",
),
(
"Verlobung & Ja-Wort",
"Kleine Zeremonien, Standesamt, freie Trauung — begleitend oder als Portraitstrecke.",
),
(
"Jubiläum & Familientreffen",
"Wenn die ganze Verwandtschaft zusammenkommt, lohnt sich ein gemeinsames Bild.",
),
(
"Generationenbilder",
"Großeltern, Eltern, Enkel: Bilder, die in Jahren mehr wert sind als heute.",
@@ -832,6 +820,18 @@ def familien_und_paare() -> str:
"Kinderportraits",
"Kurz, spielerisch und geduldig — lieber zehn ehrliche Bilder als eine Stunde Stillhalten.",
),
(
"Geschwister & Patchwork",
"Nicht jede Familie sieht gleich aus. Gezeigt wird, wer zu euch gehört.",
),
(
"Jubiläum & Familientreffen",
"Wenn die ganze Verwandtschaft zusammenkommt, lohnt sich ein gemeinsames Bild.",
),
(
"Bei euch oder im Studio",
"Zuhause, im Park oder im Studio in Neumünster: dort, wo ihr euch wohlfühlt.",
),
]
)
+ "</div>",
@@ -845,20 +845,16 @@ def familien_und_paare() -> str:
)
+ section(
kicker="Beispiele",
title="Paare und Menschen",
title="Menschen aus meiner Arbeit",
lead="Familienshootings laufen ruhig und ohne Regieanweisungen ab. Genau so entstehen die Portraits, die ich hier zeige.",
content='<div class="grid grid--3">'
+ "".join(
picture(slug, "(min-width: 62rem) 30vw, 92vw", css_class="frame frame--4x5", fit=fit)
for slug, fit in [
("portrait-natural", "50% 30%"),
("people-kiss", "50% 32%"),
("portrait-couple", "50% 30%"),
("couple-park", "50% 30%"),
("couple-backlight-kiss", "50% 45%"),
("couple-forest-walk", "50% 35%"),
("couple-laugh-walk", "50% 35%"),
("couple-sun-silhouette", "50% 50%"),
("free-lake-person", "50% 45%"),
("portrait-marina-snow", "50% 30%"),
("free-modern-portrait", "50% 30%"),
("free-woman-coast", "50% 40%"),
("portrait-marina-sunset", "50% 45%"),
]
)
@@ -871,12 +867,16 @@ def familien_und_paare() -> str:
)
)
return page(
title="Familien- & Paarfotografie in Neumünster · desfoto",
description="Familien- und Paarshootings aus Neumünster: natürliche Bilder ohne Pose, im Studio, in der Natur oder bei euch zuhause.",
title="Familienfotografie in Neumünster · desfoto",
description="Familienshootings aus Neumünster: natürliche Bilder ohne Pose, im Studio, in der Natur oder bei euch zuhause.",
path=path,
json_ld=[
service_ld("Familien- und Paarfotografie", path, "Familienshootings und Paarportraits ohne gestellte Posen."),
breadcrumb_ld("Familien & Paare", path),
service_ld(
"Familienfotografie",
path,
"Familienshootings ohne gestellte Posen, im Studio, in der Natur oder zuhause.",
),
breadcrumb_ld("Familie", path),
],
body=body,
)
@@ -889,7 +889,7 @@ def minishootings() -> str:
kicker="Minishootings",
title="Kurze Session, sofort brauchbares Bild",
lead="Der Termin für alle, die schnell ein gutes Bild brauchen: klarer Ablauf, kleine Auswahl, kurze Lieferzeit. Meist in rund 20 bis 30 Minuten erledigt.",
image="portrait-natural",
image="free-modern-portrait",
breadcrumb=("Minishootings", path),
fit="50% 30%",
actions='<a class="btn btn--solid" href="/kontakt/">Minishooting buchen</a>',
@@ -902,11 +902,11 @@ def minishootings() -> str:
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
("Profilbild", "Für LinkedIn, Bewerbung, Vereinsseite oder die Website."),
("Paar & Familie", "Ein, zwei schöne Bilder zusammen — ohne großen Termin."),
("Familie", "Eltern und Kinder, ohne großen Termin — ein, zwei schöne Bilder zusammen."),
("Social Media", "Frisches Material für deine Kanäle, passend zum Format."),
("Verein & Mannschaft", "Schnell alle Gesichter für Mannschaftsfotos und Anmeldungen."),
("Businesskurzportrait", "Du brauchst nur ein Bild für die neue Stelle oder Präsentation."),
("Geschenkidee", "Gutschein für ein gemeinsames Shooting — auf Anfrage."),
("Geschenkidee", "Gutschein für ein Shooting — auf Anfrage."),
]
)
+ "</div>",
@@ -958,11 +958,11 @@ def minishootings() -> str:
+ "".join(
picture(slug, "(min-width: 62rem) 30vw, 92vw", css_class="frame frame--4x5", fit=fit)
for slug, fit in [
("portrait-natural", "50% 28%"),
("free-woman-coast", "50% 30%"),
("business-portrait-studio", "50% 28%"),
("editorial-black", "50% 28%"),
("editorial-studio-side", "50% 30%"),
("free-modern-portrait", "50% 30%"),
("free-portrait-light", "50% 30%"),
("portrait-marina-snow", "50% 30%"),
]
)
@@ -1006,6 +1006,7 @@ def video() -> str:
<h2 class="balance">{e(VIDEO['title'])}</h2>
<p class="lead measure" style="color:rgba(246,242,234,.82)">{e(VIDEO['description'])}</p>
</div>
<a class="link-arrow" style="color:#F6F2EA;border-color:rgba(246,242,234,.4)" href="/musik-und-buehne/">Musik &amp; Bühne {ICONS['arrow']}</a>
</div>
<div style="margin-top:clamp(1.6rem,3vw,2.6rem)">{video_block()}</div>
</div>
@@ -1102,7 +1103,7 @@ def video() -> str:
content=caption_gallery(
[
("studio-lens", "Brennweite und Licht im Studio", "g-w3", "3x2", "50% 50%"),
("event-motion", "Bewegung im Bild", "g-w3", "3x2", "50% 45%"),
("band-extinct-group", "Band vor der Kamera", "g-w3", "3x2", "50% 40%"),
("movement-light", "Lange Belichtung", "g-w2", "1x1", "50% 50%"),
("event-speaker", "Statement vor der Kamera", "g-w2", "1x1", "50% 30%"),
("event-venue", "Location vor dem Dreh", "g-w2", "1x1", "50% 45%"),
@@ -1129,6 +1130,116 @@ def video() -> str:
)
def musik_und_buehne() -> str:
path = "/musik-und-buehne/"
body = (
page_head(
kicker="Musik & Bühne",
title="Bands, Künstler und Konzerte",
lead="Live, Bandportraits und Musikvideo — als Musiker kenne ich beide Seiten der Bühne.",
image="music-vocal-blue",
breadcrumb=("Musik & Bühne", path),
fit="50% 30%",
actions='<a class="btn btn--solid" href="/kontakt/">Musikprojekt anfragen</a>'
'<a class="btn btn--outline-light" href="/projekte/">Arbeiten ansehen</a>',
)
+ section(
kicker="Für Bands und Veranstalter",
title="Was ich für Musik und Bühne mache",
content='<div class="grid grid--3" data-stagger="0.08">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
(
"Live & Konzert",
"Bühnenbilder aus Clubs, Festivals und Vereinsheimen — mitten im Geschehen statt am Rand, auch bei wenig Licht.",
),
(
"Band- & Künstlerportraits",
"Pressebilder, Bandportraits und Artwork-Motive, die zur Musik passen.",
),
(
"Musikvideo",
"Konzept, Dreh, Schnitt und Farbkorrektur — für Songs, die ein Bild brauchen.",
),
(
"Promotion & Content",
"Standbilder und Kurzclips für Ankündigungen, Kanäle und Presse.",
),
]
)
+ "</div>",
)
+ f"""<section class="section section--ink">
<div class="shell">
<div class="section-head">
<div class="section-head__text stack">
<span class="kicker">Musikvideo</span>
<h2 class="balance">{e(VIDEO['title'])}</h2>
<p class="lead measure" style="color:rgba(246,242,234,.82)">{e(VIDEO['description'])}</p>
</div>
<a class="link-arrow" style="color:#F6F2EA;border-color:rgba(246,242,234,.4)" href="/video/">Mehr zu Video {ICONS['arrow']}</a>
</div>
<div style="margin-top:clamp(1.6rem,3vw,2.6rem)">{video_block(with_meta=False)}</div>
</div>
</section>"""
+ section(
variant="paper2",
kicker="Referenz",
title="Musikvideo Thjódrörir: Konzept, Dreh, Schnitt, Farbkorrektur",
lead="Der komplette Weg vom Song zum fertigen Video — ohne zweite Crew und ohne Zwischenabstimmungen zwischen Dienstleistern.",
content='<div class="grid grid--4" data-stagger="0.08">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
("Konzept", "Idee, Bildsprache und Drehorte, abgestimmt auf den Song."),
("Dreh", "Kamera, Licht und Regie am Drehtag — aus einer Hand."),
("Schnitt", "Schnitt und Aufbau des Videos in der Länge, die der Song vorgibt."),
("Farbkorrektur", "Farbkorrektur und Ausgabe in den Formaten, die YouTube und die Kanäle brauchen."),
]
)
+ "</div>",
)
+ section(
kicker="Aus der Arbeit",
title="Konzerte, Bands, Backstage",
lead="Für Extinct und Sagenbringer entstehen Bilder und Videoarbeiten — live, auf der Probe und für Ankündigungen.",
content=media_grid(
[
("music-guitar-silhouette", "Gitarrist als Silhouette im Bühnennebel", "4x5", "50% 42%"),
("band-extinct-guitar", "Zwei Bandmitglieder stehen Rücken an Rücken mit Gitarre", "4x5", "50% 35%"),
("music-neck", "Hand am Gitarrenhals im dunklen Bühnenlicht", "4x5", "50% 50%"),
("music-mic", "Bärtiger Sänger am Mikrofon mit erhobener Hand", "3x2", "50% 35%"),
("music-banner", "Gitarrist vor einem beleuchteten Bandbanner", "3x2", "50% 40%"),
("band-sagenbringer", "Musiker in Fellkleidung im Wald, von unten fotografiert", "3x2", "50% 30%"),
("music-drums", "Schlagzeuger am Drumset in Schwarzweiß", "1x1", "50% 45%"),
("band-extinct-group", "Vier Bandmitglieder hintereinander in Schwarzweiß", "1x1", "50% 40%"),
("music-horns", "Zwei Musiker trinken aus Trinkhörnern", "1x1", "50% 35%"),
]
),
)
+ cta_band(
title="Konzert, Band oder Musikvideo?",
text="Schreib mir, was ansteht: Termin, Ort und was du mit den Bildern oder dem Film vorhast.",
secondary=("Videoprojekte ansehen", "/video/"),
)
)
return page(
title="Musik & Bühne · Konzerte, Bands, Musikvideos · desfoto",
description="Musik- und Bühnenfotografie aus Neumünster: Konzerte, Bandportraits und Musikvideos — plus Video für Künstler und Veranstalter.",
path=path,
json_ld=[
service_ld(
"Musik- und Bühnenfotografie",
path,
"Konzertfotografie, Bandportraits und Musikvideos für Bands, Künstler und Veranstalter.",
),
breadcrumb_ld("Musik & Bühne", path),
],
body=body,
)
def social_media() -> str:
path = "/social-media/"
body = (
@@ -1136,7 +1247,7 @@ def social_media() -> str:
kicker="Social Media",
title="Content, der regelmäßig kommt",
lead="Foto- und Videomaterial im Format deiner Kanäle — wiederkehrend, planbar und über die Monate hinweg einheitlich.",
image="event-motion",
image="event-speaker",
breadcrumb=("Social Media", path),
fit="50% 42%",
actions='<a class="btn btn--solid" href="/kontakt/">Content anfragen</a>',
@@ -1214,9 +1325,9 @@ def social_media() -> str:
+ "".join(
picture(slug, "(min-width: 62rem) 30vw, 92vw", css_class="frame frame--4x5", fit=fit)
for slug, fit in [
("event-motion", "50% 45%"),
("event-dance", "50% 36%"),
("event-speaker", "50% 30%"),
("event-venue", "50% 45%"),
("free-modern-portrait", "50% 40%"),
("music-dreadlocks", "50% 35%"),
("business-hotel-tea", "50% 45%"),
("business-hotel-laugh", "50% 45%"),
("music-drummer", "50% 45%"),
@@ -1274,7 +1385,7 @@ def projekte() -> str:
]
)
+ '<div class="btn-row" style="margin-top:2rem">'
'<a class="link-arrow" href="/video/">Zum Musikvideo '
'<a class="link-arrow" href="/musik-und-buehne/">Musik &amp; Bühne '
+ ICONS["arrow"]
+ "</a></div>",
)
@@ -1343,7 +1454,7 @@ def projekte() -> str:
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p><div class="card__foot"><a class="link-arrow" href="{h}">Ansehen {ICONS["arrow"]}</a></div></article>'
for t, d, h in [
("Shootings", "Business, Portrait, Familie und Minishootings.", "/shootings/"),
("Fotografie", "Menschen, Unternehmen, Produkte und Veranstaltungen.", "/fotografie/"),
("Businessfotografie", "Portraits, Teams und Produkte für Unternehmen.", "/businessfotografie/"),
("Videoprojekte", "Musikvideo, Imagefilm und Eventnachbericht.", "/video/"),
]
@@ -1367,8 +1478,8 @@ def ueber() -> str:
page_head(
kicker="Über",
title="Fotograf und Videograf aus Neumünster",
lead="Ich bin Denny Schulz. Ich fotografiere Menschen, Marken und Musik — und produziere seit einigen Jahren genauso gern Bewegtbild.",
image="studio-setup",
lead="Hinter desfoto stehe ich, Denny Schulz. Fotograf, Videograf und Musiker aus Neumünster.",
image="gear-cases",
breadcrumb=("Über", path),
fit="50% 45%",
actions='<a class="btn btn--solid" href="/kontakt/">Schreib mir</a>',
@@ -1398,7 +1509,7 @@ def ueber() -> str:
for t, d in [
(
"Menschen fotografieren",
"Portrait, Business, Model, Familie und Paar. Die Arbeit an Gesichtern, Licht und Ausdruck ist mein Kern.",
"Portrait, Business, Model und Familie. Die Arbeit an Gesichtern, Licht und Ausdruck ist mein Kern.",
),
(
"Musik & Bühne begleiten",
@@ -1443,9 +1554,9 @@ def ueber() -> str:
content=media_grid(
[
("business-portrait-studio", "Mann im weißen Hemd vor dunklem Hintergrund, Softbox im Bild", "4x5", "50% 28%"),
("studio-setup", "Studioequipment mit Logo von Denny Schulz Fotografie", "4x5", "50% 45%"),
("studio-lens", "Kameraobjektiv im Studiolicht", "4x5", "50% 45%"),
("business-hotel-window", "Drei Frauen in Bademänteln und Handtuchturbanen am Fenster", "4x5", "50% 40%"),
("couple-park", "Sara und Carlos stehen Arm in Arm im Grünen", "4x5", "50% 30%"),
("portrait-marina-snow", "Marina mit roten Zöpfen auf einem verschneiten Waldweg", "4x5", "50% 30%"),
("free-fox", "Nahaufnahme eines Rotfuchses", "1x1", "50% 45%"),
],
columns="grid--2",
@@ -1455,7 +1566,7 @@ def ueber() -> str:
variant="paper2",
kicker="Netzwerk",
title="Wo ich sonst zu finden bin",
lead="Zwei eigene Projekte, die zu desfoto gehören.",
lead="Zwei eigene Projekte des Betreibers.",
content='<div class="grid grid--2">'
+ "".join(
'<article class="card reveal">'
@@ -1484,7 +1595,7 @@ def ueber() -> str:
)
return page(
title="Über desfoto · Denny Schulz, Neumünster",
description="desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster. Arbeitsweise, mobiles Studio, Netzwerk und Kontaktmöglichkeiten.",
description="desfoto steht für Fotografie, Video und mediale Produktionen von Denny Schulz aus Neumünster: Arbeitsweise, mobiles Studio, Musik & Bühne, Netzwerk und Kontakt.",
path=path,
json_ld=[
breadcrumb_ld("Über", path),
@@ -1494,7 +1605,7 @@ def ueber() -> str:
"name": LEGAL["owner"],
"jobTitle": "Fotograf und Videograf",
"url": SITE["url"] + path,
"worksFor": {"@type": "Organization", "name": LEGAL["business"], "url": SITE["url"]},
"worksFor": {"@type": "Organization", "name": SITE["brand"], "url": SITE["url"] + "/"},
"sameAs": [item["href"] for item in NETWORK],
"address": {
"@type": "PostalAddress",
@@ -1576,7 +1687,7 @@ def kontakt() -> str:
title="Lieber direkt telefonieren?",
text="Kein Problem. Ruf einfach an — falls ich gerade im Termin bin, melde ich mich zurück.",
primary=("Anrufen", "tel:+491754083133"),
secondary=("E-Mail schreiben", "mailto:info@dennyschulz.de"),
secondary=("E-Mail schreiben", f"mailto:{CONTACT['email']}"),
)
)
return page(
@@ -1831,12 +1942,13 @@ def not_found() -> str:
ROUTES = [
("/", "index.html", home, None, "1.0"),
("/shootings/", "shootings/index.html", shootings, None, "0.9"),
("/fotografie/", "fotografie/index.html", fotografie, None, "0.9"),
("/businessfotografie/", "businessfotografie/index.html", businessfotografie, None, "0.9"),
("/portrait-und-model/", "portrait-und-model/index.html", portrait_und_model, None, "0.9"),
("/familien-und-paare/", "familien-und-paare/index.html", familien_und_paare, None, "0.9"),
("/familie/", "familie/index.html", familie, None, "0.9"),
("/minishootings/", "minishootings/index.html", minishootings, None, "0.9"),
("/video/", "video/index.html", video, None, "0.9"),
("/musik-und-buehne/", "musik-und-buehne/index.html", musik_und_buehne, None, "0.9"),
("/social-media/", "social-media/index.html", social_media, None, "0.9"),
("/projekte/", "projekte/index.html", projekte, None, "0.8"),
("/ueber/", "ueber/index.html", ueber, None, "0.8"),

View File

@@ -204,6 +204,11 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
.grid--2{grid-template-columns:repeat(auto-fit,minmax(19rem,1fr))}
.grid--3{grid-template-columns:repeat(auto-fit,minmax(16.5rem,1fr))}
.grid--4{grid-template-columns:repeat(auto-fit,minmax(13.5rem,1fr))}
/* Vier Arbeitsbereiche: vier Spalten auf großen, zwei auf mittleren und eine
auf kleinen Schirmen. auto-fit würde bei mittlerer Breite 3+1 Karten setzen. */
.grid--areas{grid-template-columns:1fr}
@media (min-width:40rem){.grid--areas{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:72rem){.grid--areas{grid-template-columns:repeat(4,minmax(0,1fr))}}
.card{display:flex;flex-direction:column;gap:1rem;height:100%}
.card__title{display:flex;align-items:baseline;gap:.6rem}
.card__num{font-size:.72rem;font-weight:600;letter-spacing:.16em;color:var(--accent)}

View File

@@ -262,10 +262,11 @@ class Content(unittest.TestCase):
def test_all_requested_services_are_reachable(self):
home = read(SITE / "index.html")
for href in (
"/shootings/",
"/fotografie/",
"/businessfotografie/",
"/familien-und-paare/",
"/familie/",
"/minishootings/",
"/musik-und-buehne/",
"/projekte/",
"/video/",
"/social-media/",
@@ -315,31 +316,110 @@ class Content(unittest.TestCase):
class Redesign(unittest.TestCase):
"""The 2026 redesign: Shootings umbrella, real portfolio, logo, motion layer."""
"""The 2026 redesign: Arbeitsbereiche, real portfolio, logo, motion layer."""
def test_nav_uses_shootings_as_the_photo_umbrella(self):
def test_nav_uses_fotografie_as_the_photo_umbrella(self):
for file in HTML_FILES:
text = read(file)
with self.subTest(file=file.name):
self.assertIn('href="/shootings/"', text)
self.assertNotIn('href="/fotografie/', text)
self.assertIn('href="/fotografie/"', text)
self.assertIn('href="/familie/"', text)
self.assertNotIn('href="/shootings', text)
self.assertNotIn('href="/familien-und-paare', text)
def test_all_four_shootings_are_grouped_under_the_umbrella(self):
shootings = read(SITE / "shootings" / "index.html")
def test_all_four_photo_schwerpunkte_are_grouped_under_the_umbrella(self):
fotografie = read(SITE / "fotografie" / "index.html")
for href in (
"/businessfotografie/",
"/portrait-und-model/",
"/familien-und-paare/",
"/familie/",
"/minishootings/",
):
with self.subTest(href=href):
self.assertIn(f'href="{href}"', shootings)
self.assertIn(f'href="{href}"', fotografie)
def test_photo_and_video_are_separate_but_both_on_the_homepage(self):
def test_homepage_explains_the_four_arbeitsbereiche(self):
home = read(SITE / "index.html")
self.assertEqual(home.count('<article class="pillar spot reveal">'), 2)
self.assertIn('<a class="pillar__go" href="/shootings/">Shootings ansehen', home)
self.assertIn('<a class="pillar__go" href="/video/">Videoprojekte ansehen', home)
start = home.index("Arbeitsbereiche")
end = home.index("</section>", start)
block = home[start:end]
self.assertEqual(4, block.count('<article class="card reveal">'))
for href in ("/fotografie/", "/video/", "/musik-und-buehne/", "/projekte/"):
with self.subTest(href=href):
self.assertIn(f'href="{href}"', block)
for scope in (
"Menschen, Unternehmen, Produkte, Veranstaltungen.",
"Unternehmen, Musik, Events, Social Content.",
"Bands, Künstler, Konzerte, Musikvideo.",
"Freie Arbeiten, Kooperationen, besondere Produktionen.",
):
with self.subTest(scope=scope):
self.assertIn(scope, block)
def test_homepage_separates_bookable_work_from_own_projects(self):
home = read(SITE / "index.html")
self.assertIn("Was du bei mir buchen kannst", home)
self.assertIn("Was ich außerdem mache", home)
self.assertLess(
home.index("Was du bei mir buchen kannst"),
home.index("Was ich außerdem mache"),
)
def test_couples_are_not_advertised_on_desfoto(self):
"""Paare/Engagement stay with dennyschulz.de; desfoto keeps single portraits."""
for file in HTML_FILES:
text = read(file)
with self.subTest(file=file.name):
for needle in ("Paarshooting", "Paarportrait", "Verlobung", "Ja-Wort", "Brautpaar"):
self.assertNotIn(needle, text)
# Der einzige erlaubte Paar-Bezug ist der Hinweis auf den eigenen
# Schwerpunkt auf dennyschulz.de im Footer.
remainder = visible_text(text).replace("Hochzeiten und Paare", "")
self.assertNotIn("Paar", remainder)
def test_footer_carries_the_ownership_line_without_the_legal_entity(self):
"""The brand is tied to its owner in one unobtrusive footer sentence."""
for file in HTML_FILES:
text = read(file)
with self.subTest(file=file.name):
self.assertIn("desfoto ist ein Angebot von Denny Schulz, Neumünster.", text)
footer = text[text.index("<footer"):]
self.assertNotIn("Denny Schulz Fotografie", footer)
def test_legal_entity_only_survives_in_legal_pages_and_structured_data(self):
"""„Denny Schulz Fotografie" ist die Rechtsperson, nicht die Marke.
Sichtbar darf sie nur auf Impressum und Datenschutz auftauchen; auf allen
anderen Seiten ist ausschließlich das unsichtbare ``legalName`` im
JSON-LD erlaubt.
"""
for file in HTML_FILES:
if file.parent.name in ("impressum", "datenschutz"):
continue
remainder = read(file).replace('"legalName":"Denny Schulz Fotografie"', "")
with self.subTest(file=str(file.relative_to(SITE))):
self.assertNotIn("Denny Schulz Fotografie", remainder)
def test_brand_is_written_lowercase_only(self):
"""Der Betreiber schreibt die Marke klein: ``desfoto``, nie ``DESFOTO``."""
for file in HTML_FILES:
with self.subTest(file=str(file.relative_to(SITE))):
self.assertNotIn("DESFOTO", read(file))
def test_built_pages_carry_no_trailing_whitespace(self):
"""Zeilen aus Leerzeichen lehnt das Release-Gateway (``git diff --check``) ab.
Eine bedingte Zeile in einer f-String-Vorlage erzeugt genau das: leer
eingesetzt bleibt die Einrückung stehen. Der Test hält die Ausgabe sauber,
damit die Freigabe nicht erst am Gateway scheitert.
"""
offenders = [
f"{file.relative_to(SITE)}:{number}"
for file in HTML_FILES
for number, line in enumerate(read(file).splitlines(), start=1)
if line != line.rstrip()
]
self.assertEqual([], offenders)
def test_links_are_never_nested(self):
"""Nested <a> is invalid and the parser splits the surrounding layout."""
@@ -370,30 +450,54 @@ class Redesign(unittest.TestCase):
self.assertEqual([], guard.problems)
self.assertEqual(0, guard.depth, "unbalanced <a>")
def test_pillar_grid_has_exactly_two_children(self):
"""Regression: the two pillars must stay two grid children."""
def test_every_area_card_links_to_its_area(self):
"""Each Arbeitsbereich card keeps its own link (no nested anchors)."""
home = read(SITE / "index.html")
start = home.index('<div class="pillars"')
start = home.index("Arbeitsbereiche")
end = home.index("</section>", start)
self.assertEqual(2, home[start:end].count('<article class="pillar spot reveal">'))
block = home[start:end]
self.assertEqual(4, block.count('class="link-arrow"'))
self.assertNotIn("<a class=\"card", block)
def test_nginx_redirects_the_retired_fotografie_url(self):
def test_nginx_redirects_the_renamed_photo_urls(self):
conf = read(ROOT / "nginx.conf")
for line in (
"location = /fotografie { return 301 /shootings/; }",
"location = /fotografie/ { return 301 /shootings/; }",
"location = /fotografie.html { return 301 /shootings/; }",
"location = /shootings { return 301 /fotografie/; }",
"location = /shootings/ { return 301 /fotografie/; }",
"location = /shootings.html { return 301 /fotografie/; }",
"location = /fotografie.html { return 301 /fotografie/; }",
"location = /familien-und-paare { return 301 /familie/; }",
"location = /familien-und-paare/ { return 301 /familie/; }",
"location = /familien-und-paare.html { return 301 /familie/; }",
):
with self.subTest(line=line):
self.assertIn(line, conf)
# The old umbrella must not win over the new one.
self.assertNotIn("return 301 /shootings/;", conf)
# /fotografie/index.html is canonicalised to /fotografie/ by the
# server-level index rule and reaches /shootings/ on the second hop.
# server-level index rule instead of bouncing back to itself.
self.assertIn(r"if ($request_uri ~ ^/(.*/)?index\.html(\?.*)?$)", conf)
def test_retired_fotografie_build_output_is_gone(self):
self.assertFalse((SITE / "fotografie").exists())
self.assertFalse((SITE / "assets" / "img" / "og-fotografie.jpg").exists())
def test_retired_routes_and_brand_bleed_assets_are_gone(self):
self.assertFalse((SITE / "shootings").exists())
self.assertFalse((SITE / "familien-und-paare").exists())
self.assertFalse((SITE / "assets" / "img" / "og-shootings.jpg").exists())
self.assertFalse((SITE / "assets" / "img" / "og-familien.jpg").exists())
manifest = json.loads(read(SRC / "images.json"))
slugs = {entry["slug"] for entry in manifest["images"]}
for slug in (
# Studiopanorama with a visible "Denny Schulz Fotograf" business card.
"studio-setup",
# Romantic couple imagery: advertised on dennyschulz.de only.
"people-joy",
"people-kiss",
"portrait-couple",
"couple-park",
"couple-backlight-kiss",
"couple-forest-walk",
"couple-sun-silhouette",
"couple-laugh-walk",
# Retired from an earlier build.
"people-forest",
"people-veil",
"people-walk",
@@ -405,8 +509,12 @@ class Redesign(unittest.TestCase):
"travel-norway-pano",
"travel-norway-valley",
):
for image in (SITE / "assets" / "img").glob(f"{slug}-*.webp"):
self.fail(f"stale image built: {image.name}")
with self.subTest(slug=slug):
self.assertNotIn(slug, slugs)
for image in (SITE / "assets" / "img").glob(f"{slug}-*.webp"):
self.fail(f"stale image built: {image.name}")
for file in HTML_FILES:
self.assertNotIn(slug, read(file))
def test_wedding_crowd_photos_are_not_shipped(self):
"""The operator asked to drop the wedding photos showing the guests."""
@@ -419,10 +527,29 @@ class Redesign(unittest.TestCase):
for file in HTML_FILES:
self.assertNotIn(slug, read(file))
def test_sitemap_lists_the_new_route_only(self):
def test_couple_photography_assets_are_gone_entirely(self):
"""Paare gehören zu dennyschulz.de — auch als Bild dürfen sie nicht mitlaufen.
``portrait-natural``, ``event-motion`` und ``event-dance`` zeigen Brautpaare
und sind deshalb aus dem Bildbestand entfernt, nicht nur von den Seiten.
"""
manifest = json.loads(read(SRC / "images.json"))
slugs = {entry["slug"] for entry in manifest["images"]}
for slug in ("portrait-natural", "event-motion", "event-dance"):
with self.subTest(slug=slug):
self.assertNotIn(slug, slugs)
self.assertEqual([], list((SITE / "assets" / "img").glob(f"{slug}-*.webp")))
for file in HTML_FILES:
self.assertNotIn(slug, read(file))
def test_sitemap_lists_the_new_routes_only(self):
sitemap = read(SITE / "sitemap.xml")
self.assertIn("<loc>https://desfoto.de/shootings/</loc>", sitemap)
self.assertNotIn("<loc>https://desfoto.de/fotografie/</loc>", sitemap)
for route in ("fotografie", "familie", "musik-und-buehne"):
with self.subTest(route=route):
self.assertIn(f"<loc>https://desfoto.de/{route}/</loc>", sitemap)
for retired in ("shootings", "familien-und-paare"):
with self.subTest(route=retired):
self.assertNotIn(f"<loc>https://desfoto.de/{retired}/</loc>", sitemap)
def test_operator_logo_is_the_brand_mark(self):
self.assertTrue((SITE / "assets" / "img" / "logo-160.png").is_file())
@@ -459,7 +586,7 @@ class Redesign(unittest.TestCase):
def test_mobile_studio_is_described_where_photo_work_is_sold(self):
needles = ("Mobiles Studio", "Autarke Blitzanlage", "autarker Blitzanlage")
for rel in ("index.html", "shootings/index.html", "businessfotografie/index.html"):
for rel in ("index.html", "fotografie/index.html", "businessfotografie/index.html"):
text = read(SITE / rel)
for needle in needles:
with self.subTest(file=rel, needle=needle):
@@ -469,7 +596,7 @@ class Redesign(unittest.TestCase):
"""Projects can also be photographed in the operator's own studio on site."""
for rel in (
"index.html",
"shootings/index.html",
"fotografie/index.html",
"businessfotografie/index.html",
"ueber/index.html",
):
@@ -489,13 +616,20 @@ class Redesign(unittest.TestCase):
"free-sparrow",
"free-dogs-forest",
),
"familien-und-paare": (
"couple-park",
"couple-backlight-kiss",
"couple-forest-walk",
"couple-sun-silhouette",
"couple-laugh-walk",
"familie": (
"free-lake-person",
"free-portrait-light",
"portrait-marina-snow",
"free-modern-portrait",
"free-woman-coast",
"portrait-marina-sunset",
),
"musik-und-buehne": (
"music-guitar-silhouette",
"band-extinct-guitar",
"music-neck",
"music-banner",
"music-drums",
),
"portrait-und-model": ("editorial-studio-side", "editorial-balance", "editorial-leap"),
"businessfotografie": (
@@ -522,6 +656,16 @@ class Redesign(unittest.TestCase):
with self.subTest(page="video", slug=slug):
self.assertIn(f"/assets/img/{slug}-1200.webp", video)
def test_musik_und_buehne_page_shows_the_real_cases(self):
page = read(SITE / "musik-und-buehne" / "index.html")
for needle in ("Thjódrörir", "Extinct", "Sagenbringer"):
with self.subTest(needle=needle):
self.assertIn(needle, page)
for step in ("Konzept", "Dreh", "Schnitt", "Farbkorrektur"):
with self.subTest(step=step):
self.assertIn(f"<h3>{step}</h3>", page)
self.assertIn("NWWFTf7l8g0", page)
def test_invented_japan_content_is_absent(self):
for file in HTML_FILES:
text = read(file).lower()
@@ -601,11 +745,17 @@ class Release(unittest.TestCase):
self.assertEqual(2, deploy.count("up -d --force-recreate --remove-orphans"))
self.assertIn("docker exec desfoto-web-1 wget -q --spider http://127.0.0.1/", deploy)
def test_verify_hook_covers_the_renamed_route(self):
def test_verify_hook_covers_the_renamed_routes(self):
verify = read(ROOT / ".ocauto" / "verify")
self.assertIn('check_redirect "$base/fotografie/" "$base/shootings/"', verify)
self.assertIn("""check_not_contains "$base/" 'href="/fotografie/'""", verify)
self.assertNotIn('/fotografie/ /businessfotografie/', verify)
for check in (
'check_redirect "$base/shootings/" "$base/fotografie/"',
'check_redirect "$base/familien-und-paare/" "$base/familie/"',
'check_contains "$base/fotografie/"',
'check_contains "$base/musik-und-buehne/"',
):
with self.subTest(check=check):
self.assertIn(check, verify)
self.assertNotIn("/shootings/ /businessfotografie/", verify)
self.assertNotIn("hero-studio-1200.webp", verify)
def test_deploy_hooks_are_executable(self):