Files
Normtor/tools/build_pages.py
T
NotPika69andClaude Opus 5.5 bec24e5bbc Website-Entwurf für HEB Bauelemente (normtor.de)
Akquise-Demo mit Startseite (Tor-Intro, 24-h-Renovierung mit Uhr,
Über uns, Fachbetrieb statt Discount), Galerie mit Filter und
Großansicht, Kontaktseite mit Anfrageformular sowie Impressum- und
Datenschutz-Entwürfen. Statisches HTML/CSS/JS, Seiten und Bilder
werden über tools/ erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:52:10 +02:00

568 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Erzeugt die HTML-Seiten aus gemeinsamen Bausteinen (Header, Footer, Galerie-Daten).
Quelle der Wahrheit für Inhalt und Struktur ist diese Datei — die HTML-Dateien nicht von Hand
ändern, sondern hier ändern und neu erzeugen:
python3 tools/build_images.py (nur wenn sich Fotos geändert haben)
python3 tools/build_pages.py (braucht Pillow: pip install pillow)
"""
import json
from html import escape
from pathlib import Path
from urllib.parse import quote
from PIL import Image
OUT = Path(__file__).resolve().parent.parent
IMG = OUT / "assets/img"
SITE = "https://normtor.de"
DEMO = True # Entwurf: noindex + Hinweis im Formular. Vor dem Launch auf False setzen.
FIRMA = "HEB Bauelemente"
INHABER = "Wolfgang Haas"
STRASSE = "Am Ring 20"
ORT = "84051 Essenbach / Mirskofen"
TEL = "08703 / 13 62"
TEL_HREF = "tel:+4987031362"
MAPS = "https://www.google.com/maps/search/?api=1&query=" + quote("Am Ring 20, 84051 Essenbach")
FACEBOOK = "https://www.facebook.com/heb.bauelemente"
# ---------- Icons ----------
I_ARROW = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" aria-hidden="true"><path d="M4 12h15M13 6l6 6-6 6"/></svg>'
I_PHONE = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg>'
I_PIN = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></svg>'
I_MAIL = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18v14H3z"/><path d="M3 6l9 7 9-7"/></svg>'
I_CHECK = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="square" aria-hidden="true"><path d="M4 12.5l5 5L20 6.5"/></svg>'
I_X = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="square" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>'
I_PREV = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>'
I_NEXT = '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>'
I_FB = '<svg class="i" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14 8h3V4h-3a4 4 0 0 0-4 4v2H8v4h2v8h4v-8h3l1-4h-4V8.5c0-.3.2-.5.5-.5z"/></svg>'
# Logo nachgezeichnet: H und E teilen sich den Mittelstrich, wie im Original-Rasterlogo
HEB_MARK = ('<svg class="heb" viewBox="0 0 200 100" aria-hidden="true"><g fill="currentColor">'
'<rect x="0" y="0" width="13" height="100"/><rect x="13" y="42" width="27" height="16"/>'
'<rect x="40" y="0" width="15" height="100"/><rect x="55" y="0" width="52" height="16"/>'
'<rect x="55" y="42" width="42" height="16"/><rect x="55" y="84" width="52" height="16"/>'
'<path fill-rule="evenodd" d="M116 0h58c15 0 24 9 24 23 0 11-6 19-14 22 10 3 16 11 16 23 0 19-12 32-30 32h-54zM134 16v26h36c6 0 10-5 10-13s-4-13-10-13zM134 58v26h38c7 0 11-5 11-13s-4-13-11-13z"/>'
'</g></svg>')
def pic(name, alt, sizes="100vw", eager=False, cls=""):
w, h = Image.open(IMG / f"{name}-800.webp").size
big = (IMG / f"{name}-1200.webp").exists()
if big:
w, h = Image.open(IMG / f"{name}-1200.webp").size
load = 'fetchpriority="high"' if eager else 'loading="lazy" decoding="async"'
c = f' class="{cls}"' if cls else ""
def srcset(ext):
return f"assets/img/{name}-800.{ext} 800w" + (f", assets/img/{name}-1200.{ext} 1200w" if big else "")
src = f"assets/img/{name}-{1200 if big else 800}.webp"
return (f'<picture{c}>'
f'<source type="image/avif" srcset="{srcset("avif")}" sizes="{sizes}">'
f'<source type="image/webp" srcset="{srcset("webp")}" sizes="{sizes}">'
f'<img src="{src}" width="{w}" height="{h}" alt="{escape(alt)}" {load}></picture>')
# ---------- Galerie ----------
# (Bildname, Titel, Unterzeile, Kategorien) — Titel von der alten Seite übernommen, wo vorhanden.
# Kategorien: dst = Deckensektionaltor, sst = Seitensektionaltor, schwing = Schwingtor,
# holz = Holz & Sonderlösungen, region = Projekte aus der Region
GALLERY = [
("flair-1", "Normstahl Flair", "Farbe DB 703", "dst region"),
("garagenhof", "Garagenhof", "Alt und neu nebeneinander", "dst region"),
("condor-schraegschnitt", "Sektionaltor mit Schrägschnitt", "320 mm Gefälle ausgeglichen", "dst holz region"),
("anthrazit-iso", "Normstahl Style für Fertiggaragen", "Anthrazit, glatt", "dst region"),
("condor-ligna", "Condor Wood Ligna Light", "Mit Schlupftür und Glassektion", "holz region"),
("essenbach-ohu", "Deckensektionaltor Eurostyle", "Mit Sturzblende, Essenbach-Ohu", "dst region"),
("superior-graualu", "Superior Style", "Graualuminium", "dst"),
("doppelgarage-beige", "Doppelgarage", "Zwei Sektionaltore", "dst region"),
("sst-style", "Seitensektionaltor Style", "Anthrazitgrau", "sst"),
("condor-granit", "Condor Granit", "Sektionaltor, glatt", "dst region"),
("rustiko", "Normstahl Rustiko", "Holz mit aufgesetzter Zarge", "schwing holz region"),
("smart-trend", "Smart Trend", "Eiche dunkel, Woodgrain", "dst"),
("anthrazit-solo", "Sektionaltor in Anthrazit", "Mit Verkleidung", "dst region"),
("prominent-rottenburg", "Schwingtor Prominent", "Taubenblau, Rottenburg", "schwing region"),
("superior-linea", "Superior Linea", "Verkehrsweiß, glatt", "dst"),
("condor-schlupftuer", "Condor mit Schlupftür", "Holzoptik mit Fenstern", "holz region"),
("sst-trend", "Seitensektionaltor Trend", "Anthrazitgrau", "sst"),
("flair-2", "Normstahl Flair", "Farbe DB 703", "dst region"),
("eurostyle-iso", "Eurostyle ISO", "Weißaluminium mit Design-Motiven", "dst region"),
("smart-style", "Smart Style", "Anthrazit, glatt", "dst"),
("variant", "Normstahl Variant", "Vom Kunden selbst belegt", "schwing holz region"),
("doppelgarage-hell", "Doppelgarage", "Zwei helle Sektionaltore", "dst region"),
("superior-eiche", "Superior Style", "Eiche hell", "dst"),
("sst-design", "Seitensektionaltor", "Mit Design-Applikation", "sst region"),
("elegant", "Schwingtor Elegant", "Taubenblau mit Messinggriff", "schwing region"),
("topline-iso", "Topline ISO", "Moosgrün, links laufend", "schwing region"),
("mueller-muenster", "Sektionaltor", "Anthrazit", "dst region"),
]
FILTERS = [("alle", "Alle"), ("region", "Aus der Region"), ("dst", "Deckensektionaltore"),
("sst", "Seitensektionaltore"), ("schwing", "Schwingtore"), ("holz", "Holz & Sonderlösungen")]
NAV = [("index.html#leistungen", "Leistungen"), ("index.html#renovierung", "24-h-Renovierung"),
("projekte.html", "Tore & Projekte"), ("index.html#ueber-uns", "Über uns"), ("kontakt.html", "Kontakt")]
HEAD_SCRIPT = ("(function(r){r.classList.add('js');try{if(r.hasAttribute('data-intro')&&!sessionStorage.getItem('heb-intro')"
"&&matchMedia('(prefers-reduced-motion: no-preference)').matches)r.classList.add('has-intro')}catch(e){}"
"setTimeout(function(){if(!r.classList.contains('ready'))r.classList.remove('has-intro')},3500)})(document.documentElement)")
LD = {
"@context": "https://schema.org",
"@type": "HomeAndConstructionBusiness",
"name": "HEB Bauelemente – Wolfgang Haas",
"description": "Garagentore, Torantriebe, Reparatur und Garagen-Renovierung in 24 Stunden im Raum Landshut und Rottenburg.",
"url": SITE + "/",
"telephone": "+49 8703 1362",
"founder": {"@type": "Person", "name": INHABER},
"address": {"@type": "PostalAddress", "streetAddress": STRASSE, "postalCode": "84051",
"addressLocality": "Essenbach", "addressRegion": "Bayern", "addressCountry": "DE"},
"areaServed": ["Landshut", "Rottenburg an der Laaber", "Essenbach"],
"brand": ["Normstahl", "Sommer", "Aperto"],
"image": SITE + "/assets/img/og.jpg",
"sameAs": [FACEBOOK],
}
def head(title, desc, file, intro=False):
ld = f'<script type="application/ld+json">{json.dumps(LD, ensure_ascii=False)}</script>' if file == "index.html" else ""
canonical = SITE + "/" + ("" if file == "index.html" else file)
robots = '<meta name="robots" content="noindex, nofollow">\n' if DEMO else ""
return f'''<!doctype html>
<html lang="de"{' data-intro' if intro else ''}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<meta name="description" content="{desc}">
{robots}<link rel="canonical" href="{canonical}">
<meta name="theme-color" content="#00569D">
<meta property="og:type" content="website">
<meta property="og:locale" content="de_DE">
<meta property="og:site_name" content="{FIRMA}">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{desc}">
<meta property="og:url" content="{canonical}">
<meta property="og:image" content="{SITE}/assets/img/og.jpg">
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<link rel="preload" href="assets/fonts/barlow-condensed-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="assets/fonts/barlow-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="assets/site.css">
<script>{HEAD_SCRIPT}</script>
<script src="assets/site.js" defer></script>
{ld}
</head>'''
def brand():
return (f'<a class="brand" href="index.html" aria-label="{FIRMA}, {INHABER} – Startseite">{HEB_MARK}'
'<span class="brand__text"><b>Bauelemente</b><span>Wolfgang Haas · Essenbach</span></span></a>')
def header(active):
def cur(f):
return ' aria-current="page"' if f == active else ""
links = "".join(f'<a class="nav__link" href="{f}"{cur(f)}>{n}</a>' for f, n in NAV)
mlinks = "".join(f'<li><a href="{f}"{cur(f)}>{n}</a></li>' for f, n in NAV)
return f'''
<a class="skip-link" href="#inhalt">Zum Inhalt springen</a>
<header class="site-header">
<div class="wrap site-header__inner">
{brand()}
<nav class="nav" aria-label="Hauptnavigation">{links}</nav>
<a class="btn btn--blue btn--sm header-call" href="{TEL_HREF}">{I_PHONE}<span>{TEL}</span></a>
<button class="menu-btn" type="button" aria-expanded="false" aria-controls="mobile-nav" aria-label="Menü öffnen"><span></span><span></span></button>
</div>
<div class="mobile-nav" id="mobile-nav" hidden>
<ul>{mlinks}</ul>
<a class="btn btn--blue" href="{TEL_HREF}">{I_PHONE}<span>Anrufen: {TEL}</span></a>
</div>
</header>'''
FOOTER = f'''
<section class="cta-band">
<div class="wrap cta-band__inner">
<div>
<p class="eyebrow eyebrow--light">Fragen kostet nichts</p>
<h2 class="cta-band__title">Rufen Sie einfach an.</h2>
<p class="cta-band__text">Ob neues Tor, klemmender Antrieb oder gebrochene Feder: Sie sprechen direkt mit Wolfgang Haas, ganz ohne Warteschleife und Callcenter.</p>
</div>
<div class="cta-band__actions">
<a class="cta-band__phone" href="{TEL_HREF}">{I_PHONE}<span>{TEL}</span></a>
<a class="btn btn--ghost-light" href="kontakt.html">Lieber schriftlich anfragen {I_ARROW}</a>
</div>
</div>
</section>
<footer class="site-footer">
<div class="wrap site-footer__grid">
<div class="site-footer__brand">
{brand()}
<p>Garagentore, Antriebe und Ersatzteilservice im Großraum Landshut und Rottenburg.</p>
</div>
<div>
<h3 class="site-footer__h">Adresse</h3>
<address>{FIRMA}<br>{INHABER}<br>{STRASSE}<br>{ORT}</address>
<a class="link" href="{MAPS}" rel="noopener" target="_blank">Route planen</a>
</div>
<div>
<h3 class="site-footer__h">Kontakt</h3>
<p><a class="link" href="{TEL_HREF}">{TEL}</a></p>
<p><a class="link" href="kontakt.html">Anfrageformular</a></p>
<p><a class="link link--icon" href="{FACEBOOK}" rel="noopener" target="_blank">{I_FB}Facebook</a></p>
</div>
<div>
<h3 class="site-footer__h">Seiten</h3>
<ul class="site-footer__nav">{"".join(f'<li><a class="link" href="{f}">{n}</a></li>' for f, n in NAV)}</ul>
</div>
</div>
<div class="wrap site-footer__bottom">
<span>© <span data-year>2026</span> {FIRMA} · {INHABER}</span>
<span><a class="link" href="impressum.html">Impressum</a> · <a class="link" href="datenschutz.html">Datenschutz</a></span>
<span class="site-footer__credit">Entwurf: <a class="link" href="https://webklar.com" rel="noopener" target="_blank">webklar</a></span>
</div>
</footer>
<nav class="callbar" aria-label="Schnellkontakt">
<a href="{TEL_HREF}">{I_PHONE}<span>Anrufen</span></a>
<a href="kontakt.html">{I_MAIL}<span>Anfrage</span></a>
</nav>'''
def page(file, title, desc, body, intro=False, active=None):
html = head(title, desc, file, intro) + "\n<body>" + header(active or file) + \
f'\n<main id="inhalt">\n{body}\n</main>' + FOOTER + "\n</body>\n</html>\n"
(OUT / file).write_text(html, encoding="utf-8")
print("geschrieben:", file)
def page_hero(title, lead, eyebrow):
return f'''
<section class="page-hero">
<div class="wrap">
<p class="eyebrow">{eyebrow}</p>
<h1 class="page-hero__title">{title}</h1>
<p class="page-hero__lead">{lead}</p>
</div>
</section>'''
# ============================== Startseite ==============================
GATE = '<div class="gate" aria-hidden="true">' + "".join('<div class="gate__panel"></div>' for _ in range(6)) + \
f'<div class="gate__plate">{HEB_MARK}<span>normtor.de</span></div><div class="gate__light"></div></div>'
LEISTUNGEN = [
("superior-linea", "Neue Garagentore",
"Schwing-, Deckensektional- und Seitensektionaltore von Normstahl, dazu Industrietore. Wir beraten bei Ihnen vor Ort, nehmen Maß und montieren selbst.",
["Schwingtore", "Sektionaltore", "Industrietore"]),
("sst-style", "Torantriebe",
"Antriebe von Sommer und Aperto für Garagen- und Hoftore. Für Reihengaragen ohne Stromanschluss gibt es Akku-Antriebe mit Wechselakku, auf Wunsch mit Solarmodul.",
["Garagentor", "Hoftor", "Akku & Solar"]),
("flair-2", "Reparatur & Wartung",
"Das Tor klemmt, eine Feder ist gebrochen oder der Antrieb streikt? Wir haben alle gängigen Ersatzteile und bringen auch ältere Tore wieder in Gang.",
["Ersatzteile", "Federn", "Instandhaltung"]),
]
STEPS = [
("Altes Tor ausbauen", "Wir demontieren Ihr altes Garagentor fachgerecht."),
("Neues Tor einbauen", "Das neue Normstahl-Tor wird passgenau montiert, auf Wunsch gleich mit Antrieb."),
("Beiputzen & Estrich", "Sturz und Laibung werden beigeputzt, fehlender Estrich wird eingebracht."),
("Besenrein übergeben", "Sie bekommen Ihre Garage besenrein und voll funktionsfähig zurück."),
]
TEASER = ["flair-1", "anthrazit-iso", "doppelgarage-hell", "condor-granit", "doppelgarage-beige"]
G = {g[0]: g for g in GALLERY}
INDEX = f'''
<section class="hero">
{pic("hero", "Weißes Einfamilienhaus mit anthrazitfarbenem Normstahl-Sektionaltor in der Doppelgarage", eager=True, cls="hero__img")}
{GATE}
<div class="wrap hero__inner">
<p class="eyebrow eyebrow--light hero__eyebrow">Normstahl-Fachbetrieb in Essenbach</p>
<h1 class="hero__title"><span>Ihr Garagentor.</span> <span>In guten Händen.</span></h1>
<p class="hero__lead">Beratung, Aufmaß, Montage und Reparatur aus einer Hand, seit über 25 Jahren im Großraum Landshut und Rottenburg.</p>
<div class="hero__actions">
<a class="btn btn--blue" href="{TEL_HREF}">{I_PHONE}<span>{TEL}</span></a>
<a class="btn btn--ghost-light" href="kontakt.html">Angebot anfragen {I_ARROW}</a>
</div>
</div>
</section>
<section class="trust" aria-label="Darauf können Sie sich verlassen">
<ul class="wrap trust__list">
<li><b>25+ Jahre</b><span>Erfahrung mit Normstahl</span></li>
<li><b>Ein Ansprechpartner</b><span>von der Beratung bis zur Montage</span></li>
<li><b>Sommer-Servicepartner</b><span>mit regelmäßigen Schulungen</span></li>
<li><b>Festpreis</b><span>bei der Garagen-Renovierung</span></li>
</ul>
</section>
<section class="section" id="leistungen">
<div class="wrap">
<div class="section__head reveal">
<p class="eyebrow">Leistungen</p>
<h2 class="h2">Alles rund ums Garagentor.</h2>
<p class="lead">Wir verkaufen Tore nicht nur, wir bauen sie auch ein und sind danach weiter für Sie da.</p>
</div>
<div class="cards">
{"".join(f"""
<article class="card reveal">
<div class="card__img">{pic(img, title, sizes="(min-width: 960px) 30vw, 100vw")}</div>
<div class="card__body">
<span class="card__num">0{i + 1}</span>
<h3 class="card__title">{title}</h3>
<p>{text}</p>
<ul class="tags">{"".join(f"<li>{t}</li>" for t in tags)}</ul>
</div>
</article>""" for i, (img, title, text, tags) in enumerate(LEISTUNGEN))}
<a class="card card--feature reveal" href="#renovierung">
<span class="card__num">04</span>
<span class="card__big">24 h</span>
<h3 class="card__title">Garagen-Renovierung</h3>
<p>Altes Tor raus, neues Tor rein, alles verputzt, in nur einem Tag und zum Festpreis.</p>
<span class="card__more">So läuft es ab {I_ARROW}</span>
</a>
</div>
</div>
</section>
<section class="section section--dark renovation" id="renovierung">
<div class="wrap renovation__grid">
<div class="renovation__sticky">
<p class="eyebrow eyebrow--light">Unser Serviceangebot</p>
<h2 class="h2">Alt raus. Neu rein.<br>In 24 Stunden.</h2>
<p class="lead">Wir renovieren Ihre Garage innerhalb eines Tages, zum vorher vereinbarten Festpreis. Danach ist alles sauber, und das Tor läuft.</p>
<div class="clock" aria-hidden="true">
<svg viewBox="0 0 120 120"><circle class="clock__track" cx="60" cy="60" r="52"/><circle class="clock__bar" cx="60" cy="60" r="52" pathLength="100"/></svg>
<div class="clock__label"><b data-clock>0</b><span>von 24 h</span></div>
</div>
</div>
<ol class="steps">
{"".join(f"""
<li class="step" data-hours="{h}">
<span class="step__num">{i + 1}</span>
<div><h3 class="step__title">{t}</h3><p>{d}</p></div>
</li>""" for i, ((t, d), h) in enumerate(zip(STEPS, (6, 12, 19, 24))))}
<li class="step step--photo">
<figure class="renovation__photo">
{pic("garagenhof", "Garagenhof mit drei Toren: links ein altes Schwingtor, daneben zwei neue Sektionaltore", sizes="(min-width: 960px) 50vw, 100vw")}
<figcaption>Alt neben neu: ein Garagenhof aus unseren Projekten.</figcaption>
</figure>
<a class="btn btn--blue" href="kontakt.html?anliegen=renovierung">Festpreis anfragen {I_ARROW}</a>
</li>
</ol>
</div>
</section>
<section class="section">
<div class="wrap">
<div class="section__head section__head--row reveal">
<div>
<p class="eyebrow">Tore & Projekte</p>
<h2 class="h2">Aus Essenbach, Rottenburg und Umgebung.</h2>
</div>
<a class="btn btn--outline" href="projekte.html">Alle {len(GALLERY)} Projekte ansehen {I_ARROW}</a>
</div>
</div>
<div class="strip reveal" tabindex="0" aria-label="Ausgewählte Projekte, horizontal scrollbar">
{"".join(f"""<a class="strip__item" href="projekte.html#{n}">{pic(n, G[n][1] + ", " + G[n][2], sizes="(min-width: 960px) 34vw, 80vw")}<span class="strip__cap"><b>{G[n][1]}</b>{G[n][2]}</span></a>""" for n in TEASER)}
</div>
</section>
<section class="section section--paper" id="ueber-uns">
<div class="wrap about">
<figure class="about__photo reveal">
{pic("haas", "Porträt von Wolfgang Haas, Inhaber von HEB Bauelemente", sizes="(min-width: 960px) 40vw, 100vw")}
<figcaption><b>Wolfgang Haas</b>Inhaber · Essenbach-Mirskofen</figcaption>
</figure>
<div class="about__text reveal">
<p class="eyebrow">Über uns</p>
<h2 class="h2">Bei uns sprechen Sie mit dem Chef.</h2>
<blockquote class="quote">
<p>„Seit mehr als 25 Jahren befasse ich mich mit Normstahl-Toren. Von der Planung bis zur Beratung, vom Aufmaß bis zur Montage bekommen Sie bei mir alles aus einer Hand.“</p>
</blockquote>
<p>Weil nicht immer alles passt, setze ich bei Antrieben auf Sommer Antriebs- und Funktechnik aus Deutschland. Auch die können keinen Kreis gerade biegen, aber sie bieten Lösungen, wo andere aufgeben müssen.</p>
<p>Damit wir technisch auf dem neuesten Stand bleiben, besuchen wir regelmäßig die Servicepartner-Schulungen unserer Lieferanten.</p>
<p class="signature">Ihr Wolfgang Haas</p>
</div>
</div>
</section>
<section class="section">
<div class="wrap">
<div class="section__head reveal">
<p class="eyebrow">Fachbetrieb statt Discount</p>
<h2 class="h2">Warum nicht einfach online bestellen?</h2>
<p class="lead">Ein Tor aus dem Internet ist schnell gekauft. Spätestens bei der ersten Störung zeigt sich, ob jemand in der Nähe ist, der hilft.</p>
</div>
<div class="compare reveal">
<div class="compare__col">
<h3 class="compare__h">Tor aus dem Internet</h3>
<ul>
<li>{I_X}Maße nehmen Sie selbst und hoffen, dass sie stimmen</li>
<li>{I_X}Den Einbau übernehmen Sie oder jemand anderes</li>
<li>{I_X}Bei Störungen hilft nur eine Hotline in der Ferne</li>
</ul>
</div>
<div class="compare__col compare__col--us">
<h3 class="compare__h">HEB Bauelemente vor Ort</h3>
<ul>
<li>{I_CHECK}Beratung und Aufmaß bei Ihnen zu Hause</li>
<li>{I_CHECK}Fachgerechte Montage aus einer Hand</li>
<li>{I_CHECK}Im Ernstfall schnell da, mit allen gängigen Ersatzteilen</li>
</ul>
</div>
</div>
<p class="compare__quote reveal">Die Freude an der richtigen Entscheidung währt länger als die Freude am billigen Einkauf.</p>
</div>
</section>
<section class="section section--paper partners">
<div class="wrap partners__grid reveal">
<div>
<p class="eyebrow">Unsere Partner</p>
<h2 class="h3">Marken, auf die wir uns verlassen.</h2>
<p>Wir sind Komplettanbieter für Normstahl-Tore samt Ersatzteilen und Zubehör. Bei Antrieben arbeiten wir mit Sommer und der Fachhandelsmarke Aperto. Auf Wunsch leiten wir Ihre Anfrage auch an die Vertretung weiter, die für Ihre Postleitzahl zuständig ist.</p>
</div>
<ul class="partners__list">
<li><b>Normstahl</b><span>Garagen- und Industrietore</span></li>
<li><b>Sommer</b><span>Antriebs- und Funktechnik</span></li>
<li><b>Aperto</b><span>Garagen- und Hoftorantriebe</span></li>
</ul>
</div>
</section>
'''
# ============================== Projekte ==============================
PROJEKTE = page_hero("Tore & Projekte",
"Eine Auswahl an Toren, die wir eingebaut haben, und an Modellen, die wir anbieten. Ein Klick vergrößert das Bild.",
"Galerie") + f'''
<section class="section section--tight">
<div class="wrap">
<div class="filters" role="group" aria-label="Galerie filtern">
{"".join(f'<button type="button" class="chip" data-filter="{k}" aria-pressed="{str(k == "alle").lower()}">{n}</button>' for k, n in FILTERS)}
</div>
<p class="filters__count" aria-live="polite"><span data-count>{len(GALLERY)}</span> Tore</p>
<ul class="gallery">
{"".join(f"""<li class="gallery__item" id="{n}" data-tags="{tags}"><button type="button" class="gallery__btn" data-full="assets/img/{n}-{1200 if (IMG / f"{n}-1200.webp").exists() else 800}.webp" data-title="{escape(t)}" data-sub="{escape(s)}">{pic(n, t + ", " + s, sizes="(min-width: 1100px) 25vw, (min-width: 640px) 33vw, 50vw")}<span class="gallery__cap"><b>{t}</b>{s}</span></button></li>""" for n, t, s, tags in GALLERY)}
</ul>
<p class="note">Ihr Wunschmodell ist nicht dabei? Normstahl bietet viele weitere Oberflächen, Farben und Größen. Wir bringen Muster mit, wenn wir zu Ihnen kommen.</p>
</div>
</section>
<dialog class="lightbox" aria-label="Bildansicht">
<figure class="lightbox__fig"><img alt=""><figcaption><b></b><span></span></figcaption></figure>
<button type="button" class="lightbox__btn lightbox__close" aria-label="Schließen">{I_X}</button>
<button type="button" class="lightbox__btn lightbox__prev" aria-label="Vorheriges Bild">{I_PREV}</button>
<button type="button" class="lightbox__btn lightbox__next" aria-label="Nächstes Bild">{I_NEXT}</button>
</dialog>'''
# ============================== Kontakt ==============================
ANLIEGEN = [("tor", "Neues Garagentor"), ("antrieb", "Antrieb"), ("reparatur", "Reparatur"), ("renovierung", "24-h-Renovierung")]
DEMO_NOTE = ('<p class="form__demo">Hinweis: Dies ist ein Entwurf. Das Formular verschickt noch nichts, '
'denn der Versand wird erst beim Launch eingerichtet.</p>') if DEMO else ""
KONTAKT = page_hero("Kontakt", "Am schnellsten geht es per Telefon. Oder Sie schreiben uns kurz, worum es geht, und wir rufen zurück.", "Wir sind für Sie da") + f'''
<section class="section section--tight">
<div class="wrap contact">
<aside class="contact__info">
<a class="contact__phone" href="{TEL_HREF}">{I_PHONE}<span><small>Telefon</small>{TEL}</span></a>
<div class="contact__block">
<h2 class="contact__h">{I_PIN}Adresse</h2>
<address>{FIRMA}<br>{INHABER}<br>{STRASSE}<br>{ORT}</address>
<a class="link" href="{MAPS}" rel="noopener" target="_blank">Route planen (Google Maps)</a>
</div>
<div class="contact__block">
<h2 class="contact__h">Einzugsgebiet</h2>
<p>Großraum Landshut und Rottenburg. Liegen Sie außerhalb, leiten wir Ihre Anfrage gern an die zuständige Vertretung weiter, oder wir kümmern uns selbst darum.</p>
</div>
<figure class="contact__person">
{pic("haas", "Wolfgang Haas", sizes="96px")}
<figcaption><b>Wolfgang Haas</b>Ihr Ansprechpartner</figcaption>
</figure>
</aside>
<form class="form" id="anfrage" novalidate>
<h2 class="h3">Anfrage senden</h2>
<fieldset class="form__group">
<legend>Worum geht es?</legend>
<div class="choices">
{"".join(f'<label class="choice"><input type="radio" name="anliegen" value="{k}"{" checked" if k == "tor" else ""}><span>{n}</span></label>' for k, n in ANLIEGEN)}
</div>
</fieldset>
<div class="form__row">
<label class="field"><span>Name *</span><input name="name" autocomplete="name" required></label>
<label class="field"><span>Telefon *</span><input name="telefon" type="tel" autocomplete="tel" required></label>
</div>
<div class="form__row">
<label class="field"><span>E-Mail</span><input name="email" type="email" autocomplete="email"></label>
<label class="field"><span>PLZ und Ort *</span><input name="ort" autocomplete="postal-code" required></label>
</div>
<label class="field"><span>Ihre Nachricht</span><textarea name="nachricht" rows="5" placeholder="z. B. Doppelgarage, Breite ca. 5 m, Tor soll elektrisch werden"></textarea></label>
<fieldset class="form__group">
<legend>Wann dürfen wir zurückrufen?</legend>
<div class="choices choices--small">
{"".join(f'<label class="choice"><input type="radio" name="rueckruf" value="{v}"{" checked" if v == "egal" else ""}><span>{n}</span></label>' for v, n in (("vormittags", "Vormittags"), ("nachmittags", "Nachmittags"), ("egal", "Egal")))}
</div>
</fieldset>
<label class="check"><input type="checkbox" name="datenschutz" required><span>Ich bin einverstanden, dass meine Angaben zur Bearbeitung der Anfrage verwendet werden. Mehr dazu in der <a class="link" href="datenschutz.html">Datenschutzerklärung</a>. *</span></label>
{DEMO_NOTE}
<button class="btn btn--blue btn--block" type="submit">Anfrage absenden {I_ARROW}</button>
<div class="form__done" hidden tabindex="-1">
{I_CHECK}
<div><b>Danke, Ihre Anfrage ist angekommen.</b><span data-done-text>Wir melden uns so schnell wie möglich.</span></div>
</div>
</form>
</div>
</section>'''
# ============================== Rechtliches ==============================
IMPRESSUM = page_hero("Impressum", "Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).", "Rechtliches") + f'''
<section class="section section--tight">
<div class="wrap legal">
<h2>Anbieter</h2>
<address>{FIRMA}<br>Inhaber: {INHABER}<br>{STRASSE}<br>{ORT}</address>
<h2>Kontakt</h2>
<p>Telefon: <a class="link" href="{TEL_HREF}">{TEL}</a><br>E-Mail: über das <a class="link" href="kontakt.html">Kontaktformular</a></p>
<span class="todo">Offen: Eine direkt erreichbare E-Mail-Adresse ist nach § 5 DDG Pflicht, ein Kontaktformular allein reicht nicht. Bitte nachtragen.</span>
<h2>Umsatzsteuer</h2>
<p>Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG: DE 128 883 228<br>Steuernummer: 132/224/00370, Finanzamt Landshut</p>
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p>{INHABER}, Anschrift wie oben.</p>
<h2>Verbraucherstreitbeilegung</h2>
<p>Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
<h2>Bildnachweise</h2>
<p>Projektfotos: {FIRMA}. Modellfotos: Normstahl (Herstellerbilder). Schriften: Barlow und Barlow Condensed (SIL Open Font License), lokal eingebunden.</p>
</div>
</section>'''
DATENSCHUTZ = page_hero("Datenschutz", "Diese Website setzt keine Cookies und verwendet kein Tracking.", "Rechtliches") + f'''
<section class="section section--tight">
<div class="wrap legal">
<h2>1. Verantwortlicher</h2>
<address>{FIRMA}, {INHABER}<br>{STRASSE}, {ORT}<br>Telefon: {TEL}</address>
<h2>2. Hosting und Server-Logfiles</h2>
<p>Beim Aufruf dieser Website verarbeitet unser Hosting-Anbieter technisch notwendige Daten (IP-Adresse, Datum und Uhrzeit, aufgerufene Seite, Referrer, Browser und Betriebssystem), um die Seite auszuliefern und den sicheren Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.</p>
<span class="todo">Offen: Hosting-Anbieter, Auftragsverarbeitungsvertrag (Art. 28 DSGVO) und Speicherdauer der Logfiles eintragen.</span>
<h2>3. Keine Cookies, kein Tracking</h2>
<p>Diese Website setzt keine Cookies und verwendet keine Analyse- oder Marketingwerkzeuge. Schriften und Skripte liegen auf unserem eigenen Server. Im Sitzungsspeicher Ihres Browsers (sessionStorage) wird lediglich vermerkt, dass die Eröffnungsanimation schon gezeigt wurde. Dieser Eintrag enthält keine personenbezogenen Daten und wird beim Schließen des Browsers gelöscht.</p>
<h2>4. Kontaktformular und Anfragen</h2>
<p>Wenn Sie uns über das Formular oder per Telefon kontaktieren, verarbeiten wir Ihre Angaben (Name, Telefon, Ort, optional E-Mail und Nachricht), um Ihre Anfrage zu bearbeiten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, soweit es um einen Vertrag oder ein Angebot geht, ansonsten Art. 6 Abs. 1 lit. f DSGVO. Wir löschen die Daten, sobald sie nicht mehr erforderlich sind und keine gesetzlichen Aufbewahrungspflichten bestehen.</p>
<span class="todo">Offen: Technischen Versand des Formulars festlegen (z. B. PHP-Mailer beim Hoster) und hier beschreiben.</span>
<h2>5. Externe Links</h2>
<p>Die Links „Route planen“ (Google Maps) und „Facebook“ führen zu externen Anbietern. Daten werden erst übertragen, wenn Sie einen dieser Links anklicken. Dann gilt die Datenschutzerklärung des jeweiligen Anbieters.</p>
<h2>6. Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit sowie Widerspruch gegen Verarbeitungen auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Außerdem können Sie sich beim Bayerischen Landesamt für Datenschutzaufsicht (BayLDA), Promenade 18, 91522 Ansbach, beschweren.</p>
<p><em>Stand: Oktober 2026. Entwurf, vor der Veröffentlichung juristisch prüfen lassen.</em></p>
</div>
</section>'''
page("index.html", "HEB Bauelemente – Garagentore & Antriebe in Essenbach | Wolfgang Haas",
"Normstahl-Garagentore, Sommer-Antriebe, Reparatur und Garagen-Renovierung in 24 Stunden zum Festpreis. Ihr Fachbetrieb im Raum Landshut und Rottenburg.",
INDEX, intro=True)
page("projekte.html", "Tore & Projekte – HEB Bauelemente Essenbach",
"Garagentore aus der Region Landshut und Rottenburg: Deckensektionaltore, Seitensektionaltore, Schwingtore und Holztore von Normstahl.",
PROJEKTE)
page("kontakt.html", "Kontakt – HEB Bauelemente, Wolfgang Haas",
"Rufen Sie an unter 08703 1362 oder senden Sie eine Anfrage für Garagentor, Antrieb, Reparatur oder 24-h-Renovierung.",
KONTAKT)
page("impressum.html", "Impressum – HEB Bauelemente", "Impressum von HEB Bauelemente, Wolfgang Haas, Am Ring 20, 84051 Essenbach.", IMPRESSUM)
page("datenschutz.html", "Datenschutz – HEB Bauelemente", "Datenschutzerklärung von HEB Bauelemente: keine Cookies, kein Tracking.", DATENSCHUTZ)