This commit is contained in:
25
.env.example
Normal file
25
.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# WooCommerce API (Legacy - not currently used)
|
||||||
|
WOOCOMMERCE_URL=https://klz-cables.com
|
||||||
|
WOOCOMMERCE_CONSUMER_KEY=
|
||||||
|
WOOCOMMERCE_CONSUMER_SECRET=
|
||||||
|
WORDPRESS_APP_PASSWORD=
|
||||||
|
|
||||||
|
# Umami Analytics
|
||||||
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
|
||||||
|
|
||||||
|
# GlitchTip (Sentry protocol)
|
||||||
|
SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
|
||||||
|
# SMTP Configuration
|
||||||
|
MAIL_HOST=smtp.eu.mailgun.org
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
|
||||||
|
MAIL_RECIPIENTS=info@klz-cables.com
|
||||||
|
|
||||||
|
# Redis Configuration (optional)
|
||||||
|
REDIS_URL=redis://redis:6379/2
|
||||||
|
REDIS_KEY_PREFIX=klz:
|
||||||
@@ -39,6 +39,9 @@ jobs:
|
|||||||
# docker push registry.infra.mintel.me/mintel/klz-cables.com:latest
|
# docker push registry.infra.mintel.me/mintel/klz-cables.com:latest
|
||||||
|
|
||||||
- name: Deploy to production server
|
- name: Deploy to production server
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||||
@@ -46,13 +49,11 @@ jobs:
|
|||||||
|
|
||||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new deploy@alpha.mintel.me << EOF
|
ssh -o StrictHostKeyChecking=accept-new deploy@alpha.mintel.me bash -c "'
|
||||||
set -e
|
set -e
|
||||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me \
|
echo \"$REGISTRY_PASS\" | docker login registry.infra.mintel.me -u \"$REGISTRY_USER\" --password-stdin
|
||||||
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
|
||||||
|
|
||||||
cd /home/deploy/sites/klz-cables.com
|
cd /home/deploy/sites/klz-cables.com
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d --force-recreate --remove-orphans
|
docker compose up -d --force-recreate --remove-orphans
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
EOF
|
'"
|
||||||
66
README.md
66
README.md
@@ -225,31 +225,61 @@ GET /robots.txt
|
|||||||
|
|
||||||
## 🚀 Deployment
|
## 🚀 Deployment
|
||||||
|
|
||||||
### Vercel (Recommended)
|
### Automatic Deployment (Current Setup)
|
||||||
```bash
|
|
||||||
# Install Vercel CLI
|
|
||||||
npm i -g vercel
|
|
||||||
|
|
||||||
# Deploy
|
The project uses **Gitea Actions** for CI/CD. Every push to `main` triggers:
|
||||||
vercel --prod
|
|
||||||
|
1. **Build**: Docker image built for `linux/arm64`
|
||||||
|
2. **Push**: Image pushed to `registry.infra.mintel.me`
|
||||||
|
3. **Deploy**: SSH to production server, pull and restart containers
|
||||||
|
|
||||||
|
**Workflow**: `.gitea/workflows/deploy.yml`
|
||||||
|
|
||||||
|
**Required Secrets** (configure in Gitea repository settings):
|
||||||
|
- `REGISTRY_USER` - Docker registry username
|
||||||
|
- `REGISTRY_PASS` - Docker registry password
|
||||||
|
- `ALPHA_SSH_KEY` - SSH private key for deployment
|
||||||
|
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
|
||||||
|
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
|
||||||
|
- `SENTRY_DSN` - Error tracking DSN
|
||||||
|
|
||||||
|
### Manual Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into production server
|
||||||
|
ssh deploy@alpha.mintel.me
|
||||||
|
|
||||||
|
# Navigate to project
|
||||||
|
cd /home/deploy/sites/klz-cables.com
|
||||||
|
|
||||||
|
# Pull latest image and restart
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d --force-recreate --remove-orphans
|
||||||
|
docker image prune -f
|
||||||
```
|
```
|
||||||
|
|
||||||
### Static Export
|
Or use the convenience script:
|
||||||
```bash
|
```bash
|
||||||
# Build and export
|
bash scripts/deploy-webhook.sh
|
||||||
npm run build
|
|
||||||
npm run export
|
|
||||||
|
|
||||||
# Deploy to any static host
|
|
||||||
# Upload /out directory
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Netlify
|
### Architecture
|
||||||
```bash
|
|
||||||
# Connect repository
|
|
||||||
# Set build command: npm run build
|
|
||||||
# Set publish directory: out
|
|
||||||
```
|
```
|
||||||
|
Client → Traefik (TLS) → Varnish (Cache) → Next.js App
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domains**:
|
||||||
|
- `klz-cables.com` - Production
|
||||||
|
- `www.klz-cables.com` - Production (www)
|
||||||
|
- `staging.klz-cables.com` - Staging
|
||||||
|
|
||||||
|
**Services**:
|
||||||
|
- `app`: Next.js application (port 3000)
|
||||||
|
- `varnish`: HTTP cache layer
|
||||||
|
- `traefik`: Reverse proxy (external)
|
||||||
|
|
||||||
|
For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
|
||||||
|
|
||||||
## 📈 Performance
|
## 📈 Performance
|
||||||
|
|
||||||
|
|||||||
@@ -16,21 +16,23 @@ export default function ContactForm() {
|
|||||||
setStatus('submitting');
|
setStatus('submitting');
|
||||||
|
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const email = formData.get('email') as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sendContactFormAction(formData);
|
const result = await sendContactFormAction(formData);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
trackEvent('contact_form_submission', {
|
trackEvent('contact_form_submission', {
|
||||||
form_type: 'general',
|
form_type: 'general',
|
||||||
email: formData.get('email') as string,
|
email,
|
||||||
});
|
});
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
(e.target as HTMLFormElement).reset();
|
(e.target as HTMLFormElement).reset();
|
||||||
} else {
|
} else {
|
||||||
|
console.error('Contact form submission failed:', { email, error: result.error });
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Form submission error:', error);
|
console.error('Contact form submission error:', { email, error });
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
197
docs/DEPLOYMENT.md
Normal file
197
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
This document describes the deployment setup for KLZ Cables website.
|
||||||
|
|
||||||
|
## Automatic Deployment (Gitea Actions)
|
||||||
|
|
||||||
|
The project uses Gitea Actions for CI/CD. On every push to the `main` branch:
|
||||||
|
|
||||||
|
1. **Build**: Docker image is built with platform `linux/arm64`
|
||||||
|
2. **Push**: Image is pushed to `registry.infra.mintel.me/mintel/klz-cables.com:latest`
|
||||||
|
3. **Deploy**: SSH connection to production server pulls and restarts containers
|
||||||
|
|
||||||
|
### Workflow File
|
||||||
|
|
||||||
|
Location: `.gitea/workflows/deploy.yml`
|
||||||
|
|
||||||
|
### Required Secrets
|
||||||
|
|
||||||
|
Configure these in your Gitea repository settings:
|
||||||
|
|
||||||
|
- `REGISTRY_USER` - Docker registry username
|
||||||
|
- `REGISTRY_PASS` - Docker registry password
|
||||||
|
- `ALPHA_SSH_KEY` - SSH private key for deployment user
|
||||||
|
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics website ID
|
||||||
|
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami analytics script URL
|
||||||
|
- `SENTRY_DSN` - Sentry/GlitchTip DSN for error tracking
|
||||||
|
|
||||||
|
## Manual Deployment
|
||||||
|
|
||||||
|
If you need to deploy manually:
|
||||||
|
|
||||||
|
### On the Production Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into the server
|
||||||
|
ssh deploy@alpha.mintel.me
|
||||||
|
|
||||||
|
# Navigate to the project directory
|
||||||
|
cd /home/deploy/sites/klz-cables.com
|
||||||
|
|
||||||
|
# Pull the latest image
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# Restart containers
|
||||||
|
docker compose up -d --force-recreate --remove-orphans
|
||||||
|
|
||||||
|
# Clean up old images
|
||||||
|
docker image prune -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Workflow Not Triggering
|
||||||
|
|
||||||
|
1. Check Gitea Actions is enabled in repository settings
|
||||||
|
2. Verify the workflow file syntax
|
||||||
|
3. Check runner availability with label `docker`
|
||||||
|
|
||||||
|
### Build Failures
|
||||||
|
|
||||||
|
1. Check build logs in Gitea Actions tab
|
||||||
|
2. Verify all secrets are configured correctly
|
||||||
|
3. Ensure Dockerfile is valid
|
||||||
|
|
||||||
|
### Deployment Failures
|
||||||
|
|
||||||
|
1. Verify SSH key has correct permissions (600)
|
||||||
|
2. Check deploy user has Docker permissions
|
||||||
|
3. Verify registry credentials are correct
|
||||||
|
4. Check server disk space: `df -h`
|
||||||
|
|
||||||
|
### Container Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f app
|
||||||
|
docker compose logs -f varnish
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
docker compose exec app wget -O- http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Client
|
||||||
|
↓
|
||||||
|
Traefik (TLS termination, routing)
|
||||||
|
↓
|
||||||
|
Varnish (HTTP caching)
|
||||||
|
↓
|
||||||
|
Next.js App (port 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
- **app**: Next.js application
|
||||||
|
- **varnish**: HTTP cache layer
|
||||||
|
- **traefik**: Reverse proxy (external network)
|
||||||
|
|
||||||
|
### Domains
|
||||||
|
|
||||||
|
- `klz-cables.com` - Production
|
||||||
|
- `www.klz-cables.com` - Production (www)
|
||||||
|
- `staging.klz-cables.com` - Staging
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Build-time (in Dockerfile/Workflow)
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID`
|
||||||
|
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL`
|
||||||
|
- `NEXT_PUBLIC_SENTRY_DSN`
|
||||||
|
|
||||||
|
### Runtime (in docker-compose.yml)
|
||||||
|
|
||||||
|
- `SENTRY_DSN`
|
||||||
|
- `REDIS_URL`
|
||||||
|
- `REDIS_KEY_PREFIX`
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
- App: `https://klz-cables.com/health`
|
||||||
|
- Varnish: Configured in docker-compose.yml
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application logs
|
||||||
|
docker compose logs -f app
|
||||||
|
|
||||||
|
# Varnish logs
|
||||||
|
docker compose logs -f varnish
|
||||||
|
|
||||||
|
# All logs
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analytics
|
||||||
|
|
||||||
|
- Umami: Configured via environment variables
|
||||||
|
- Sentry/GlitchTip: Error tracking
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
To rollback to a previous version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the server
|
||||||
|
cd /home/deploy/sites/klz-cables.com
|
||||||
|
|
||||||
|
# Pull a specific version (if tagged)
|
||||||
|
docker pull registry.infra.mintel.me/mintel/klz-cables.com:TAG
|
||||||
|
|
||||||
|
# Or rebuild from a specific commit
|
||||||
|
# (requires access to the repository on the server)
|
||||||
|
|
||||||
|
# Restart with the older image
|
||||||
|
docker compose up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Cache Invalidation
|
||||||
|
|
||||||
|
Varnish caches static assets. To clear cache:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec varnish varnishadm "ban req.url ~ ."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Configuration
|
||||||
|
|
||||||
|
Edit `varnish/default.vcl` and restart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose restart varnish
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- All secrets are stored in Gitea repository settings
|
||||||
|
- SSH key is injected at deployment time
|
||||||
|
- Registry credentials are not stored in the repository
|
||||||
|
- Deploy webhook requires secret token
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check logs first
|
||||||
|
2. Review this documentation
|
||||||
|
3. Contact the development team
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
|
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
|
||||||
|
import { getServerAppServices } from '../create-services.server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definition for the Umami global object.
|
* Type definition for the Umami global object.
|
||||||
@@ -79,11 +80,16 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
|
|
||||||
if (!websiteId) return;
|
if (!websiteId) return;
|
||||||
|
|
||||||
|
const logger = getServerAppServices().logger.child({ component: 'analytics' });
|
||||||
|
logger.info('Sending analytics event', { eventName, props });
|
||||||
|
|
||||||
fetch(`${umamiUrl}/api/send`, {
|
fetch(`${umamiUrl}/api/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
||||||
body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }),
|
body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }),
|
||||||
}).catch(() => {});
|
}).catch((error) => {
|
||||||
|
logger.error('Failed to send analytics event', { eventName, props, error });
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,11 +127,16 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
|
|
||||||
if (!websiteId || !url) return;
|
if (!websiteId || !url) return;
|
||||||
|
|
||||||
|
const logger = getServerAppServices().logger.child({ component: 'analytics' });
|
||||||
|
logger.info('Sending analytics pageview', { url });
|
||||||
|
|
||||||
fetch(`${umamiUrl}/api/send`, {
|
fetch(`${umamiUrl}/api/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
||||||
body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }),
|
body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }),
|
||||||
}).catch(() => {});
|
}).catch((error) => {
|
||||||
|
logger.error('Failed to send analytics pageview', { url, error });
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user