diff --git a/.env b/.env
index 7523ee9f..ab749cb0 100644
--- a/.env
+++ b/.env
@@ -1,8 +1,8 @@
# Application
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
-UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
+UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
LOG_LEVEL=info
diff --git a/.env.example b/.env.example
index f2fa2702..64642189 100644
--- a/.env.example
+++ b/.env.example
@@ -20,8 +20,8 @@ TARGET=development
# Analytics (Umami)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable analytics
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=
-NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
+UMAMI_WEBSITE_ID=
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# ────────────────────────────────────────────────────────────────────────────
# Error Tracking (GlitchTip/Sentry)
diff --git a/.env.production b/.env.production
index 0d41ad03..8e779c14 100644
--- a/.env.production
+++ b/.env.production
@@ -12,8 +12,8 @@ NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics (Umami)
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=
-NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
+UMAMI_WEBSITE_ID=
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking (GlitchTip/Sentry)
SENTRY_DSN=
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 7f379d8f..4e756abb 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -198,16 +198,16 @@ jobs:
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }}
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
- NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
+ UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
+ UMAMI_API_ENDPOINT: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
run: |
docker buildx build \
--pull \
--platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
- --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
- --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
+ --build-arg UMAMI_WEBSITE_ID="$UMAMI_WEBSITE_ID" \
+ --build-arg UMAMI_API_ENDPOINT="$UMAMI_API_ENDPOINT" \
--build-arg NEXT_PUBLIC_TARGET="$TARGET" \
--build-arg DIRECTUS_URL="$DIRECTUS_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
@@ -231,8 +231,8 @@ jobs:
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }}
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
- NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
+ UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
+ UMAMI_API_ENDPOINT: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN || (needs.prepare.outputs.target == 'production' && secrets.SENTRY_DSN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN)) }}
MAIL_HOST: ${{ secrets.MAIL_HOST || vars.MAIL_HOST || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_HOST || vars.MAIL_HOST) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_HOST || vars.STAGING_MAIL_HOST) || (secrets.TESTING_MAIL_HOST || vars.TESTING_MAIL_HOST) || (secrets.MAIL_HOST || vars.MAIL_HOST))) }}
MAIL_PORT: ${{ secrets.MAIL_PORT || vars.MAIL_PORT || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_PORT || vars.MAIL_PORT) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_PORT || vars.STAGING_MAIL_PORT) || (secrets.TESTING_MAIL_PORT || vars.TESTING_MAIL_PORT) || (secrets.MAIL_PORT || vars.MAIL_PORT))) }}
@@ -273,8 +273,8 @@ jobs:
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_TARGET=$TARGET
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
- NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
+ UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
+ UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
SENTRY_DSN=$SENTRY_DSN
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
MAIL_HOST=$MAIL_HOST
diff --git a/Dockerfile b/Dockerfile
index 9a1ede8a..34f68483 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,14 +25,18 @@ ENV NEXT_TELEMETRY_DISABLED=1
# Build-time environment variables for Next.js
# These are baked into the client bundle during build
ARG NEXT_PUBLIC_BASE_URL
+ARG NEXT_PUBLIC_BASE_URL
+ARG UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
+ARG UMAMI_API_ENDPOINT
+ARG UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
-ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
-ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
+ENV UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID:-$NEXT_PUBLIC_UMAMI_WEBSITE_ID}
+ENV UMAMI_API_ENDPOINT=${UMAMI_API_ENDPOINT:-${UMAMI_SCRIPT_URL:-$NEXT_PUBLIC_UMAMI_SCRIPT_URL}}
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
diff --git a/README.md b/README.md
index 093e10ba..f09b12fa 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,13 @@ A complete WordPress to Next.js static site migration for KLZ Cables, transformi
## 🚀 Quick Start
### Prerequisites
-- Node.js 18+
+
+- Node.js 18+
- npm or yarn
### Installation
-```bash
+
+````bash
# Install dependencies
npm install --legacy-peer-deps
@@ -42,11 +44,12 @@ npm run cms:logs
# Stop the CMS
npm run cms:stop
-```
+````
Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`.
### 🔄 Data & Migration
+
To sync data or migrate existing content:
```bash
@@ -61,6 +64,7 @@ npm run cms:migrate
```
### Environment Variables
+
```bash
# .env
SITE_URL=https://klz-cables.com
@@ -69,8 +73,8 @@ TURNSTILE_SITE_KEY=your_turnstile_key
TURNSTILE_SECRET_KEY=your_turnstile_secret
# Umami
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=your_umami_website_id
-NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
+UMAMI_WEBSITE_ID=your_umami_website_id
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# GlitchTip (Sentry compatible)
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
@@ -81,6 +85,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
## 📊 Project Overview
### Migration Statistics
+
- **Content Exported**: 141 items
- 18 pages (9 EN + 9 DE)
- 59 posts (29 EN + 30 DE)
@@ -91,6 +96,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
- **Translation Pairs**: 16
### Performance Benefits
+
- **Before**: Dynamic WordPress with database queries
- **After**: Static HTML with CDN delivery
- **Load Time**: <100ms (vs 500ms+)
@@ -99,6 +105,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
## 🏗️ Architecture
### Tech Stack
+
- **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript
- **Styling**: SCSS
@@ -109,6 +116,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
- **CAPTCHA**: Cloudflare Turnstile
### Project Structure
+
```
app/
├── layout.tsx # Root layout
@@ -163,6 +171,7 @@ scripts/
## 🎯 Features
### ✅ Implemented
+
- **Multi-language**: EN/DE with `/de/` prefix routing
- **Contact Forms**: Resend integration with validation
- **GDPR Compliance**: Cookie consent banner
@@ -175,12 +184,14 @@ scripts/
- **Asset Management**: WordPress → local path mapping
### 🔄 In Progress
+
- Analytics integration (consent-based)
- Turnstile CAPTCHA
- Build testing
- Deployment configuration
### 📝 Remaining
+
- Performance optimization
- Final QA testing
- Documentation updates
@@ -188,6 +199,7 @@ scripts/
## 📝 Content Management
### Data Export
+
```bash
# Export from WordPress
npm run data:export
@@ -203,6 +215,7 @@ npm run data:improve-mapping
```
### Adding New Content
+
1. Export new content from WordPress
2. Process the data
3. Rebuild the site
@@ -210,17 +223,20 @@ npm run data:improve-mapping
## 🎨 Design System
### Colors
+
- Primary: `#0066cc` (KLZ Blue)
- Secondary: `#00a896` (Teal)
- Text: `#1a1a1a`
- Background: `#f8f9fa`
### Typography
+
- Font: Inter
- Base: 16px
- Scale: 1.25 (Major Third)
### Layout
+
- Max width: 1200px
- Responsive grid
- Mobile-first
@@ -228,6 +244,7 @@ npm run data:improve-mapping
## 🔧 API Endpoints
### Contact Form
+
```
POST /api/contact
{
@@ -239,11 +256,13 @@ POST /api/contact
```
### Sitemap
+
```
GET /sitemap.xml
```
### Robots
+
```
GET /robots.txt
```
@@ -261,6 +280,7 @@ The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging`
**Workflow**: `.gitea/workflows/deploy.yml`
**Branch Deployments**:
+
- `main` branch: Deploys to production using `.env.prod`
- `staging` branch: Deploys to staging using `.env.staging`
@@ -268,12 +288,13 @@ The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging`
The CI/CD workflow supports `STAGING_`-prefixed secrets (e.g., `STAGING_NEXT_PUBLIC_BASE_URL`) to override default secrets when deploying the `staging` branch.
**Required Secrets** (configure in Gitea repository settings):
+
- `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
-- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
-- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
+- `UMAMI_WEBSITE_ID` - Analytics ID
+- `UMAMI_API_ENDPOINT` - Analytics API endpoint (formerly NEXT_PUBLIC_UMAMI_SCRIPT_URL)
- `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
@@ -293,6 +314,7 @@ docker image prune -f
```
Or use the convenience script:
+
```bash
bash scripts/deploy-webhook.sh
```
@@ -304,11 +326,13 @@ Client → Traefik (TLS) → Next.js App
```
**Domains**:
+
- `klz-cables.com` - Production
- `www.klz-cables.com` - Production (www)
- `staging.klz-cables.com` - Staging
**Services**:
+
- `app`: Next.js application (port 3000)
- `traefik`: Reverse proxy (external)
@@ -317,25 +341,30 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 📈 Performance
### Build Time
+
- **Target**: < 2 minutes
- **Current**: ~1-2 minutes
### Page Load
+
- **Target**: < 100ms
- **Current**: Static HTML from CDN
### Bundle Size
+
- **Target**: < 100KB gzipped
- **Current**: Optimized with code splitting
## 🔒 Security
### Environment Variables
+
- Never commit `.env` file
- Rotate keys regularly
- Use secrets in deployment platform
### Form Security
+
- Email validation
- Rate limiting (recommended)
- Turnstile CAPTCHA (pending)
@@ -343,6 +372,7 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 🎓 WordPress Specifics
### WPBakery Shortcodes Removed
+
- `[vc_row]`, `[vc_column]`, `[vc_column_text]`
- `[nectar_*]` (Salient theme)
- `[image_with_animation]`
@@ -350,13 +380,16 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
- `[divider]`
### HTML Sanitization
+
- Removes inline event handlers
- Strips scripts
- Normalizes classes
- Preserves structure
### Asset Mapping
+
WordPress URLs → Local paths:
+
```
https://klz-cables.com/wp-content/uploads/... → /media/...
```
@@ -364,11 +397,13 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
## 📚 Documentation
### Internal
+
- `PROJECT_STRUCTURE.md` - Detailed structure
- `IMPLEMENTATION_SUMMARY.md` - Progress tracking
- `FINAL_SUMMARY.md` - Complete overview
### External
+
- [Next.js Docs](https://nextjs.org/docs)
- [WordPress REST API](https://developer.wordpress.org/rest-api/)
- [Resend Docs](https://resend.com/docs)
@@ -379,17 +414,20 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
### Common Issues
**TypeScript Errors**
+
- The TypeScript errors shown in the editor are expected
- They occur because modules reference each other
- The build process resolves these correctly
- Run `npm run build` to verify
**Build Failures**
+
- Check environment variables
- Verify data files exist
- Clear `.next` cache: `rm -rf .next`
**Missing Modules**
+
- Run `npm install --legacy-peer-deps`
- Check `package.json` dependencies
@@ -404,11 +442,12 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
✅ **i18n**: Multi-language support
✅ **SEO**: Metadata and sitemaps
✅ **Compatibility**: WPBakery content handled
-✅ **Media**: All images downloaded
+✅ **Media**: All images downloaded
## 📞 Support
For issues or questions:
+
1. Check the documentation
2. Review the troubleshooting section
3. Check environment variables
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index fff060f6..257f8c32 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -8,6 +8,7 @@ import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
+import { config } from '@/lib/config';
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
@@ -51,7 +52,7 @@ export default async function LocaleLayout({
{/* Sends pageviews for client-side navigations */}
-
+