feat: reposition desfoto as an independent brand with four Arbeitsbereiche
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user