Compare commits

..

12 Commits

Author SHA1 Message Date
c4bc10ef76 fix: restore missing SVG animations in HeroIllustration
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Failing after 17s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-21 01:56:09 +01:00
e95f7c6dd2 fix: restore missing translations on 404 page 2026-02-21 00:38:49 +01:00
17a91e48e6 perf: pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 8m19s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Smoke Test (push) Successful in 54s
Build & Deploy / ⚡ Lighthouse (push) Successful in 8m38s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 23:42:33 +01:00
4d0a94d288 perf: pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 9m1s
Build & Deploy / 🏗️ Build (push) Successful in 12m31s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / ⚡ Lighthouse (push) Successful in 2m50s
Build & Deploy / 🧪 Smoke Test (push) Successful in 3m34s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 21:24:12 +01:00
3568c13941 perf: pipeline 2026-02-20 19:06:19 +01:00
d538d7b9ec fix(blog): ensure target environment vars are parsed for accurate strict filtering in prod, and integrate face detection gravity for blog thumbnails
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-02-20 18:54:09 +01:00
8c08b552cf fix: svg stroke width 2026-02-20 18:48:10 +01:00
1dd74a3861 ci: fix pipeline cache corruption and secrets warning
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-20 16:20:53 +01:00
8d77ca45f7 feat(blog): implement scheduled and draft posts filtering and preview UI
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m59s
Build & Deploy / 🏗️ Build (push) Failing after 34s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 15:41:07 +01:00
c646815a3a chore(analytics): completely scrub NEXT_PUBLIC prefix from umami website id across codebase and docs
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m14s
Build & Deploy / 🧪 QA (push) Successful in 3m20s
Build & Deploy / 🧪 Smoke Test (push) Failing after 49s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 15:29:50 +01:00
23bf327670 fix(analytics): relay umami events via secure nextjs proxy route handler
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 5m14s
Build & Deploy / 🧪 Smoke Test (push) Successful in 51s
Build & Deploy / 🏗️ Build (push) Successful in 4m21s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / ⚡ Lighthouse (push) Failing after 2m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 15:18:20 +01:00
c77f99ef37 feat(blog): johannes image
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-20 14:56:06 +01:00
25 changed files with 452 additions and 238 deletions

View File

@@ -10,17 +10,31 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Configure Private Registry
run: |
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"

View File

@@ -154,14 +154,26 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -206,8 +218,8 @@ jobs:
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache-v2
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache-v2,mode=max
secrets: |
"NPM_TOKEN=${{ secrets.REGISTRY_PASS }}"
@@ -386,14 +398,26 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -418,14 +442,26 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc

View File

@@ -8,7 +8,6 @@ ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ARG UMAMI_WEBSITE_ID
ARG UMAMI_API_ENDPOINT
ARG NPM_TOKEN
# Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
@@ -25,7 +24,7 @@ COPY pnpm-lock.yaml package.json .npmrc* ./
# Configure private registry and install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
pnpm install --frozen-lockfile && \

View File

@@ -5,6 +5,7 @@ import { MDXRemote } from 'next-mdx-remote/rsc';
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
@@ -69,13 +70,24 @@ export default async function BlogPost({ params }: BlogPostProps) {
category={post.frontmatter.category}
readingTime={getReadingTime(post.content)}
/>
{(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && (
<div className="bg-orange-500 text-white text-center py-2 px-4 font-bold text-sm tracking-wider uppercase relative z-50">
Preview (Not visible in production)
</div>
)}
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
<div
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
/>
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
<Image
src={`${post.frontmatter.featuredImage}?gravity=obj:face`}
alt={post.frontmatter.title}
fill
priority
className="object-cover"
sizes="100vw"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
{/* Title overlay on image */}

View File

@@ -62,7 +62,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<Image
src={featuredPost.frontmatter.featuredImage}
src={`${featuredPost.frontmatter.featuredImage}?gravity=obj:face`}
alt={featuredPost.frontmatter.title}
fill
className="absolute inset-0 w-full h-full object-cover scale-105 animate-slow-zoom opacity-40 md:opacity-60"
@@ -75,9 +75,16 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-6">
{t('featuredPost')}
</Badge>
<div className="flex flex-wrap items-center gap-3 mb-4 md:mb-6">
<Badge variant="saturated">{t('featuredPost')}</Badge>
{featuredPost &&
(new Date(featuredPost.frontmatter.date) > new Date() ||
featuredPost.frontmatter.public === false) && (
<Badge variant="accent" className="bg-orange-500 text-white border-none">
Preview
</Badge>
)}
</div>
{featuredPost && (
<>
<Heading level={1} className="text-white mb-4 md:mb-8">
@@ -153,7 +160,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{post.frontmatter.featuredImage && (
<div className="relative h-48 md:h-72 overflow-hidden">
<Image
src={post.frontmatter.featuredImage}
src={`${post.frontmatter.featuredImage}?gravity=obj:face`}
alt={post.frontmatter.title}
fill
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
@@ -168,6 +175,15 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{post.frontmatter.category}
</Badge>
)}
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<Badge
variant="accent"
className="absolute top-3 right-3 md:top-6 md:right-6 shadow-lg bg-orange-500 text-white border-none"
>
Preview
</Badge>
)}
</div>
)}
<div className="p-5 md:p-10 flex flex-col flex-1">

View File

@@ -81,7 +81,16 @@ export default async function Layout(props: {
}
// Pick only the namespaces required by client components to reduce the hydration payload size
const clientKeys = ['Footer', 'Navigation', 'Contact', 'Products', 'Team', 'Home'];
const clientKeys = [
'Footer',
'Navigation',
'Contact',
'Products',
'Team',
'Home',
'Error',
'StandardPage',
];
const clientMessages: Record<string, any> = {};
for (const key of clientKeys) {
if (messages[key]) {

View File

@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerAppServices } from '@/lib/services/create-services.server';
import { config } from '@/lib/config';
/**
* Smart Proxy for Umami Analytics.
*
* This Route Handler receives tracking events from the browser,
* injects the secret UMAMI_WEBSITE_ID, and forwards them to the
* internal Umami API endpoint.
*
* This ensures:
* 1. The Website ID is NOT leaked to the client bundle.
* 2. The Umami API endpoint is hidden behind our domain.
* 3. We have full control over the tracking data.
*/
export async function POST(request: NextRequest) {
const services = getServerAppServices();
const logger = services.logger.child({ component: 'umami-smart-proxy' });
try {
const body = await request.json();
const { type, payload } = body;
// Inject the secret websiteId from server config
const websiteId = config.analytics.umami.websiteId;
if (!websiteId) {
logger.warn('Umami tracking received but no Website ID configured on server');
return NextResponse.json({ status: 'ignored' }, { status: 200 });
}
// Prepare the enhanced payload with the secret ID
const enhancedPayload = {
...payload,
website: websiteId,
};
const umamiEndpoint = config.analytics.umami.apiEndpoint;
// Log the event (internal only)
logger.debug('Forwarding analytics event', {
type,
url: payload.url,
website: websiteId.slice(0, 8) + '...',
});
const response = await fetch(`${umamiEndpoint}/api/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': request.headers.get('user-agent') || 'KLZ-Smart-Proxy',
'X-Forwarded-For': request.headers.get('x-forwarded-for') || '',
},
body: JSON.stringify({ type, payload: enhancedPayload }),
});
if (!response.ok) {
const errorText = await response.text();
logger.error('Umami API responded with error', {
status: response.status,
error: errorText.slice(0, 100),
});
return new NextResponse(errorText, { status: response.status });
}
return NextResponse.json({ status: 'ok' });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
// Console error to ensure it appears in logs even if logger fails
console.error('CRITICAL PROXY ERROR:', {
message: errorMessage,
stack: errorStack,
endpoint: config.analytics.umami.apiEndpoint,
});
logger.error('Failed to proxy analytics request', {
error: errorMessage,
stack: errorStack,
});
return NextResponse.json(
{
error: 'Internal Server Error',
details: errorMessage, // Expose error for debugging
endpoint: config.analytics.umami.apiEndpoint ? 'configured' : 'missing',
},
{ status: 500 },
);
}
}

View File

@@ -136,18 +136,14 @@ function AddToCartButton({ product, quantity = 1 }) {
product_category: product.category,
price: product.price,
quantity: quantity,
cart_total: 150.00, // Current cart total
cart_total: 150.0, // Current cart total
});
// Actual add to cart logic
// addToCart(product, quantity);
};
return (
<button onClick={handleAddToCart}>
Add to Cart
</button>
);
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
```
@@ -171,7 +167,7 @@ function CheckoutComplete({ order }) {
transaction_tax: order.tax,
transaction_shipping: order.shipping,
product_count: order.items.length,
products: order.items.map(item => ({
products: order.items.map((item) => ({
product_id: item.product.id,
product_name: item.product.name,
quantity: item.quantity,
@@ -198,27 +194,21 @@ function WishlistButton({ product }) {
const toggleWishlist = () => {
const newState = !isInWishlist;
trackEvent(
newState
? AnalyticsEvents.PRODUCT_WISHLIST_ADD
: AnalyticsEvents.PRODUCT_WISHLIST_REMOVE,
newState ? AnalyticsEvents.PRODUCT_WISHLIST_ADD : AnalyticsEvents.PRODUCT_WISHLIST_REMOVE,
{
product_id: product.id,
product_name: product.name,
product_category: product.category,
}
},
);
setIsInWishlist(newState);
// Update wishlist in backend
};
return (
<button onClick={toggleWishlist}>
{isInWishlist ? '❤️' : '🤍'}
</button>
);
return <button onClick={toggleWishlist}>{isInWishlist ? '❤️' : '🤍'}</button>;
}
```
@@ -268,7 +258,7 @@ function ContactForm() {
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData(prev => ({
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
@@ -310,9 +300,7 @@ function NewsletterSignup() {
return (
<div>
<input placeholder="Enter email" />
<button onClick={() => handleSubscribe('user@example.com')}>
Subscribe
</button>
<button onClick={() => handleSubscribe('user@example.com')}>Subscribe</button>
</div>
);
}
@@ -396,10 +384,12 @@ function LoginForm() {
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleLogin('user@example.com', 'password');
}}>
<form
onSubmit={(e) => {
e.preventDefault();
handleLogin('user@example.com', 'password');
}}
>
{/* Form fields */}
<button type="submit">Login</button>
</form>
@@ -418,11 +408,7 @@ import { AnalyticsEvents } from '@/components/analytics/analytics-events';
function SignupForm() {
const { trackEvent } = useAnalytics();
const handleSignup = (userData: {
email: string;
name: string;
company?: string;
}) => {
const handleSignup = (userData: { email: string; name: string; company?: string }) => {
trackEvent(AnalyticsEvents.USER_SIGNUP, {
user_email: userData.email,
user_name: userData.name,
@@ -436,14 +422,16 @@ function SignupForm() {
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSignup({
email: 'user@example.com',
name: 'John Doe',
company: 'ACME Corp',
});
}}>
<form
onSubmit={(e) => {
e.preventDefault();
handleSignup({
email: 'user@example.com',
name: 'John Doe',
company: 'ACME Corp',
});
}}
>
{/* Form fields */}
<button type="submit">Sign Up</button>
</form>
@@ -483,7 +471,7 @@ function SearchBar() {
return (
<div>
<input
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
@@ -549,7 +537,7 @@ function ProductFilters() {
<option value="cables">Cables</option>
<option value="connectors">Connectors</option>
</select>
<button onClick={handleClearFilters}>Clear Filters</button>
</div>
);
@@ -631,11 +619,7 @@ function VideoPlayer({ videoId, videoTitle }) {
};
return (
<video
onPlay={handlePlay}
onPause={handlePause}
onEnded={handleComplete}
>
<video onPlay={handlePlay} onPause={handlePause} onEnded={handleComplete}>
<source src="/video.mp4" type="video/mp4" />
</video>
);
@@ -665,11 +649,7 @@ function DownloadButton({ fileName, fileType, fileSize }) {
// window.location.href = `/downloads/${fileName}`;
};
return (
<button onClick={handleDownload}>
Download {fileName}
</button>
);
return <button onClick={handleDownload}>Download {fileName}</button>;
}
```
@@ -700,7 +680,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const { trackEvent } = useAnalytics();
trackEvent(AnalyticsEvents.ERROR, {
error_message: error.message,
error_stack: error.stack,
@@ -742,14 +722,14 @@ function ApiClient() {
const fetchData = async (endpoint: string) => {
try {
const response = await fetch(endpoint);
if (!response.ok) {
trackEvent(AnalyticsEvents.API_ERROR, {
endpoint: endpoint,
status_code: response.status,
error_message: response.statusText,
});
throw new Error(`HTTP ${response.status}`);
}
@@ -765,7 +745,7 @@ function ApiClient() {
error_message: error.message,
error_type: error.name,
});
throw error;
}
};
@@ -963,15 +943,9 @@ function CableProductPage({ cable }) {
return (
<div>
<h1>{cable.name}</h1>
<button onClick={handleTechnicalSpecDownload}>
Download Technical Specs
</button>
<button onClick={handleRequestQuote}>
Request Quote
</button>
<button onClick={handleBrochureDownload}>
Download Brochure
</button>
<button onClick={handleTechnicalSpecDownload}>Download Technical Specs</button>
<button onClick={handleRequestQuote}>Request Quote</button>
<button onClick={handleBrochureDownload}>Download Brochure</button>
</div>
);
}
@@ -1010,12 +984,8 @@ function WindFarmProjectPage({ project }) {
return (
<div>
<h1>{project.name}</h1>
<button onClick={handleProjectInquiry}>
Request Project Consultation
</button>
<button onClick={handleCableCalculation}>
Calculate Cable Requirements
</button>
<button onClick={handleProjectInquiry}>Request Project Consultation</button>
<button onClick={handleCableCalculation}>Calculate Cable Requirements</button>
</div>
);
}
@@ -1066,7 +1036,7 @@ test('tracks button click', () => {
// [Umami] Tracked pageview: /products/123
// To test without sending data to Umami:
// 1. Remove NEXT_PUBLIC_UMAMI_WEBSITE_ID from .env
// 1. Remove UMAMI_WEBSITE_ID from .env
// 2. Or set it to an empty string
// 3. Check console logs to verify events are being tracked
```
@@ -1169,7 +1139,9 @@ function WebVitalsTracker() {
}
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input-delay', 'layout-shift'] });
observer.observe({
entryTypes: ['largest-contentful-paint', 'first-input-delay', 'layout-shift'],
});
}
}, []);
@@ -1194,6 +1166,7 @@ This examples file demonstrates how to implement comprehensive analytics trackin
-**Business-specific events** (KLZ Cables, wind farms)
Remember to:
1. Use the `useAnalytics` hook for client-side tracking
2. Import events from `AnalyticsEvents` for consistency
3. Include relevant context in your events

View File

@@ -2,7 +2,7 @@
## Setup Checklist
- [ ] Add `NEXT_PUBLIC_UMAMI_WEBSITE_ID` to `.env` file
- [ ] Add `UMAMI_WEBSITE_ID` to `.env` file
- [ ] Verify `UmamiScript` is in your layout
- [ ] Verify `AnalyticsProvider` is in your layout
- [ ] Test in development mode
@@ -12,7 +12,7 @@
```bash
# Required
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
# Optional (defaults to https://analytics.infra.mintel.me/script.js)
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
@@ -86,16 +86,16 @@ function ProductCard({ product }) {
## Common Events
| Event | When to Use | Example Properties |
|-------|-------------|-------------------|
| `pageview` | Page loads | `{ url: '/products/123' }` |
| `button_click` | Button clicked | `{ button_id: 'cta', page: 'homepage' }` |
| `form_submit` | Form submitted | `{ form_id: 'contact', form_name: 'Contact Us' }` |
| `product_view` | Product page viewed | `{ product_id: '123', price: 99.99 }` |
| `product_add_to_cart` | Add to cart | `{ product_id: '123', quantity: 1 }` |
| `search` | Search performed | `{ search_query: 'cable', results: 42 }` |
| `user_login` | User logged in | `{ user_email: 'user@example.com' }` |
| `error` | Error occurred | `{ error_message: 'Something went wrong' }` |
| Event | When to Use | Example Properties |
| --------------------- | ------------------- | ------------------------------------------------- |
| `pageview` | Page loads | `{ url: '/products/123' }` |
| `button_click` | Button clicked | `{ button_id: 'cta', page: 'homepage' }` |
| `form_submit` | Form submitted | `{ form_id: 'contact', form_name: 'Contact Us' }` |
| `product_view` | Product page viewed | `{ product_id: '123', price: 99.99 }` |
| `product_add_to_cart` | Add to cart | `{ product_id: '123', quantity: 1 }` |
| `search` | Search performed | `{ search_query: 'cable', results: 42 }` |
| `user_login` | User logged in | `{ user_email: 'user@example.com' }` |
| `error` | Error occurred | `{ error_message: 'Something went wrong' }` |
## Testing
@@ -112,7 +112,7 @@ In development, you'll see console logs:
```bash
# .env.local
# NEXT_PUBLIC_UMAMI_WEBSITE_ID=
# UMAMI_WEBSITE_ID=
```
## Troubleshooting
@@ -120,8 +120,9 @@ In development, you'll see console logs:
### Analytics Not Working?
1. **Check environment variables:**
```bash
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID
echo $UMAMI_WEBSITE_ID
```
2. **Verify script is loading:**
@@ -136,12 +137,12 @@ In development, you'll see console logs:
### Common Issues
| Issue | Solution |
|-------|----------|
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify `useAnalytics` hook is used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
| Issue | Solution |
| ------------------- | ----------------------------------- |
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify `useAnalytics` hook is used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
## Performance Tips

View File

@@ -20,7 +20,7 @@ Add these to your `.env` file:
```bash
# Required: Your Umami website ID
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
# Optional: Custom Umami script URL (defaults to https://analytics.infra.mintel.me/script.js)
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
@@ -32,7 +32,7 @@ The `docker-compose.yml` already includes the environment variables:
```yaml
environment:
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
- NEXT_PUBLIC_UMAMI_SCRIPT_URL=${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.infra.mintel.me/script.js}
```
@@ -75,11 +75,7 @@ function ProductCard({ product }) {
});
};
return (
<button onClick={handleAddToCart}>
Add to Cart
</button>
);
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
```
@@ -96,7 +92,7 @@ function CustomNavigation() {
const navigateToCustomPage = () => {
// Track a custom pageview
trackPageview('/custom-path?param=value');
// Then perform navigation
window.location.href = '/custom-path?param=value';
};
@@ -277,11 +273,7 @@ function ErrorBoundary({ children }) {
});
};
return (
<ErrorBoundary onError={handleError}>
{children}
</ErrorBoundary>
);
return <ErrorBoundary onError={handleError}>{children}</ErrorBoundary>;
}
```
@@ -289,20 +281,20 @@ function ErrorBoundary({ children }) {
### Common Events
| Event Name | Description | Example Properties |
|------------|-------------|-------------------|
| `pageview` | Page view | `{ url: '/products/123' }` |
| `button_click` | Button clicked | `{ button_id: 'cta-primary', page: 'homepage' }` |
| `link_click` | Link clicked | `{ link_url: '/products', link_text: 'View Products' }` |
| `form_submit` | Form submitted | `{ form_id: 'contact-form', form_name: 'Contact Us' }` |
| `product_view` | Product page viewed | `{ product_id: '123', product_name: 'Cable', price: 99.99 }` |
| `product_add_to_cart` | Product added to cart | `{ product_id: '123', quantity: 1 }` |
| `product_purchase` | Product purchased | `{ product_id: '123', transaction_id: 'TXN-123' }` |
| `search` | Search performed | `{ search_query: 'cable', search_results_count: 42 }` |
| `filter_apply` | Filter applied | `{ filter_type: 'category', filter_value: 'high-voltage' }` |
| `user_login` | User logged in | `{ user_email: 'user@example.com' }` |
| `user_signup` | User signed up | `{ user_email: 'user@example.com' }` |
| `error` | Error occurred | `{ error_message: 'Something went wrong' }` |
| Event Name | Description | Example Properties |
| --------------------- | --------------------- | ------------------------------------------------------------ |
| `pageview` | Page view | `{ url: '/products/123' }` |
| `button_click` | Button clicked | `{ button_id: 'cta-primary', page: 'homepage' }` |
| `link_click` | Link clicked | `{ link_url: '/products', link_text: 'View Products' }` |
| `form_submit` | Form submitted | `{ form_id: 'contact-form', form_name: 'Contact Us' }` |
| `product_view` | Product page viewed | `{ product_id: '123', product_name: 'Cable', price: 99.99 }` |
| `product_add_to_cart` | Product added to cart | `{ product_id: '123', quantity: 1 }` |
| `product_purchase` | Product purchased | `{ product_id: '123', transaction_id: 'TXN-123' }` |
| `search` | Search performed | `{ search_query: 'cable', search_results_count: 42 }` |
| `filter_apply` | Filter applied | `{ filter_type: 'category', filter_value: 'high-voltage' }` |
| `user_login` | User logged in | `{ user_email: 'user@example.com' }` |
| `user_signup` | User signed up | `{ user_email: 'user@example.com' }` |
| `error` | Error occurred | `{ error_message: 'Something went wrong' }` |
### Custom Events
@@ -385,8 +377,9 @@ The analytics system includes development mode logging:
### Analytics Not Working
1. **Check environment variables:**
```bash
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID
echo $UMAMI_WEBSITE_ID
```
2. **Verify the script is loading:**
@@ -405,11 +398,11 @@ In development mode, you'll see console logs for all tracked events. This helps
### Disabling Analytics
To disable analytics (e.g., for local development), simply remove the `NEXT_PUBLIC_UMAMI_WEBSITE_ID` environment variable:
To disable analytics (e.g., for local development), simply remove the `UMAMI_WEBSITE_ID` environment variable:
```bash
# .env.local (not committed to git)
# NEXT_PUBLIC_UMAMI_WEBSITE_ID=
# UMAMI_WEBSITE_ID=
```
## Performance
@@ -438,6 +431,7 @@ The analytics implementation is optimized for performance:
## Support
For issues or questions about the analytics implementation, check:
1. This README for usage examples
2. The component source code for implementation details
3. The Umami documentation for platform-specific questions

View File

@@ -16,6 +16,7 @@ The project already had a solid foundation:
## What Was Enhanced
### 1. **Enhanced UmamiScript** (`components/analytics/UmamiScript.tsx`)
- ✅ Added TypeScript props interface for customization
- ✅ Added JSDoc documentation with usage examples
- ✅ Added error handling for script loading failures
@@ -23,11 +24,13 @@ The project already had a solid foundation:
- ✅ Improved type safety and comments
### 2. **Enhanced AnalyticsProvider** (`components/analytics/AnalyticsProvider.tsx`)
- ✅ Added comprehensive JSDoc documentation
- ✅ Added development mode logging
- ✅ Improved code comments
### 3. **New Custom Hook** (`components/analytics/useAnalytics.ts`)
- ✅ Type-safe `useAnalytics` hook for easy event tracking
-`trackEvent()` method for custom events
-`trackPageview()` method for manual pageview tracking
@@ -35,12 +38,14 @@ The project already had a solid foundation:
- ✅ Development mode logging
### 4. **Event Definitions** (`components/analytics/analytics-events.ts`)
- ✅ Centralized event constants for consistency
- ✅ Type-safe event names
- ✅ Helper functions for common event properties
- ✅ 30+ predefined events for various use cases
### 5. **Comprehensive Documentation**
-**README.md** - Full documentation with setup, usage, and best practices
-**EXAMPLES.md** - 20+ practical examples for different scenarios
-**QUICK_REFERENCE.md** - Quick start guide and troubleshooting
@@ -63,12 +68,14 @@ components/analytics/
## Key Features
### 🚀 Modern Implementation
- Uses Next.js `Script` component (not old-school `<script>` tags)
- TypeScript for type safety
- React hooks for clean API
- Environment variable configuration
### 📊 Comprehensive Tracking
- Automatic pageview tracking on route changes
- Custom event tracking with properties
- E-commerce events (products, cart, purchases)
@@ -77,6 +84,7 @@ components/analytics/
- Error and performance tracking
### 🎯 Developer Experience
- Type-safe event tracking
- Centralized event definitions
- Development mode logging
@@ -84,6 +92,7 @@ components/analytics/
- 20+ practical examples
### 🔒 Privacy & Performance
- No PII tracking by default
- Script loads after page is interactive
- Minimal performance impact
@@ -95,7 +104,7 @@ The project is already configured in `docker-compose.yml`:
```yaml
environment:
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
- NEXT_PUBLIC_UMAMI_SCRIPT_URL=${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.infra.mintel.me/script.js}
```
@@ -104,7 +113,7 @@ environment:
Add to your `.env` file:
```bash
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
```
## Usage Examples
@@ -188,7 +197,7 @@ In development, you'll see console logs:
```bash
# .env.local
# NEXT_PUBLIC_UMAMI_WEBSITE_ID=
# UMAMI_WEBSITE_ID=
```
## Troubleshooting
@@ -196,8 +205,9 @@ In development, you'll see console logs:
### Analytics Not Working?
1. **Check environment variables:**
```bash
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID
echo $UMAMI_WEBSITE_ID
```
2. **Verify script is loading:**
@@ -212,12 +222,12 @@ In development, you'll see console logs:
### Common Issues
| Issue | Solution |
|-------|----------|
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify `useAnalytics` hook is used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
| Issue | Solution |
| ------------------- | ----------------------------------- |
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify `useAnalytics` hook is used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
## Performance Tips
@@ -239,13 +249,13 @@ In development, you'll see console logs:
1. ✅ **Setup complete** - All files are in place
2. ✅ **Documentation complete** - README, EXAMPLES, QUICK_REFERENCE
3. ✅ **Code enhanced** - Better TypeScript, error handling, docs
4. 📝 **Add to .env** - Set `NEXT_PUBLIC_UMAMI_WEBSITE_ID`
4. 📝 **Add to .env** - Set `UMAMI_WEBSITE_ID`
5. 🧪 **Test in development** - Verify events are tracked
6. 🚀 **Deploy** - Analytics will work in production
## Quick Start Checklist
- [ ] Add `NEXT_PUBLIC_UMAMI_WEBSITE_ID` to `.env` file
- [ ] Add `UMAMI_WEBSITE_ID` to `.env` file
- [ ] Verify `UmamiScript` is in `app/[locale]/layout.tsx`
- [ ] Verify `AnalyticsProvider` is in `app/[locale]/layout.tsx`
- [ ] Test in development mode (check console logs)

View File

@@ -125,8 +125,9 @@ export default function HeroIllustration() {
}, []);
const viewBox = isMobile ? '400 0 1000 1100' : '-400 -200 1800 1100';
const scale = isMobile ? 1.44 : 1;
const opacity = isMobile ? 0.6 : 0.85;
// Increase scale slightly and opacity significantly on mobile to fix the "thin" appearance
const scale = isMobile ? 1.6 : 1;
const opacity = isMobile ? 0.9 : 0.85;
return (
<div className="absolute inset-0 z-0 overflow-visible bg-primary w-full h-full">
@@ -154,15 +155,15 @@ export default function HeroIllustration() {
<stop offset="70%" stopColor="white" stopOpacity="0.4" />
<stop offset="100%" stopColor="white" stopOpacity="0" />
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur" />
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="soft-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1" result="blur" />
<filter id="soft-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
@@ -215,10 +216,10 @@ export default function HeroIllustration() {
</g>
{/* ANIMATED ENERGY FLOW */}
<g>
<g filter="url(#glow)">
{POWER_LINES.map((line, i) => {
// Only animate a small subset of lines to reduce main-thread work significantly
if (i % 5 !== 0) return null;
// Only animate a subset of lines to reduce main-thread work
if (i % 2 !== 0) return null;
const from = gridToScreen(line.from.col, line.from.row);
const to = gridToScreen(line.to.col, line.to.row);
const length = Math.sqrt(Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2));
@@ -232,12 +233,16 @@ export default function HeroIllustration() {
stroke="url(#energy-pulse)"
strokeWidth="3"
strokeLinecap="round"
style={{
strokeDasharray: `${length * 0.2} ${length * 0.8}`,
strokeDashoffset: length,
animation: `flow ${1.5 + (i % 3) * 0.5}s linear infinite`,
}}
/>
strokeDasharray={`${length * 0.2} ${length * 0.8}`}
>
<animate
attributeName="stroke-dashoffset"
from={length}
to={0}
dur={`${1.5 + (i % 3) * 0.5}s`}
repeatCount="indefinite"
/>
</line>
);
})}
</g>
@@ -263,13 +268,14 @@ export default function HeroIllustration() {
strokeWidth="1"
strokeOpacity="0.3"
/>
<circle
r="3"
fill="#82ed20"
fillOpacity="0.3"
filter="url(#soft-glow)"
style={{ animation: 'solar-pulse 2s ease-in-out infinite' }}
/>
<circle r="3" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate
attributeName="fillOpacity"
values="0.2;0.5;0.2"
dur="2s"
repeatCount="indefinite"
/>
</circle>
</g>
);
})}
@@ -289,26 +295,28 @@ export default function HeroIllustration() {
strokeOpacity="0.3"
/>
<g transform="translate(0, -60)">
<g
style={{
transformOrigin: '0px 0px',
animation: `spin-slow ${3 + i}s linear infinite`,
}}
>
{[0, 120, 240].map((angle, j) => (
<line
key={`blade-${i}-${j}`}
x1="0"
y1="0"
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeOpacity="0.4"
transform={`rotate(${angle})`}
{[0, 120, 240].map((angle, j) => (
<line
key={`blade-${i}-${j}`}
x1="0"
y1="0"
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeOpacity="0.4"
transform={`rotate(${angle})`}
>
<animateTransform
attributeName="transform"
type="rotate"
from={`${angle} 0 0`}
to={`${angle + 360} 0 0`}
dur={`${3 + i}s`}
repeatCount="indefinite"
/>
))}
</g>
</line>
))}
</g>
</g>
);

View File

@@ -5,6 +5,7 @@ featuredImage: /uploads/2026/01/1767353529807.jpg
locale: de
category: Kabel Technologie
excerpt: 'KLZ Cables startet mit einer starken Verstärkung ins neue Jahr: Johannes Gleich übernimmt die Rolle des Senior Key Account Managers. Erfahren Sie mehr über unseren neuen Experten für Infrastruktur und Energieversorger.'
public: false
---
# Herzlich willkommen bei KLZ: Johannes Gleich startet als Senior Key Account Manager durch

View File

@@ -5,6 +5,7 @@ featuredImage: /uploads/2026/01/1767353529807.jpg
locale: en
category: Cable Technology
excerpt: 'KLZ Cables kicks off the new year with a strong addition: Johannes Gleich takes on the role of Senior Key Account Manager. Learn more about our new expert for infrastructure and energy suppliers.'
public: false
---
# Welcome to KLZ: Johannes Gleich starts as Senior Key Account Manager

View File

@@ -39,7 +39,7 @@ NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id
UMAMI_WEBSITE_ID=your-umami-website-id
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
# Error Tracking (GlitchTip/Sentry)

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { mapSlugToFileSlug } from './slugs';
import { config } from '@/lib/config';
export interface PostFrontmatter {
title: string;
@@ -10,6 +11,7 @@ export interface PostFrontmatter {
featuredImage?: string | null;
category?: string;
locale: string;
public?: boolean;
}
export interface PostMdx {
@@ -18,6 +20,17 @@ export interface PostMdx {
content: string;
}
export function isPostVisible(post: { frontmatter: { date: string; public?: boolean } }) {
// If explicitly marked as not public, hide in production
if (post.frontmatter.public === false && config.isProduction) {
return false;
}
const postDate = new Date(post.frontmatter.date);
const now = new Date();
return !(postDate > now && config.isProduction);
}
export async function getPostBySlug(slug: string, locale: string): Promise<PostMdx | null> {
// Map translated slug to file slug
const fileSlug = await mapSlugToFileSlug(slug, locale);
@@ -31,11 +44,17 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
return {
const postInfo = {
slug: fileSlug,
frontmatter: data as PostFrontmatter,
content,
};
if (!isPostVisible(postInfo)) {
return null;
}
return postInfo;
}
export async function getAllPosts(locale: string): Promise<PostMdx[]> {
@@ -55,6 +74,7 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
content,
};
})
.filter(isPostVisible)
.sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
@@ -78,6 +98,7 @@ export async function getAllPostsMetadata(locale: string): Promise<Partial<PostM
frontmatter: data as PostFrontmatter,
};
})
.filter(isPostVisible)
.sort(
(a, b) =>
new Date(b.frontmatter.date as string).getTime() -

View File

@@ -35,9 +35,9 @@ function createConfig() {
analytics: {
umami: {
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
websiteId: env.UMAMI_WEBSITE_ID,
apiEndpoint: env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me',
enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID),
enabled: typeof window !== 'undefined' || Boolean(env.UMAMI_WEBSITE_ID),
},
},

View File

@@ -20,6 +20,8 @@ const booleanSchema = z.preprocess((val) => {
const envExtension = {
// Project specific overrides or additions
AUTH_COOKIE_NAME: z.string().default('klz_gatekeeper_session'),
TARGET: z.string().optional(),
NEXT_PUBLIC_TARGET: z.string().optional(),
// Gatekeeper specifics not in base
GATEKEEPER_URL: z.string().url().default('http://gatekeeper:3000'),
@@ -31,7 +33,7 @@ const envExtension = {
INFRA_DIRECTUS_TOKEN: z.string().optional(),
// Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
UMAMI_WEBSITE_ID: z.string().optional(),
UMAMI_API_ENDPOINT: z.string().optional(),
// Mail Configuration

View File

@@ -23,11 +23,28 @@ export default function imgproxyLoader({
return src;
}
// Check if src contains custom gravity query parameter
let gravity = 'sm'; // Use smart gravity (content-aware) by default
let cleanSrc = src;
try {
// Dummy base needed for relative URLs
const url = new URL(src, 'http://localhost');
const customGravity = url.searchParams.get('gravity');
if (customGravity) {
gravity = customGravity;
url.searchParams.delete('gravity');
cleanSrc = src.startsWith('http') ? url.href : url.pathname + url.search;
}
} catch (e) {
// Fallback if parsing fails
}
// We use the width provided by Next.js for responsive images
// Height is set to 0 to maintain aspect ratio
return getImgproxyUrl(src, {
return getImgproxyUrl(cleanSrc, {
width,
resizing_type: 'fit',
gravity: 'sm', // Use smart gravity (content-aware) instead of face detection (requires ML)
gravity,
});
}

View File

@@ -30,6 +30,7 @@ export interface AnalyticsService {
```
**Key Features:**
- Type-safe event properties
- Consistent API across implementations
- Well-documented with JSDoc comments
@@ -39,6 +40,7 @@ export interface AnalyticsService {
Implements the `AnalyticsService` interface for Umami analytics.
**Features:**
- Type-safe event tracking
- Automatic pageview tracking
- Browser environment detection
@@ -46,6 +48,7 @@ Implements the `AnalyticsService` interface for Umami analytics.
- Comprehensive JSDoc documentation
**Usage:**
```typescript
import { UmamiAnalyticsService } from '@/lib/services/analytics/umami-analytics-service';
@@ -59,12 +62,14 @@ service.trackPageview('/products/123');
A no-op implementation used as a fallback when analytics are disabled.
**Features:**
- Maintains the same API as other services
- Safe to call even when analytics are disabled
- No performance impact
- Comprehensive JSDoc documentation
**Usage:**
```typescript
import { NoopAnalyticsService } from '@/lib/services/analytics/noop-analytics-service';
@@ -79,7 +84,7 @@ The service layer automatically selects the appropriate implementation based on
```typescript
// In lib/services/create-services.ts
const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
const umamiEnabled = Boolean(process.env.UMAMI_WEBSITE_ID);
const analytics = umamiEnabled
? new UmamiAnalyticsService({ enabled: true })
@@ -91,7 +96,7 @@ const analytics = umamiEnabled
### Required for Umami
```bash
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
```
### Optional (defaults provided)
@@ -109,10 +114,12 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
Track a custom event with optional properties.
**Parameters:**
- `eventName` - The name of the event to track
- `props` - Optional event properties (metadata)
**Example:**
```typescript
service.track('product_add_to_cart', {
product_id: '123',
@@ -127,9 +134,11 @@ service.track('product_add_to_cart', {
Track a pageview.
**Parameters:**
- `url` - The URL to track (defaults to current location)
**Example:**
```typescript
// Track current page
service.trackPageview();
@@ -147,9 +156,11 @@ new UmamiAnalyticsService(options: UmamiAnalyticsServiceOptions)
```
**Options:**
- `enabled: boolean` - Whether analytics are enabled
**Example:**
```typescript
const service = new UmamiAnalyticsService({ enabled: true });
```
@@ -159,10 +170,11 @@ const service = new UmamiAnalyticsService({ enabled: true });
#### Constructor
```typescript
new NoopAnalyticsService()
new NoopAnalyticsService();
```
**Example:**
```typescript
const service = new NoopAnalyticsService();
```
@@ -172,13 +184,11 @@ const service = new NoopAnalyticsService();
### AnalyticsEventProperties
```typescript
type AnalyticsEventProperties = Record<
string,
string | number | boolean | null | undefined
>;
type AnalyticsEventProperties = Record<string, string | number | boolean | null | undefined>;
```
**Example:**
```typescript
const properties: AnalyticsEventProperties = {
product_id: '123',
@@ -253,7 +263,7 @@ services.analytics.track('button_click', {
The service layer gracefully handles disabled analytics:
```typescript
// When NEXT_PUBLIC_UMAMI_WEBSITE_ID is not set:
// When UMAMI_WEBSITE_ID is not set:
// - NoopAnalyticsService is used
// - All calls are safe (no-op)
// - No errors are thrown
@@ -366,13 +376,13 @@ import { getAppServices } from '@/lib/services/create-services';
async function MyServerComponent() {
const services = getAppServices();
// Note: Analytics won't work in server components
// Use client components for analytics tracking
// But you can still access other services like cache
const data = await services.cache.get('key');
return <div>{data}</div>;
}
```
@@ -382,14 +392,16 @@ async function MyServerComponent() {
### Analytics Not Working
1. **Check environment variables:**
```bash
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID
echo $UMAMI_WEBSITE_ID
```
2. **Verify service selection:**
```typescript
import { getAppServices } from '@/lib/services/create-services';
const services = getAppServices();
console.log(services.analytics); // Should be UmamiAnalyticsService
```
@@ -401,12 +413,12 @@ async function MyServerComponent() {
### Common Issues
| Issue | Solution |
|-------|----------|
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify service is being used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
| Issue | Solution |
| ------------------- | ----------------------------------- |
| No data in Umami | Check website ID and script URL |
| Events not tracking | Verify service is being used |
| Script not loading | Check network connection, CORS |
| Wrong data | Verify event properties are correct |
## Related Files

View File

@@ -19,7 +19,7 @@ import type { AnalyticsEventProperties, AnalyticsService } from './analytics-ser
* @example
* ```typescript
* // Automatic fallback in create-services.ts
* const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
* const umamiEnabled = Boolean(process.env.UMAMI_WEBSITE_ID);
* const analytics = umamiEnabled
* ? new UmamiAnalyticsService({ enabled: true })
* : new NoopAnalyticsService(); // Fallback when no website ID

View File

@@ -55,7 +55,7 @@ let singleton: AppServices | undefined;
* @example
* ```typescript
* // Automatic service selection based on environment
* // If NEXT_PUBLIC_UMAMI_WEBSITE_ID is set:
* // If UMAMI_WEBSITE_ID is set:
* // services.analytics = UmamiAnalyticsService
* // If not set:
* // services.analytics = NoopAnalyticsService (safe no-op)

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -348,10 +348,6 @@ const nextConfig = {
}
return [
{
source: '/stats/:path*',
destination: `${umamiUrl}/:path*`,
},
{
source: '/cms/:path*',
destination: `${directusUrl}/:path*`,

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB