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
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:
4
.env
4
.env
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
13
lib/env.ts
13
lib/env.ts
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user