refactor: Standardize Umami analytics environment variables to non-public names with fallbacks to NEXT_PUBLIC_ prefixed versions.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s

This commit is contained in:
2026-02-06 22:35:49 +01:00
parent 259d712105
commit e179e8162c
15 changed files with 243 additions and 245 deletions

4
.env
View File

@@ -1,8 +1,8 @@
# Application # Application
NODE_ENV=production NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com NEXT_PUBLIC_BASE_URL=https://klz-cables.com
UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3 UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1 SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
LOG_LEVEL=info LOG_LEVEL=info

View File

@@ -20,8 +20,8 @@ TARGET=development
# Analytics (Umami) # Analytics (Umami)
# ──────────────────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable analytics # Optional: Leave empty to disable analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID= UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# ──────────────────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────────────────
# Error Tracking (GlitchTip/Sentry) # Error Tracking (GlitchTip/Sentry)

View File

@@ -12,8 +12,8 @@ NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics (Umami) # Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID= UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking (GlitchTip/Sentry) # Error Tracking (GlitchTip/Sentry)
SENTRY_DSN= SENTRY_DSN=

View File

@@ -198,16 +198,16 @@ jobs:
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
TARGET: ${{ needs.prepare.outputs.target }} TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} 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) }} 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_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 }} DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
run: | run: |
docker buildx build \ docker buildx build \
--pull \ --pull \
--platform linux/arm64 \ --platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ --build-arg UMAMI_WEBSITE_ID="$UMAMI_WEBSITE_ID" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \ --build-arg UMAMI_API_ENDPOINT="$UMAMI_API_ENDPOINT" \
--build-arg NEXT_PUBLIC_TARGET="$TARGET" \ --build-arg NEXT_PUBLIC_TARGET="$TARGET" \
--build-arg DIRECTUS_URL="$DIRECTUS_URL" \ --build-arg DIRECTUS_URL="$DIRECTUS_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \ -t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
@@ -231,8 +231,8 @@ jobs:
ENV_FILE: ${{ needs.prepare.outputs.env_file }} ENV_FILE: ${{ needs.prepare.outputs.env_file }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} 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) }} 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_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)) }} 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_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))) }} 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 NODE_ENV=production
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_TARGET=$TARGET NEXT_PUBLIC_TARGET=$TARGET
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 UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
SENTRY_DSN=$SENTRY_DSN SENTRY_DSN=$SENTRY_DSN
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" ) LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
MAIL_HOST=$MAIL_HOST MAIL_HOST=$MAIL_HOST

View File

@@ -25,14 +25,18 @@ ENV NEXT_TELEMETRY_DISABLED=1
# Build-time environment variables for Next.js # Build-time environment variables for Next.js
# These are baked into the client bundle during build # These are baked into the client bundle during build
ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_BASE_URL
ARG UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_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_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_TARGET ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL ARG DIRECTUS_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID ENV UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID:-$NEXT_PUBLIC_UMAMI_WEBSITE_ID}
ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL ENV UMAMI_API_ENDPOINT=${UMAMI_API_ENDPOINT:-${UMAMI_SCRIPT_URL:-$NEXT_PUBLIC_UMAMI_SCRIPT_URL}}
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL ENV DIRECTUS_URL=$DIRECTUS_URL

View File

@@ -5,11 +5,13 @@ A complete WordPress to Next.js static site migration for KLZ Cables, transformi
## 🚀 Quick Start ## 🚀 Quick Start
### Prerequisites ### Prerequisites
- Node.js 18+
- Node.js 18+
- npm or yarn - npm or yarn
### Installation ### Installation
```bash
````bash
# Install dependencies # Install dependencies
npm install --legacy-peer-deps npm install --legacy-peer-deps
@@ -42,11 +44,12 @@ npm run cms:logs
# Stop the CMS # Stop the CMS
npm run cms:stop npm run cms:stop
``` ````
Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`. Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`.
### 🔄 Data & Migration ### 🔄 Data & Migration
To sync data or migrate existing content: To sync data or migrate existing content:
```bash ```bash
@@ -61,6 +64,7 @@ npm run cms:migrate
``` ```
### Environment Variables ### Environment Variables
```bash ```bash
# .env # .env
SITE_URL=https://klz-cables.com SITE_URL=https://klz-cables.com
@@ -69,8 +73,8 @@ TURNSTILE_SITE_KEY=your_turnstile_key
TURNSTILE_SECRET_KEY=your_turnstile_secret TURNSTILE_SECRET_KEY=your_turnstile_secret
# Umami # 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 UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# GlitchTip (Sentry compatible) # GlitchTip (Sentry compatible)
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID 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 ## 📊 Project Overview
### Migration Statistics ### Migration Statistics
- **Content Exported**: 141 items - **Content Exported**: 141 items
- 18 pages (9 EN + 9 DE) - 18 pages (9 EN + 9 DE)
- 59 posts (29 EN + 30 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 - **Translation Pairs**: 16
### Performance Benefits ### Performance Benefits
- **Before**: Dynamic WordPress with database queries - **Before**: Dynamic WordPress with database queries
- **After**: Static HTML with CDN delivery - **After**: Static HTML with CDN delivery
- **Load Time**: <100ms (vs 500ms+) - **Load Time**: <100ms (vs 500ms+)
@@ -99,6 +105,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
## 🏗️ Architecture ## 🏗️ Architecture
### Tech Stack ### Tech Stack
- **Framework**: Next.js 14 (App Router) - **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript - **Language**: TypeScript
- **Styling**: SCSS - **Styling**: SCSS
@@ -109,6 +116,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
- **CAPTCHA**: Cloudflare Turnstile - **CAPTCHA**: Cloudflare Turnstile
### Project Structure ### Project Structure
``` ```
app/ app/
├── layout.tsx # Root layout ├── layout.tsx # Root layout
@@ -163,6 +171,7 @@ scripts/
## 🎯 Features ## 🎯 Features
### ✅ Implemented ### ✅ Implemented
- **Multi-language**: EN/DE with `/de/` prefix routing - **Multi-language**: EN/DE with `/de/` prefix routing
- **Contact Forms**: Resend integration with validation - **Contact Forms**: Resend integration with validation
- **GDPR Compliance**: Cookie consent banner - **GDPR Compliance**: Cookie consent banner
@@ -175,12 +184,14 @@ scripts/
- **Asset Management**: WordPress → local path mapping - **Asset Management**: WordPress → local path mapping
### 🔄 In Progress ### 🔄 In Progress
- Analytics integration (consent-based) - Analytics integration (consent-based)
- Turnstile CAPTCHA - Turnstile CAPTCHA
- Build testing - Build testing
- Deployment configuration - Deployment configuration
### 📝 Remaining ### 📝 Remaining
- Performance optimization - Performance optimization
- Final QA testing - Final QA testing
- Documentation updates - Documentation updates
@@ -188,6 +199,7 @@ scripts/
## 📝 Content Management ## 📝 Content Management
### Data Export ### Data Export
```bash ```bash
# Export from WordPress # Export from WordPress
npm run data:export npm run data:export
@@ -203,6 +215,7 @@ npm run data:improve-mapping
``` ```
### Adding New Content ### Adding New Content
1. Export new content from WordPress 1. Export new content from WordPress
2. Process the data 2. Process the data
3. Rebuild the site 3. Rebuild the site
@@ -210,17 +223,20 @@ npm run data:improve-mapping
## 🎨 Design System ## 🎨 Design System
### Colors ### Colors
- Primary: `#0066cc` (KLZ Blue) - Primary: `#0066cc` (KLZ Blue)
- Secondary: `#00a896` (Teal) - Secondary: `#00a896` (Teal)
- Text: `#1a1a1a` - Text: `#1a1a1a`
- Background: `#f8f9fa` - Background: `#f8f9fa`
### Typography ### Typography
- Font: Inter - Font: Inter
- Base: 16px - Base: 16px
- Scale: 1.25 (Major Third) - Scale: 1.25 (Major Third)
### Layout ### Layout
- Max width: 1200px - Max width: 1200px
- Responsive grid - Responsive grid
- Mobile-first - Mobile-first
@@ -228,6 +244,7 @@ npm run data:improve-mapping
## 🔧 API Endpoints ## 🔧 API Endpoints
### Contact Form ### Contact Form
``` ```
POST /api/contact POST /api/contact
{ {
@@ -239,11 +256,13 @@ POST /api/contact
``` ```
### Sitemap ### Sitemap
``` ```
GET /sitemap.xml GET /sitemap.xml
``` ```
### Robots ### Robots
``` ```
GET /robots.txt 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` **Workflow**: `.gitea/workflows/deploy.yml`
**Branch Deployments**: **Branch Deployments**:
- `main` branch: Deploys to production using `.env.prod` - `main` branch: Deploys to production using `.env.prod`
- `staging` branch: Deploys to staging using `.env.staging` - `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. 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): **Required Secrets** (configure in Gitea repository settings):
- `REGISTRY_USER` - Docker registry username - `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password - `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment - `ALPHA_SSH_KEY` - SSH private key for deployment
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`) - `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID - `UMAMI_WEBSITE_ID` - Analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL - `UMAMI_API_ENDPOINT` - Analytics API endpoint (formerly NEXT_PUBLIC_UMAMI_SCRIPT_URL)
- `SENTRY_DSN` - Error tracking DSN - `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration - `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: Or use the convenience script:
```bash ```bash
bash scripts/deploy-webhook.sh bash scripts/deploy-webhook.sh
``` ```
@@ -304,11 +326,13 @@ Client → Traefik (TLS) → Next.js App
``` ```
**Domains**: **Domains**:
- `klz-cables.com` - Production - `klz-cables.com` - Production
- `www.klz-cables.com` - Production (www) - `www.klz-cables.com` - Production (www)
- `staging.klz-cables.com` - Staging - `staging.klz-cables.com` - Staging
**Services**: **Services**:
- `app`: Next.js application (port 3000) - `app`: Next.js application (port 3000)
- `traefik`: Reverse proxy (external) - `traefik`: Reverse proxy (external)
@@ -317,25 +341,30 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 📈 Performance ## 📈 Performance
### Build Time ### Build Time
- **Target**: < 2 minutes - **Target**: < 2 minutes
- **Current**: ~1-2 minutes - **Current**: ~1-2 minutes
### Page Load ### Page Load
- **Target**: < 100ms - **Target**: < 100ms
- **Current**: Static HTML from CDN - **Current**: Static HTML from CDN
### Bundle Size ### Bundle Size
- **Target**: < 100KB gzipped - **Target**: < 100KB gzipped
- **Current**: Optimized with code splitting - **Current**: Optimized with code splitting
## 🔒 Security ## 🔒 Security
### Environment Variables ### Environment Variables
- Never commit `.env` file - Never commit `.env` file
- Rotate keys regularly - Rotate keys regularly
- Use secrets in deployment platform - Use secrets in deployment platform
### Form Security ### Form Security
- Email validation - Email validation
- Rate limiting (recommended) - Rate limiting (recommended)
- Turnstile CAPTCHA (pending) - Turnstile CAPTCHA (pending)
@@ -343,6 +372,7 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 🎓 WordPress Specifics ## 🎓 WordPress Specifics
### WPBakery Shortcodes Removed ### WPBakery Shortcodes Removed
- `[vc_row]`, `[vc_column]`, `[vc_column_text]` - `[vc_row]`, `[vc_column]`, `[vc_column_text]`
- `[nectar_*]` (Salient theme) - `[nectar_*]` (Salient theme)
- `[image_with_animation]` - `[image_with_animation]`
@@ -350,13 +380,16 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
- `[divider]` - `[divider]`
### HTML Sanitization ### HTML Sanitization
- Removes inline event handlers - Removes inline event handlers
- Strips scripts - Strips scripts
- Normalizes classes - Normalizes classes
- Preserves structure - Preserves structure
### Asset Mapping ### Asset Mapping
WordPress URLs → Local paths: WordPress URLs → Local paths:
``` ```
https://klz-cables.com/wp-content/uploads/... → /media/... https://klz-cables.com/wp-content/uploads/... → /media/...
``` ```
@@ -364,11 +397,13 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
## 📚 Documentation ## 📚 Documentation
### Internal ### Internal
- `PROJECT_STRUCTURE.md` - Detailed structure - `PROJECT_STRUCTURE.md` - Detailed structure
- `IMPLEMENTATION_SUMMARY.md` - Progress tracking - `IMPLEMENTATION_SUMMARY.md` - Progress tracking
- `FINAL_SUMMARY.md` - Complete overview - `FINAL_SUMMARY.md` - Complete overview
### External ### External
- [Next.js Docs](https://nextjs.org/docs) - [Next.js Docs](https://nextjs.org/docs)
- [WordPress REST API](https://developer.wordpress.org/rest-api/) - [WordPress REST API](https://developer.wordpress.org/rest-api/)
- [Resend Docs](https://resend.com/docs) - [Resend Docs](https://resend.com/docs)
@@ -379,17 +414,20 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
### Common Issues ### Common Issues
**TypeScript Errors** **TypeScript Errors**
- The TypeScript errors shown in the editor are expected - The TypeScript errors shown in the editor are expected
- They occur because modules reference each other - They occur because modules reference each other
- The build process resolves these correctly - The build process resolves these correctly
- Run `npm run build` to verify - Run `npm run build` to verify
**Build Failures** **Build Failures**
- Check environment variables - Check environment variables
- Verify data files exist - Verify data files exist
- Clear `.next` cache: `rm -rf .next` - Clear `.next` cache: `rm -rf .next`
**Missing Modules** **Missing Modules**
- Run `npm install --legacy-peer-deps` - Run `npm install --legacy-peer-deps`
- Check `package.json` dependencies - Check `package.json` dependencies
@@ -404,11 +442,12 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
**i18n**: Multi-language support **i18n**: Multi-language support
**SEO**: Metadata and sitemaps **SEO**: Metadata and sitemaps
**Compatibility**: WPBakery content handled **Compatibility**: WPBakery content handled
**Media**: All images downloaded **Media**: All images downloaded
## 📞 Support ## 📞 Support
For issues or questions: For issues or questions:
1. Check the documentation 1. Check the documentation
2. Review the troubleshooting section 2. Review the troubleshooting section
3. Check environment variables 3. Check environment variables

View File

@@ -8,6 +8,7 @@ import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server'; import { getMessages } from 'next-intl/server';
import '../../styles/globals.css'; import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
import { config } from '@/lib/config';
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(SITE_URL), metadataBase: new URL(SITE_URL),
@@ -51,7 +52,7 @@ export default async function LocaleLayout({
<CMSConnectivityNotice /> <CMSConnectivityNotice />
{/* Sends pageviews for client-side navigations */} {/* Sends pageviews for client-side navigations */}
<AnalyticsProvider /> <AnalyticsProvider websiteId={config.analytics.umami.websiteId} />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
</html> </html>

View File

@@ -3,7 +3,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import { getAppServices } from '@/lib/services/create-services'; import { getAppServices } from '@/lib/services/create-services';
import Script from 'next/script';
/** /**
* AnalyticsProvider Component * AnalyticsProvider Component
@@ -11,49 +10,35 @@ import Script from 'next/script';
* Automatically tracks pageviews on client-side route changes. * Automatically tracks pageviews on client-side route changes.
* This component should be placed inside your layout to handle navigation events. * This component should be placed inside your layout to handle navigation events.
* *
* @param {Object} props - Component props
* @param {string} [props.websiteId] - The Umami website ID (passed from server config)
*
* @example * @example
* ```tsx * ```tsx
* // In your layout.tsx * // In your layout.tsx
* <NextIntlClientProvider messages={messages} locale={locale}> * const { websiteId } = config.analytics.umami;
* <UmamiScript /> * <AnalyticsProvider websiteId={websiteId} />
* <Header />
* <main>{children}</main>
* <Footer />
* <AnalyticsProvider />
* </NextIntlClientProvider>
* ``` * ```
*/ */
export default function AnalyticsProvider() { export default function AnalyticsProvider({ websiteId }: { websiteId?: string }) {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
useEffect(() => { useEffect(() => {
if (!pathname) return; if (!pathname) return;
const services = getAppServices(); const services = getAppServices();
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`; const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
// Track pageview with the full URL // Track pageview with the full URL
services.analytics.trackPageview(url); services.analytics.trackPageview(url);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked pageview:', url); console.log('[Umami] Tracked pageview:', url);
} }
}, [pathname, searchParams]); }, [pathname, searchParams]);
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
if (!websiteId) return null; if (!websiteId) return null;
return ( return null;
<Script
id="umami-analytics"
src="/stats/script.js"
data-website-id={websiteId}
data-host-url="/stats"
strategy="afterInteractive"
data-domains="klz-cables.com"
defer
/>
);
} }

View File

@@ -42,15 +42,15 @@ The application uses a clean, robust, **fully automated** environment variable s
## Environment Variables ## Environment Variables
### Build-Time Variables (NEXT_PUBLIC_*) ### Build-Time Variables (NEXT*PUBLIC*\*)
These are embedded into the JavaScript bundle during build and are visible to the client: These are embedded into the JavaScript bundle during build and are visible to the client:
| Variable | Required | Description | | Variable | Required | Description |
|----------|----------|-------------| | ---------------------- | -------- | ------------------------------------------------------------ |
| `NEXT_PUBLIC_BASE_URL` | ✅ Yes | Base URL of the application (e.g., `https://klz-cables.com`) | | `NEXT_PUBLIC_BASE_URL` | ✅ Yes | Base URL of the application (e.g., `https://klz-cables.com`) |
| `NEXT_PUBLIC_UMAMI_WEBSITE_ID` | ❌ No | Umami analytics website ID | | `UMAMI_WEBSITE_ID` | ❌ No | Umami analytics website ID (passed as prop) |
| `NEXT_PUBLIC_UMAMI_SCRIPT_URL` | ❌ No | Umami analytics script URL (default: `https://analytics.infra.mintel.me/script.js`) | | `UMAMI_API_ENDPOINT` | ❌ No | Backend-only Umami analytics API target (internal) |
**Important**: These must be provided as `--build-arg` when building the Docker image. **Important**: These must be provided as `--build-arg` when building the Docker image.
@@ -58,38 +58,40 @@ These are embedded into the JavaScript bundle during build and are visible to th
These are loaded from the `.env` file at runtime and are only available on the server: These are loaded from the `.env` file at runtime and are only available on the server:
| Variable | Required | Description | | Variable | Required | Description |
|----------|----------|-------------| | -------------------------- | -------- | ------------------------------------------------------ |
| `NODE_ENV` | ✅ Yes | Environment mode (`production`, `development`, `test`) | | `NODE_ENV` | ✅ Yes | Environment mode (`production`, `development`, `test`) |
| `SENTRY_DSN` | ❌ No | GlitchTip/Sentry error tracking DSN | | `SENTRY_DSN` | ❌ No | GlitchTip/Sentry error tracking DSN |
| `MAIL_HOST` | ❌ No | SMTP server hostname | | `MAIL_HOST` | ❌ No | SMTP server hostname |
| `MAIL_PORT` | ❌ No | SMTP server port (default: `587`) | | `MAIL_PORT` | ❌ No | SMTP server port (default: `587`) |
| `MAIL_USERNAME` | ❌ No | SMTP authentication username | | `MAIL_USERNAME` | ❌ No | SMTP authentication username |
| `MAIL_PASSWORD` | ❌ No | SMTP authentication password | | `MAIL_PASSWORD` | ❌ No | SMTP authentication password |
| `MAIL_FROM` | ❌ No | Email sender address | | `MAIL_FROM` | ❌ No | Email sender address |
| `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails | | `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails |
| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) | | `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) |
| `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) | | `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) |
| `STRAPI_DATABASE_NAME` | ✅ Yes | Strapi database name | | `STRAPI_DATABASE_NAME` | ✅ Yes | Strapi database name |
| `STRAPI_DATABASE_USERNAME` | ✅ Yes | Strapi database username | | `STRAPI_DATABASE_USERNAME` | ✅ Yes | Strapi database username |
| `STRAPI_DATABASE_PASSWORD` | ✅ Yes | Strapi database password | | `STRAPI_DATABASE_PASSWORD` | ✅ Yes | Strapi database password |
| `STRAPI_URL` | ✅ Yes | URL of the Strapi CMS | | `STRAPI_URL` | ✅ Yes | URL of the Strapi CMS |
| `APP_KEYS` | ✅ Yes | Strapi application keys (comma-separated) | | `APP_KEYS` | ✅ Yes | Strapi application keys (comma-separated) |
| `API_TOKEN_SALT` | ✅ Yes | Strapi API token salt | | `API_TOKEN_SALT` | ✅ Yes | Strapi API token salt |
| `ADMIN_JWT_SECRET` | ✅ Yes | Strapi admin JWT secret | | `ADMIN_JWT_SECRET` | ✅ Yes | Strapi admin JWT secret |
| `TRANSFER_TOKEN_SALT` | ✅ Yes | Strapi transfer token salt | | `TRANSFER_TOKEN_SALT` | ✅ Yes | Strapi transfer token salt |
| `JWT_SECRET` | ✅ Yes | Strapi JWT secret | | `JWT_SECRET` | ✅ Yes | Strapi JWT secret |
## Local Development ## Local Development
### Setup ### Setup
1. Copy the example environment file: 1. Copy the example environment file:
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
2. Edit `.env` and fill in your local configuration: 2. Edit `.env` and fill in your local configuration:
```bash ```bash
NODE_ENV=development NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3000 NEXT_PUBLIC_BASE_URL=http://localhost:3000
@@ -97,6 +99,7 @@ These are loaded from the `.env` file at runtime and are only available on the s
``` ```
3. Install dependencies: 3. Install dependencies:
```bash ```bash
npm install npm install
``` ```
@@ -112,8 +115,8 @@ These are loaded from the `.env` file at runtime and are only available on the s
# Build with build-time arguments # Build with build-time arguments
docker build \ docker build \
--build-arg NEXT_PUBLIC_BASE_URL=http://localhost:3000 \ --build-arg NEXT_PUBLIC_BASE_URL=http://localhost:3000 \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-id \ --build-arg UMAMI_WEBSITE_ID=your-id \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js \ --build-arg UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me \
-t klz-cables:local . -t klz-cables:local .
# Run with runtime environment file # Run with runtime environment file
@@ -138,8 +141,8 @@ docker run --env-file .env -p 3000:3000 klz-cables:local
**Build-Time Variables:** **Build-Time Variables:**
- `NEXT_PUBLIC_BASE_URL` - Production URL (e.g., `https://klz-cables.com`) - `NEXT_PUBLIC_BASE_URL` - Production URL (e.g., `https://klz-cables.com`)
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics ID - `UMAMI_WEBSITE_ID` - Umami analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami script URL - `UMAMI_API_ENDPOINT` - Umami API endpoint
**Runtime Variables:** **Runtime Variables:**
- `SENTRY_DSN` - Error tracking DSN - `SENTRY_DSN` - Error tracking DSN
@@ -209,11 +212,12 @@ docker-compose logs -f app
**Problem**: Build fails with "Environment validation failed" **Problem**: Build fails with "Environment validation failed"
**Solution**: Ensure all required `NEXT_PUBLIC_*` variables are provided as build arguments: **Solution**: Ensure all required `NEXT_PUBLIC_*` variables are provided as build arguments:
```bash ```bash
docker build \ docker build \
--build-arg NEXT_PUBLIC_BASE_URL=https://klz-cables.com \ --build-arg NEXT_PUBLIC_BASE_URL=https://klz-cables.com \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-id \ --build-arg UMAMI_WEBSITE_ID=your-id \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js \ --build-arg UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me \
-t klz-cables . -t klz-cables .
``` ```
@@ -222,6 +226,7 @@ docker build \
**Problem**: Container starts but application crashes **Problem**: Container starts but application crashes
**Solution**: Check that the `.env` file exists and contains all required runtime variables: **Solution**: Check that the `.env` file exists and contains all required runtime variables:
```bash ```bash
# On the server # On the server
cat /home/deploy/sites/klz-cables.com/.env cat /home/deploy/sites/klz-cables.com/.env
@@ -235,9 +240,11 @@ docker-compose logs app
**Problem**: Features not working (email, analytics, etc.) **Problem**: Features not working (email, analytics, etc.)
**Solution**: **Solution**:
1. Check that the secret is configured in Gitea 1. Check that the secret is configured in Gitea
2. Verify the workflow includes it in the `.env` generation (see `.gitea/workflows/deploy.yml`) 2. Verify the workflow includes it in the `.env` generation (see `.gitea/workflows/deploy.yml`)
3. Redeploy to regenerate the `.env` file: 3. Redeploy to regenerate the `.env` file:
```bash ```bash
git commit --allow-empty -m "Trigger redeploy" git commit --allow-empty -m "Trigger redeploy"
git push origin main git push origin main
@@ -255,6 +262,7 @@ docker-compose logs app
**Problem**: `docker-compose up` fails with "env file not found" **Problem**: `docker-compose up` fails with "env file not found"
**Solution**: The `.env` file should be automatically created by the workflow. If it's missing: **Solution**: The `.env` file should be automatically created by the workflow. If it's missing:
1. Check the workflow logs for errors in the "📝 Preparing environment configuration" step 1. Check the workflow logs for errors in the "📝 Preparing environment configuration" step
2. Manually trigger a deployment by pushing to main 2. Manually trigger a deployment by pushing to main
3. If still missing, check server permissions and disk space 3. If still missing, check server permissions and disk space
@@ -264,6 +272,7 @@ docker-compose logs app
**Problem**: Container can't connect to Traefik **Problem**: Container can't connect to Traefik
**Solution**: Verify the `infra` network exists: **Solution**: Verify the `infra` network exists:
```bash ```bash
docker network ls | grep infra docker network ls | grep infra
docker network inspect infra docker network inspect infra

View File

@@ -7,29 +7,31 @@ This guide helps you migrate from the old fragile environment variable setup to
### Before (Fragile & Overkill) ### Before (Fragile & Overkill)
**Problems:** **Problems:**
- Environment variables passed individually via SSH (12+ vars) - Environment variables passed individually via SSH (12+ vars)
- Duplicate definitions in Dockerfile, docker-compose.yml, and deploy.yml - Duplicate definitions in Dockerfile, docker-compose.yml, and deploy.yml
- Build args included runtime-only variables (SENTRY_DSN, MAIL_*, REDIS_*) - Build args included runtime-only variables (SENTRY*DSN, MAIL*_, REDIS\__)
- No single source of truth - No single source of truth
- Difficult to maintain and error-prone - Difficult to maintain and error-prone
```yaml ```yaml
# Old deploy.yml - FRAGILE! # Old deploy.yml - FRAGILE!
ssh root@alpha.mintel.me \ ssh root@alpha.mintel.me \
"MAIL_FROM='${{ secrets.MAIL_FROM }}' \ "MAIL_FROM='${{ secrets.MAIL_FROM }}' \
MAIL_HOST='${{ secrets.MAIL_HOST }}' \ MAIL_HOST='${{ secrets.MAIL_HOST }}' \
MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}' \ MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}' \
MAIL_PORT='${{ secrets.MAIL_PORT }}' \ MAIL_PORT='${{ secrets.MAIL_PORT }}' \
MAIL_RECIPIENTS='${{ secrets.MAIL_RECIPIENTS }}' \ MAIL_RECIPIENTS='${{ secrets.MAIL_RECIPIENTS }}' \
MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}' \ MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}' \
NEXT_PUBLIC_BASE_URL='${{ secrets.NEXT_PUBLIC_BASE_URL }}' \ NEXT_PUBLIC_BASE_URL='${{ secrets.NEXT_PUBLIC_BASE_URL }}' \
... (12+ variables) \ ... (12+ variables) \
/home/deploy/deploy.sh" /home/deploy/deploy.sh"
``` ```
### After (Clean & Robust) ### After (Clean & Robust)
**Benefits:** **Benefits:**
- Single `.env` file on server contains all runtime variables - Single `.env` file on server contains all runtime variables
- Only `NEXT_PUBLIC_*` variables passed as build args (3 vars) - Only `NEXT_PUBLIC_*` variables passed as build args (3 vars)
- Clear separation: build-time vs runtime - Clear separation: build-time vs runtime
@@ -46,6 +48,7 @@ ssh root@alpha.mintel.me "/home/deploy/deploy.sh"
### Step 1: Update Gitea Secrets ### Step 1: Update Gitea Secrets
**Remove these secrets** (no longer needed in CI/CD): **Remove these secrets** (no longer needed in CI/CD):
-`MAIL_FROM` -`MAIL_FROM`
-`MAIL_HOST` -`MAIL_HOST`
-`MAIL_PASSWORD` -`MAIL_PASSWORD`
@@ -58,9 +61,11 @@ ssh root@alpha.mintel.me "/home/deploy/deploy.sh"
-`SENTRY_DSN` (from build args) -`SENTRY_DSN` (from build args)
**Keep these secrets** (still needed for build): **Keep these secrets** (still needed for build):
-`NEXT_PUBLIC_BASE_URL` -`NEXT_PUBLIC_BASE_URL`
-`NEXT_PUBLIC_UMAMI_WEBSITE_ID` -`NEXT_PUBLIC_BASE_URL`
-`NEXT_PUBLIC_UMAMI_SCRIPT_URL` -`UMAMI_WEBSITE_ID`
-`UMAMI_API_ENDPOINT`
-`REGISTRY_USER` -`REGISTRY_USER`
-`REGISTRY_PASS` -`REGISTRY_PASS`
-`ALPHA_SSH_KEY` -`ALPHA_SSH_KEY`
@@ -81,8 +86,8 @@ NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics # Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-actual-id UMAMI_WEBSITE_ID=your-actual-id
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking # Error Tracking
SENTRY_DSN=your-actual-dsn SENTRY_DSN=your-actual-dsn
@@ -168,6 +173,7 @@ git push origin main
``` ```
The CI/CD workflow will: The CI/CD workflow will:
1. Build with only `NEXT_PUBLIC_*` build args 1. Build with only `NEXT_PUBLIC_*` build args
2. Push to registry 2. Push to registry
3. SSH to server and run deploy.sh 3. SSH to server and run deploy.sh
@@ -197,21 +203,22 @@ curl -I https://klz-cables.com
## Comparison Table ## Comparison Table
| Aspect | Before | After | | Aspect | Before | After |
|--------|--------|-------| | ----------------- | ------------------------------- | ---------------------------- |
| **Gitea Secrets** | 15+ secrets | 8 secrets | | **Gitea Secrets** | 15+ secrets | 8 secrets |
| **Build Args** | 4 vars (including runtime-only) | 3 vars (NEXT_PUBLIC_* only) | | **Build Args** | 4 vars (including runtime-only) | 3 vars (NEXT*PUBLIC*\* only) |
| **Runtime Vars** | Passed via SSH command | Loaded from .env file | | **Runtime Vars** | Passed via SSH command | Loaded from .env file |
| **Maintenance** | Update in 3 places | Update in 1 place | | **Maintenance** | Update in 3 places | Update in 1 place |
| **Security** | Secrets in CI logs | Secrets only on server | | **Security** | Secrets in CI logs | Secrets only on server |
| **Clarity** | Confusing duplication | Clear separation | | **Clarity** | Confusing duplication | Clear separation |
| **Robustness** | Fragile SSH command | Robust file-based config | | **Robustness** | Fragile SSH command | Robust file-based config |
## Rollback Plan ## Rollback Plan
If you need to rollback to the old system: If you need to rollback to the old system:
1. Revert the changes in git: 1. Revert the changes in git:
```bash ```bash
git revert HEAD git revert HEAD
git push origin main git push origin main
@@ -229,7 +236,8 @@ A: `NEXT_PUBLIC_*` variables are special in Next.js - they're embedded into the
**Q: Can I update environment variables without rebuilding?** **Q: Can I update environment variables without rebuilding?**
A: Yes, for runtime-only variables (MAIL_*, REDIS_*, SENTRY_DSN, etc.). Just edit the `.env` file on the server and restart containers: A: Yes, for runtime-only variables (MAIL*\*, REDIS*\*, SENTRY_DSN, etc.). Just edit the `.env` file on the server and restart containers:
```bash ```bash
nano /home/deploy/sites/klz-cables.com/.env nano /home/deploy/sites/klz-cables.com/.env
docker-compose down && docker-compose up -d docker-compose down && docker-compose up -d
@@ -240,6 +248,7 @@ For `NEXT_PUBLIC_*` variables, you need to rebuild the Docker image since they'r
**Q: Where should I store the .env file backup?** **Q: Where should I store the .env file backup?**
A: Keep a secure backup outside the server: A: Keep a secure backup outside the server:
```bash ```bash
# Download from server # Download from server
scp root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env \ scp root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env \
@@ -250,7 +259,8 @@ scp root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env \
**Q: What if I accidentally commit .env to git?** **Q: What if I accidentally commit .env to git?**
A: A:
1. Remove it immediately: `git rm .env && git commit -m "Remove .env"` 1. Remove it immediately: `git rm .env && git commit -m "Remove .env"`
2. Rotate all credentials in the file 2. Rotate all credentials in the file
3. Update the `.gitignore` to ensure it doesn't happen again (already done) 3. Update the `.gitignore` to ensure it doesn't happen again (already done)
@@ -267,6 +277,7 @@ If you encounter issues during migration:
## Summary ## Summary
The new system is: The new system is:
-**Simpler**: One .env file instead of scattered variables -**Simpler**: One .env file instead of scattered variables
-**Cleaner**: Clear separation of build vs runtime -**Cleaner**: Clear separation of build vs runtime
-**Robust**: File-based config instead of fragile SSH commands -**Robust**: File-based config instead of fragile SSH commands

View File

@@ -27,11 +27,9 @@ function createConfig() {
analytics: { analytics: {
umami: { umami: {
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, websiteId: env.UMAMI_WEBSITE_ID,
scriptUrl: env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, apiEndpoint: env.UMAMI_API_ENDPOINT,
// The proxied path used in the frontend enabled: Boolean(env.UMAMI_WEBSITE_ID),
proxyPath: '/stats/script.js',
enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID),
}, },
}, },
@@ -152,7 +150,7 @@ export function getMaskedConfig() {
analytics: { analytics: {
umami: { umami: {
websiteId: mask(c.analytics.umami.websiteId), websiteId: mask(c.analytics.umami.websiteId),
scriptUrl: c.analytics.umami.scriptUrl, apiEndpoint: c.analytics.umami.apiEndpoint,
enabled: c.analytics.umami.enabled, enabled: c.analytics.umami.enabled,
}, },
}, },

View File

@@ -15,10 +15,10 @@ export const envSchema = z
NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(), NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
// Analytics // Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()), UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess( UMAMI_API_ENDPOINT: z.preprocess(
preprocessEmptyString, preprocessEmptyString,
z.string().url().default('https://analytics.infra.mintel.me/script.js'), z.string().url().default('https://analytics.infra.mintel.me'),
), ),
// Error Tracking // Error Tracking
@@ -82,8 +82,11 @@ export function getRawEnv() {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET, NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, UMAMI_WEBSITE_ID: process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, UMAMI_API_ENDPOINT:
process.env.UMAMI_API_ENDPOINT ||
process.env.UMAMI_SCRIPT_URL ||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
SENTRY_DSN: process.env.SENTRY_DSN, SENTRY_DSN: process.env.SENTRY_DSN,
LOG_LEVEL: process.env.LOG_LEVEL, LOG_LEVEL: process.env.LOG_LEVEL,
MAIL_HOST: process.env.MAIL_HOST, MAIL_HOST: process.env.MAIL_HOST,

View File

@@ -1,14 +1,5 @@
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service'; import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
import { config } from '../../config';
/**
* Type definition for the Umami global object.
*
* This represents the `window.umami` object that the Umami script exposes.
* The `track` function can accept either an event name or a URL.
*/
type UmamiGlobal = {
track?: (eventOrUrl: string, props?: AnalyticsEventProperties) => void;
};
/** /**
* Configuration options for UmamiAnalyticsService. * Configuration options for UmamiAnalyticsService.
@@ -20,133 +11,90 @@ export type UmamiAnalyticsServiceOptions = {
}; };
/** /**
* Umami Analytics Service Implementation. * Umami Analytics Service Implementation (Script-less/Proxy edition).
* *
* This service implements the AnalyticsService interface for Umami analytics. * This version implements the Umami tracking protocol directly via fetch,
* It provides type-safe event tracking and pageview tracking. * eliminating the need to load an external script.js file.
* *
* @example * In the browser, it gathers standard metadata (screen, language, referrer)
* ```typescript * and sends it to the proxied '/stats/api/send' endpoint.
* // Service creation (usually done by create-services.ts)
* const service = new UmamiAnalyticsService({ enabled: true });
*
* // Track events
* service.track('button_click', { button_id: 'cta' });
* service.trackPageview('/products/123');
* ```
*
* @example
* ```typescript
* // Using through the service layer (recommended)
* import { getAppServices } from '@/lib/services/create-services';
*
* const services = getAppServices();
* services.analytics.track('product_add_to_cart', {
* product_id: '123',
* price: 99.99,
* });
* ```
*/ */
export class UmamiAnalyticsService implements AnalyticsService { export class UmamiAnalyticsService implements AnalyticsService {
constructor(private readonly options: UmamiAnalyticsServiceOptions) {} private websiteId?: string;
private endpoint: string;
constructor(private readonly options: UmamiAnalyticsServiceOptions) {
this.websiteId = config.analytics.umami.websiteId;
// On server, use the full internal URL; on client, use the proxied path
this.endpoint = typeof window === 'undefined' ? config.analytics.umami.apiEndpoint : '/stats';
}
/** /**
* Track a custom event with optional properties. * Internal method to send the payload to Umami API.
* */
* This method checks if analytics are enabled and if we're in a browser environment private async sendPayload(type: 'event', data: Record<string, any>) {
* before attempting to track the event. if (!this.options.enabled || !this.websiteId) return;
*
* @param eventName - The name of the event to track try {
* @param props - Optional event properties const payload = {
* website: this.websiteId,
* @example hostname: typeof window !== 'undefined' ? window.location.hostname : 'server',
* ```typescript screen:
* service.track('product_add_to_cart', { typeof window !== 'undefined'
* product_id: '123', ? `${window.screen.width}x${window.screen.height}`
* product_name: 'Cable', : undefined,
* price: 99.99, language: typeof window !== 'undefined' ? navigator.language : undefined,
* quantity: 1, referrer: typeof window !== 'undefined' ? document.referrer : undefined,
* }); ...data,
* ``` };
const response = await fetch(`${this.endpoint}/api/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': typeof window === 'undefined' ? 'KLZ-Server' : navigator.userAgent,
},
body: JSON.stringify({ type, payload }),
// Use keepalive for page navigation events to ensure they complete
keepalive: true,
} as any);
if (!response.ok && process.env.NODE_ENV === 'development') {
const errorText = await response.text();
console.warn(`[Umami] API responded with ${response.status}: ${errorText}`);
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('[Umami] Failed to send analytics:', error);
}
}
}
/**
* Track a custom event.
*/ */
track(eventName: string, props?: AnalyticsEventProperties) { track(eventName: string, props?: AnalyticsEventProperties) {
if (!this.options.enabled) return; this.sendPayload('event', {
name: eventName,
// Server-side tracking via proxy data: props,
if (typeof window === 'undefined') { url:
const { getServerAppServices } = require('../create-services.server'); typeof window !== 'undefined'
const { config } = require('../../config'); ? window.location.pathname + window.location.search
const websiteId = config.analytics.umami.websiteId; : undefined,
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', ''); });
if (!websiteId) return;
const logger = getServerAppServices().logger.child({ component: 'analytics' });
logger.info('Sending analytics event', { eventName, props });
fetch(`${umamiUrl}/api/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }),
}).catch((error) => {
logger.error('Failed to send analytics event', { eventName, props, error });
});
return;
}
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
umami?.track?.(eventName, props);
} }
/** /**
* Track a pageview. * Track a pageview.
*
* This method checks if analytics are enabled and if we're in a browser environment
* before attempting to track the pageview.
*
* Umami treats `track(url)` as a pageview override, so we can use the same
* `track` function for both events and pageviews.
*
* @param url - The URL to track (defaults to current location)
*
* @example
* ```typescript
* // Track current page
* service.trackPageview();
*
* // Track custom URL
* service.trackPageview('/products/123?category=cables');
* ```
*/ */
trackPageview(url?: string) { trackPageview(url?: string) {
if (!this.options.enabled) return; this.sendPayload('event', {
url:
// Server-side tracking via proxy url ||
if (typeof window === 'undefined') { (typeof window !== 'undefined'
const { getServerAppServices } = require('../create-services.server'); ? window.location.pathname + window.location.search
const { config } = require('../../config'); : undefined),
const websiteId = config.analytics.umami.websiteId; });
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', '');
if (!websiteId || !url) return;
const logger = getServerAppServices().logger.child({ component: 'analytics' });
logger.info('Sending analytics pageview', { url });
fetch(`${umamiUrl}/api/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }),
}).catch((error) => {
logger.error('Failed to send analytics pageview', { url, error });
});
return;
}
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
// Umami treats `track(url)` as a pageview override.
if (url) umami?.track?.(url);
else umami?.track?.(window.location.pathname + window.location.search);
} }
} }

View File

@@ -28,7 +28,7 @@ let singleton: AppServices | undefined;
* - Cache service (in-memory) * - Cache service (in-memory)
* *
* The services are configured based on environment variables: * The services are configured based on environment variables:
* - `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Enables Umami analytics * - `UMAMI_WEBSITE_ID` - Enables Umami analytics
* - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting * - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting
* - `SENTRY_DSN` - Enables server-side error reporting * - `SENTRY_DSN` - Enables server-side error reporting
* *

View File

@@ -322,7 +322,7 @@ const nextConfig = {
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
}, },
async rewrites() { async rewrites() {
const umamiUrl = (process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || 'https://analytics.infra.mintel.me').replace('/script.js', ''); const umamiUrl = (process.env.UMAMI_API_ENDPOINT || process.env.UMAMI_SCRIPT_URL || process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || 'https://analytics.infra.mintel.me');
const glitchtipUrl = process.env.SENTRY_DSN const glitchtipUrl = process.env.SENTRY_DSN
? new URL(process.env.SENTRY_DSN).origin ? new URL(process.env.SENTRY_DSN).origin
: 'https://errors.infra.mintel.me'; : 'https://errors.infra.mintel.me';