Revision vor Kundenübergabe: Plausibilität, SEO, A11y, OpenPanel
This commit is contained in:
Generated
+108
-616
File diff suppressed because it is too large
Load Diff
+4
-6
@@ -26,8 +26,7 @@
|
|||||||
"@calcom/embed-react": "^1.5.3",
|
"@calcom/embed-react": "^1.5.3",
|
||||||
"@fontsource-variable/nunito-sans": "^5.3.0",
|
"@fontsource-variable/nunito-sans": "^5.3.0",
|
||||||
"@fontsource-variable/public-sans": "^5.3.0",
|
"@fontsource-variable/public-sans": "^5.3.0",
|
||||||
"@react-three/drei": "^10.7.8",
|
"@openpanel/web": "^1.4.1",
|
||||||
"@react-three/fiber": "^9.7.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-devtools": "latest",
|
"@tanstack/react-devtools": "latest",
|
||||||
"@tanstack/react-router": "latest",
|
"@tanstack/react-router": "latest",
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
"better-auth": "^1.5.3",
|
"better-auth": "^1.5.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-kit": "^0.31.10",
|
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"nitro": "3.0.260610-beta",
|
"nitro": "3.0.260610-beta",
|
||||||
@@ -47,9 +45,8 @@
|
|||||||
"shadcn": "^4.19.0",
|
"shadcn": "^4.19.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"three": "^0.185.1",
|
"tsx": "^4.21.0",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6"
|
||||||
"tsx": "^4.21.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.5",
|
"@biomejs/biome": "2.4.5",
|
||||||
@@ -64,6 +61,7 @@
|
|||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"vite": "^8.0.0"
|
"vite": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /login
|
||||||
|
Disallow: /dashboard
|
||||||
|
Disallow: /admin
|
||||||
|
Disallow: /api/
|
||||||
|
|
||||||
|
Sitemap: https://webkulisse.de/sitemap.xml
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://webkulisse.de/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://webkulisse.de/termin</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://webkulisse.de/impressum</loc>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://webkulisse.de/datenschutz</loc>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// OpenPanel wird ausschließlich im Browser initialisiert. Ein dynamischer Import
|
||||||
|
// verhindert, dass der Konstruktor bereits beim Server-Rendering läuft.
|
||||||
|
export function Analytics() {
|
||||||
|
useEffect(() => {
|
||||||
|
void import("@/openpanel.ts");
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+25
-10
@@ -24,14 +24,25 @@ export const Footer = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 sm:flex-row lg:flex-col">
|
<div className="flex flex-col gap-3 sm:flex-row lg:flex-col">
|
||||||
<BookingDialog>
|
<BookingDialog>
|
||||||
<Button data-type="accent" size="lg" className="gap-2">
|
<Button
|
||||||
|
data-type="accent"
|
||||||
|
size="lg"
|
||||||
|
className="gap-2"
|
||||||
|
data-track="cta_erstgespraech"
|
||||||
|
data-source="footer"
|
||||||
|
>
|
||||||
Erstgespräch buchen
|
Erstgespräch buchen
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</BookingDialog>
|
</BookingDialog>
|
||||||
<Button variant="outline" size="lg" asChild>
|
<Button variant="outline" size="lg" asChild>
|
||||||
<a href="tel:+4917622222222" className="no-underline">
|
<a
|
||||||
+49 176 22222222
|
href="tel:+491718084830"
|
||||||
|
className="no-underline"
|
||||||
|
data-track="kontakt_telefon"
|
||||||
|
data-source="footer_cta"
|
||||||
|
>
|
||||||
|
+49 171 8084830
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,10 +56,10 @@ export const Footer = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<img
|
||||||
alt="Placeholder Logo"
|
alt=""
|
||||||
height="32"
|
height="520"
|
||||||
width="32"
|
width="690"
|
||||||
src="https://placehold.co/64"
|
src="/img/logo.png"
|
||||||
className="h-8 w-8 rounded-md"
|
className="h-8 w-8 rounded-md"
|
||||||
/>
|
/>
|
||||||
<span className="font-heading text-lg font-bold text-(--sea-ink)">
|
<span className="font-heading text-lg font-bold text-(--sea-ink)">
|
||||||
@@ -66,7 +77,7 @@ export const Footer = () => {
|
|||||||
Anbieter
|
Anbieter
|
||||||
</h3>
|
</h3>
|
||||||
<address className="mt-3 text-sm not-italic text-(--sea-ink)">
|
<address className="mt-3 text-sm not-italic text-(--sea-ink)">
|
||||||
Mathias Hurler Webdesign
|
Hurler Webdesign
|
||||||
<br />
|
<br />
|
||||||
Untermagerbein 30
|
Untermagerbein 30
|
||||||
<br />
|
<br />
|
||||||
@@ -83,6 +94,8 @@ export const Footer = () => {
|
|||||||
<a
|
<a
|
||||||
href="mailto:info@webkulisse.de"
|
href="mailto:info@webkulisse.de"
|
||||||
className="inline-flex items-center gap-2 no-underline hover:underline"
|
className="inline-flex items-center gap-2 no-underline hover:underline"
|
||||||
|
data-track="kontakt_mail"
|
||||||
|
data-source="footer_list"
|
||||||
>
|
>
|
||||||
<Mail className="h-4 w-4" aria-hidden />
|
<Mail className="h-4 w-4" aria-hidden />
|
||||||
info@webkulisse.de
|
info@webkulisse.de
|
||||||
@@ -90,11 +103,13 @@ export const Footer = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="tel:+4917622222222"
|
href="tel:+491718084830"
|
||||||
className="inline-flex items-center gap-2 no-underline hover:underline"
|
className="inline-flex items-center gap-2 no-underline hover:underline"
|
||||||
|
data-track="kontakt_telefon"
|
||||||
|
data-source="footer_list"
|
||||||
>
|
>
|
||||||
<Phone className="h-4 w-4" aria-hidden />
|
<Phone className="h-4 w-4" aria-hidden />
|
||||||
+49 176 22222222
|
+49 171 8084830
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="inline-flex items-center gap-2 text-(--sea-ink-soft)">
|
<li className="inline-flex items-center gap-2 text-(--sea-ink-soft)">
|
||||||
|
|||||||
+38
-17
@@ -6,11 +6,11 @@ import { Button } from "@/components/ui/button.tsx";
|
|||||||
import { authClient } from "@/lib/auth-client.ts";
|
import { authClient } from "@/lib/auth-client.ts";
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: "#zielgruppen", label: "Für wen" },
|
{ hash: "zielgruppen", label: "Für wen" },
|
||||||
{ href: "#warum-wir", label: "Warum wir" },
|
{ hash: "warum-wir", label: "Warum wir" },
|
||||||
{ href: "#ablauf", label: "Ablauf" },
|
{ hash: "ablauf", label: "Ablauf" },
|
||||||
{ href: "#pricing", label: "Angebote" },
|
{ hash: "pricing", label: "Angebote" },
|
||||||
{ href: "#kontakt", label: "Kontakt" },
|
{ hash: "kontakt", label: "Kontakt" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
@@ -52,7 +52,7 @@ export default function Header() {
|
|||||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3 md:px-6">
|
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3 md:px-6">
|
||||||
<Link to="/" aria-label="Startseite" className="flex items-center gap-2 no-underline">
|
<Link to="/" aria-label="Startseite" className="flex items-center gap-2 no-underline">
|
||||||
<img
|
<img
|
||||||
alt="Logo Webkulisse"
|
alt=""
|
||||||
height="520"
|
height="520"
|
||||||
width="690"
|
width="690"
|
||||||
src="/img/logo.png"
|
src="/img/logo.png"
|
||||||
@@ -66,13 +66,17 @@ export default function Header() {
|
|||||||
<nav aria-label="Hauptnavigation" className="hidden md:block">
|
<nav aria-label="Hauptnavigation" className="hidden md:block">
|
||||||
<ul className="flex items-center gap-1">
|
<ul className="flex items-center gap-1">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<li key={link.href}>
|
<li key={link.hash}>
|
||||||
<a
|
<Link
|
||||||
href={link.href}
|
to="/"
|
||||||
|
hash={link.hash}
|
||||||
className="nav-link rounded-md px-3 py-2 text-sm font-medium no-underline"
|
className="nav-link rounded-md px-3 py-2 text-sm font-medium no-underline"
|
||||||
|
data-track="nav_anchor"
|
||||||
|
data-hash={link.hash}
|
||||||
|
data-source="header_desktop"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -89,7 +93,14 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button asChild variant="default" className="hidden sm:inline-flex">
|
<Button asChild variant="default" className="hidden sm:inline-flex">
|
||||||
<Link to={loginTarget}>{loginLabel}</Link>
|
<Link
|
||||||
|
to={loginTarget}
|
||||||
|
data-track="nav_kundenlogin"
|
||||||
|
data-source="header_desktop"
|
||||||
|
data-authenticated={session?.user ? "true" : "false"}
|
||||||
|
>
|
||||||
|
{loginLabel}
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -97,7 +108,7 @@ export default function Header() {
|
|||||||
onClick={() => setMobileOpen((v) => !v)}
|
onClick={() => setMobileOpen((v) => !v)}
|
||||||
aria-expanded={mobileOpen}
|
aria-expanded={mobileOpen}
|
||||||
aria-controls="mobile-nav"
|
aria-controls="mobile-nav"
|
||||||
aria-label="Menü öffnen"
|
aria-label={mobileOpen ? "Menü schließen" : "Menü öffnen"}
|
||||||
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-[var(--line)] bg-[var(--chip-bg)] text-[var(--sea-ink)] md:hidden"
|
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-[var(--line)] bg-[var(--chip-bg)] text-[var(--sea-ink)] md:hidden"
|
||||||
>
|
>
|
||||||
{mobileOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
{mobileOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
||||||
@@ -113,19 +124,29 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<ul className="mx-auto flex max-w-6xl flex-col gap-1 px-4 py-3">
|
<ul className="mx-auto flex max-w-6xl flex-col gap-1 px-4 py-3">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<li key={link.href}>
|
<li key={link.hash}>
|
||||||
<a
|
<Link
|
||||||
href={link.href}
|
to="/"
|
||||||
|
hash={link.hash}
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className="nav-link block rounded-md px-3 py-2 text-base font-medium no-underline"
|
className="nav-link block rounded-md px-3 py-2 text-base font-medium no-underline"
|
||||||
|
data-track="nav_anchor"
|
||||||
|
data-hash={link.hash}
|
||||||
|
data-source="header_mobile"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li className="pt-2">
|
<li className="pt-2">
|
||||||
<Button asChild variant="default" className="w-full">
|
<Button asChild variant="default" className="w-full">
|
||||||
<Link to={loginTarget} onClick={() => setMobileOpen(false)}>
|
<Link
|
||||||
|
to={loginTarget}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
data-track="nav_kundenlogin"
|
||||||
|
data-source="header_mobile"
|
||||||
|
data-authenticated={session?.user ? "true" : "false"}
|
||||||
|
>
|
||||||
{loginLabel}
|
{loginLabel}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+16
-16
@@ -10,8 +10,7 @@ const trustPoints = [
|
|||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-6xl px-4 pt-12 pb-16 md:px-6 md:pt-20 md:pb-24">
|
<section className="mx-auto max-w-4xl px-4 pt-12 pb-16 md:px-6 md:pt-20 md:pb-24">
|
||||||
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
|
|
||||||
<div className="space-y-6 rise-in">
|
<div className="space-y-6 rise-in">
|
||||||
<span className="island-kicker inline-flex items-center gap-2">
|
<span className="island-kicker inline-flex items-center gap-2">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-[var(--lagoon)]" aria-hidden />
|
<span className="h-1.5 w-1.5 rounded-full bg-[var(--lagoon)]" aria-hidden />
|
||||||
@@ -29,13 +28,26 @@ export default function Hero() {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<BookingDialog>
|
<BookingDialog>
|
||||||
<Button data-type="accent" size="lg" className="gap-2">
|
<Button
|
||||||
|
data-type="accent"
|
||||||
|
size="lg"
|
||||||
|
className="gap-2"
|
||||||
|
data-track="cta_erstgespraech"
|
||||||
|
data-source="hero"
|
||||||
|
>
|
||||||
Erstgespräch buchen
|
Erstgespräch buchen
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</BookingDialog>
|
</BookingDialog>
|
||||||
<Button variant="ghost" size="lg" asChild>
|
<Button variant="ghost" size="lg" asChild>
|
||||||
<a href="#pricing" className="no-underline">Pakete ansehen</a>
|
<a
|
||||||
|
href="#pricing"
|
||||||
|
className="no-underline"
|
||||||
|
data-track="cta_pakete_ansehen"
|
||||||
|
data-source="hero"
|
||||||
|
>
|
||||||
|
Pakete ansehen
|
||||||
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-(--sea-ink-soft)">
|
<p className="text-sm text-(--sea-ink-soft)">
|
||||||
@@ -51,18 +63,6 @@ export default function Hero() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="island-shell relative overflow-hidden rounded-3xl">
|
|
||||||
<img
|
|
||||||
className="block h-full w-full object-cover"
|
|
||||||
src="https://placehold.co/400x600"
|
|
||||||
alt="Aufgeräumter Schreibtisch aus Holz mit Laptop"
|
|
||||||
width="400"
|
|
||||||
height="600"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+279
-17
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Check, Sparkles } from "lucide-react";
|
import { Check, Sparkles } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { BookingDialog } from "@/components/booking-dialog.tsx";
|
import { BookingDialog } from "@/components/booking-dialog.tsx";
|
||||||
@@ -5,6 +6,7 @@ import { BookingDialog } from "@/components/booking-dialog.tsx";
|
|||||||
type PricingPlan = {
|
type PricingPlan = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: number;
|
||||||
features: string[];
|
features: string[];
|
||||||
@@ -12,16 +14,26 @@ type PricingPlan = {
|
|||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HostingPlan = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
priceMonthly: number;
|
||||||
|
priceYearly: number;
|
||||||
|
features: string[];
|
||||||
|
highlighted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const offerlist: PricingPlan[] = [
|
const offerlist: PricingPlan[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Das Kompakt-Paket",
|
name: "Das Kompakt-Paket",
|
||||||
|
slug: "kompakt",
|
||||||
description:
|
description:
|
||||||
"Perfekt für die schnelle, moderne Präsentation. Ideal für Therapeuten, Berater und regionale Handwerker.",
|
"Perfekt für die schnelle, moderne Präsentation. Ideal für Therapeuten, Berater und regionale Handwerker.",
|
||||||
price: 900,
|
price: 1800,
|
||||||
features: [
|
features: [
|
||||||
"Alle wichtigen Infos auf einer Seite",
|
"Alle wichtigen Infos auf einer Seite",
|
||||||
"Kontaktformular & Anfahrt",
|
|
||||||
"Für Smartphone optimiert",
|
"Für Smartphone optimiert",
|
||||||
"SSL-Verschlüsselung inklusive",
|
"SSL-Verschlüsselung inklusive",
|
||||||
"SEO-Grundlagen (Meta-Tags, schnelle Ladezeit)",
|
"SEO-Grundlagen (Meta-Tags, schnelle Ladezeit)",
|
||||||
@@ -31,40 +43,60 @@ const offerlist: PricingPlan[] = [
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "Das Klassik-Paket",
|
name: "Das Klassik-Paket",
|
||||||
|
slug: "klassik",
|
||||||
description:
|
description:
|
||||||
"Für Praxen und Betriebe mit breitem Angebot. Maximale Übersicht für Ihre Besucher.",
|
"Für Praxen und Betriebe mit breitem Angebot. Maximale Übersicht für Ihre Besucher.",
|
||||||
price: 1700,
|
price: 2500,
|
||||||
features: [
|
features: [
|
||||||
"Separate Leistungs- und Teamseiten",
|
"Separate Leistungs- und Teamseiten",
|
||||||
"Bildergalerie & Referenzen",
|
"Bildergalerie & Referenzen",
|
||||||
"Erweiterte SEO-Optimierung",
|
"Erweiterte SEO-Optimierung",
|
||||||
"Rechtstexte-Assistenz",
|
"Rechtstexte-Assistenz",
|
||||||
"3 Monate kostenloser Support",
|
|
||||||
],
|
],
|
||||||
priceNote: "Aufpreise z. B. für zusätzliche Sprachen oder Shop-Anbindung.",
|
priceNote: "Aufpreise z. B. für zusätzliche Sprachen oder Shop-Anbindung.",
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const hostinglist: HostingPlan[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Basis",
|
||||||
|
description: "",
|
||||||
|
priceMonthly: 79,
|
||||||
|
priceYearly: 790,
|
||||||
|
features: [
|
||||||
|
"Premium-Hosting",
|
||||||
|
"SSL-Zertifikat",
|
||||||
|
"Uptime Monitoring",
|
||||||
|
"30 Minuten pro Monat für Textänderungen oder Anpassungen",
|
||||||
|
],
|
||||||
|
highlighted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 2,
|
||||||
name: "Ihre neue Webapp",
|
name: "Standard",
|
||||||
description:
|
description: "",
|
||||||
"Für digitale Lösungen mit echtem Mehrwert. Ideal für Unternehmen, die mehr als eine Website brauchen.",
|
priceMonthly: 149,
|
||||||
price: 2500,
|
priceYearly: 1490,
|
||||||
features: [
|
features: [
|
||||||
"Login- und Nutzerbereich",
|
"Alles aus Basis",
|
||||||
"Individuelle Funktionen",
|
"1 Stunde pro Monat für inhaltliche Pflege, z. B. neue Texte, Bilder, Teammitglieder",
|
||||||
"Terminbuchung oder Kundenportal",
|
"Monatlicher Performance-Report zu Besucherzahlen und Analytics",
|
||||||
"SEO-Grundlagen (Meta-Tags, schnelle Ladezeit)",
|
|
||||||
"Skalierbare Infrastruktur",
|
|
||||||
],
|
],
|
||||||
priceNote: "Endpreis abhängig vom Funktionsumfang - Details im Erstgespräch.",
|
highlighted: true,
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const priceFormatter = new Intl.NumberFormat("de-DE");
|
const priceFormatter = new Intl.NumberFormat("de-DE");
|
||||||
|
|
||||||
|
type Billing = "monthly" | "yearly";
|
||||||
|
|
||||||
export const Pricing = () => {
|
export const Pricing = () => {
|
||||||
|
const [billing, setBilling] = useState<Billing>("monthly");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<section id="pricing" className="mx-auto max-w-6xl px-4 py-16 md:px-6 md:py-24">
|
<section id="pricing" className="mx-auto max-w-6xl px-4 py-16 md:px-6 md:py-24">
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
<span className="island-kicker">Preise</span>
|
<span className="island-kicker">Preise</span>
|
||||||
@@ -77,7 +109,7 @@ export const Pricing = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 grid gap-6 lg:grid-cols-3 lg:items-stretch">
|
<div className="mx-auto mt-12 grid max-w-4xl gap-6 lg:grid-cols-2 lg:items-stretch">
|
||||||
{offerlist.map((offer) => {
|
{offerlist.map((offer) => {
|
||||||
const isHighlighted = offer.highlighted;
|
const isHighlighted = offer.highlighted;
|
||||||
return (
|
return (
|
||||||
@@ -132,6 +164,8 @@ export const Pricing = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant={isHighlighted ? "default" : "outline"}
|
variant={isHighlighted ? "default" : "outline"}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
data-track="cta_erstgespraech"
|
||||||
|
data-source={`pricing_${offer.slug}`}
|
||||||
>
|
>
|
||||||
Erstgespräch buchen
|
Erstgespräch buchen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -141,6 +175,234 @@ export const Pricing = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-24 border-t border-(--line) pt-16">
|
||||||
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
|
<span className="island-kicker">Hosting & Betreuung</span>
|
||||||
|
<h3 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
||||||
|
Rundum betreut - auch nach dem Launch.
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 text-(--sea-ink-soft) sm:text-lg">
|
||||||
|
Wir übernehmen Technik, Sicherheit und laufende Anpassungen -
|
||||||
|
damit Ihre Seite dauerhaft schnell, sicher und aktuell bleibt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Abrechnungszeitraum"
|
||||||
|
className="inline-flex rounded-full border border-(--line) bg-white p-1 text-sm shadow-sm"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={billing === "monthly"}
|
||||||
|
onClick={() => setBilling("monthly")}
|
||||||
|
data-track="pricing_billing_toggle"
|
||||||
|
data-mode="monthly"
|
||||||
|
className={[
|
||||||
|
"rounded-full px-4 py-1.5 font-medium transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--lagoon-deep)",
|
||||||
|
billing === "monthly"
|
||||||
|
? "bg-(--lagoon-deep) text-white shadow"
|
||||||
|
: "text-(--sea-ink-soft) hover:text-(--sea-ink)",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
Monatlich
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={billing === "yearly"}
|
||||||
|
onClick={() => setBilling("yearly")}
|
||||||
|
data-track="pricing_billing_toggle"
|
||||||
|
data-mode="yearly"
|
||||||
|
className={[
|
||||||
|
"flex items-center gap-2 rounded-full px-4 py-1.5 font-medium transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--lagoon-deep)",
|
||||||
|
billing === "yearly"
|
||||||
|
? "bg-(--lagoon-deep) text-white shadow"
|
||||||
|
: "text-(--sea-ink-soft) hover:text-(--sea-ink)",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
Jährlich
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"rounded-full px-2 py-0.5 text-xs font-semibold",
|
||||||
|
billing === "yearly"
|
||||||
|
? "bg-white/20 text-white"
|
||||||
|
: "bg-(--surface-strong) text-(--lagoon-deep)",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
2 Monate frei
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-10 grid max-w-4xl gap-6 lg:grid-cols-2 lg:items-stretch">
|
||||||
|
{hostinglist.map((plan) => {
|
||||||
|
const isHighlighted = plan.highlighted;
|
||||||
|
const price =
|
||||||
|
billing === "monthly" ? plan.priceMonthly : plan.priceYearly;
|
||||||
|
const suffix = billing === "monthly" ? "/ Monat" : "/ Jahr";
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={plan.id}
|
||||||
|
className={[
|
||||||
|
"relative flex flex-col gap-5 rounded-3xl border p-6 sm:p-7",
|
||||||
|
isHighlighted
|
||||||
|
? "border-(--lagoon-deep) bg-(--surface-strong) shadow-[0_20px_50px_rgba(30,90,72,0.18)] lg:-translate-y-2"
|
||||||
|
: "feature-card border-(--line)",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{isHighlighted && (
|
||||||
|
<span className="absolute -top-3 left-1/2 inline-flex -translate-x-1/2 items-center gap-1 rounded-full bg-(--lagoon-deep) px-3 py-1 text-xs font-semibold text-white shadow">
|
||||||
|
<Sparkles className="h-3.5 w-3.5" aria-hidden />
|
||||||
|
Empfehlung
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<header className="space-y-2">
|
||||||
|
<h4 className="font-heading text-xl font-bold text-(--sea-ink)">
|
||||||
|
{plan.name}
|
||||||
|
</h4>
|
||||||
|
{plan.description && (
|
||||||
|
<p className="text-sm text-(--sea-ink-soft)">
|
||||||
|
{plan.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="font-heading text-4xl font-bold text-(--sea-ink)">
|
||||||
|
{priceFormatter.format(price)} €
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-(--sea-ink-soft)">
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="-mt-3 text-xs text-(--sea-ink-soft)">
|
||||||
|
zzgl. gesetzl. Umsatzsteuer
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="space-y-2.5 text-sm text-(--sea-ink)">
|
||||||
|
{plan.features.map((feature) => (
|
||||||
|
<li key={feature} className="flex items-start gap-2">
|
||||||
|
<Check
|
||||||
|
className="mt-0.5 h-4 w-4 shrink-0 text-(--lagoon-deep)"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mx-auto mt-6 max-w-4xl text-center text-xs text-(--sea-ink-soft)">
|
||||||
|
Nicht genutzte Zeitkontingente verfallen zum Monatsende.
|
||||||
|
Kündbar zum Ende der jeweiligen Laufzeit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="startup"
|
||||||
|
className="mx-auto max-w-6xl px-4 pb-16 md:px-6 md:pb-24"
|
||||||
|
>
|
||||||
|
<div className="island-shell relative overflow-hidden rounded-3xl p-6 sm:p-10 lg:p-12">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-2 lg:items-center">
|
||||||
|
<div>
|
||||||
|
<span className="island-kicker">Für Startups</span>
|
||||||
|
<h2 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
||||||
|
Ohne Vorabkosten starten.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-(--sea-ink-soft) sm:text-lg">
|
||||||
|
Als Startup steckt jeder Euro ins Wachstum. Wir übernehmen
|
||||||
|
die Website und finanzieren die Erstellung mit - Sie zahlen
|
||||||
|
von Tag eins nur einen festen Monatspreis.
|
||||||
|
</p>
|
||||||
|
<ul className="mt-6 space-y-2.5 text-sm text-(--sea-ink)">
|
||||||
|
{[
|
||||||
|
"0 € Einmalkosten für die Erstellung",
|
||||||
|
"Klassik-Paket inkl. Hosting, SSL und laufender Pflege",
|
||||||
|
"1 Stunde inhaltliche Betreuung pro Monat inklusive",
|
||||||
|
"Mindestlaufzeit 24 Monate, danach monatlich kündbar",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex items-start gap-2">
|
||||||
|
<Check
|
||||||
|
className="mt-0.5 h-4 w-4 shrink-0 text-(--lagoon-deep)"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="mt-4 text-xs text-(--sea-ink-soft)">
|
||||||
|
Für Unternehmen mit Gründung innerhalb der letzten 3 Jahre.
|
||||||
|
Nachweis über Handelsregister- oder Gewerbeanmeldung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-(--lagoon-deep) bg-white p-6 shadow-[0_20px_50px_rgba(30,90,72,0.18)] sm:p-8">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-widest text-(--sea-ink-soft)">
|
||||||
|
Startup-Paket
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 font-heading text-sm text-(--sea-ink-soft)">
|
||||||
|
Alles inklusive
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-(--lagoon-deep) px-3 py-1 text-xs font-semibold text-white shadow">
|
||||||
|
<Sparkles className="h-3.5 w-3.5" aria-hidden />
|
||||||
|
Angebot
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-baseline gap-1">
|
||||||
|
<span className="font-heading text-5xl font-bold text-(--sea-ink)">
|
||||||
|
300 €
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-(--sea-ink-soft)">/ Monat</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-(--sea-ink-soft)">
|
||||||
|
zzgl. gesetzl. Umsatzsteuer
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl className="mt-5 grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="rounded-xl bg-(--surface-strong) px-3 py-2">
|
||||||
|
<dt className="text-xs text-(--sea-ink-soft)">
|
||||||
|
Einmalkosten
|
||||||
|
</dt>
|
||||||
|
<dd className="font-heading font-bold text-(--sea-ink)">
|
||||||
|
0 €
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-(--surface-strong) px-3 py-2">
|
||||||
|
<dt className="text-xs text-(--sea-ink-soft)">Laufzeit</dt>
|
||||||
|
<dd className="font-heading font-bold text-(--sea-ink)">
|
||||||
|
24 Monate
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<BookingDialog>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="mt-6 w-full"
|
||||||
|
data-track="cta_erstgespraech"
|
||||||
|
data-source="pricing_startup"
|
||||||
|
>
|
||||||
|
Erstgespräch buchen
|
||||||
|
</Button>
|
||||||
|
</BookingDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Header from "@/components/header.tsx";
|
||||||
|
import { Footer } from "@/components/footer.tsx";
|
||||||
|
|
||||||
|
export function PublicShell({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-screen flex-col">
|
||||||
|
<a
|
||||||
|
href="#main"
|
||||||
|
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-(--lagoon-deep) focus:px-4 focus:py-2 focus:font-medium focus:text-white focus:shadow-lg"
|
||||||
|
>
|
||||||
|
Direkt zum Hauptinhalt springen
|
||||||
|
</a>
|
||||||
|
<Header />
|
||||||
|
<main id="main" tabIndex={-1} className="flex-1 outline-none">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState, type ReactNode, type CSSProperties } from 'react';
|
import { useEffect, useRef, useState, type ReactNode, type CSSProperties } from 'react';
|
||||||
|
import './devtool.css';
|
||||||
|
|
||||||
interface CrosshairProps {
|
interface CrosshairProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { OpenPanel } from '@openpanel/web';
|
||||||
|
|
||||||
|
// Do-Not-Track-Signal des Browsers respektieren. Wenn der Besucher DNT aktiv
|
||||||
|
// hat, wird das SDK deaktiviert – keine Events, keine Screen Views. Damit die
|
||||||
|
// Zusage in der Datenschutzerklärung technisch abgedeckt ist.
|
||||||
|
const respectDnt =
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
(navigator.doNotTrack === '1' ||
|
||||||
|
// Legacy-Fallbacks für ältere Browser-Implementierungen.
|
||||||
|
(navigator as unknown as { msDoNotTrack?: string }).msDoNotTrack === '1' ||
|
||||||
|
(window as unknown as { doNotTrack?: string }).doNotTrack === '1');
|
||||||
|
|
||||||
|
export const op = new OpenPanel({
|
||||||
|
apiUrl: 'https://data.hurler-webdesign.de',
|
||||||
|
clientId: '40700ec3-9beb-45f9-9a2a-c9321d307d1a',
|
||||||
|
trackScreenViews: true,
|
||||||
|
trackOutgoingLinks: true,
|
||||||
|
trackAttributes: true,
|
||||||
|
disabled: respectDnt,
|
||||||
|
});
|
||||||
+51
-18
@@ -1,16 +1,38 @@
|
|||||||
import { HeadContent, Scripts, createRootRouteWithContext } from '@tanstack/react-router'
|
import { HeadContent, Scripts, createRootRouteWithContext } from '@tanstack/react-router'
|
||||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
import { lazy, Suspense } from 'react'
|
||||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
|
||||||
|
|
||||||
import type { Session } from '#/lib/auth.ts'
|
import type { Session } from '#/lib/auth.ts'
|
||||||
import { getSessionFn } from '#/lib/session.ts'
|
import { getSessionFn } from '#/lib/session.ts'
|
||||||
|
import { Analytics } from '#/components/analytics.tsx'
|
||||||
import appCss from '../styles.css?url'
|
import appCss from '../styles.css?url'
|
||||||
|
|
||||||
export interface RouterAppContext {
|
export interface RouterAppContext {
|
||||||
session: Session | null
|
session: Session | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeItnitScript = `
|
const DevtoolsPanel = import.meta.env.DEV
|
||||||
|
? lazy(async () => {
|
||||||
|
const [{ TanStackDevtools }, { TanStackRouterDevtoolsPanel }] = await Promise.all([
|
||||||
|
import('@tanstack/react-devtools'),
|
||||||
|
import('@tanstack/react-router-devtools'),
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
default: () => (
|
||||||
|
<TanStackDevtools
|
||||||
|
config={{ position: 'bottom-right' }}
|
||||||
|
plugins={[
|
||||||
|
{
|
||||||
|
name: 'Tanstack Router',
|
||||||
|
render: <TanStackRouterDevtoolsPanel />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
|
const themeInitScript = `
|
||||||
(function() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
var stored = localStorage.getItem('theme');
|
var stored = localStorage.getItem('theme');
|
||||||
@@ -57,19 +79,31 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
|||||||
content:
|
content:
|
||||||
'Handgemachte Webseiten – blitzschnell, sicher und individuell.',
|
'Handgemachte Webseiten – blitzschnell, sicher und individuell.',
|
||||||
},
|
},
|
||||||
{ property: 'og:image', content: '/img/logo.png' },
|
{ property: 'og:image', content: 'https://webkulisse.de/img/og-cover.png' },
|
||||||
|
{ property: 'og:image:width', content: '1200' },
|
||||||
|
{ property: 'og:image:height', content: '630' },
|
||||||
|
{ property: 'og:image:alt', content: 'webkulisse – Webdesign für Handwerk, Praxis & Therapie' },
|
||||||
|
{ property: 'og:url', content: 'https://webkulisse.de/' },
|
||||||
// Twitter/X Card
|
// Twitter/X Card
|
||||||
{ name: 'twitter:card', content: 'summary' },
|
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||||
{ name: 'twitter:title', content: 'webkulisse' },
|
{ name: 'twitter:title', content: 'webkulisse' },
|
||||||
{
|
{
|
||||||
name: 'twitter:description',
|
name: 'twitter:description',
|
||||||
content:
|
content:
|
||||||
'Handgemachte Webseiten – blitzschnell, sicher und individuell.',
|
'Handgemachte Webseiten – blitzschnell, sicher und individuell.',
|
||||||
},
|
},
|
||||||
{ name: 'twitter:image', content: '/img/logo.png' },
|
{ name: 'twitter:image', content: 'https://webkulisse.de/img/og-cover.png' },
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{ rel: 'stylesheet', href: appCss },
|
{ rel: 'stylesheet', href: appCss },
|
||||||
|
// Variable-Font vorladen, damit erste Textausgabe nicht auf den System-Fallback rutscht.
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: '/fonts/VendSans-VariableFont_wght.woff2',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
// Favicon – wird in Browser-Tabs, Bookmarks und PWA-Icons genutzt.
|
// Favicon – wird in Browser-Tabs, Bookmarks und PWA-Icons genutzt.
|
||||||
{ rel: 'icon', type: 'image/png', href: '/img/logo.png' },
|
{ rel: 'icon', type: 'image/png', href: '/img/logo.png' },
|
||||||
{ rel: 'apple-touch-icon', href: '/img/logo.png' },
|
{ rel: 'apple-touch-icon', href: '/img/logo.png' },
|
||||||
@@ -78,7 +112,11 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
|||||||
// z. B. im Blog: { rel: 'canonical', href: `${SITE_URL}/blog/${slug}` }.
|
// z. B. im Blog: { rel: 'canonical', href: `${SITE_URL}/blog/${slug}` }.
|
||||||
{ rel: 'canonical', href: 'https://webkulisse.de/' },
|
{ rel: 'canonical', href: 'https://webkulisse.de/' },
|
||||||
],
|
],
|
||||||
scripts: [{ children: themeItnitScript }]
|
scripts: [
|
||||||
|
{ children: themeInitScript },
|
||||||
|
// OpenPanel-Loader (self-hosted). Async, damit der Haupt-Thread nicht blockiert wird.
|
||||||
|
{ src: 'https://hub.hurler-webdesign.de/op1.js', async: true, defer: true },
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
shellComponent: RootDocument,
|
shellComponent: RootDocument,
|
||||||
})
|
})
|
||||||
@@ -91,17 +129,12 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
<TanStackDevtools
|
<Analytics />
|
||||||
config={{
|
{DevtoolsPanel ? (
|
||||||
position: 'bottom-right',
|
<Suspense fallback={null}>
|
||||||
}}
|
<DevtoolsPanel />
|
||||||
plugins={[
|
</Suspense>
|
||||||
{
|
) : null}
|
||||||
name: 'Tanstack Router',
|
|
||||||
render: <TanStackRouterDevtoolsPanel />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,5 +13,8 @@ export const Route = createFileRoute('/_admin')({
|
|||||||
}
|
}
|
||||||
return { session: context.session }
|
return { session: context.session }
|
||||||
},
|
},
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
|
||||||
|
}),
|
||||||
component: () => <Outlet />,
|
component: () => <Outlet />,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,5 +10,8 @@ export const Route = createFileRoute('/_authenticated')({
|
|||||||
}
|
}
|
||||||
return { session: context.session }
|
return { session: context.session }
|
||||||
},
|
},
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
|
||||||
|
}),
|
||||||
component: () => <Outlet />,
|
component: () => <Outlet />,
|
||||||
})
|
})
|
||||||
|
|||||||
+99
-15
@@ -1,6 +1,5 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import Header from "@/components/header.tsx";
|
import { PublicShell } from "@/components/public-shell.tsx";
|
||||||
import { Footer } from "@/components/footer.tsx";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/datenschutz")({
|
export const Route = createFileRoute("/datenschutz")({
|
||||||
component: DatenschutzPage,
|
component: DatenschutzPage,
|
||||||
@@ -19,9 +18,7 @@ export const Route = createFileRoute("/datenschutz")({
|
|||||||
|
|
||||||
function DatenschutzPage() {
|
function DatenschutzPage() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen flex-col">
|
<PublicShell>
|
||||||
<Header />
|
|
||||||
<main className="flex-1">
|
|
||||||
<section className="mx-auto max-w-3xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
<section className="mx-auto max-w-3xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
||||||
<span className="island-kicker">Rechtliches</span>
|
<span className="island-kicker">Rechtliches</span>
|
||||||
<h1 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
<h1 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
||||||
@@ -323,16 +320,105 @@ function DatenschutzPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
* TODO Datenschutz — vor Launch juristisch prüfen lassen.
|
||||||
|
* Die konkreten Zusagen dieser Ziffer 10 basieren auf den
|
||||||
|
* offiziellen OpenPanel-Angaben (openpanel.dev/for/gdpr-compliant,
|
||||||
|
* Stand September 2026) und der Default-Konfiguration der
|
||||||
|
* self-hosted Instanz auf data.hurler-webdesign.de:
|
||||||
|
* • Cookieless by default (kein Consent-Banner nötig).
|
||||||
|
* • Session-Kennung = täglich rotierender Hash aus IP +
|
||||||
|
* User-Agent; rohe IP wird nicht gespeichert.
|
||||||
|
* • Geo-Daten aus dem Hash abgeleitet, nicht aus der IP.
|
||||||
|
* • DNT-Signal wird respektiert – in src/openpanel.ts
|
||||||
|
* via `disabled: navigator.doNotTrack === '1'` gesetzt.
|
||||||
|
* • Retention aggregierter Statistiken: aktuell keine
|
||||||
|
* TTL auf ClickHouse konfiguriert – Daten bleiben
|
||||||
|
* dauerhaft. Wenn eine Frist zugesagt werden soll,
|
||||||
|
* ClickHouse-TTL setzen und Text hier anpassen.
|
||||||
|
* Empfehlung: BayLDA-Muster / Anwalt vor Kundenübergabe.
|
||||||
|
*/}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="font-heading text-xl font-semibold">
|
<h2 className="font-heading text-xl font-semibold">
|
||||||
10. Keine externen Analyse- oder Marketingdienste
|
10. Reichweitenmessung mit OpenPanel (selbst gehostet)
|
||||||
</h2>
|
</h2>
|
||||||
<p className="leading-relaxed text-(--sea-ink-soft)">
|
<p className="leading-relaxed text-(--sea-ink-soft)">
|
||||||
Wir setzen bewusst keine Analyse-Tools wie Google
|
Zur bedarfsgerechten Gestaltung und laufenden Verbesserung
|
||||||
Analytics, Matomo, Meta-Pixel oder ähnliche Werkzeuge zur
|
unseres Onlineangebots erfassen wir anonymisierte
|
||||||
Nachverfolgung Ihres Nutzungsverhaltens ein. Es werden
|
Zugriffsstatistiken mit <strong>OpenPanel</strong>, einer
|
||||||
keine Nutzerprofile erstellt und keine Daten an
|
quelloffenen Analytics-Software. OpenPanel läuft auf einer
|
||||||
Werbepartner übermittelt.
|
von uns selbst betriebenen Instanz unter der Subdomain{" "}
|
||||||
|
<a
|
||||||
|
href="https://data.hurler-webdesign.de"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
className="underline underline-offset-2"
|
||||||
|
>
|
||||||
|
data.hurler-webdesign.de
|
||||||
|
</a>
|
||||||
|
. Das begleitende Ladeskript wird von{" "}
|
||||||
|
<a
|
||||||
|
href="https://hub.hurler-webdesign.de/op1.js"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
className="underline underline-offset-2"
|
||||||
|
>
|
||||||
|
hub.hurler-webdesign.de
|
||||||
|
</a>{" "}
|
||||||
|
ausgeliefert - ebenfalls unsere eigene Infrastruktur. Es
|
||||||
|
handelt sich also nicht um einen externen Dienstleister
|
||||||
|
und nicht um einen Drittanbieter im Rechtssinne - die
|
||||||
|
Daten verlassen unsere eigene Infrastruktur in der
|
||||||
|
Europäischen Union nicht. Eine Übermittlung in ein
|
||||||
|
Drittland (insbesondere in die USA) findet nicht statt.
|
||||||
|
</p>
|
||||||
|
<p className="leading-relaxed text-(--sea-ink-soft)">
|
||||||
|
Verarbeitet werden ausschließlich aggregierte, nicht
|
||||||
|
personenbezogene Daten:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc space-y-1 pl-6 leading-relaxed text-(--sea-ink-soft)">
|
||||||
|
<li>aufgerufene Seiten und Zeitpunkt des Aufrufs</li>
|
||||||
|
<li>Referrer (zuvor besuchte Seite, sofern übermittelt)</li>
|
||||||
|
<li>Herkunftsland (aus dem Sitzungs-Hash abgeleitet)</li>
|
||||||
|
<li>verwendete Browser- und Geräteklasse</li>
|
||||||
|
<li>Bildschirmauflösung und Spracheinstellung</li>
|
||||||
|
<li>ausgehende Klicks auf externe Links</li>
|
||||||
|
</ul>
|
||||||
|
<p className="leading-relaxed text-(--sea-ink-soft)">
|
||||||
|
Es werden <strong>keine Cookies</strong> zur
|
||||||
|
Wiedererkennung gesetzt, kein browserübergreifendes
|
||||||
|
Tracking durchgeführt und keine Nutzerprofile gebildet.
|
||||||
|
Zur Zusammenfassung mehrerer Aufrufe innerhalb eines
|
||||||
|
Besuchs bildet OpenPanel einen kurzlebigen Sitzungs-
|
||||||
|
schlüssel: einen Hashwert aus IP-Adresse und Browser-
|
||||||
|
Kennung, der sich <strong>täglich neu bildet</strong>.
|
||||||
|
Nach spätestens 24 Stunden lässt sich ein Besuch also
|
||||||
|
keiner bestimmten Kennung mehr zuordnen. Die
|
||||||
|
IP-Adresse selbst wird <strong>zu keinem Zeitpunkt
|
||||||
|
gespeichert</strong>; das Herkunftsland wird aus dem
|
||||||
|
Hashwert abgeleitet und die IP anschließend verworfen.
|
||||||
|
Eine Weitergabe an Werbepartner oder Dritte findet
|
||||||
|
nicht statt.
|
||||||
|
</p>
|
||||||
|
<p className="leading-relaxed text-(--sea-ink-soft)">
|
||||||
|
Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser
|
||||||
|
berechtigtes Interesse liegt in der statistischen
|
||||||
|
Auswertung des Nutzungsverhaltens zur Verbesserung
|
||||||
|
unseres Angebots. Da nach 24 Stunden kein Bezug zu
|
||||||
|
einer einzelnen Sitzung mehr hergestellt werden kann,
|
||||||
|
werden die verbleibenden aggregierten Kennzahlen
|
||||||
|
(Seitenaufrufe, Ereignisse, Herkunftsland) unbefristet
|
||||||
|
zu statistischen Zwecken vorgehalten. Sie sind
|
||||||
|
anonym und lassen keinen Rückschluss auf einzelne
|
||||||
|
Personen zu.
|
||||||
|
</p>
|
||||||
|
<p className="leading-relaxed text-(--sea-ink-soft)">
|
||||||
|
Wir respektieren das <strong>Do-Not-Track-Signal</strong>{" "}
|
||||||
|
Ihres Browsers: Ist DNT aktiviert, wird die
|
||||||
|
Reichweitenmessung für Ihren Besuch vollständig
|
||||||
|
deaktiviert - es werden keinerlei Ereignisse an
|
||||||
|
unsere Instanz übermittelt. Alternativ können Sie
|
||||||
|
uns per E-Mail (siehe Ziffer 1) kontaktieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -350,13 +436,11 @@ function DatenschutzPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="pt-4 text-sm text-(--sea-ink-soft)">
|
<p className="pt-4 text-sm text-(--sea-ink-soft)">
|
||||||
Stand: August 2026
|
Stand: September 2026
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</PublicShell>
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import Header from "@/components/header.tsx";
|
import { PublicShell } from "@/components/public-shell.tsx";
|
||||||
import { Footer } from "@/components/footer.tsx";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/impressum")({
|
export const Route = createFileRoute("/impressum")({
|
||||||
component: ImpressumPage,
|
component: ImpressumPage,
|
||||||
@@ -19,9 +18,7 @@ export const Route = createFileRoute("/impressum")({
|
|||||||
|
|
||||||
function ImpressumPage() {
|
function ImpressumPage() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen flex-col">
|
<PublicShell>
|
||||||
<Header />
|
|
||||||
<main className="flex-1">
|
|
||||||
<section className="mx-auto max-w-3xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
<section className="mx-auto max-w-3xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
||||||
<span className="island-kicker">Rechtliches</span>
|
<span className="island-kicker">Rechtliches</span>
|
||||||
<h1 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
<h1 className="mt-3 font-heading text-3xl font-bold leading-tight tracking-tight text-(--sea-ink) sm:text-4xl">
|
||||||
@@ -77,16 +74,6 @@ function ImpressumPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h2 className="font-heading text-xl font-semibold">
|
|
||||||
Umsatzsteuer
|
|
||||||
</h2>
|
|
||||||
<p className="leading-relaxed text-(--sea-ink-soft)">
|
|
||||||
Kleinunternehmer im Sinne von § 19 UStG. Es wird daher keine
|
|
||||||
Umsatzsteuer ausgewiesen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="font-heading text-xl font-semibold">
|
<h2 className="font-heading text-xl font-semibold">
|
||||||
Redaktionell verantwortlich
|
Redaktionell verantwortlich
|
||||||
@@ -212,13 +199,11 @@ function ImpressumPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="pt-4 text-sm text-(--sea-ink-soft)">
|
<p className="pt-4 text-sm text-(--sea-ink-soft)">
|
||||||
Stand: August 2026
|
Stand: September 2026
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</PublicShell>
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-11
@@ -1,28 +1,61 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import Header from '#/components/header.tsx'
|
|
||||||
import Hero from '#/components/hero.tsx'
|
import Hero from '#/components/hero.tsx'
|
||||||
import { Services } from '#/components/services.tsx'
|
import { Services } from '#/components/services.tsx'
|
||||||
import { Features } from '#/components/features.tsx'
|
import { Features } from '#/components/features.tsx'
|
||||||
import { Showcase } from '#/components/showcase.tsx'
|
// import { Showcase } from '#/components/showcase.tsx'
|
||||||
import { Process } from '#/components/process.tsx'
|
import { Process } from '#/components/process.tsx'
|
||||||
import { Pricing } from '#/components/pricing.tsx'
|
import { Pricing } from '#/components/pricing.tsx'
|
||||||
import { Footer } from '#/components/footer.tsx'
|
import { PublicShell } from '#/components/public-shell.tsx'
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({ component: Home })
|
const structuredData = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'ProfessionalService',
|
||||||
|
name: 'Hurler Webdesign',
|
||||||
|
alternateName: 'webkulisse',
|
||||||
|
description:
|
||||||
|
'Handgemachte Webseiten für Handwerk, Praxis und Therapie in Bayern – schnell, sicher und persönlich betreut.',
|
||||||
|
url: 'https://webkulisse.de/',
|
||||||
|
logo: 'https://webkulisse.de/img/logo.png',
|
||||||
|
image: 'https://webkulisse.de/img/og-cover.png',
|
||||||
|
telephone: '+49 171 8084830',
|
||||||
|
email: 'info@webkulisse.de',
|
||||||
|
priceRange: '€€',
|
||||||
|
address: {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
streetAddress: 'Untermagerbein 30',
|
||||||
|
postalCode: '86751',
|
||||||
|
addressLocality: 'Mönchsdeggingen',
|
||||||
|
addressRegion: 'BY',
|
||||||
|
addressCountry: 'DE',
|
||||||
|
},
|
||||||
|
areaServed: [
|
||||||
|
{ '@type': 'AdministrativeArea', name: 'Bayern' },
|
||||||
|
{ '@type': 'AdministrativeArea', name: 'Bayerisch-Schwaben' },
|
||||||
|
],
|
||||||
|
founder: { '@type': 'Person', name: 'Mathias Hurler' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/')({
|
||||||
|
component: Home,
|
||||||
|
head: () => ({
|
||||||
|
scripts: [
|
||||||
|
{
|
||||||
|
type: 'application/ld+json',
|
||||||
|
children: JSON.stringify(structuredData),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen flex-col">
|
<PublicShell>
|
||||||
<Header />
|
|
||||||
<main className="flex-1">
|
|
||||||
<Hero />
|
<Hero />
|
||||||
<Services />
|
<Services />
|
||||||
<Features />
|
<Features />
|
||||||
<Showcase />
|
{/* <Showcase /> — Referenz-Sektion vorerst deaktiviert, wird mit echten Projekten reaktiviert */}
|
||||||
<Process />
|
<Process />
|
||||||
<Pricing />
|
<Pricing />
|
||||||
</main>
|
</PublicShell>
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export const Route = createFileRoute('/login')({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ title: 'Kundenlogin - webkulisse' },
|
||||||
|
{ name: 'robots', content: 'noindex,nofollow' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
component: LoginPage,
|
component: LoginPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import Header from "@/components/header.tsx";
|
|
||||||
import { Footer } from "@/components/footer.tsx";
|
|
||||||
import BookingEmbed from "@/components/booking-embed.tsx";
|
import BookingEmbed from "@/components/booking-embed.tsx";
|
||||||
|
import { PublicShell } from "@/components/public-shell.tsx";
|
||||||
|
|
||||||
export const Route = createFileRoute("/termin")({
|
export const Route = createFileRoute("/termin")({
|
||||||
component: TerminPage,
|
component: TerminPage,
|
||||||
@@ -20,9 +19,7 @@ export const Route = createFileRoute("/termin")({
|
|||||||
|
|
||||||
function TerminPage() {
|
function TerminPage() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen flex-col">
|
<PublicShell>
|
||||||
<Header />
|
|
||||||
<main className="flex-1">
|
|
||||||
<section className="mx-auto max-w-6xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
<section className="mx-auto max-w-6xl px-4 pt-10 pb-6 md:px-6 md:pt-16">
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
<span className="island-kicker">Erstgespräch</span>
|
<span className="island-kicker">Erstgespräch</span>
|
||||||
@@ -43,8 +40,6 @@ function TerminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</PublicShell>
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-4
@@ -1,16 +1,15 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
@import "tw-animate-css";
|
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
@import "@fontsource-variable/nunito-sans";
|
@import "@fontsource-variable/nunito-sans";
|
||||||
@import "@fontsource-variable/public-sans";
|
@import "@fontsource-variable/public-sans";
|
||||||
@import "dev-component/devtool.css";
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Vend Sans;
|
font-family: Vend Sans;
|
||||||
src: url('/public/fonts/VendSans-VariableFont_wght.woff2');
|
src: url('/fonts/VendSans-VariableFont_wght.woff2') format('woff2');
|
||||||
font-weight: normal;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
@@ -333,6 +332,13 @@ a {
|
|||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link:focus-visible {
|
||||||
|
outline: 2px solid var(--lagoon-deep);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--sea-ink);
|
||||||
|
}
|
||||||
|
|
||||||
.rise-in {
|
.rise-in {
|
||||||
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
@@ -348,6 +354,30 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-image-in {
|
||||||
|
animation: hero-image-in 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-image-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.rise-in,
|
||||||
|
.hero-image-in {
|
||||||
|
animation-duration: 0.01ms;
|
||||||
|
animation-delay: 0ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
background: color-mix(in oklab, var(--header-bg) 84%, transparent 16%);
|
background: color-mix(in oklab, var(--header-bg) 84%, transparent 16%);
|
||||||
|
|||||||
Reference in New Issue
Block a user