feat: automate availability and fix KLZ showcase 404s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🏗️ Build (push) Successful in 8m59s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🩺 Smoke Test (push) Failing after 4s
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-04-13 21:11:23 +02:00
parent d4b586f29f
commit 2cfe9115d2
5 changed files with 99 additions and 21 deletions

View File

@@ -78,6 +78,28 @@
box-shadow: none !important;
}
/* Interactive elements pointer */
a,
button,
[role="button"],
input[type="button"],
input[type="submit"],
input[type="reset"],
select,
summary,
label[for] {
cursor: pointer;
}
/* Ensure disabled elements don't show pointer */
button:disabled,
[role="button"]:disabled,
input:disabled,
select:disabled,
button[disabled] {
cursor: not-allowed;
}
/* Remove default tap highlight on mobile */
* {
-webkit-tap-highlight-color: transparent !important;

View File

@@ -26,6 +26,7 @@ import { HeroSection } from "@/src/components/HeroSection";
import { GlitchText } from "@/src/components/GlitchText";
import { Marker } from "@/src/components/Marker";
import { PenCircle } from "@/src/components/PenCircle";
import { Availability } from "@/src/components/Availability";
export default function LandingPage() {
return (
@@ -313,7 +314,7 @@ export default function LandingPage() {
</div>
<BodyText className="text-sm md:text-base leading-snug">
Aktuell nehme ich Projekte für{" "}
<span className="font-bold text-slate-900">Q2 2026</span>{" "}
<span className="font-bold text-slate-900"><Availability /></span>{" "}
an.
</BodyText>
</div>

View File

@@ -0,0 +1,26 @@
"use client";
import React, { useEffect, useState } from "react";
/**
* Automatisierte Anzeige der Projekt-Verfügbarkeit nach Quartalen.
* Berechnet das aktuelle Quartal basierend auf dem heutigen Datum.
*/
export const Availability: React.FC = () => {
const [text, setText] = useState<string>("");
useEffect(() => {
const now = new Date();
const month = now.getMonth(); // 0-11
const year = now.getFullYear();
const quarter = Math.floor(month / 3) + 1;
// Aktuelles Quartal formatiert (z.B. Q2 2026)
setText(`Q${quarter} ${year}`);
}, []);
// Während Hydrierung nichts rendern, um Mismatches zu vermeiden (bei Client-only Logic)
if (!text) return <span className="opacity-0">Q- 202-</span>;
return <>{text}</>;
};

View File

@@ -11,19 +11,32 @@ export function middleware(request: NextRequest) {
// 2. Legacy Showcase Asset Mapping
// Matches: /wp-content/*, /wp-includes/*, /assets/*, and their relative subpaths
const showcaseAssetPattern = /^\/(?:wp-content|wp-includes|assets|(?:case-studies|work|blog)\/(?:wp-content|wp-includes|assets))\/(.*)/;
const match = pathname.match(showcaseAssetPattern);
// Handles:
// - Global assets (/wp-content/*, /wp-includes/*, /assets/*)
// - Showcase-specific resources (/case-studies/klz-cables/index.html, etc.)
// Map Pattern 1: Global assets (directly under root)
if (pathname.match(/^\/(?:wp-content|wp-includes|assets)\//)) {
return NextResponse.rewrite(new URL(`/showcase/klz-cables.com${pathname}`, request.url));
}
if (match) {
const assetPath = match[0];
// Map Pattern 2: Showcase-prefixed assets
const showcaseMatch = pathname.match(/^\/(?:case-studies|work|blog)\/([^\/]+)\/(.*)/);
if (showcaseMatch) {
const [, slug, remainingPath] = showcaseMatch;
// Normalize path by stripping the case-studies/work/blog prefix if present
const normalizedPath = assetPath.replace(/^\/(?:case-studies|work|blog)/, '');
// Map to the literal directory in public/
// Destination: /showcase/klz-cables.com/...
return NextResponse.rewrite(new URL(`/showcase/klz-cables.com${normalizedPath}`, request.url));
// Safety: Only rewrite if it looks like a static asset (has extension)
// or belongs to known legacy folders. This prevents catching Next.js routes.
if (remainingPath.match(/\.(?:html|php|js|css|png|jpg|jpeg|svg|gif|woff2?|ttf|pdf|json|xml)$/) ||
remainingPath.includes('wp-content/') ||
remainingPath.includes('wp-includes/') ||
remainingPath.includes('assets/')) {
// Normalize slug: klz-cables -> klz-cables.com (literal folder name)
const directory = slug === 'klz-cables' ? 'klz-cables.com' : slug;
return NextResponse.rewrite(new URL(`/showcase/${directory}/${remainingPath}`, request.url));
}
}
return NextResponse.next();
@@ -35,14 +48,8 @@ export const config = {
'/wp-content/:path*',
'/wp-includes/:path*',
'/assets/:path*',
'/case-studies/wp-content/:path*',
'/case-studies/wp-includes/:path*',
'/case-studies/assets/:path*',
'/work/wp-content/:path*',
'/work/wp-includes/:path*',
'/work/assets/:path*',
'/blog/wp-content/:path*',
'/blog/wp-includes/:path*',
'/blog/assets/:path*',
'/case-studies/:path*',
'/work/:path*',
'/blog/:path*',
],
};

View File

@@ -79,6 +79,28 @@
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Interactive elements pointer */
a,
button,
[role="button"],
input[type="button"],
input[type="submit"],
input[type="reset"],
select,
summary,
label[for] {
cursor: pointer;
}
/* Ensure disabled elements don't show pointer */
button:disabled,
[role="button"]:disabled,
input:disabled,
select:disabled,
button[disabled] {
cursor: not-allowed;
}
}
/* Components - Tailwind utility classes */