This is an Astro 6 site with React, Tailwind CSS 4, and shadcn/ui (Base Luma style). It uses hybrid rendering: pages are pre-rendered by default, with SSR opt-in per page.
- Framework: Astro 6 with
@astrojs/nodeadapter (standalone mode) - UI: React 19, shadcn/ui v4 (Base Luma style, neutral base color)
- Styling: Tailwind CSS 4 via Vite plugin,
tw-animate-css - Fonts: Google Fonts via Astro Font API (configured in
astro.config.mjs, applied via<Font />in RootLayout) - Icons: astro-icon (Iconify-Sets, rendert zur Buildzeit als reines SVG). shadcn-Komponenten nutzen intern lucide-react.
- SEO: Meta-Tags (title, description, canonical) im RootLayout,
@astrojs/sitemap, dynamischerobots.txt - Favicons:
astro-faviconsgeneriert auspublic/favicon.svgalle Varianten (ICO, Apple Touch, Manifest) - Kontaktformular: React-Komponente mit valibot-Validierung, Honeypot, Time-Based Spam-Schutz, nodemailer (SMTP)
- Language: TypeScript (strict), TSX for components
src/
config.ts # Zentrale Site-Konfiguration (Name, URL, Navigation, API-Keys)
components/
RootLayout.astro # Root HTML layout mit Header, Footer, Font, SEO-Meta
Header.astro # Responsive Header mit Desktop-Nav und mobilem Menü
Footer.astro # Footer mit Copyright und Links
DesktopNav.tsx # Desktop-Navigation mit Hover-Dropdowns (shadcn NavigationMenu)
MobileNav.tsx # Mobile Navigation (shadcn Sheet, von rechts)
ContactForm.tsx # Kontaktformular (React, valibot, Honeypot, Time-Based)
ui/ # shadcn/ui components
lib/
utils.ts # cn() utility for class merging
erecht24.ts # eRecht24 API client (Impressum/Datenschutz)
pages/
*.astro # Astro pages (pre-rendered by default)
404.astro # 404-Fehlerseite
kontakt.astro # Kontaktseite mit Formular
impressum.astro # Impressum (eRecht24 API oder Fallback)
datenschutz.astro # Datenschutzerklärung (eRecht24 API oder Fallback)
robots.txt.ts # Dynamische robots.txt mit Sitemap-Link
api/contact.ts # API-Endpoint für Kontaktformular (SSR)
styles/
global.css # Tailwind imports, theme variables
public/
favicon.svg # Quell-Favicon (wird von astro-favicons zu allen Formaten generiert)
- Astro pages live in
src/pages/as.astrofiles - Pages are pre-rendered (static) by default
- For SSR pages (forms, dynamic content), add
export const prerender = falsein the frontmatter - Use
RootLayoutas the wrapping component for all pages - Language is German (
lang="de")
- Interactive components are React TSX in
src/components/ - Use shadcn/ui components from
src/components/ui/ - Install new shadcn components with:
pnpm dlx shadcn@latest add <component> - Use the
cn()utility from@/lib/utilsfor conditional classes
- Use Tailwind utility classes, not custom CSS
- Theme colors are defined as CSS custom properties in
src/styles/global.css - Use semantic color tokens:
bg-background,text-foreground,bg-primary, etc.
- Anything that reads as a card, panel, button, input, sheet, or visible border must carry an explicit Tailwind
rounded-*class so it stays tied to the global radius. The point is that the whole site can be flipped between sharp and soft corners later by editing one value. - The actual radius is controlled globally by the
--radiusCSS variable insrc/styles/global.css. Tailwind'srounded-*utilities derive their values from it, so any surface with arounded-*class follows that single knob. rounded-noneis allowed as a deliberate local override — when a specific element should stay sharp regardless of the global setting. Don't reach for it just because the current theme happens to render at radius 0; that hides the element from the global switch.- Stick to the shadcn scale: small inline elements
rounded-sm/rounded-md, cards and panelsrounded-lg/rounded-xl, prominent hero or feature surfacesrounded-2xl/rounded-3xl.
- When two or more cards sit side-by-side in a grid or flex row, they must all render at the same height — the height of the tallest one in the row. Uneven card frames look broken even when each card is internally fine. The content inside cards is allowed to vary (longer descriptions, different CTAs); only the outer frame must stay uniform.
- Default pattern: parent is
grid grid-cols-N gap-…and each card is a direct grid child. CSS Grid stretches row cells to match the tallest content automatically — noh-fullneeded unless something disables the stretch. - If the card has internal vertical layout (e.g. a CTA pinned to the bottom), use
flex flex-colon the card andmt-autoon the bottom-anchored element so it fills the extra space the stretch produces. - If you wrap each card in an extra container (animation wrapper, scroll-reveal, link), put
h-fullon the wrapper AND on the card so the stretch propagates through. In flex-row setups, keep the parent's defaultitems-stretch— don't switch toitems-start/items-center, which would undo the stretch.
- Zentrale Einstellungen in
src/config.ts(Name, URL, Navigation, API-Keys) config.name: Seitenname, wird in Header, Footer, Mobile Nav und Seitentiteln verwendetconfig.tagline: Tagline, wird auf der Startseite angezeigtconfig.site: URL der Seite — WICHTIG: muss auch inastro.config.mjs(Zeile 10) identisch gepflegt werden (Astro kannconfig.tsnicht importieren wegen Vite-Init).allowedDomainsinastro.config.mjsleitet sich automatisch aus der dortigen URL ab.config.navigation.header/config.navigation.footer: Nav-Links vom TypNavLink[](ausconfig.ts)- Einfacher Link:
{ label, href } - Dropdown (Hover-Menü mit Unterlinks):
{ label, items: [{ label, href }, …] }—hrefam Dropdown-Eintrag ist optional (z.B. Übersichtsseite) - Hat ein Header-Eintrag
items, rendertHeader.astroautomatisch die interaktiveDesktopNav-Island (client:load). Ohne Dropdown bleibt die Desktop-Nav statisches HTML ohne Client-JS. Das mobile Menü (MobileNav) zeigt Unterlinks als eingerückte Gruppe.
- Einfacher Link:
config.smtp: SMTP-Zugangsdaten für Kontaktformular. Default-Host:mail.agenturserver.de. STARTTLS: Port 25/587 (secure: false), SSL: Port 465 (secure: true). Default ist STARTTLS auf Port 587.config.erecht24.apiKey: API-Key für eRecht24 Impressum/Datenschutz — ohne Key werden Platzhalter ausgeliefert
- RootLayout akzeptiert
titleunddescriptionProps - Canonical URL wird automatisch aus
config.site+ Pfad generiert - Sitemap wird beim Build von
@astrojs/sitemaperzeugt - robots.txt wird dynamisch generiert mit Sitemap-Link
- Favicons werden von
astro-faviconsauspublic/favicon.svggeneriert
- Google Fonts über Astros eingebaute Font API (
astro.config.mjs→fonts-Array) <Font cssVariable="--font-inter" />im<head>von RootLayout einbinden- In
global.cssvia@theme inlinedie Tailwind-Variable setzen:--font-sans: var(--font-inter), sans-serif - Neue Fonts: im
fonts-Array inastro.config.mjsergänzen,<Font />im Layout hinzufügen
astro-iconfür Astro-Seiten:import { Icon } from "astro-icon/components"- Rendert zur Buildzeit als inline SVG — kein Client-JS, kein Laden
- Erlaubte Icon-Sets: nur
lucideundhugeicons— keine anderen Icon-Librarys verwenden. - Icons ausschließlich über
@iconify-jsonPakete einbinden. Vorinstalliert:@iconify-json/lucideund@iconify-json/hugeicons. - Syntax:
<Icon name="lucide:search" />,<Icon name="hugeicons:star" /> astro-iconkann NICHT in React-Komponenten verwendet werden, nur in.astro-Dateien- shadcn/ui-Komponenten nutzen intern
lucide-react— das nicht entfernen
/kontaktmit React-KomponenteContactForm.tsx(client:load)- API-Endpoint
src/pages/api/contact.ts(SSR,prerender = false) - Validierung serverseitig mit valibot (Vorname, Nachname, E-Mail, Nachricht Pflicht; Telefon optional)
- Spam-Schutz: Honeypot-Feld (
_gotcha) + Time-Based Check (min. 5s zwischen Laden und Absenden) - SMTP-Versand via nodemailer, Konfiguration in
config.smtp - Ohne SMTP-Config → Fehlermeldung "SMTP ist nicht konfiguriert"
/impressumund/datenschutzwerden über eRecht24 API befüllt (wennconfig.erecht24.apiKeygesetzt)- Ohne API-Key: Platzhalter-Texte (Mustermann-Daten)
- Mit API-Key aber API-Fehler: Build bricht ab (kein stilles Fallback auf falsche Daten)
- ESLint:
pnpm lint(check),pnpm lint:fix(auto-fix) - Prettier:
pnpm format(write),pnpm format:check(check) - Config:
eslint.config.mjs,.prettierrc.mjs - Prettier plugins:
prettier-plugin-astro,prettier-plugin-tailwindcss(Tailwind class sorting) - ESLint plugins:
typescript-eslint,eslint-plugin-astro,@eslint-react/eslint-plugin eslint-config-prettierdeaktiviert ESLint-Regeln die mit Prettier kollidieren- shadcn/ui-Komponenten (
src/components/ui/) haben gelockerte Lint-Regeln (generierter Code)
@/*maps to./src/*(e.g.,import { Button } from "@/components/ui/button")
- Für lokale Bilder (im
src/-Verzeichnis): Immerimport { Image } from 'astro:assets'verwenden, nicht rohes<img>. Astro optimiert die Bilder automatisch (WebP/AVIF, responsive srcset, lazy loading). - Syntax:
<Image src={import("../assets/hero.jpg")} alt="Beschreibung" width={1200} height={630} /> - Für Bilder in
public/: Normales<img>ist okay, aberloading="lazy"unddecoding="async"setzen (außer beim Hero-Bild above the fold). - Für externe Bilder (URLs):
<img>mitloading="lazy"und explizitenwidth/heightAttributen um Layout Shifts zu vermeiden. - Jedes
<img>und<Image>braucht ein aussagekräftigesalt-Attribut. Dekorative Bilder:alt="".
ScrollReveal(src/components/ScrollReveal.astro) ist optional — nicht jede Seite braucht Animationen.- Nutze es sparsam: Hero-Bereich braucht keine Animation (ist sofort sichtbar), Sektionen unterhalb des Folds können dezent reinfahren.
- Maximal 2-3 verschiedene Animationstypen pro Seite, sonst wirkt es unruhig.
- Staffelung über
delaynur bei Elementen die gleichzeitig sichtbar werden (z.B. Feature-Karten in einer Reihe), nicht bei Elementen die untereinander stehen. - Respektiert
prefers-reduced-motionautomatisch. - Verwendung:
<ScrollReveal animation="fade-up">
<section class="py-20">
<h2>Features</h2>
</section>
</ScrollReveal>
<!-- Mit Staffelung -->
<div class="grid grid-cols-3 gap-8">
<ScrollReveal animation="fade-up" delay={0}>
<div>Feature 1</div>
</ScrollReveal>
<ScrollReveal animation="fade-up" delay={100}>
<div>Feature 2</div>
</ScrollReveal>
<ScrollReveal animation="fade-up" delay={200}>
<div>Feature 3</div>
</ScrollReveal>
</div>- RootLayout unterstützt
ogImageundogTypeProps. - OG/Twitter-Image-Tags werden nur gerendert wenn
ogImageexplizit gesetzt ist. OhneogImagewird kein Bild-Tag ausgegeben. - Jedes Projekt sollte, wenn es sich anbietet, ein eigenes OG-Image erstellen (1200×630px) und als
ogImage-Prop an RootLayout übergeben:<RootLayout ogImage="/og.png"> - Für Unterseiten mit eigenem OG-Image: den Pfad pro Seite setzen.
Jedes Projekt bekommt eine eigene visuelle Persönlichkeit. Bevor du Code schreibst, entscheide dich für einen Gestaltungsansatz: Ist die Seite luftig und elegant? Kompakt und direkt? Verspielt? Editorial? Bold und plakatartig?
Leite daraus konkrete Entscheidungen ab: Wie groß ist die Typografie? Wie viel Whitespace? Wie dicht stehen Elemente beieinander? Welche Farben dominieren?
- Nicht das Standard-Layout kopieren: Nicht jede Seite braucht das Muster "zentrierter Hero → 3er-Karten-Grid → Testimonials → CTA". Das ist das Standardlayout das jeder AI-Builder ausspuckt. Brich bewusst davon ab.
- Nicht jede Sektion braucht zentrierten Titel mit Untertitel darunter. Titel können links stehen, können übergroß sein, können in die nächste Sektion reinragen, können fehlen.
- Nicht jede Aufzählung muss ein gleichmäßiges Grid sein. Nutze auch: gestaffelte Layouts, eine einzelne große Karte neben zwei kleinen, horizontale Scrollbereiche, oder einfach gut gesetzte Fließtext-Absätze.
- Vermeide generische Stockfoto-Beschreibungen. Wenn Bilder gebraucht werden, nutze Platzhalter-Services oder beschreibe dem Kunden was dort hin soll.
- Vermeide den "AI-Look": Übermäßiger Einsatz von Schatten, abgerundete Karten mit Border und gleich große Icons in einem Grid.
- Vermeide einheitliche Container-Breiten: Nicht jede Sektion in
max-w-7xl mx-autopacken. Manche Sektionen dürfen volle Breite nutzen, manche bewusst schmaler sein (max-w-2xl,max-w-4xl). Der Wechsel der Container-Breiten erzeugt visuellen Rhythmus.
- Kontrast durch Abwechslung: Wechsle zwischen Sektionen mit viel Whitespace und kompakteren Bereichen. Zwischen hellen und farbigen Hintergründen. Zwischen großer und normaler Typografie. Der Wechsel erzeugt Spannung.
- Weniger ist oft besser: Eine Landing Page mit 4 starken Sektionen schlägt eine mit 8 generischen. Nicht jede mögliche Information muss auf die Startseite.
- Typografie als Gestaltungselement: Übergroße Headlines (
text-5xlbistext-8xl), bewusst gesetzteleading-tight/tracking-tightKombinationen, oder ein einzelner Satz der eine ganze Viewport-Höhe einnimmt — Typografie kann das stärkste visuelle Element der Seite sein. - Farbflächen statt Karten: Statt alles in
bg-card rounded-lg border shadowKarten zu packen, nutze farbige Sektions-Hintergründe (bg-primary,bg-muted, eine Custom-Farbe aus dem Theme), volle Breite, mit Content darin. Das wirkt erwachsener als Karten-Layouts. - Bewusster Einsatz von
bg-primary: Die Primärfarbe nicht nur für Buttons nutzen, sondern auch für ganze Sektionen, große Flächen, oder typografische Akzente. Das verankert die Markenfarbe in der Seite.
- Mobile ist nicht "Desktop kleiner machen". Überlege bei jeder Sektion: Was ändert sich auf Mobile tatsächlich? Große Headlines werden kleiner, aber nicht winzig. Mehrspaltige Layouts stacken, aber vielleicht nicht alle — manche Inhalte können auf Mobile einfach entfallen oder anders dargestellt werden.
- Nutze die volle Breite des Tailwind Breakpoint-Systems:
sm:,md:,lg:,xl:— nicht nurmd:für den einen Breakpoint.
- shadcn/ui-Komponenten sind Werkzeuge, nicht Bausteine. Nutze sie für interaktive Elemente (Accordion für FAQs, Sheet für Mobile-Nav, Button für CTAs), aber bau Sektions-Layouts in reinem HTML + Tailwind. Eine Landing Page die hauptsächlich aus shadcn-Cards und -Badges besteht sieht aus wie ein Dashboard, nicht wie eine Marketing-Seite.
- Vermeide Dashboard-UI-Komponenten auf Landing Pages: Table, Command, Combobox, Calendar, Resizable, Menubar haben auf einer Landing Page nichts verloren.
Docker-based with nginx reverse proxy. See docker/README.md for details. Auto-rebuilds on git push to the watched branch.