diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index c7bd7f4..abef024 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -472,36 +472,30 @@ jobs: exit 1 fi - - name: 🌐 Check Case Study Assets and Rewrites + - name: 🌐 Check Legal Pages and Old URLs shell: sh run: | BASE_URL="${{ needs.prepare.outputs.next_public_url }}" - echo "Verifying case study page..." - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables") - if [ "$STATUS" != "200" ]; then - echo "❌ Case study page failed! (Status: $STATUS)" - exit 1 - fi - echo "✅ Case study page UP" + # The legal pages must be reachable. + for PAGE in /imprint /privacy; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$PAGE") + if [ "$STATUS" != "200" ]; then + echo "❌ $PAGE failed! (Status: $STATUS)" + exit 1 + fi + echo "✅ $PAGE OK" + done - echo "Verifying breeze CSS (root path)..." - CSS_PATH="/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$CSS_PATH") - if [ "$STATUS" != "200" ]; then - echo "❌ Root asset path failed! (Status: $STATUS)" - exit 1 - fi - echo "✅ Root asset path OK" - - echo "Verifying breeze CSS (relative path from case-study)..." - REL_CSS_PATH="/case-studies/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$REL_CSS_PATH") - if [ "$STATUS" != "200" ]; then - echo "❌ Relative asset path failed! (Status: $STATUS)" - exit 1 - fi - echo "✅ Relative asset path OK" + # URLs of the previous site must redirect instead of returning 404. + for OLD in /blog /contact /websites /case-studies/klz-cables /impressum; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$OLD") + if [ "$STATUS" != "308" ]; then + echo "❌ $OLD does not redirect! (Status: $STATUS)" + exit 1 + fi + echo "✅ $OLD redirects" + done # ────────────────────────────────────────────────────────────────────────────── # JOB 6: Notifications diff --git a/CLAUDE.md b/CLAUDE.md index 5cb4e22..b726477 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,19 @@ App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.m 5. Report the exact commands and results. "Done" means they were run on the current code. Never weaken, skip or delete a test to make it pass. +## Copy rules (client-facing pages) + +- Never mention AI as the owner's tool, and no internal method vocabulary (specs, TDD, mutation, coverage, + pull requests). The automation service may name AI as part of what the client gets. Plain language. +- Modest and factual: no promises the owner did not confirm, no superlatives. See the concept, section 1. +- One page: everything lives on the homepage. The header has only "Get in touch", which jumps to the contact + section (`#contact`). Contact is one email address: no calendar integration, no form. The only other + pages are `/imprint` and `/privacy`. URLs of the previous site redirect to `/` (see `next.config.mjs`). +- The email address is kept in parts (`src/content/site.ts`) and assembled in the browser by `EmailLink`. + Never write the full address into markup, metadata or copy. +- The hero design (binary-digit headline) is approved: do not change its design. +- Look at the result before reporting: take screenshots (desktop and mobile), do not ship blind. + ## Architecture rules - Functional core, imperative shell. Pure logic goes in `apps/web/src/domain/` (no I/O, no clock, no @@ -40,7 +53,12 @@ App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.m ## Gotchas +- `tsconfig` includes `.next/types`: after deleting a route, stale generated files there break `tsc`. + Run `next typegen` and remove the stale file. +- Run only one Stryker process at a time; parallel runs share `.stryker-tmp` and hang. - Stryker needs `plugins` listed explicitly in `stryker.config.json` (pnpm layout) and ignores the large asset folders via `ignorePatterns`; keep them in sync when adding top-level folders. -- `@mintel/*` packages are `link:` dependencies on a sibling `at-mintel` checkout. +- `@mintel/*` packages are `link:` dependencies on a sibling `at-mintel` checkout. The app itself now only + uses `@mintel/next-config`; the other entries in `package.json` are leftovers from the old site. +- Pushing `main` deploys to the testing environment; production is deployed from a `v*` tag. - Git branches: work on `feat/*`, never directly on `main`. diff --git a/apps/web/app/(site)/about/page.tsx b/apps/web/app/(site)/about/page.tsx deleted file mode 100644 index fbaaf18..0000000 --- a/apps/web/app/(site)/about/page.tsx +++ /dev/null @@ -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 ( - - - -
-

- 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. -

-

- 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. -

-
- -
-

- How I work -

-
    - {WORKING_MODE.map((line) => ( -
  • {line}
  • - ))} -
-
- -
-

- Timeline -

- -
- -
- - -
-
- ); -} diff --git a/apps/web/app/(site)/blog/[slug]/opengraph-image.tsx b/apps/web/app/(site)/blog/[slug]/opengraph-image.tsx deleted file mode 100644 index 5866d5e..0000000 --- a/apps/web/app/(site)/blog/[slug]/opengraph-image.tsx +++ /dev/null @@ -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( - , - { - ...OG_IMAGE_SIZE, - fonts: fonts as any, - }, - ); -} diff --git a/apps/web/app/(site)/blog/[slug]/page.tsx b/apps/web/app/(site)/blog/[slug]/page.tsx deleted file mode 100644 index ebdbc67..0000000 --- a/apps/web/app/(site)/blog/[slug]/page.tsx +++ /dev/null @@ -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 { - 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 ( -
- - - - -
- - -
-
- - {post.tags && post.tags.length > 0 && ( -
- {post.tags?.map((tag: string) => ( - - #{tag} - - ))} -
- )} - -
- - -
-
-
-
-
- - -
- ); -} diff --git a/apps/web/app/(site)/blog/layout.tsx b/apps/web/app/(site)/blog/layout.tsx deleted file mode 100644 index 1191fef..0000000 --- a/apps/web/app/(site)/blog/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { LegacyLightSurface } from "@/src/components/LegacyLightSurface"; - -export default function LegacyLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/apps/web/app/(site)/blog/page.tsx b/apps/web/app/(site)/blog/page.tsx deleted file mode 100644 index a7c42ab..0000000 --- a/apps/web/app/(site)/blog/page.tsx +++ /dev/null @@ -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 ; -} diff --git a/apps/web/app/(site)/book/page.tsx b/apps/web/app/(site)/book/page.tsx deleted file mode 100644 index b7c6ba3..0000000 --- a/apps/web/app/(site)/book/page.tsx +++ /dev/null @@ -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 ( - - -
- -
- -
-

- What to prepare -

-
    - {PREPARE.map((item) => ( -
  • {item}
  • - ))} -
-
- -
- -
-
- ); -} diff --git a/apps/web/app/(site)/case-studies/klz-cables/page.tsx b/apps/web/app/(site)/case-studies/klz-cables/page.tsx deleted file mode 100644 index bc4db55..0000000 --- a/apps/web/app/(site)/case-studies/klz-cables/page.tsx +++ /dev/null @@ -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 ( -
- - - - - {/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */} -
- - - - - - {" "} - Zurück - - -
- -
- -
- - SYSTEM-ARCHITEKTUR // 2025 - - -
-
-
- -
- - KLZ Cables - -
- - Case Study. - - - -
- - Engineering eines
- Systems. -
- - Vom statischen Altsystem zum industriellen Standard. Ich - habe das KLZ-System auf das Wesentliche reduziert: Hardened - Infrastructure, parametrische Datenpflege und zero - maintenance. - -
-
-
- - -
-
- -
-
- - Relational Data - -
-
-
- -
- - WP + Varnish -
-
-
- -
- -
- - {/* --- SECTION 01: ARCHITECTURE --- */} -
- {/* Binary overlay background */} -
- {Array.from({ length: 5 }).map((_, i) => ( -
01001101 01001001 01001110 01010100
- ))} -
- -
-
-

- Architektur-
- Refactor. -

-
-
- -
- - Vom statischen HTML zur zentralen Daten-Instanz. - - - 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{" "} - Design. - -
-
-
-
- - -
- -
- {[ - { - label: "Edge Caching", - desc: "Varnish + W3TC Object Cache", - icon: , - }, - { - label: "Analytics", - desc: "Independent (Global Data Compliance)", - icon: , - }, - { - label: "Custom Core", - desc: "REST via Native Services", - icon: , - }, - ].map((item, i) => ( - -
{item.icon}
-
- - {item.label} - - - {item.desc} - -
-
- ))} -
-
-
-
-
-
-
- - {/* --- SHOWCASE: LANDING --- */} -
-
- -
-
- -

- Global Hub. -

-
- - {/* Binary overlay left */} -
- {Array.from({ length: 4 }).map((_, i) => ( -
HANDSHAKE_0x00{i}A // SYNC_ACTIVE
- ))} -
-
-
- - - - - -
-
- - {/* --- SECTION 02: TECHNICAL DETAIL --- */} -
-
-
-
- -
- -

- Automated Documentation. -

-
-
-
- - - 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. - - -
- - - -
- -
-
-
-
-
- -
-
-
-

- Fokus auf
- Spezifikationen. -

-
- -
- - - - - -
- -
- -
- - - 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. - - - - - Strukturierte Aufbereitung technischer Produktdaten. - - -
-
-
-
-
- - {/* --- SECTION 04: CONTENT ENGINE --- */} -
-
-
- -
- -

- Insights & News. -

- - 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. - -
-
-
-
- - - - - -
-
-
- - {/* --- SECTION 05: TEAM & TRUST --- */} -
-
- -

- System-Lifecycle. -

- - 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. - -
- - -
-
- -
-
-
-
-
- - {/* --- SECTION 06: CONVERSION --- */} -
-
-
- -
- -

- Direkter Draht. -

- - Das Kontakt-System wurde auf maximale Reduktion getrimmt. Ein - deterministischer Kanal zwischen technischem Bedarf und - individueller Beratung – ohne Umwege, ohne Rauschen. - -
-
- - -
-
- - RESPONSE_TIME - -
- < 120ms -
-
-
- - PROTOCOL - -
- mTLS -
-
-
- - AVAILABILITY - -
- 99.9% -
-
-
- - ENCRYPTION - -
- AES-256 -
-
-
-
-
- -
- - - - - -
-
-
- {/* --- FINAL CTA: ARCHITECTURE & VALUE --- */} -
- - -
-
- -
- - CONSULTING // ENGINEERING - -

- Architektur
- ohne Altlasten. -

-
-
- - - Vom Prototyp zum industriellen Standard. Ich entwickle - digitale Infrastrukturen, die technische Freiheit und - operative Stabilität garantieren – wartungsfrei und - skalierbar. - - -
- -
- -
-
- Operational Excellence -
-
- {[ - { - 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) => ( -
- - {item.title} - - - {item.desc} - -
- ))} -
- - - - -
-
- -
-
- ); -} diff --git a/apps/web/app/(site)/case-studies/layout.tsx b/apps/web/app/(site)/case-studies/layout.tsx deleted file mode 100644 index 1191fef..0000000 --- a/apps/web/app/(site)/case-studies/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { LegacyLightSurface } from "@/src/components/LegacyLightSurface"; - -export default function LegacyLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/apps/web/app/(site)/case-studies/page.tsx b/apps/web/app/(site)/case-studies/page.tsx deleted file mode 100644 index bcb8dad..0000000 --- a/apps/web/app/(site)/case-studies/page.tsx +++ /dev/null @@ -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 ( -
- - - {/* Featured Case Study Hero */} -
-
- -
-

- Case Studies. -

- - Ergebnisse statt Versprechen. Dokumentierte Architektur-Lösungen - für komplexe Anforderungen. - -
-
- - - - - {/* Brand Gradient Background */} -
- - {/* Coming Soon */} -
- - -
-
-
- -
-

- Weitere Case Studies in Kürze. -

- - Ich dokumentiere laufende Projekte – schauen Sie bald wieder - vorbei oder kontaktieren Sie mich direkt. - -
- -
-
- - -
-
- ); -} diff --git a/apps/web/app/(site)/contact/layout.tsx b/apps/web/app/(site)/contact/layout.tsx deleted file mode 100644 index 1191fef..0000000 --- a/apps/web/app/(site)/contact/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { LegacyLightSurface } from "@/src/components/LegacyLightSurface"; - -export default function LegacyLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/apps/web/app/(site)/contact/opengraph-image.tsx b/apps/web/app/(site)/contact/opengraph-image.tsx deleted file mode 100644 index eb9891e..0000000 --- a/apps/web/app/(site)/contact/opengraph-image.tsx +++ /dev/null @@ -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( - , - { - ...OG_IMAGE_SIZE, - fonts: fonts as any, - }, - ); -} diff --git a/apps/web/app/(site)/contact/page.tsx b/apps/web/app/(site)/contact/page.tsx deleted file mode 100644 index 4d2d164..0000000 --- a/apps/web/app/(site)/contact/page.tsx +++ /dev/null @@ -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 ( -
- - -
} - className="pt-24 pb-12 md:pt-32 md:pb-20" - > - {/* Full-width Form */} - -
-
- ); -} diff --git a/apps/web/app/(site)/globals.css b/apps/web/app/(site)/globals.css index e1d1060..670000e 100644 --- a/apps/web/app/(site)/globals.css +++ b/apps/web/app/(site)/globals.css @@ -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 */ diff --git a/apps/web/app/(site)/imprint/page.tsx b/apps/web/app/(site)/imprint/page.tsx new file mode 100644 index 0000000..d7cd2c1 --- /dev/null +++ b/apps/web/app/(site)/imprint/page.tsx @@ -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 ( + + +

+ {OWNER.name} +
+ {OWNER.street} +
+ {OWNER.city} +
+ {OWNER.country} +

+
+ + +

+ Email: +

+
+ + +

+ VAT identification number according to section 27a of the German VAT + Act (Umsatzsteuergesetz): {OWNER.vatId} +

+
+ + +

{OWNER.name}, address as above (section 18 paragraph 2 MStV).

+
+ + +

+ I am neither willing nor obliged to take part in dispute resolution + proceedings before a consumer arbitration board. +

+
+
+ ); +} diff --git a/apps/web/app/(site)/layout.tsx b/apps/web/app/(site)/layout.tsx index 3d82c4f..e3d6e8b 100644 --- a/apps/web/app/(site)/layout.tsx +++ b/apps/web/app/(site)/layout.tsx @@ -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 ( - +
{children}