Compare commits
12 Commits
v1.15.19
...
7bceb6cd2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bceb6cd2b | |||
| 569bb4e270 | |||
| 0a6f5f34ce | |||
| 5f27bb9950 | |||
| d83199ab1a | |||
| 2cfe9115d2 | |||
| d4b586f29f | |||
| 9013d38e31 | |||
| f3cbdd8076 | |||
| f143fc6099 | |||
| 83190f1d00 | |||
| 12848a4f23 |
@@ -485,14 +485,6 @@ jobs:
|
||||
run: |
|
||||
BASE_URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
|
||||
echo "Verifying diagnostic rewrite..."
|
||||
STATUS=$(curl -s -L -o /dev/null -w "%{http_code}" "$BASE_URL/robots-test")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Diagnostic rewrite /robots-test failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Diagnostic rewrite OK"
|
||||
|
||||
echo "Verifying case study page..."
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
|
||||
@@ -76,6 +76,8 @@ WORKDIR /app
|
||||
# Copy standalone output and static files (Monorepo paths)
|
||||
# Note: Base image already handles the non-root user and basic env
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./apps/web/public
|
||||
# Fallback target: Next.js sometimes resolves from the process root depending on version
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./public
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/static ./apps/web/.next/static
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,61 +46,38 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
// Map Patttern: Global assets moved into KLZ showcase folder
|
||||
{
|
||||
source: '/assets/klz-cables.com/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-content/:path*',
|
||||
destination: '/showcase/klz-cables.com/wp-content/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-includes/:path*',
|
||||
destination: '/showcase/klz-cables.com/wp-includes/:path*',
|
||||
},
|
||||
]
|
||||
};
|
||||
},
|
||||
// In Standalone mode, Next.js expects the tracing root to be the monorepo root
|
||||
outputFileTracingRoot: path.join(dirname, '../../'),
|
||||
};
|
||||
|
||||
const withMDX = createMDX({});
|
||||
|
||||
// Apply wrappers
|
||||
const wrappedConfig = withPayload(withMintelConfig(withMDX(nextConfig)));
|
||||
// Clean, standard wrapper application
|
||||
// Rewrites are now handled by src/middleware.ts for maximum robustness
|
||||
const config = withMintelConfig(withMDX(nextConfig));
|
||||
|
||||
// HARDEN REWRITES: Manually merge showcase rules to prevent wrapper overwrites
|
||||
const originalRewrites = wrappedConfig.rewrites;
|
||||
wrappedConfig.rewrites = async () => {
|
||||
const existing = await originalRewrites?.() || [];
|
||||
|
||||
// Normalize existing to object pattern (Next.js supports both Array and Object)
|
||||
const normalized = Array.isArray(existing)
|
||||
? { afterFiles: existing, beforeFiles: [], fallback: [] }
|
||||
: existing;
|
||||
// Cleanup config to prevent Next.js 16 warnings about deprecated keys
|
||||
if (config.serverActions) {
|
||||
delete config.serverActions;
|
||||
}
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
beforeFiles: [
|
||||
...(normalized.beforeFiles || []),
|
||||
// Diagnostic rule
|
||||
{
|
||||
source: '/robots-test',
|
||||
destination: '/robots.txt',
|
||||
},
|
||||
// Assets requested from root
|
||||
{
|
||||
source: '/assets/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-content/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-includes/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*',
|
||||
},
|
||||
// Assets requested relatively from case-studies/work/blog subpages
|
||||
{
|
||||
source: '/:folder(case-studies|work|blog)/assets/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/:path*',
|
||||
},
|
||||
{
|
||||
source: '/:folder(case-studies|work|blog)/wp-content/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*',
|
||||
},
|
||||
{
|
||||
source: '/:folder(case-studies|work|blog)/wp-includes/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default wrappedConfig;
|
||||
export default withPayload(config);
|
||||
|
||||
@@ -28,7 +28,8 @@ const dirname = path.dirname(filename);
|
||||
|
||||
const isCLI =
|
||||
process.argv.includes("migrate") ||
|
||||
process.argv.includes("generate:importmap");
|
||||
process.argv.includes("generate:importmap") ||
|
||||
process.argv.includes("build");
|
||||
let aiPlugin: any;
|
||||
if (!isCLI) {
|
||||
const { payloadChatPlugin } = await import("@mintel/payload-ai");
|
||||
@@ -67,19 +68,21 @@ export default buildConfig({
|
||||
globals: [
|
||||
/* AiSettings as any */
|
||||
],
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || "localhost",
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME || "user",
|
||||
pass: process.env.MAIL_PASSWORD || "pass",
|
||||
},
|
||||
...(process.env.MAIL_HOST ? {} : { ignoreTLS: true }),
|
||||
},
|
||||
}),
|
||||
email: isCLI
|
||||
? undefined
|
||||
: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || "localhost",
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME || "user",
|
||||
pass: process.env.MAIL_PASSWORD || "pass",
|
||||
},
|
||||
...(process.env.MAIL_HOST ? {} : { ignoreTLS: true }),
|
||||
},
|
||||
}),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
|
||||
26
apps/web/src/components/Availability.tsx
Normal file
26
apps/web/src/components/Availability.tsx
Normal 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}</>;
|
||||
};
|
||||
@@ -4,59 +4,86 @@ export const getInquiryEmailHtml = (data: any) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Courier New', Courier, monospace; background-color: #0f172a; color: #f8fafc; margin: 0; padding: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: #1e293b; border: 1px solid #334155; padding: 40px; border-radius: 8px; }
|
||||
.header { border-bottom: 2px solid #22c55e; padding-bottom: 20px; margin-bottom: 30px; }
|
||||
.title { font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #f8fafc; }
|
||||
.label { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
|
||||
.value { font-size: 16px; margin-bottom: 20px; color: #22c55e; }
|
||||
.section { margin-bottom: 30px; }
|
||||
.footer { font-size: 10px; color: #64748b; margin-top: 40px; border-top: 1px solid #334155; padding-top: 20px; }
|
||||
body { font-family: Georgia, 'Times New Roman', Times, serif; background-color: #f8fafc; color: #1e293b; margin: 0; padding: 40px 20px; line-height: 1.6; }
|
||||
.wrapper { max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 4px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
||||
.top-bar { background-color: #0f172a; color: #ffffff; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.brand { font-family: system-ui, sans-serif; font-size: 10px; font-weight: 800; letter-spacing: 0.2em; text-transform: uppercase; }
|
||||
.status { font-family: ui-monospace, monospace; font-size: 9px; color: #10b981; text-transform: uppercase; font-weight: bold; }
|
||||
.header { padding: 40px 32px 24px 32px; border-bottom: 2px solid #f1f5f9; }
|
||||
.heading { font-family: system-ui, sans-serif; font-size: 24px; font-weight: 800; color: #0f172a; margin: 0; letter-spacing: -0.02em; }
|
||||
.content { padding: 32px; }
|
||||
.data-grid { width: 100%; border-collapse: collapse; margin-bottom: 32px; }
|
||||
.data-row td { padding: 12px 0; border-bottom: 1px solid #f8fafc; vertical-align: top; }
|
||||
.label { font-family: system-ui, sans-serif; font-size: 9px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.1em; width: 140px; }
|
||||
.value { font-size: 15px; color: #334155; }
|
||||
.message-container { background-color: #f8fafc; border: 1px solid #f1f5f9; border-radius: 4px; padding: 24px; margin-top: 12px; }
|
||||
.message-text { font-family: Georgia, serif; font-size: 16px; color: #1e293b; white-space: pre-wrap; margin: 0; }
|
||||
.config-code { font-family: ui-monospace, monospace; font-size: 12px; color: #64748b; background: #f1f5f9; padding: 16px; border-radius: 4px; display: block; overflow-x: auto; }
|
||||
.footer { padding: 32px; color: #94a3b8; font-size: 11px; text-align: center; border-top: 1px solid #f1f5f9; }
|
||||
.footer-meta { font-family: ui-monospace, monospace; font-size: 9px; margin-top: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="title">NEUE_ANFRAGE_INPUT</div>
|
||||
<div class="wrapper">
|
||||
<div class="top-bar">
|
||||
<div class="brand">MINTEL_TECHNICAL_OPERATIONS</div>
|
||||
<div class="status">● INCOMING_INQUIRY</div>
|
||||
</div>
|
||||
|
||||
<div class="label">ABSENDER</div>
|
||||
<div class="value">${data.name} (${data.email})</div>
|
||||
|
||||
${data.phone ? `<div class="label">TELEFON</div><div class="value">${data.phone}</div>` : ""}
|
||||
${data.role ? `<div class="label">POSITION</div><div class="value">${data.role}</div>` : ""}
|
||||
|
||||
<div class="label">UNTERNEHMEN</div>
|
||||
<div class="value">${data.companyName || "N/A"}</div>
|
||||
|
||||
<div class="label">PROJEKT_TYP</div>
|
||||
<div class="value">${data.projectType}</div>
|
||||
|
||||
${data.deadline ? `<div class="label">ZEITRAUM</div><div class="value">${data.deadline}</div>` : ""}
|
||||
<div class="header">
|
||||
<h1 class="heading">${data.isFreeText ? "Direktanfrage" : "System-Konfiguration"}</h1>
|
||||
<p style="margin: 8px 0 0 0; color: #64748b; font-size: 14px;">Eingang für ${data.companyName || data.name}</p>
|
||||
</div>
|
||||
|
||||
${
|
||||
data.isFreeText
|
||||
? `
|
||||
<div class="section">
|
||||
<div class="label">NACHRICHT (FREITEXT)</div>
|
||||
<div class="value" style="white-space: pre-wrap; color: #f8fafc;">${data.message}</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="section">
|
||||
<div class="label">KONFIGURATION</div>
|
||||
<div class="value" style="font-size: 12px; color: #94a3b8; background: #0f172a; padding: 15px; border-radius: 4px;">
|
||||
${JSON.stringify(data.config, null, 2)}
|
||||
<div class="content">
|
||||
<table class="data-grid">
|
||||
<tr class="data-row">
|
||||
<td class="label">Absender</td>
|
||||
<td class="value"><strong>${data.name}</strong><br/><span style="color: #94a3b8; font-size: 13px;">${data.email}</span></td>
|
||||
</tr>
|
||||
${data.phone ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Telefon</td>
|
||||
<td class="value">${data.phone}</td>
|
||||
</tr>` : ""}
|
||||
${data.role ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Position</td>
|
||||
<td class="value">${data.role}</td>
|
||||
</tr>` : ""}
|
||||
<tr class="data-row">
|
||||
<td class="label">Unternehmen</td>
|
||||
<td class="value">${data.companyName || "—"}</td>
|
||||
</tr>
|
||||
<tr class="data-row">
|
||||
<td class="label">Projekt-Typ</td>
|
||||
<td class="value">${data.projectType}</td>
|
||||
</tr>
|
||||
${data.deadline ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Zeitraum</td>
|
||||
<td class="value">${data.deadline}</td>
|
||||
</tr>` : ""}
|
||||
</table>
|
||||
|
||||
${data.isFreeText ? `
|
||||
<div class="label" style="margin-bottom: 8px;">Nachricht</div>
|
||||
<div class="message-container">
|
||||
<div class="message-text">${data.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
` : `
|
||||
<div class="label" style="margin-bottom: 8px;">Konfigurations-Daten</div>
|
||||
<code class="config-code">${JSON.stringify(data.config, null, 2)}</code>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
SISTEM_STATUS: VALIDATED<br>
|
||||
TIMESTAMP: ${new Date().toISOString()}
|
||||
Dies ist eine systemgenerierte Benachrichtigung von mintel.me.<br/>
|
||||
<div class="footer-meta">
|
||||
TIMESTAMP: ${new Date().toISOString()} | STATUS: VALIDATED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -67,32 +94,42 @@ export const getConfirmationEmailHtml = (data: any) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Courier New', Courier, monospace; background-color: #f8fafc; color: #0f172a; margin: 0; padding: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e2e8f0; padding: 40px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
||||
.header { text-align: center; margin-bottom: 40px; }
|
||||
.status-badge { display: inline-block; padding: 4px 12px; background-color: #22c55e; color: #0f172a; font-size: 10px; font-weight: bold; border-radius: 9999px; margin-bottom: 16px; }
|
||||
.title { font-size: 28px; font-weight: bold; letter-spacing: -0.02em; margin-bottom: 8px; }
|
||||
.subtitle { color: #64748b; font-size: 16px; line-height: 1.5; }
|
||||
.content { line-height: 1.6; color: #334155; margin-bottom: 40px; }
|
||||
.footer { text-align: center; font-size: 12px; color: #94a3b8; border-top: 1px solid #f1f5f9; padding-top: 30px; }
|
||||
body { font-family: Georgia, 'Times New Roman', Times, serif; background-color: #ffffff; color: #1e293b; margin: 0; padding: 40px 20px; line-height: 1.8; }
|
||||
.wrapper { max-width: 600px; margin: 0 auto; }
|
||||
.logo-container { padding-bottom: 48px; border-bottom: 1px solid #f1f5f9; margin-bottom: 48px; }
|
||||
.status-badge { display: inline-block; font-family: system-ui, sans-serif; font-size: 9px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 0.2em; border: 1px solid #dbeafe; background: #eff6ff; padding: 4px 12px; border-radius: 99px; margin-bottom: 24px; }
|
||||
.greeting { font-family: system-ui, sans-serif; font-size: 32px; font-weight: 800; color: #0f172a; margin: 0 0 16px 0; letter-spacing: -0.03em; }
|
||||
.body-text { font-size: 18px; color: #334155; margin-bottom: 32px; }
|
||||
.cta-box { border-left: 2px solid #0f172a; padding: 8px 0 8px 24px; margin: 40px 0; }
|
||||
.footer { margin-top: 80px; padding-top: 24px; border-top: 1px solid #f1f5f9; font-family: system-ui, sans-serif; font-size: 12px; color: #94a3b8; text-align: center; }
|
||||
.footer a { color: #0f172a; text-decoration: none; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="status-badge">SEQUENZ_INITIIERT</div>
|
||||
<div class="title">Hallo ${data.name.split(" ")[0]},</div>
|
||||
<div class="subtitle">vielen Dank für deine Anfrage.</div>
|
||||
<div class="wrapper">
|
||||
<div class="logo-container">
|
||||
<div class="status-badge">Anfrage eingegangen</div>
|
||||
<h1 class="greeting">Hallo ${data.name.split(" ")[0]},</h1>
|
||||
<p class="body-text">
|
||||
vielen Dank für dein Interesse an einer Zusammenarbeit. Deine Nachricht bezüglich <strong>${data.companyName || "deines Projekts"}</strong> ist sicher bei mir angekommen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Ich habe deine Nachricht erhalten und schaue mir die Details zu <strong>${data.companyName || "deinem Projekt"}</strong> umgehend an.</p>
|
||||
<p>Normalerweise melde ich mich innerhalb von 24 Stunden bei dir zurück, um die nächsten Schritte zu besprechen.</p>
|
||||
<div class="body-text">
|
||||
Ich werde mir die Details umgehend ansehen und mich in der Regel innerhalb der nächsten 24 Stunden persönlich bei dir zurückmelden, um die nächsten Schritte zu besprechen.
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<p style="margin:0; font-style: italic; color: #64748b;">
|
||||
„Technical problems are just puzzles with more moving parts.“
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
© ${new Date().getFullYear()} mintel.me — Technical Problem Solving
|
||||
© ${new Date().getFullYear()} <a href="https://mintel.me">mintel.me</a> — Marc Mintel<br/>
|
||||
Professional Website Systems & Technical Architecture
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
58
apps/web/src/middleware.ts
Normal file
58
apps/web/src/middleware.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
/**
|
||||
* PRODUCTION STABILIZATION MIDDLEWARE
|
||||
* This middleware handles legacy asset routing for the KLZ showcase,
|
||||
* ensuring assets are correctly mapped regardless of Next.js config wrapper behavior.
|
||||
*/
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
|
||||
// 2. Legacy Showcase Asset Mapping
|
||||
// Handles:
|
||||
// - Global assets (/wp-content/*, /wp-includes/*, /assets/*)
|
||||
// - Showcase-specific resources (/case-studies/klz-cables/index.html, etc.)
|
||||
|
||||
// Normalize pathname to ensure we're matching correctly
|
||||
const path = pathname;
|
||||
|
||||
// Map Pattern 2: Showcase-prefixed assets (used for relative links within the showcase)
|
||||
const showcaseMatch = path.match(/^\/(?:case-studies|work|blog)\/([^\/]+)\/(.*)/);
|
||||
if (showcaseMatch) {
|
||||
const [, slug, remainingPath] = showcaseMatch;
|
||||
|
||||
// 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|webp|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;
|
||||
|
||||
// If the remaining path already starts with assets/directory, don't double it
|
||||
const targetPath = remainingPath.startsWith(`assets/${directory}/`)
|
||||
? `/${remainingPath}`
|
||||
: `/${remainingPath}`;
|
||||
|
||||
return NextResponse.rewrite(new URL(`/showcase/${directory}${targetPath}`, request.url));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Optimization: Only run middleware for legacy asset paths to minimize overhead
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/wp-content/:path*',
|
||||
'/wp-includes/:path*',
|
||||
'/assets/:path*',
|
||||
'/case-studies/:path*',
|
||||
'/work/:path*',
|
||||
'/blog/:path*',
|
||||
],
|
||||
};
|
||||
@@ -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 */
|
||||
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||
|
||||
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
||||
- 'traefik.http.routers.${PROJECT_NAME}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/api/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathPrefix("/assets") || PathPrefix("/wp-content") || PathPrefix("/wp-includes") || PathPrefix("/showcase") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
|
||||
- 'traefik.http.routers.${PROJECT_NAME}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/api/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathPrefix("/assets") || PathPrefix("/wp-content") || PathPrefix("/wp-includes") || PathPrefix("/showcase") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*") || PathRegexp(".*\\.(png|jpg|jpeg|webp|svg|ico|pdf|mp4|webm|woff2)$"))'
|
||||
- "traefik.http.routers.${PROJECT_NAME}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-public.tls=${TRAEFIK_TLS:-false}"
|
||||
|
||||
Reference in New Issue
Block a user