feat: rebuild mintel.me as a single-page freelance site
The site now presents one freelance developer on one page: hero, introduction, four typical cases (developer for a product, make an app reliable, automate a process, a website) with what you get, how it runs and what it costs, a process automation section, technology and project fit, how I work, two live references, questions and contact. - Hero and closing headline are drawn from running binary digits. - Each case explains its four steps with an animated drawing; the automation section shows example workflows with work travelling between systems; a pipeline run shows what a test catches before a release. - Contact is one email address, kept in parts and assembled in the browser so it never appears in the page source (src/domain/email.ts, spec first). - Imprint and privacy pages added. - The old site (blog, case studies, contact form, fixed-price pages, video rendering and related scripts) is removed. Its URLs redirect to the homepage. - The deploy smoke test checks the legal pages and the redirects instead of the removed case study. - Dev scripts use docker compose v2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Container, SectionHeader } from "@/src/components/site";
|
||||
import { Timeline } from "@/src/components/about/Timeline";
|
||||
import { TagList } from "@/src/components/about/TagList";
|
||||
import {
|
||||
NOT_MY_THING,
|
||||
STACK_ITEMS,
|
||||
TIMELINE,
|
||||
WORKING_MODE,
|
||||
} from "@/src/components/about/aboutData";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About",
|
||||
description:
|
||||
"Senior engineer who makes AI-built software dependable. Web since 2011, remote since 2021, now independent.",
|
||||
alternates: { canonical: "/about" },
|
||||
};
|
||||
|
||||
// TODO(owner): link a CV download here once the PDF exists (no file yet).
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Container className="py-16 md:py-24">
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
kicker="/about"
|
||||
title="About"
|
||||
lead="Senior engineer who makes AI-built software dependable."
|
||||
/>
|
||||
|
||||
<div className="mt-12 max-w-2xl space-y-4 text-base leading-relaxed text-muted">
|
||||
<p>
|
||||
I have worked on the web since 2011, starting with an apprenticeship.
|
||||
Since then I have worked in agencies and product companies, and led
|
||||
the frontend team at an agency. I have been fully remote since 2021
|
||||
and am now independent, while running a small product business.
|
||||
</p>
|
||||
<p>
|
||||
Design and frontend are my strength. I am comfortable with backend
|
||||
concepts and architecture. Today I mostly write TypeScript and
|
||||
Next.js, plus Rust and Solidity for systems work. The path started
|
||||
with Vue and plain JavaScript, and I would rather say so than pretend
|
||||
I always wrote Rust.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="mt-16 max-w-2xl" aria-labelledby="how-i-work">
|
||||
<h2 id="how-i-work" className="!mb-3 !mt-0 !text-xl">
|
||||
How I work
|
||||
</h2>
|
||||
<ul className="!m-0 space-y-2 !pl-5 text-muted">
|
||||
{WORKING_MODE.map((line) => (
|
||||
<li key={line}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-16 max-w-3xl" aria-labelledby="timeline">
|
||||
<h2 id="timeline" className="!mb-4 !mt-0 !text-xl">
|
||||
Timeline
|
||||
</h2>
|
||||
<Timeline entries={TIMELINE} />
|
||||
</section>
|
||||
|
||||
<div className="mt-16 grid max-w-3xl gap-10 md:grid-cols-2">
|
||||
<TagList title="Stack" items={STACK_ITEMS} />
|
||||
<TagList title="Not my thing" items={NOT_MY_THING} />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getAllPosts } from "../../../../src/lib/posts";
|
||||
import { blogThumbnails } from "../../../../src/components/blog/blogThumbnails";
|
||||
import { BlogOGImageTemplate } from "../../../../src/components/BlogOGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
let backgroundImageSrc: string | undefined = undefined;
|
||||
|
||||
// If we have a custom generated thumbnail, convert it to a data URI for Satori
|
||||
if (post?.thumbnail) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), "public", post.thumbnail);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
||||
const mimeType =
|
||||
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
|
||||
err,
|
||||
);
|
||||
// Fall through to standard plain background
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnail = blogThumbnails[slug];
|
||||
|
||||
const title = post?.title || "Marc Mintel";
|
||||
const description =
|
||||
post?.description ||
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
const label = post ? "Blog Post" : "Engineering";
|
||||
const accentColor = thumbnail?.accent;
|
||||
const keyword = thumbnail?.keyword;
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<BlogOGImageTemplate
|
||||
title={title}
|
||||
description={description}
|
||||
label={label}
|
||||
accentColor={accentColor}
|
||||
keyword={keyword}
|
||||
backgroundImageSrc={backgroundImageSrc}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { BlogPostClient } from "@/src/components/BlogPostClient";
|
||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||
import { MDXContent } from "@/src/components/MDXContent";
|
||||
|
||||
import { TableOfContents } from "@/src/components/TableOfContents";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
return allPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) return {};
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString("de-DE", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<BlogPostHeader
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
date={formattedDate}
|
||||
readingTime={readingTime}
|
||||
slug={slug}
|
||||
thumbnail={post.thumbnail}
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<BlogPostStickyBar
|
||||
title={post.title}
|
||||
url={`https://mintel.me/blog/${slug}`}
|
||||
/>
|
||||
|
||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||
<div className="max-w-4xl mx-auto px-5 md:px-0">
|
||||
<Reveal delay={0.4} width="100%">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags?.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="article-content max-w-none">
|
||||
<TableOfContents />
|
||||
<MDXContent code={post.mdxContent || ""} />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
<TextSelectionShare />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
|
||||
};
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getAllPosts();
|
||||
return <BlogClient allPosts={posts as any} />;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Container, SectionHeader } from "@/src/components/site";
|
||||
import { AvailabilityLine } from "@/src/components/book/AvailabilityLine";
|
||||
import { BookingEmbed } from "@/src/components/book/BookingEmbed";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Book a call",
|
||||
description:
|
||||
"Book a 20-minute intro call to talk about your project, team and timeline.",
|
||||
alternates: { canonical: "/book" },
|
||||
};
|
||||
|
||||
const PREPARE: readonly string[] = [
|
||||
"What you are building",
|
||||
"Your team size",
|
||||
"A rough timeline",
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Container className="py-16 md:py-24">
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
kicker="/book"
|
||||
title="Book a call"
|
||||
lead="20 minutes to check whether we fit: your project, where it is stuck or unproven, and how a weekly capacity block would work."
|
||||
/>
|
||||
<div className="mt-6 min-h-5">
|
||||
<AvailabilityLine />
|
||||
</div>
|
||||
|
||||
<section className="mt-10 max-w-2xl" aria-labelledby="prepare">
|
||||
<h2 id="prepare" className="!mb-3 !mt-0 !text-xl">
|
||||
What to prepare
|
||||
</h2>
|
||||
<ul className="!m-0 space-y-2 !pl-5 text-muted">
|
||||
{PREPARE.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-12" aria-label="Booking calendar">
|
||||
<BookingEmbed />
|
||||
</section>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,640 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
LeadText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
BodyText,
|
||||
} from "@/src/components/Typography";
|
||||
import { BackgroundGrid, Container } from "@/src/components/Layout";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IframeSection } from "@/src/components/IframeSection";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ShieldCheck,
|
||||
Cpu,
|
||||
Server,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
|
||||
export default function KLZCablesCaseStudy() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const heroY = useTransform(scrollYProgress, [0, 0.2], [0, -20]);
|
||||
const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]);
|
||||
const gridRotate = useTransform(scrollYProgress, [0, 1], [0, 2]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white relative min-h-screen selection:bg-slate-900 selection:text-white overflow-hidden">
|
||||
<motion.div
|
||||
style={{ opacity: heroOpacity }}
|
||||
className="fixed inset-0 z-0 pointer-events-none"
|
||||
>
|
||||
<BackgroundGrid />
|
||||
</motion.div>
|
||||
|
||||
{/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */}
|
||||
<section className="relative min-h-[30vh] md:min-h-[40vh] py-10 md:py-20 overflow-hidden border-b border-slate-100 bg-white">
|
||||
<motion.div
|
||||
style={{ y: heroY, rotate: gridRotate }}
|
||||
className="absolute inset-0 bg-[linear-gradient(to_right,#f1f5f9_1px,transparent_1px),linear-gradient(to_bottom,#f1f5f9_1px,transparent_1px)] bg-[size:6rem_6rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_20%,transparent_100%)] pointer-events-none opacity-40"
|
||||
/>
|
||||
|
||||
<Container variant="narrow" className="relative z-10">
|
||||
<Reveal>
|
||||
<Link
|
||||
href="/case-studies"
|
||||
className="inline-flex items-center gap-2 text-slate-400 hover:text-slate-900 mb-8 md:mb-12 transition-colors font-bold text-[10px] uppercase tracking-[0.4em] group"
|
||||
>
|
||||
<ArrowLeft className="w-3 h-3 group-hover:-translate-x-1 transition-transform" />{" "}
|
||||
Zurück
|
||||
</Link>
|
||||
</Reveal>
|
||||
<div className="space-y-12">
|
||||
<Reveal direction="down" blur>
|
||||
<div className="inline-flex items-center gap-6">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: 48 }}
|
||||
transition={{ duration: 1, ease: "circOut" }}
|
||||
className="h-px bg-slate-900"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-slate-900 tracking-[0.4em]">
|
||||
SYSTEM-ARCHITEKTUR // 2025
|
||||
</MonoLabel>
|
||||
<Label className="text-[10px] text-slate-400 font-mono">
|
||||
HARDENED WORDPRESS // VARNISH STACK
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<GlitchText
|
||||
as="h1"
|
||||
className="text-4xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900"
|
||||
>
|
||||
KLZ Cables
|
||||
</GlitchText>
|
||||
<br className="hidden md:block" />
|
||||
<span className="text-slate-100 text-3xl md:text-6xl font-bold tracking-tighter">
|
||||
Case Study.
|
||||
</span>
|
||||
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-6 md:pl-12">
|
||||
<LeadText className="text-lg md:text-4xl leading-tight text-slate-900 font-medium">
|
||||
Engineering eines <br className="hidden md:block" />
|
||||
<Marker delay={0.2}>Systems.</Marker>
|
||||
</LeadText>
|
||||
<BodyText className="mt-4 md:mt-6 text-base md:text-xl text-slate-500 max-w-xl leading-relaxed font-serif italic">
|
||||
Vom statischen Altsystem zum industriellen Standard. Ich
|
||||
habe das KLZ-System auf das Wesentliche reduziert: Hardened
|
||||
Infrastructure, parametrische Datenpflege und zero
|
||||
maintenance.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.4} direction="up" scale={0.98} blur>
|
||||
<div className="flex flex-wrap gap-8 md:gap-24 pt-6 md:pt-12 border-t border-slate-100">
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label className="text-slate-400 text-[10px] md:text-xs">
|
||||
Data Integrity
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="w-2 h-2 md:w-2.5 md:h-2.5 bg-[rgba(129,199,132,1)] rounded-full animate-pulse" />
|
||||
<span className="text-base md:text-2xl font-bold font-mono text-slate-900 tracking-tight">
|
||||
Relational Data
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label className="text-slate-400 text-[10px] md:text-xs">
|
||||
Security Layer
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 md:gap-3 text-base md:text-2xl font-bold font-mono text-slate-900">
|
||||
<ShieldCheck className="w-5 h-5 md:w-6 md:h-6 text-[rgba(129,199,132,1)]" />
|
||||
<span>WP + Varnish</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 01: ARCHITECTURE --- */}
|
||||
<Section
|
||||
number="01"
|
||||
title="System-Hardening & Logic"
|
||||
borderBottom
|
||||
containerVariant="normal"
|
||||
>
|
||||
{/* Binary overlay background */}
|
||||
<div className="absolute top-0 right-0 p-8 opacity-[0.03] select-none pointer-events-none font-mono text-[10px] hidden md:block">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i}>01001101 01001001 01001110 01010100</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 lg:gap-24 items-start">
|
||||
<div className="md:col-span-12 mb-4 md:mb-12">
|
||||
<H2 className="text-3xl md:text-8xl tracking-tighter mb-4 md:mb-12">
|
||||
<GlitchText>Architektur-</GlitchText> <br />
|
||||
Refactor.
|
||||
</H2>
|
||||
</div>
|
||||
<div className="md:col-span-7 space-y-8 md:space-y-12">
|
||||
<Reveal delay={0.1} direction="up" blur>
|
||||
<div className="space-y-6 md:space-y-10">
|
||||
<BodyText className="text-xl md:text-2xl leading-relaxed font-serif italic text-slate-500">
|
||||
Vom statischen HTML zur zentralen Daten-Instanz.
|
||||
</BodyText>
|
||||
<BodyText className="text-lg md:text-xl text-slate-600 leading-relaxed">
|
||||
Ich habe die KLZ-Architektur radikal auf einen entkoppelten
|
||||
High-Performance-Stack umgestellt. WordPress fungiert hier
|
||||
nicht als CMS-Baukasten, sondern speichert alle technischen
|
||||
Attribute in einer zentralen relationalen Instanz. Durch die
|
||||
Implementierung nativer PHP-Services und den Verzicht auf
|
||||
volatile Drittanbieter-Plugins wurde ein System geschaffen,
|
||||
das keine technologischen Überraschungen zulässt. Stability by{" "}
|
||||
<Marker delay={0.5}>Design.</Marker>
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5 relative mt-8 md:mt-0">
|
||||
<Reveal delay={0.3} direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ y: -5, scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 25 }}
|
||||
className="p-6 md:p-12 bg-slate-50 rounded-[2.5rem] md:rounded-[3rem] border border-slate-100 space-y-6 md:space-y-12 relative overflow-hidden group shadow-sm text-left"
|
||||
>
|
||||
<div className="space-y-6 md:space-y-8 relative z-10">
|
||||
<Label className="text-slate-900">System Metriken</Label>
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{[
|
||||
{
|
||||
label: "Edge Caching",
|
||||
desc: "Varnish + W3TC Object Cache",
|
||||
icon: <Server className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
desc: "Independent (Global Data Compliance)",
|
||||
icon: <Activity className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
{
|
||||
label: "Custom Core",
|
||||
desc: "REST via Native Services",
|
||||
icon: <Cpu className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
].map((item, i) => (
|
||||
<Reveal
|
||||
key={i}
|
||||
direction="right"
|
||||
delay={0.5 + i * 0.1}
|
||||
width="100%"
|
||||
className="flex gap-4 md:gap-6 border-b border-slate-200/50 pb-6 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="shrink-0 mt-1">{item.icon}</div>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-[9px] md:text-[10px] text-slate-400">
|
||||
{item.label}
|
||||
</MonoLabel>
|
||||
<BodyText className="text-base font-bold text-slate-900">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SHOWCASE: LANDING --- */}
|
||||
<section className="py-12 md:py-32 bg-slate-50 border-y border-slate-100 overflow-hidden relative">
|
||||
<div className="w-full max-w-[1920px] mx-auto px-4 md:px-12 relative z-10">
|
||||
<Reveal direction="none" blur>
|
||||
<div className="relative mb-8 md:mb-16 flex justify-between items-end">
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-500">
|
||||
Infrastructure Validation
|
||||
</Label>
|
||||
<H3 className="text-3xl md:text-8xl tracking-tighter">
|
||||
<GlitchText>Global Hub.</GlitchText>
|
||||
</H3>
|
||||
</div>
|
||||
|
||||
{/* Binary overlay left */}
|
||||
<div className="absolute left-0 bottom-0 p-8 opacity-[0.03] select-none pointer-events-none font-mono text-[10px] hidden md:block group-hover:opacity-10 transition-opacity duration-1000">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i}>HANDSHAKE_0x00{i}A // SYNC_ACTIVE</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/index.html"
|
||||
height="400px"
|
||||
mobileHeight="350px"
|
||||
desktopHeight="850px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full transition-all duration-1000 ease-in-out no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 02: TECHNICAL DETAIL --- */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Asset Management"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-8 md:gap-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-24 items-end">
|
||||
<div className="md:col-span-8">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6 md:space-y-12">
|
||||
<Label className="text-slate-400 text-xs md:text-sm">
|
||||
Asset Pipelines
|
||||
</Label>
|
||||
<H3 className="text-2xl md:text-6xl tracking-tighter">
|
||||
Automated Documentation.
|
||||
</H3>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<Reveal delay={0.1} direction="right" blur>
|
||||
<BodyText className="text-lg md:text-xl text-slate-500 pb-2 font-serif italic leading-relaxed">
|
||||
Für Hochspannungs-N2XS(F)2Y Kabel ist Datentreue eine
|
||||
Sicherheitsanforderung. Ich habe eine automatisierte
|
||||
Asset-Pipeline entwickelt, die technische Datenblätter
|
||||
serverseitig generiert und validiert.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative w-full group"
|
||||
>
|
||||
<div className="relative w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/power-cables-medium-voltage-cables.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="1000px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
offsetY={100}
|
||||
browserFrame
|
||||
className="w-full transition-all duration-1000 no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
number="03"
|
||||
title="Katalog-Architektur"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
|
||||
<div className="lg:col-span-12 mb-4 md:mb-12 text-center lg:text-left relative z-10">
|
||||
<H3 className="text-3xl md:text-6xl max-w-4xl tracking-tighter">
|
||||
Fokus auf <br />
|
||||
<Marker delay={0.2}>Spezifikationen.</Marker>
|
||||
</H3>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-8 relative group">
|
||||
<Reveal width="100%" direction="left" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[400px] md:h-[650px] w-full overflow-hidden group"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/which-cables-for-wind-power-differences-from-low-to-extra-high-voltage-explained-2.html"
|
||||
height="100%"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full transition-all duration-700 no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-10 relative z-10">
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<Label className="text-slate-400">Katalog-Struktur</Label>
|
||||
<LeadText className="text-base md:text-lg leading-relaxed">
|
||||
Der Produktbereich wurde konsequent auf die Bedürfnisse
|
||||
technischer Planer optimiert. Klare Hierarchien und der
|
||||
Verzicht auf E-Commerce-Rauschen ermöglichen einen direkten
|
||||
Zugriff auf Kabel-Parameter und Datenblätter.
|
||||
</LeadText>
|
||||
<motion.div
|
||||
whileHover={{ x: 10 }}
|
||||
className="p-5 md:p-8 bg-white border border-slate-200 rounded-2xl md:rounded-3xl shadow-sm"
|
||||
>
|
||||
<Layers className="w-5 h-5 md:w-6 md:h-6 text-slate-400 mb-3 md:mb-4" />
|
||||
<BodyText className="text-sm font-medium">
|
||||
Strukturierte Aufbereitung technischer Produktdaten.
|
||||
</BodyText>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 04: CONTENT ENGINE --- */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Content Strategy"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-4 space-y-10 order-2 lg:order-1">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Knowledge Transfer</Label>
|
||||
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
||||
Insights & News.
|
||||
</H3>
|
||||
<BodyText className="text-xl text-slate-500 font-serif italic">
|
||||
Die News-Engine dient als technischer Hub für
|
||||
Industrie-Standards. Durch die Implementierung eines
|
||||
performanten Blog-Systems wird Fachwissen direkt an die
|
||||
Zielgruppe kommuniziert.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="lg:col-span-8 order-1 lg:order-2">
|
||||
<Reveal width="100%" direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[450px] md:h-[700px] w-full"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/blog.html"
|
||||
height="100%"
|
||||
desktopWidth={1600}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 05: TEAM & TRUST --- */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Ergebnis"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="space-y-10 md:space-y-16 text-center">
|
||||
<Reveal direction="up" blur>
|
||||
<H3 className="text-3xl md:text-8xl tracking-tighter">
|
||||
System-Lifecycle.
|
||||
</H3>
|
||||
<LeadText className="mx-auto max-w-2xl pt-2 md:pt-6 text-lg md:text-xl leading-relaxed">
|
||||
Die Migration von einer statischen Datei-Struktur zu einer
|
||||
zentralisierten Daten-Instanz eliminiert technische Schulden und
|
||||
manuelle Fehlerquellen. Das Ergebnis ist eine wartungsfreie
|
||||
Architektur, die technische Datentreue über den gesamten
|
||||
Produkt-Lifecycle sicherstellt.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<div className="relative group w-full text-left">
|
||||
<div className="relative block w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/team.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="1100px"
|
||||
desktopWidth={1440}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 06: CONVERSION --- */}
|
||||
<Section
|
||||
number="06"
|
||||
title="Lead Engineering"
|
||||
variant="white"
|
||||
containerVariant="wide"
|
||||
className="!pb-32"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-24 items-center">
|
||||
<div className="md:col-span-6 md:order-2">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Interaction Layer</Label>
|
||||
<H3 className="text-3xl md:text-7xl tracking-tighter text-slate-900 leading-[1.1]">
|
||||
Direkter Draht.
|
||||
</H3>
|
||||
<BodyText className="text-lg md:text-xl text-slate-500 font-serif italic leading-relaxed">
|
||||
Das Kontakt-System wurde auf maximale Reduktion getrimmt. Ein
|
||||
deterministischer Kanal zwischen technischem Bedarf und
|
||||
individueller Beratung – ohne Umwege, ohne Rauschen.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2} direction="left" blur>
|
||||
<div className="grid grid-cols-2 gap-8 border-t border-slate-100 pt-10">
|
||||
<div className="space-y-2">
|
||||
<MonoLabel className="text-slate-400 text-[9px]">
|
||||
RESPONSE_TIME
|
||||
</MonoLabel>
|
||||
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||
< 120ms
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<MonoLabel className="text-slate-400 text-[9px]">
|
||||
PROTOCOL
|
||||
</MonoLabel>
|
||||
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||
mTLS
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<MonoLabel className="text-slate-400 text-[9px]">
|
||||
AVAILABILITY
|
||||
</MonoLabel>
|
||||
<div className="text-xl font-bold text-[rgba(129,199,132,1)] font-mono">
|
||||
99.9%
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<MonoLabel className="text-slate-400 text-[9px]">
|
||||
ENCRYPTION
|
||||
</MonoLabel>
|
||||
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||
AES-256
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-7">
|
||||
<Reveal direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.002 }}
|
||||
transition={{ type: "spring", stiffness: 100, damping: 30 }}
|
||||
className="relative rounded-[2.5rem] overflow-hidden shadow-2xl shadow-slate-200/50 ring-1 ring-slate-100"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/contact.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="750px"
|
||||
desktopWidth={1200}
|
||||
mobileWidth={390}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
|
||||
<section className="py-40 md:py-64 bg-white relative overflow-hidden border-t border-slate-100">
|
||||
<BackgroundGrid />
|
||||
<Container variant="normal" className="relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center text-left">
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-3 md:space-y-6">
|
||||
<MonoLabel className="text-slate-400 tracking-[0.2em] md:tracking-[0.4em]">
|
||||
CONSULTING // ENGINEERING
|
||||
</MonoLabel>
|
||||
<H2 className="text-4xl md:text-8xl tracking-tighter leading-none font-bold">
|
||||
Architektur <br />
|
||||
<span className="text-slate-100">ohne Altlasten.</span>
|
||||
</H2>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} direction="left" blur>
|
||||
<BodyText className="text-xl md:text-2xl text-slate-500 max-w-xl font-serif italic leading-relaxed">
|
||||
Vom Prototyp zum industriellen Standard. Ich entwickle
|
||||
digitale Infrastrukturen, die technische Freiheit und
|
||||
operative Stabilität garantieren – wartungsfrei und
|
||||
skalierbar.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50/80 backdrop-blur-sm border border-slate-100 p-6 md:p-14 rounded-[3.5rem] space-y-10 md:space-y-12 relative group shadow-sm">
|
||||
<Reveal direction="right" blur>
|
||||
<div className="inline-flex items-center gap-2 md:gap-3 px-3 py-1.5 md:px-4 md:py-2 bg-white rounded-full border border-slate-200 mb-4 md:mb-8 font-mono text-[9px] md:text-[10px] tracking-widest text-slate-500 uppercase">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
|
||||
Operational Excellence
|
||||
</div>
|
||||
<div className="space-y-8 md:space-y-10">
|
||||
{[
|
||||
{
|
||||
title: "Hardened Infrastructure",
|
||||
desc: "Zentralisierte Datenpflege und entkoppelte WordPress-Instanzen.",
|
||||
},
|
||||
{
|
||||
title: "Automated Data Pipelines",
|
||||
desc: "Validierung technischer Spezifikationen ohne manuelle Eingriffe.",
|
||||
},
|
||||
{
|
||||
title: "Maintenance-Free Core",
|
||||
desc: "Plugin-freie Logik für deterministische System-Sicherheit.",
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="space-y-2 md:space-y-3 group/item text-left"
|
||||
>
|
||||
<MonoLabel className="text-[9px] md:text-[10px] text-slate-400 group-hover/item:text-slate-900 transition-colors duration-500">
|
||||
{item.title}
|
||||
</MonoLabel>
|
||||
<BodyText className="text-base md:text-lg font-bold text-slate-900 leading-tight">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.5} direction="up" blur className="pt-6">
|
||||
<Button
|
||||
href="/contact"
|
||||
variant="outline"
|
||||
showArrow={false}
|
||||
className="w-full py-6 md:py-8 text-base md:text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700"
|
||||
>
|
||||
Jetzt anfragen
|
||||
<ArrowRight className="inline-block ml-3 md:ml-4 w-5 h-5 md:w-6 md:h-6 group-hover:translate-x-4 transition-transform duration-700" />
|
||||
</Button>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { H3, LeadText, Label, BodyText } from "@/src/components/Typography";
|
||||
import { Card } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function CaseStudiesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
|
||||
{/* Featured Case Study Hero */}
|
||||
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||
<div className="space-y-12 md:space-y-24">
|
||||
<Reveal>
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<H3 className="text-4xl md:text-8xl tracking-tighter leading-none">
|
||||
Case <span className="text-slate-400">Studies.</span>
|
||||
</H3>
|
||||
<LeadText className="text-lg md:text-2xl text-slate-400 max-w-2xl">
|
||||
Ergebnisse statt Versprechen. Dokumentierte Architektur-Lösungen
|
||||
für komplexe Anforderungen.
|
||||
</LeadText>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal>
|
||||
<a href="/case-studies/klz-cables" className="block group">
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="none"
|
||||
techBorder
|
||||
className="overflow-hidden relative group min-h-[400px] md:min-h-[500px] flex flex-col md:flex-row"
|
||||
>
|
||||
{/* Brand Gradient Background */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(14,165,233,0.08)_0%,transparent_50%),radial-gradient(circle_at_70%_70%,rgba(99,102,241,0.05)_0%,transparent_50%)]" />
|
||||
|
||||
{/* Left Column: Content */}
|
||||
<div className="flex-1 p-4 md:p-12 relative z-10 flex flex-col justify-between">
|
||||
<div className="space-y-4 md:space-y-8">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<Image
|
||||
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
||||
alt="KLZ Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-6 md:h-8 w-auto invert opacity-80 group-hover:opacity-100 transition-opacity duration-500"
|
||||
/>
|
||||
<div className="h-px w-8 md:w-12 bg-slate-100" />
|
||||
<Label className="text-slate-400 text-[9px] md:text-[10px]">
|
||||
Case Study 2025
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
||||
KLZ <span className="text-slate-300">Cables</span>
|
||||
</H3>
|
||||
<LeadText className="text-slate-500 text-base md:text-xl max-w-xl leading-relaxed">
|
||||
Engineering eines industriellen B2B-Systems mit
|
||||
<span className="text-slate-900 font-medium">
|
||||
{" "}
|
||||
automatisierter Asset-Pipeline
|
||||
</span>{" "}
|
||||
und hochperformantem Headless-Stack.
|
||||
</LeadText>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-1 md:pt-2">
|
||||
{["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map(
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 md:px-2.5 md:py-1 border border-slate-100 bg-white/50 rounded-md text-[8px] md:text-[9px] font-mono text-slate-400 uppercase tracking-widest group-hover:border-slate-300 transition-colors duration-500"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 md:pt-12">
|
||||
<div className="inline-flex items-center gap-2 md:gap-3 text-[10px] md:text-sm font-bold text-slate-400 group-hover:text-slate-900 transition-all duration-500">
|
||||
<span>EXPLORE PROJECT</span>
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-2 transition-transform duration-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Visual/Technical Decor */}
|
||||
<div className="w-full md:w-1/3 min-h-[150px] md:min-h-0 bg-slate-50 relative overflow-hidden border-t md:border-t-0 md:border-l border-slate-100">
|
||||
<div className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[6px] md:text-[8px] p-4 flex flex-col gap-1 overflow-hidden">
|
||||
{Array.from({ length: 40 }).map((_, i) => (
|
||||
<div key={i} className="whitespace-nowrap">
|
||||
{Array.from({ length: 15 })
|
||||
.map((_, j) => (
|
||||
<span
|
||||
key={j}
|
||||
className={
|
||||
Math.random() > 0.5
|
||||
? "text-slate-900"
|
||||
: "text-slate-400"
|
||||
}
|
||||
>
|
||||
{Math.floor(Math.random() * 2)}
|
||||
</span>
|
||||
))
|
||||
.join(" ")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Abstract "Cable" lines */}
|
||||
<div className="absolute inset-0 flex items-center justify-center p-8 md:p-12">
|
||||
<div className="w-full h-full relative">
|
||||
{[1, 2, 3].map((v) => (
|
||||
<motion.div
|
||||
key={v}
|
||||
initial={{ scaleY: 0 }}
|
||||
whileInView={{ scaleY: 1 }}
|
||||
transition={{ duration: 2, delay: 0.5 + v * 0.2 }}
|
||||
className="absolute inset-y-0 border-r border-slate-200 origin-top"
|
||||
style={{ right: `${v * 25}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 md:bottom-8 md:right-8 text-[8px] md:text-[10px] font-mono text-slate-300 rotate-90 origin-right uppercase tracking-[0.3em]">
|
||||
Industrial Grade
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Coming Soon */}
|
||||
<Section number="02" title="Kommt bald" borderTop>
|
||||
<Reveal>
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="large"
|
||||
techBorder
|
||||
className="text-center relative overflow-hidden group"
|
||||
>
|
||||
<div className="relative z-10 space-y-4 py-4 md:py-8">
|
||||
<div className="flex items-center justify-center gap-2 md:gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-300 animate-pulse" />
|
||||
<Label className="text-slate-400">In Arbeit</Label>
|
||||
</div>
|
||||
<H3 className="text-2xl md:text-3xl text-slate-400">
|
||||
Weitere Case Studies in Kürze.
|
||||
</H3>
|
||||
<BodyText className="text-slate-400 max-w-md mx-auto">
|
||||
Ich dokumentiere laufende Projekte – schauen Sie bald wieder
|
||||
vorbei oder kontaktieren Sie mich direkt.
|
||||
</BodyText>
|
||||
<div className="pt-4">
|
||||
<Button href="/contact" variant="outline">
|
||||
Kontakt aufnehmen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Kontakt."
|
||||
description="Bereit für eine Zusammenarbeit? Lassen Sie uns gemeinsam etwas bauen, das wirklich funktioniert."
|
||||
label="Get in touch"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { ContactForm } from "@/src/components/ContactForm";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white min-h-screen overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
|
||||
<Section
|
||||
containerVariant="wide"
|
||||
effects={<></>}
|
||||
className="pt-24 pb-12 md:pt-32 md:pb-20"
|
||||
>
|
||||
{/* Full-width Form */}
|
||||
<ContactForm />
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,40 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Theme tokens. Dark is the site default; legacy pages (blog, legal, ...) opt into
|
||||
the light variant by wrapping their content in `.theme-light` (LegacyLightSurface). */
|
||||
:root {
|
||||
--color-bg: #0a0d12;
|
||||
--color-surface: #0d1219;
|
||||
--color-fg: #e6edf3;
|
||||
--color-muted: #8b98a8;
|
||||
--color-faint: #5b6879;
|
||||
--color-border: #1f2630;
|
||||
--color-border-strong: #2b3544;
|
||||
--color-accent: #3fb98a;
|
||||
--color-accent-hover: #55c99d;
|
||||
--color-accent-fg: #0a0d12;
|
||||
--color-code-bg: #0d1219;
|
||||
--color-code-border: #1f2630;
|
||||
--color-scrollbar-track: #0a0d12;
|
||||
--color-scrollbar-thumb: #2b3544;
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
--color-bg: #ffffff;
|
||||
--color-surface: #f8fafc;
|
||||
--color-fg: #0f172a;
|
||||
--color-muted: #334155;
|
||||
--color-faint: #64748b;
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-strong: #cbd5e1;
|
||||
--color-code-bg: #f8fafc;
|
||||
--color-code-border: #f1f5f9;
|
||||
background-color: var(--color-bg);
|
||||
color: #1e293b;
|
||||
font-family: var(--font-newsreader), Georgia, serif;
|
||||
}
|
||||
|
||||
/* Base styles - Tailwind only */
|
||||
@layer base {
|
||||
html {
|
||||
@@ -43,7 +9,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bg text-fg font-sans antialiased;
|
||||
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -54,7 +20,7 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-sans font-bold text-fg tracking-tighter;
|
||||
@apply font-sans font-bold text-slate-900 tracking-tighter;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -74,7 +40,7 @@
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-6 text-base leading-relaxed text-muted;
|
||||
@apply mb-6 text-base leading-relaxed text-slate-700;
|
||||
}
|
||||
|
||||
.lead {
|
||||
@@ -83,7 +49,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-fg hover:text-muted transition-colors no-underline;
|
||||
@apply text-slate-900 hover:text-slate-700 transition-colors no-underline;
|
||||
}
|
||||
|
||||
ul,
|
||||
@@ -96,34 +62,22 @@
|
||||
}
|
||||
|
||||
code:not([class*="language-"]) {
|
||||
@apply px-1.5 py-0.5 rounded-md font-mono text-[0.9em] text-fg border;
|
||||
background-color: var(--color-code-bg);
|
||||
border-color: var(--color-code-border);
|
||||
@apply bg-slate-50 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] text-slate-800 border border-slate-100;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 border-fg pl-6 italic text-muted my-8 text-lg md:text-2xl font-serif;
|
||||
@apply border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-lg md:text-2xl font-serif;
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
a:focus,
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Keep mouse focus quiet, but never hide the keyboard focus ring */
|
||||
a:focus:not(:focus-visible),
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Interactive elements pointer */
|
||||
a,
|
||||
button,
|
||||
@@ -374,11 +328,11 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-scrollbar-track);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar-thumb);
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
@@ -389,8 +343,8 @@
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-accent-fg);
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Tag Styles */
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EmailLink } from "@/src/components/EmailLink";
|
||||
import { LegalPage, LegalSection } from "@/src/components/LegalPage";
|
||||
import { OWNER } from "@/src/content/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Imprint",
|
||||
description: "Legal notice (Impressum) for mintel.me.",
|
||||
alternates: { canonical: "/imprint" },
|
||||
robots: { index: false, follow: true },
|
||||
};
|
||||
|
||||
const LINK = "font-bold text-slate-900 underline-offset-4 hover:underline";
|
||||
|
||||
export default function ImprintPage() {
|
||||
return (
|
||||
<LegalPage title="Imprint">
|
||||
<LegalSection heading="Information according to section 5 DDG">
|
||||
<p>
|
||||
{OWNER.name}
|
||||
<br />
|
||||
{OWNER.street}
|
||||
<br />
|
||||
{OWNER.city}
|
||||
<br />
|
||||
{OWNER.country}
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Contact">
|
||||
<p>
|
||||
Email: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="VAT identification number">
|
||||
<p>
|
||||
VAT identification number according to section 27a of the German VAT
|
||||
Act (Umsatzsteuergesetz): {OWNER.vatId}
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Responsible for the content">
|
||||
<p>{OWNER.name}, address as above (section 18 paragraph 2 MStV).</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Consumer dispute resolution">
|
||||
<p>
|
||||
I am neither willing nor obliged to take part in dispute resolution
|
||||
proceedings before a consumer arbitration board.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</LegalPage>
|
||||
);
|
||||
}
|
||||
@@ -19,25 +19,14 @@ const newsreader = Newsreader({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const DESCRIPTION =
|
||||
"Senior engineer for teams that build fast with AI and don't want chaos. Strong in design and frontend, provably clean in architecture and tests. Remote, booked in weekly capacity blocks.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Marc Mintel — Senior engineer for AI-built software",
|
||||
default: "Marc Mintel — Senior developer for web apps and websites",
|
||||
template: "%s | Marc Mintel",
|
||||
},
|
||||
description: DESCRIPTION,
|
||||
description:
|
||||
"Senior developer for web apps, interfaces and websites. Book weekly hours for your product, get an unreliable app back on track, or have a website built for you.",
|
||||
metadataBase: new URL("https://mintel.me"),
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "Marc Mintel",
|
||||
title: "Marc Mintel — Senior engineer for AI-built software",
|
||||
description: DESCRIPTION,
|
||||
url: "https://mintel.me",
|
||||
locale: "en_US",
|
||||
},
|
||||
twitter: { card: "summary_large_image" },
|
||||
icons: {
|
||||
icon: "/favicon.svg",
|
||||
},
|
||||
@@ -51,9 +40,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${inter.variable} ${jetbrainsMono.variable} ${newsreader.variable}`}
|
||||
className={`${inter.variable} ${newsreader.variable} ${jetbrainsMono.variable}`}
|
||||
>
|
||||
<body className="min-h-screen bg-bg font-sans text-fg antialiased">
|
||||
<body className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Container, Pill } from "@/src/components/site";
|
||||
import { NOTES } from "@/src/content/notes";
|
||||
import { SITE_NAME, SITE_URL } from "@/src/content/site";
|
||||
import { findNoteBySlug } from "@/src/domain/notes";
|
||||
|
||||
type PageProps = { readonly params: Promise<{ readonly slug: string }> };
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return NOTES.map((note) => ({ slug: note.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const found = findNoteBySlug(NOTES, (await params).slug);
|
||||
if (!found.ok) return {};
|
||||
const { title, summary, slug, publishedOn } = found.value;
|
||||
const path = `/notes/${slug}`;
|
||||
return {
|
||||
title,
|
||||
description: summary,
|
||||
alternates: { canonical: path },
|
||||
openGraph: {
|
||||
type: "article",
|
||||
title,
|
||||
description: summary,
|
||||
url: path,
|
||||
publishedTime: publishedOn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const found = findNoteBySlug(NOTES, (await params).slug);
|
||||
if (!found.ok) notFound();
|
||||
const { Body, title, summary, slug, publishedOn, readingMinutes, tags } =
|
||||
found.value;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: title,
|
||||
description: summary,
|
||||
datePublished: publishedOn,
|
||||
url: `${SITE_URL}/notes/${slug}`,
|
||||
author: { "@type": "Person", name: SITE_NAME, url: SITE_URL },
|
||||
keywords: tags.join(", "),
|
||||
};
|
||||
|
||||
return (
|
||||
<Container className="py-16 md:py-24">
|
||||
<article className="mx-auto max-w-[70ch]">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
href="/notes"
|
||||
className="font-mono text-xs uppercase tracking-widest text-accent"
|
||||
>
|
||||
← Notes
|
||||
</Link>
|
||||
<header className="mt-6">
|
||||
<h1 className="!mb-4 !text-3xl !leading-tight text-fg md:!text-5xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="!mb-4 font-mono text-xs text-muted">
|
||||
<time dateTime={publishedOn}>{publishedOn}</time>
|
||||
{" · "}
|
||||
{readingMinutes} min read
|
||||
</p>
|
||||
<span className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<Pill key={tag}>{tag}</Pill>
|
||||
))}
|
||||
</span>
|
||||
</header>
|
||||
<div className="mt-10">
|
||||
<Body />
|
||||
</div>
|
||||
</article>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Container, Pill, SectionHeader } from "@/src/components/site";
|
||||
import { NOTES } from "@/src/content/notes";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Notes",
|
||||
description:
|
||||
"Evergreen engineering write-ups with diagrams: verification, architecture and testing.",
|
||||
alternates: { canonical: "/notes" },
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Container className="py-16 md:py-24">
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
kicker="/notes"
|
||||
title="Notes"
|
||||
lead="Evergreen engineering write-ups with diagrams. No client work, no hype."
|
||||
/>
|
||||
<ul className="mt-12 max-w-3xl divide-y divide-border border-y border-border">
|
||||
{NOTES.map((note) => (
|
||||
<li key={note.slug}>
|
||||
<Link
|
||||
href={`/notes/${note.slug}`}
|
||||
className="group block py-8 hover:text-fg"
|
||||
>
|
||||
<p className="font-mono text-xs text-muted">
|
||||
<time dateTime={note.publishedOn}>{note.publishedOn}</time>
|
||||
{" · "}
|
||||
{note.readingMinutes} min read
|
||||
</p>
|
||||
<h2 className="!mb-2 !mt-2 !text-xl !leading-snug text-fg group-hover:text-accent md:!text-2xl">
|
||||
{note.title}
|
||||
</h2>
|
||||
<p className="!mb-4 text-base leading-relaxed text-muted">
|
||||
{note.summary}
|
||||
</p>
|
||||
<span className="flex flex-wrap gap-2">
|
||||
{note.tags.map((tag) => (
|
||||
<Pill key={tag}>{tag}</Pill>
|
||||
))}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,8 @@ export default async function Image() {
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Marc Mintel"
|
||||
description="Technical problem solver's blog - practical insights and learning notes"
|
||||
label="Engineering"
|
||||
description="Freelance developer. I build web apps, interfaces and websites."
|
||||
label="Web apps and websites"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
+303
-33
@@ -1,44 +1,314 @@
|
||||
import type { Metadata } from "next";
|
||||
import { BuiltAndRunning } from "@/src/components/home/BuiltAndRunning";
|
||||
import { CapacityBlocks } from "@/src/components/home/CapacityBlocks";
|
||||
import { FinalCta } from "@/src/components/home/FinalCta";
|
||||
import { Hero } from "@/src/components/home/Hero";
|
||||
import { Block } from "@/src/components/home/Block";
|
||||
import { ClosingCta } from "@/src/components/home/ClosingCta";
|
||||
import { Faq } from "@/src/components/home/Faq";
|
||||
import { HeroStatement } from "@/src/components/home/HeroStatement";
|
||||
import { HomeJsonLd } from "@/src/components/home/HomeJsonLd";
|
||||
import { HowIShip } from "@/src/components/home/HowIShip";
|
||||
import { ProofStrip } from "@/src/components/home/ProofStrip";
|
||||
import { WebsitesSection } from "@/src/components/home/WebsitesSection";
|
||||
import { WhatIDo } from "@/src/components/home/WhatIDo";
|
||||
import { SITE_NAME } from "@/src/content/site";
|
||||
|
||||
const TITLE = `${SITE_NAME}: senior engineer for AI-built software`;
|
||||
const DESCRIPTION =
|
||||
"Senior engineer for teams that build fast with AI and don't want chaos. Spec-first, test-driven, mutation-checked. Booked in weekly capacity blocks.";
|
||||
import { PathPicker } from "@/src/components/home/PathPicker";
|
||||
import { PortraitSection } from "@/src/components/home/PortraitSection";
|
||||
import { StickyBook } from "@/src/components/home/StickyBook";
|
||||
import { AutomationFlow } from "@/src/components/illustrations/AutomationFlow";
|
||||
import { CareerLine } from "@/src/components/illustrations/CareerLine";
|
||||
import { ContactDiagram } from "@/src/components/illustrations/ContactDiagram";
|
||||
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
||||
import {
|
||||
WorkShowcase,
|
||||
type WorkItem,
|
||||
} from "@/src/components/illustrations/WorkShowcase";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { CAREER, FIT, TECH } from "@/src/content/about";
|
||||
import { AUTOMATION_NOTES } from "@/src/content/automation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: { absolute: TITLE },
|
||||
description: DESCRIPTION,
|
||||
title: "Marc Mintel — Senior developer for web apps and websites",
|
||||
description:
|
||||
"Freelance developer with 15 years of experience. I build web apps, interfaces and websites: by the hour for product teams, or a complete website for your business.",
|
||||
alternates: { canonical: "/" },
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: "/",
|
||||
type: "website",
|
||||
siteName: SITE_NAME,
|
||||
},
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const MUTED = "text-slate-400";
|
||||
const BODY = "m-0 font-sans text-lg leading-relaxed text-slate-500 md:text-xl";
|
||||
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
|
||||
|
||||
const PRINCIPLES = [
|
||||
{
|
||||
title: "One contact",
|
||||
text: "You talk to me, and I am the one who does the work.",
|
||||
},
|
||||
{
|
||||
title: "Hours you can check",
|
||||
text: "I track my time. The monthly invoice shows what the hours were spent on.",
|
||||
},
|
||||
{
|
||||
title: "Careful work",
|
||||
text: "I test what I build before I hand it over, and I say so when something is not finished or I am not sure.",
|
||||
},
|
||||
{
|
||||
title: "What I build is yours",
|
||||
text: "Code and design belong to you. You can hand them to another developer.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const WORK: readonly WorkItem[] = [
|
||||
{
|
||||
name: "klz-cables.com",
|
||||
href: "https://klz-cables.com",
|
||||
client: "Cable supplier",
|
||||
text: "The company website of a supplier of energy cables, in German and English.",
|
||||
did: [
|
||||
"Product catalogue with categories and detail pages",
|
||||
"Datasheets generated from the product data",
|
||||
"Content the client can edit in a CMS",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
||||
desktop: { src: "/work/klz-long.webp", width: 1200, height: 1680 },
|
||||
mobile: { src: "/work/klz-mobile.webp", width: 390, height: 3600 },
|
||||
},
|
||||
{
|
||||
name: "e-tib.com",
|
||||
href: "https://e-tib.com",
|
||||
client: "Cable network construction",
|
||||
text: "The company website of a group in cable network construction, in German and English.",
|
||||
did: [
|
||||
"Pages for services, references, careers and events",
|
||||
"The same content in two languages",
|
||||
"Works on phone, tablet and desktop",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
||||
desktop: { src: "/work/etib-long.webp", width: 1200, height: 4333 },
|
||||
mobile: { src: "/work/etib-mobile.webp", width: 390, height: 3600 },
|
||||
},
|
||||
];
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-col bg-white">
|
||||
<HomeJsonLd />
|
||||
<Hero />
|
||||
<ProofStrip />
|
||||
<HowIShip />
|
||||
<WhatIDo />
|
||||
<CapacityBlocks />
|
||||
<WebsitesSection />
|
||||
<BuiltAndRunning />
|
||||
<FinalCta />
|
||||
</>
|
||||
<HeroStatement />
|
||||
<StickyBook />
|
||||
|
||||
<PortraitSection
|
||||
id="hello"
|
||||
index="01"
|
||||
label="Hello"
|
||||
title={<>I am Marc.</>}
|
||||
>
|
||||
<p className="m-0 font-sans text-2xl leading-snug text-white md:text-4xl">
|
||||
I have been building for the web for 15 years: first in agencies,
|
||||
later as a senior developer, and now on my own.
|
||||
</p>
|
||||
<p className="m-0 mt-8 font-sans text-lg leading-relaxed text-slate-300 md:text-xl">
|
||||
My strength is the part people see and use: design and interface. I am
|
||||
also at home with what lies behind it, so I can build a complete
|
||||
application and not just its surface.
|
||||
</p>
|
||||
<p className="m-0 mt-6 font-sans text-lg leading-relaxed text-slate-300 md:text-xl">
|
||||
You work with me directly. Whether you run a product team or are
|
||||
commissioning something like this for the first time.
|
||||
</p>
|
||||
</PortraitSection>
|
||||
|
||||
<section className="bg-white">
|
||||
<div className="mx-auto w-full max-w-[1400px] px-5 py-16 md:px-12 md:py-28">
|
||||
<p className={`${MONO} m-0 mb-10`}>The path so far</p>
|
||||
<CareerLine stations={CAREER} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Block
|
||||
index="02"
|
||||
label="Services"
|
||||
title={
|
||||
<>
|
||||
What do you need? <span className={MUTED}>Four typical cases.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Reveal>
|
||||
<PathPicker />
|
||||
</Reveal>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
index="03"
|
||||
label="Process automation"
|
||||
title={
|
||||
<>
|
||||
Let the systems <span className={MUTED}>do the copying.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Reveal>
|
||||
<p className={`${BODY} mb-12 max-w-3xl`}>
|
||||
In most companies, people move information from one tool to another:
|
||||
read it here, type it in there, send an email about it. Automation
|
||||
connects those tools so that the work passes from one to the next on
|
||||
its own. Three typical examples:
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal>
|
||||
<AutomationFlow />
|
||||
</Reveal>
|
||||
<div className="mt-16 grid grid-cols-1 gap-12 md:mt-24 md:grid-cols-3 md:gap-10">
|
||||
{AUTOMATION_NOTES.map((group, i) => (
|
||||
<Reveal key={group.title} delay={0.1 * i}>
|
||||
<div className="border-t border-slate-900 pt-6">
|
||||
<p className={`${MONO} m-0`}>{group.title}</p>
|
||||
<ul className="m-0 mt-4 list-none p-0">
|
||||
{group.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="border-b border-slate-100 py-4 font-sans text-lg text-slate-900 md:text-xl"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
index="04"
|
||||
label="Technology"
|
||||
title={
|
||||
<>
|
||||
Which projects <span className={MUTED}>I am right for.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10">
|
||||
{FIT.map((group, i) => (
|
||||
<Reveal key={group.title} delay={0.1 * i}>
|
||||
<div
|
||||
className={`border-t pt-6 ${i === 2 ? "border-slate-300" : "border-slate-900"}`}
|
||||
>
|
||||
<p className={`${MONO} m-0`}>{group.title}</p>
|
||||
<ul className="m-0 mt-4 list-none p-0">
|
||||
{group.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className={`border-b border-slate-100 py-4 font-sans text-lg md:text-xl ${
|
||||
i === 2 ? "text-slate-400" : "text-slate-900"
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal>
|
||||
<div className="mt-16 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10">
|
||||
<p className={`${MONO} m-0`}>What I work with</p>
|
||||
<div className="mt-8 grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{TECH.map((group) => (
|
||||
<div key={group.area}>
|
||||
<p className="m-0 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl">
|
||||
{group.area}
|
||||
</p>
|
||||
<ul className="m-0 mt-4 flex list-none flex-wrap gap-2 p-0">
|
||||
{group.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="rounded-full border border-slate-200 bg-white px-4 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-slate-700"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-16 md:mt-24">
|
||||
<Reveal>
|
||||
<h3 className="m-0 font-sans text-3xl font-bold tracking-tighter text-slate-900 md:text-5xl">
|
||||
What happens before a change goes live.
|
||||
</h3>
|
||||
<p className={`${BODY} mb-12 mt-6 max-w-3xl`}>
|
||||
Software is connected: a change in one place can break another. I
|
||||
secure what I build with automated tests, and every change has to
|
||||
pass them before it is released. This is what such a run looks
|
||||
like.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal>
|
||||
<PipelineRun />
|
||||
</Reveal>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
index="05"
|
||||
label="Working with me"
|
||||
title={
|
||||
<>
|
||||
How I work. <span className={MUTED}>Nothing unusual.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Reveal>
|
||||
<div className="mb-16 overflow-hidden rounded-3xl bg-slate-900 p-4 md:mb-24 md:p-10">
|
||||
<ContactDiagram />
|
||||
</div>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16">
|
||||
{PRINCIPLES.map((item, i) => (
|
||||
<Reveal key={item.title} delay={0.08 * i}>
|
||||
<div className="border-t border-slate-900 pt-6">
|
||||
<h3 className="m-0 font-sans text-3xl font-bold tracking-tight text-slate-900 md:text-4xl">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className={`${BODY} mt-4`}>{item.text}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
index="06"
|
||||
label="My work"
|
||||
title={
|
||||
<>
|
||||
Two sites I built. <span className={MUTED}>Both are live.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<WorkShowcase items={WORK} />
|
||||
<Reveal>
|
||||
<p
|
||||
className={`${BODY} mt-20 max-w-3xl border-t border-slate-200 pt-8 md:mt-28`}
|
||||
>
|
||||
Besides client work I build my own software, among other things a
|
||||
system in Rust and Solidity that runs on seven blockchains.
|
||||
</p>
|
||||
</Reveal>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
index="07"
|
||||
label="Questions"
|
||||
title={
|
||||
<>
|
||||
Good to know <span className={MUTED}>before we talk.</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Reveal>
|
||||
<Faq />
|
||||
</Reveal>
|
||||
</Block>
|
||||
|
||||
<ClosingCta />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EmailLink } from "@/src/components/EmailLink";
|
||||
import { LegalPage, LegalSection } from "@/src/components/LegalPage";
|
||||
import { OWNER } from "@/src/content/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy",
|
||||
description: "How mintel.me handles personal data.",
|
||||
alternates: { canonical: "/privacy" },
|
||||
robots: { index: false, follow: true },
|
||||
};
|
||||
|
||||
const LINK = "font-bold text-slate-900 underline-offset-4 hover:underline";
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<LegalPage title="Privacy">
|
||||
<LegalSection heading="Who is responsible">
|
||||
<p>
|
||||
{OWNER.name}, {OWNER.street}, {OWNER.city}, {OWNER.country}.
|
||||
<br />
|
||||
Email: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="In short">
|
||||
<p>
|
||||
This website sets no cookies, shows no advertising and includes no
|
||||
content from third parties. Fonts and images are served from this
|
||||
website itself.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Server logs">
|
||||
<p>
|
||||
When you open a page, the server processes technical data that your
|
||||
browser sends: your IP address, the time, the page requested and the
|
||||
browser type. This is necessary to deliver the website and to keep it
|
||||
secure. The legal basis is my legitimate interest in running the
|
||||
website (Article 6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Visitor statistics">
|
||||
<p>
|
||||
I count page views with Umami, which I run on my own server. It works
|
||||
without cookies and does not create profiles of individual visitors.
|
||||
The data is not passed on to third parties. The legal basis is my
|
||||
legitimate interest in understanding how the website is used (Article
|
||||
6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Error reports">
|
||||
<p>
|
||||
If something on this website fails technically, an error report is
|
||||
sent to an error tracking service that I run on my own server. It
|
||||
contains technical details such as the page, the browser and the error
|
||||
message. Sessions are not recorded. The legal basis is my legitimate
|
||||
interest in a working website (Article 6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Storage in your browser">
|
||||
<p>
|
||||
If loading a page fails, the website stores a timestamp in your
|
||||
browser's session storage so that it reloads only once. It is
|
||||
deleted when you close the tab and is not sent anywhere.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="When you write to me">
|
||||
<p>
|
||||
If you send me an email, I process what you write and your address in
|
||||
order to answer you and, if it comes to that, to prepare a contract
|
||||
(Article 6(1)(b) GDPR). I keep the correspondence for as long as it is
|
||||
needed for that purpose or as the law requires.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Your rights">
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>ask what data I hold about you</li>
|
||||
<li>have incorrect data corrected</li>
|
||||
<li>have your data deleted or its processing restricted</li>
|
||||
<li>object to processing that is based on legitimate interest</li>
|
||||
<li>receive your data in a portable format</li>
|
||||
<li>complain to a data protection supervisory authority</li>
|
||||
</ul>
|
||||
<p>
|
||||
To use any of these rights, write to <EmailLink className={LINK} />.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</LegalPage>
|
||||
);
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
ButtonLink,
|
||||
Container,
|
||||
SectionHeader,
|
||||
TerminalCard,
|
||||
} from "@/src/components/site";
|
||||
import { ProcessStepCard } from "@/src/components/process/ProcessStepCard";
|
||||
import { PROCESS_STEPS } from "@/src/components/process/processSteps";
|
||||
import { SAMPLE_DELIVERY_REPORT } from "@/src/components/process/sampleDeliveryReport";
|
||||
import { formatDeliveryReport } from "@/src/domain/deliveryReport";
|
||||
|
||||
const TITLE = "Process: AI speed, engineering proof";
|
||||
const DESCRIPTION =
|
||||
"How I ship AI-accelerated code that holds up: spec first, red, green, mutation check and a delivery report with exact commands and results on every pull request.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: { canonical: "/process" },
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: "/process",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
const GUARDRAILS = [
|
||||
{
|
||||
title: "Pre-commit gate",
|
||||
text: "Formatting and lint on staged files, then typecheck and the unit tests, run before a commit is created. Broken code does not enter history.",
|
||||
},
|
||||
{
|
||||
title: "Pre-push gate",
|
||||
text: "Coverage runs before a push leaves my machine, and the mutation check runs when the domain logic changed. The remote only receives green work.",
|
||||
},
|
||||
{
|
||||
title: "CI gates the deploy",
|
||||
text: "On client projects the same checks run again in CI, and a deploy only starts when they pass. My machine is not the only line of defense.",
|
||||
},
|
||||
{
|
||||
title: "Tests are never weakened",
|
||||
text: "If a test blocks a change, I do not skip, loosen or delete it to get green. If the test is wrong, I tell you why and the spec changes with your agreement.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const reportLines = formatDeliveryReport(SAMPLE_DELIVERY_REPORT);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Container className="py-16 md:py-24">
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
kicker="/process"
|
||||
title="How I ship AI-accelerated code that holds up"
|
||||
lead="AI makes code cheap to produce. What you pay for is code that is correct. Every change goes through the same five steps, and you get the evidence for each of them every week."
|
||||
/>
|
||||
|
||||
<section aria-labelledby="steps" className="mt-16">
|
||||
<h2 id="steps" className="sr-only">
|
||||
The five steps
|
||||
</h2>
|
||||
<p className="!mb-6 max-w-2xl text-base text-muted">
|
||||
One running example: parsing an availability, meaning a date and
|
||||
weekly hours. The date 2026-02-30 must be rejected, 2028-02-29 must be
|
||||
accepted, and 168 hours is the upper boundary of a week.
|
||||
</p>
|
||||
<ol className="list-none !p-0">
|
||||
{PROCESS_STEPS.map((step, index) => (
|
||||
<ProcessStepCard key={step.id} step={step} index={index} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="report" className="mt-16">
|
||||
<SectionHeader
|
||||
kicker="evidence"
|
||||
title="A delivery report"
|
||||
lead="This is what arrives with a pull request. The example below is illustrative: the numbers are made up for the availability parsing case and are not from a client project."
|
||||
/>
|
||||
<TerminalCard
|
||||
title="delivery report (illustrative example)"
|
||||
className="mt-8 max-w-3xl"
|
||||
>
|
||||
{reportLines.map((line) => (
|
||||
<p key={line} className="!mb-0 whitespace-pre">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</TerminalCard>
|
||||
<p className="!mb-0 mt-4">
|
||||
<a
|
||||
href="/sample-delivery-report.md"
|
||||
download
|
||||
className="font-mono text-sm text-accent underline underline-offset-4"
|
||||
>
|
||||
Download the sample report (sample-delivery-report.md)
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="weak" className="mt-16 max-w-2xl">
|
||||
<h2 id="weak" className="!mt-0 !text-2xl">
|
||||
What I do when tests are weak or missing
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed text-muted">
|
||||
AI-built code often arrives with few tests, or with tests that only
|
||||
repeat the implementation. I do not start by rewriting. I write the
|
||||
expected behavior as cases for the parts that matter most, run them
|
||||
against the existing code, and let the failures show what is really
|
||||
broken. Then I add a mutation baseline so you can see which tests
|
||||
would not notice a bug.
|
||||
</p>
|
||||
<h2 className="!text-2xl">Why mutation testing</h2>
|
||||
<p className="text-base leading-relaxed text-muted">
|
||||
Coverage tells you which lines ran during the tests. It does not tell
|
||||
you whether the tests would notice a bug in those lines. Mutation
|
||||
testing changes the code on purpose and checks that a test fails. That
|
||||
is the question you care about. Coverage stays useful as a gap finder:
|
||||
it points at code nothing exercises. It is not a goal, and a high
|
||||
number alone proves little.
|
||||
</p>
|
||||
<p className="text-base leading-relaxed text-muted">
|
||||
Two systems where I run this: a Rust and Solidity liquidation system
|
||||
across 7 chains, with a zero-survivor mutation gate and a coverage
|
||||
ratchet, and the domain core of this website, which has its own tests
|
||||
and mutation gate.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="guardrails" className="mt-16">
|
||||
<SectionHeader
|
||||
kicker="guardrails"
|
||||
title="Gates that run without me"
|
||||
lead="The process does not depend on my discipline on a good day."
|
||||
/>
|
||||
<ul className="mt-8 grid list-none gap-6 !p-0 md:grid-cols-2">
|
||||
{GUARDRAILS.map((item) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="rounded-lg border border-border bg-surface p-5"
|
||||
>
|
||||
<h3 className="!mb-2 !mt-0 text-lg font-semibold">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="!mb-0 text-base leading-relaxed text-muted">
|
||||
{item.text}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-16 border-t border-border pt-10">
|
||||
<h2 className="!mt-0 !text-2xl">Want this on your codebase?</h2>
|
||||
<p className="mb-6 max-w-2xl text-base text-muted">
|
||||
A 20-minute intro call is enough to see whether this fits your team.
|
||||
</p>
|
||||
<ButtonLink href="/book">Book a 20-minute intro call</ButtonLink>
|
||||
</section>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +1,14 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { technologies } from "./technologies/[slug]/data";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
/**
|
||||
* Sitemap Generator
|
||||
*
|
||||
* Standard Next.js 15 App Router sitemap generation.
|
||||
* This file dynamically generates /sitemap.xml
|
||||
*/
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const allPosts = await getAllPosts();
|
||||
/** The site is a single page. */
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mintel.me";
|
||||
|
||||
// 1. Core Pages
|
||||
const routes = [
|
||||
"",
|
||||
"/about",
|
||||
"/blog",
|
||||
"/case-studies",
|
||||
"/case-studies/klz-cables",
|
||||
"/contact",
|
||||
"/websites",
|
||||
].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: route === "" ? 1.0 : 0.8,
|
||||
}));
|
||||
|
||||
// 2. Dynamic Blog Posts
|
||||
const blogRoutes = allPosts.map((post) => ({
|
||||
url: `${baseUrl}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
// 3. Technology Detail Pages
|
||||
const techRoutes = Object.keys(technologies).map((slug) => ({
|
||||
url: `${baseUrl}/technologies/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
// 4. Tag Pages
|
||||
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
||||
const tagRoutes = allTags.map((tag) => ({
|
||||
url: `${baseUrl}/tags/${tag}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.3,
|
||||
}));
|
||||
|
||||
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { MediumCard } from "@/src/components/MediumCard";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
const allTags = Array.from(
|
||||
new Set(allPosts.flatMap((post) => post.tags || [])),
|
||||
);
|
||||
return allTags.map((tag) => ({
|
||||
tag,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function TagPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tag: string }>;
|
||||
}) {
|
||||
const { tag } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<Reveal>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
||||
Posts tagged{" "}
|
||||
<span className="highlighter-yellow px-2 rounded">{tag}</span>
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.1}>
|
||||
<p className="text-slate-600">
|
||||
{posts.length} post{posts.length === 1 ? "" : "s"}
|
||||
</p>
|
||||
</Reveal>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map((post, i) => (
|
||||
<Reveal key={post.slug} delay={0.1 + i * 0.05} width="100%">
|
||||
<MediumCard post={post} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<div className="mt-8 pt-6 border-t border-slate-200">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-slate-600 hover:text-slate-900 inline-flex items-center"
|
||||
>
|
||||
← Back to blog
|
||||
</Link>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { Container } from "@/src/components/Layout";
|
||||
import { Label } from "@/src/components/Typography";
|
||||
import { Check, ArrowLeft, Zap, ExternalLink } from "lucide-react";
|
||||
import { technologies } from "./data";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export default function TechnologyContent({ slug }: { slug: string }) {
|
||||
const tech = technologies[slug];
|
||||
|
||||
if (!tech) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Technology Not Found</h1>
|
||||
<Link href="/" className="text-blue-600 hover:underline">
|
||||
Return Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = tech.icon;
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen text-slate-900 pb-24">
|
||||
<div className="bg-slate-50 border-b border-slate-200">
|
||||
<Container className="py-24">
|
||||
<Reveal>
|
||||
<Link
|
||||
href="/case-studies/klz-cables"
|
||||
className="inline-flex items-center text-sm font-bold text-slate-500 hover:text-slate-900 mb-8 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Case Study
|
||||
</Link>
|
||||
</Reveal>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-start gap-8">
|
||||
<Reveal>
|
||||
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
|
||||
<Icon className="w-12 h-12" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<div className="flex-1">
|
||||
<Reveal delay={0.1}>
|
||||
<Label className="text-slate-400 mb-2">
|
||||
TECHNOLOGY DEEP DIVE
|
||||
</Label>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||
{tech.title}
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<p className="text-xl text-slate-500 font-medium">
|
||||
{tech.subtitle}
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Container className="py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||
<div className="lg:col-span-8 space-y-12">
|
||||
<section>
|
||||
<Reveal>
|
||||
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
|
||||
<p className="text-xl leading-relaxed text-slate-700">
|
||||
{tech.description}
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Reveal>
|
||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||
<Zap className="w-6 h-6 text-amber-500" /> Why I use it
|
||||
</h2>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{tech.benefits.map((benefit, i) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.05}>
|
||||
<div className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100 h-full">
|
||||
<Check className="w-5 h-5 text-green-600 shrink-0" />
|
||||
<span className="font-medium text-slate-700">
|
||||
{benefit}
|
||||
</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Reveal delay={0.4}>
|
||||
<section className="bg-blue-50 border border-blue-100 rounded-2xl p-8">
|
||||
<Label className="text-blue-600 mb-2">CUSTOMER IMPACT</Label>
|
||||
<h3 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
What does this mean for you?
|
||||
</h3>
|
||||
<p className="text-lg text-slate-700 leading-relaxed">
|
||||
{tech.customerValue}
|
||||
</p>
|
||||
</section>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<Reveal delay={0.5}>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 sticky top-24">
|
||||
<h3 className="font-bold text-slate-900 mb-4">
|
||||
Related Technologies
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{tech.related.map((item) => (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={`/technologies/${item.slug}`}
|
||||
className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-slate-400 hover:shadow-sm transition-all group"
|
||||
>
|
||||
<span className="font-medium text-slate-700 group-hover:text-slate-900">
|
||||
{item.name}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Layers, Code, Database, Palette, Terminal } from "lucide-react";
|
||||
|
||||
export interface TechInfo {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
icon: any; // React.ElementType
|
||||
benefits: string[];
|
||||
customerValue: string;
|
||||
color: string;
|
||||
related: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
export const technologies: Record<string, TechInfo> = {
|
||||
"next-js-14": {
|
||||
title: "Next.js 14",
|
||||
subtitle: "The React Framework for the Web",
|
||||
description:
|
||||
"Next.js 14 is the latest version of the industry-leading framework for building high-performance web applications. It allows me to create fast, scalable, and search-engine-friendly websites by rendering content on the server before sending it to your users.",
|
||||
icon: Layers,
|
||||
benefits: [
|
||||
"Lightning-fast page loads with Server Components",
|
||||
"Automatic image optimization",
|
||||
"Instant navigation between pages",
|
||||
"Top-tier SEO (Search Engine Optimization) out of the box",
|
||||
],
|
||||
customerValue:
|
||||
"For my clients, Next.js means a website that ranks higher on Google, keeps visitors engaged with instant interactions, and scales effortlessly as your traffic grows.",
|
||||
color: "bg-black text-white",
|
||||
related: [
|
||||
{ name: "TypeScript", slug: "typescript" },
|
||||
{ name: "React", slug: "react" },
|
||||
],
|
||||
},
|
||||
typescript: {
|
||||
title: "TypeScript",
|
||||
subtitle: "JavaScript with Syntax for Types",
|
||||
description:
|
||||
"TypeScript adds a powerful type system to JavaScript, catching errors before they ever reach production. It acts as a safety net for your code, ensuring that data flows exactly as expected through your entire application.",
|
||||
icon: Code,
|
||||
benefits: [
|
||||
"Eliminates whole categories of common bugs",
|
||||
"Makes large codebases easier to maintain",
|
||||
"Improves developer productivity and code confidence",
|
||||
"Ensures critical data integrity",
|
||||
],
|
||||
customerValue:
|
||||
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: "bg-blue-600 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
|
||||
"tailwind-css": {
|
||||
title: "Tailwind CSS",
|
||||
subtitle: "Utility-First CSS Framework",
|
||||
description:
|
||||
"Tailwind CSS is a utility-first framework that allows me to build custom designs directly in markup. It eliminates the need for bulky, overriding stylesheets and ensures a consistent design system across every page.",
|
||||
icon: Palette,
|
||||
benefits: [
|
||||
"Rapid UI development and prototyping",
|
||||
"Consistent spacing, colors, and typography",
|
||||
"Highly optimized final bundle size (only includes used styles)",
|
||||
"Responsive design made simple",
|
||||
],
|
||||
customerValue:
|
||||
"Tailwind ensures your brand looks pixel-perfect on every device. It also results in incredibly small CSS files, meaning your site loads faster for users on mobile networks.",
|
||||
color: "bg-cyan-500 text-white",
|
||||
related: [
|
||||
{ name: "React", slug: "react" },
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
react: {
|
||||
title: "React",
|
||||
subtitle: "The Library for Web and Native User Interfaces",
|
||||
description:
|
||||
"React is the core library powering Next.js. It lets me build encapsulated components that manage their own state, then compose them to make complex UIs.",
|
||||
icon: Terminal,
|
||||
benefits: [
|
||||
"Component-based architecture for reuse",
|
||||
"Efficient updates and rendering",
|
||||
"Rich ecosystem of libraries and tools",
|
||||
"Strong community support",
|
||||
],
|
||||
customerValue:
|
||||
"React enables rich, app-like interactions on your website. Whether it's a complex dashboard or a smooth animation, React makes it possible to build dynamic experiences that feel instantaneous.",
|
||||
color: "bg-blue-400 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
import { technologies } from "./data";
|
||||
import TechnologyContent from "./content";
|
||||
|
||||
export default async function TechnologyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
return <TechnologyContent slug={slug} />;
|
||||
}
|
||||
|
||||
// Generate static params for these dynamic routes
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(technologies).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LegacyLightSurface } from "@/src/components/LegacyLightSurface";
|
||||
|
||||
export default function LegacyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LegacyLightSurface>{children}</LegacyLightSurface>;
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import {
|
||||
H1,
|
||||
H3,
|
||||
LeadText,
|
||||
BodyText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
} from "@/src/components/Typography";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { AbstractCircuit, GradientMesh } from "@/src/components/Effects";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
|
||||
export default function WebsitesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||
<div className="space-y-16 md:space-y-32">
|
||||
<div className="space-y-10 max-w-4xl">
|
||||
<Reveal>
|
||||
<div className="space-y-6">
|
||||
<MonoLabel className="text-slate-400 tracking-[0.2em] text-xs">
|
||||
PROFESSIONELLE WEBSITES
|
||||
</MonoLabel>
|
||||
<H1 className="text-5xl md:text-8xl lg:text-9xl leading-[0.9] tracking-tighter">
|
||||
Ihre Website. <br />
|
||||
<span className="text-slate-400">Komplett sorgenfrei.</span>
|
||||
</H1>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-3xl text-slate-500 leading-snug">
|
||||
Kein WordPress-Chaos. Kein Baukasten-Frust. Ich baue Ihnen eine
|
||||
maßgeschneiderte Website, mit der Sie{" "}
|
||||
<span className="text-slate-900 font-bold underline decoration-slate-200 underline-offset-8">
|
||||
null Arbeit
|
||||
</span>{" "}
|
||||
haben.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-12 py-12 md:py-20 border-y border-slate-100">
|
||||
{[
|
||||
{
|
||||
label: "Kein CMS-Zwang",
|
||||
desc: "Sie müssen kein System lernen. Ich übernehme alles.",
|
||||
},
|
||||
{
|
||||
label: "Fixpreis",
|
||||
desc: "Klare Kosten von Anfang an. Keine versteckten Gebühren.",
|
||||
},
|
||||
{
|
||||
label: "Pfeilschnell",
|
||||
desc: "Keine langen Ladezeiten. Kunden bleiben auf der Seite.",
|
||||
},
|
||||
{
|
||||
label: "Sorglos",
|
||||
desc: "Hosting, Sicherheit, Backups? Darum kümmere ich mich.",
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div key={i} className="space-y-4">
|
||||
<Label className="text-slate-900 text-lg">{item.label}</Label>
|
||||
<BodyText className="text-slate-500">{item.desc}</BodyText>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 02: Performance */}
|
||||
<Section number="02" title="Schnelligkeit" borderTop>
|
||||
<div className="space-y-16 md:space-y-32">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl md:text-7xl lg:text-8xl leading-none tracking-tighter max-w-4xl">
|
||||
Niemand wartet gerne. <br />
|
||||
<span className="text-slate-400">
|
||||
Ihre Kunden erst recht nicht.
|
||||
</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-24">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl text-slate-500">
|
||||
Langsame Websites kosten bares Geld. Wenn Ihre Seite nicht
|
||||
sofort lädt, ist der Besucher schon bei der Konkurrenz. Meine
|
||||
Websites sind auf maximale Geschwindigkeit optimiert.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<div className="space-y-8 border-l border-slate-100 pl-8 md:pl-12">
|
||||
{[
|
||||
"Sofortiger Seitenaufbau ohne Verzögerung",
|
||||
"Automatische Bild-Optimierung für Smartphones",
|
||||
"Bessere Google-Platzierung (SEO) durch Top-Speed",
|
||||
].map((item, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Label className="text-slate-900">0{i + 1}</Label>
|
||||
<BodyText className="text-slate-500 text-lg">
|
||||
{item}
|
||||
</BodyText>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 03: Code-Qualität */}
|
||||
<Section number="03" title="Qualität" borderTop>
|
||||
<div className="space-y-16 md:space-y-32">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl md:text-7xl lg:text-8xl leading-none tracking-tighter max-w-4xl">
|
||||
Handarbeit statt <br />
|
||||
<span className="text-slate-400">Fertigbausatz.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-24">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl text-slate-500">
|
||||
Vergessen Sie Sicherheitslücken durch veraltete Plugins oder
|
||||
Themes, die ständig aktualisiert werden müssen.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<div className="space-y-12">
|
||||
<div>
|
||||
<Label className="text-slate-900 text-xl mb-4 block">
|
||||
Saubere Programmierung
|
||||
</Label>
|
||||
<BodyText className="text-slate-500 text-lg">
|
||||
Ihre Website wird von Grund auf solide entwickelt. Kein
|
||||
unnötiger Ballast, der die Seite verlangsamt oder angreifbar
|
||||
macht.
|
||||
</BodyText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-900 text-xl mb-4 block">
|
||||
Zukunftssicher
|
||||
</Label>
|
||||
<BodyText className="text-slate-500 text-lg">
|
||||
Moderne Technologien garantieren, dass Ihre Website auch in
|
||||
ein paar Jahren noch perfekt und fehlerfrei läuft.
|
||||
</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 04: Sorglos Paket */}
|
||||
<Section number="04" title="Sorglos-Paket" borderTop>
|
||||
<div className="space-y-16 md:space-y-32">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl md:text-7xl lg:text-8xl leading-none tracking-tighter max-w-4xl">
|
||||
Das Prinzip: <br />
|
||||
<span className="text-slate-400">Null Aufwand.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-24 items-start">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-2xl md:text-4xl text-slate-900 leading-snug">
|
||||
Sie haben Besseres zu tun, als Bilder zu tauschen oder Updates
|
||||
zu installieren.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<div className="space-y-8">
|
||||
<BodyText className="text-slate-500 text-lg md:text-xl leading-relaxed">
|
||||
Mein Sorglos-Paket deckt alles für ein ganzes Jahr ab. Wenn
|
||||
Sie eine Änderung brauchen, schreiben Sie mir einfach eine
|
||||
E-Mail. Kein kompliziertes System, keine Schulungen. Ich setze
|
||||
es für Sie um.
|
||||
</BodyText>
|
||||
<div className="pt-8 border-t border-slate-100 flex gap-8">
|
||||
<div>
|
||||
<Label className="text-slate-900">Inklusive</Label>
|
||||
<BodyText className="text-slate-500 mt-2">
|
||||
Änderungen & Wartung
|
||||
</BodyText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-900">Ihr Stresslevel</Label>
|
||||
<BodyText className="text-slate-500 mt-2">0%</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 05: CTA */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Ergebnis"
|
||||
borderTop
|
||||
effects={<GradientMesh variant="metallic" className="opacity-60" />}
|
||||
>
|
||||
<div className="py-12 md:py-24 relative z-10">
|
||||
<div className="space-y-16 md:space-y-24">
|
||||
<Reveal>
|
||||
<H3 className="text-5xl md:text-8xl lg:text-9xl leading-none tracking-tighter">
|
||||
Ihre fertige <br />
|
||||
<span className="text-slate-400">
|
||||
<GlitchText delay={0.5} duration={1}>
|
||||
Website.
|
||||
</GlitchText>
|
||||
</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-12">
|
||||
<LeadText className="text-xl md:text-3xl text-slate-500 max-w-xl">
|
||||
Kein Stress, kein Overhead. Schreiben Sie mir einfach eine
|
||||
Nachricht.
|
||||
</LeadText>
|
||||
<Button
|
||||
href="/contact"
|
||||
className="w-full md:w-auto px-12 py-6 text-xl"
|
||||
>
|
||||
Sagen Sie Hallo
|
||||
</Button>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getTweet } from 'react-tweet/api';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const tweet = await getTweet(id);
|
||||
|
||||
if (!tweet) {
|
||||
return NextResponse.json({ error: 'Tweet not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: tweet });
|
||||
} catch (error) {
|
||||
console.error('Error fetching tweet:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch tweet' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const pathParam = searchParams.get("path") || "/";
|
||||
const typeParam =
|
||||
(searchParams.get("type") as "page" | "layout") || "layout";
|
||||
|
||||
// Revalidate the requested path (defaults to entire app via layout)
|
||||
revalidatePath(pathParam, typeParam);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
revalidatedPath: pathParam,
|
||||
revalidatedType: typeParam,
|
||||
message: `Cache revalidated for ${pathParam} (${typeParam})`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user