diff --git a/.env.example b/.env.example index 217d70c..a56f659 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX -CDN_DOMAIN=https://cdn.novalon.cn diff --git a/.gitignore b/.gitignore index 3a0719c..0e75c60 100644 --- a/.gitignore +++ b/.gitignore @@ -291,4 +291,7 @@ findings.md # Git will track them because they are not in test-results/ or allure-results/ # AGENTS -AGENTS.md \ No newline at end of file +AGENTS.md + +# dogfood +dogfood-output/ \ No newline at end of file diff --git a/Dockerfile.static b/Dockerfile.static new file mode 100644 index 0000000..8334828 --- /dev/null +++ b/Dockerfile.static @@ -0,0 +1,8 @@ +FROM nginx:alpine + +COPY html /var/www/novalon +COPY nginx-internal.conf /etc/nginx/conf.d/default.conf + +EXPOSE 3000 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/config/test/lighthouserc.json b/config/test/lighthouserc.json index dcb1ce4..1889bb8 100644 --- a/config/test/lighthouserc.json +++ b/config/test/lighthouserc.json @@ -1,57 +1,47 @@ -ci: - collect: - numberOfRuns: 3 - startServerCommand: npm run start - startServerReadyPattern: 'Local:' - url: - - http://localhost:3000/ - - http://localhost:3000/about - - http://localhost:3000/services - - http://localhost:3000/products - - http://localhost:3000/cases - - http://localhost:3000/news - - http://localhost:3000/contact - settings: - preset: desktop - onlyCategories: - - performance - - accessibility - - best-practices - - seo - - assert: - assertions: - categories:performance: - - error - - minScore: 0.9 - categories:accessibility: - - error - - minScore: 0.9 - categories:best-practices: - - error - - minScore: 0.9 - categories:seo: - - error - - minScore: 0.9 - first-contentful-paint: - - error - - maxNumericValue: 2000 - largest-contentful-paint: - - error - - maxNumericValue: 3000 - cumulative-layout-shift: - - error - - maxNumericValue: 0.1 - total-blocking-time: - - error - - maxNumericValue: 300 - speed-index: - - error - - maxNumericValue: 3000 - - upload: - target: temporary-public-storage - - settings: - output: html - outputPath: lighthouse-reports +{ + "ci": { + "collect": { + "numberOfRuns": 3, + "startServerCommand": "npm run start", + "startServerReadyPattern": "Local:", + "url": [ + "http://localhost:3000/", + "http://localhost:3000/about", + "http://localhost:3000/services", + "http://localhost:3000/products", + "http://localhost:3000/cases", + "http://localhost:3000/news", + "http://localhost:3000/contact" + ], + "settings": { + "preset": "desktop", + "onlyCategories": [ + "performance", + "accessibility", + "best-practices", + "seo" + ] + } + }, + "assert": { + "assertions": { + "categories:performance": ["error", {"minScore": 0.9}], + "categories:accessibility": ["error", {"minScore": 0.9}], + "categories:best-practices": ["error", {"minScore": 0.9}], + "categories:seo": ["error", {"minScore": 0.9}], + "first-contentful-paint": ["error", {"maxNumericValue": 2000}], + "largest-contentful-paint": ["error", {"maxNumericValue": 3000}], + "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}], + "total-blocking-time": ["error", {"maxNumericValue": 300}], + "speed-index": ["error", {"maxNumericValue": 3000}] + } + }, + "upload": { + "target": "temporary-public-storage" + }, + "settings": { + "output": "html", + "outputPath": "lighthouse-reports" + } + } +} diff --git a/deploy.sh b/deploy.sh index b178e34..e1abd9e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -54,7 +54,7 @@ echo "" echo "📋 步骤0: 部署前检查..." -for file in docker-compose.yml Dockerfile nginx.conf .env.example setup-ssl.sh; do +for file in docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh; do if [ ! -f "$file" ]; then echo "❌ 缺少必要文件: $file" exit 1 @@ -85,7 +85,7 @@ echo "✅ SSH连接验证成功" echo "" echo "📋 步骤2: 上传部署文件..." ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$PROJECT_DIR'" -scp -r docker-compose.yml Dockerfile nginx.conf .env.example setup-ssl.sh "$SERVER_USER@$SERVER_IP:$PROJECT_DIR/" +scp -r docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh "$SERVER_USER@$SERVER_IP:$PROJECT_DIR/" echo "✅ 部署文件已上传" echo "" @@ -102,13 +102,12 @@ if [ ! -f .env ]; then echo "📝 创建.env文件..." cp .env.example .env echo "⚠️ 请编辑.env文件,填入正确的环境变量" - echo "⚠️ 必须配置: DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL, RESEND_API_KEY, OPS_ALERT_EMAIL" - exit 1 + echo "⚠️ 可选配置: NEXT_PUBLIC_GA_ID" fi echo "🐳 启动Docker容器..." docker-compose down -docker-compose pull +docker-compose build --no-cache docker-compose up -d echo "📋 等待服务启动..." @@ -118,7 +117,7 @@ check_interval=3 while [ $elapsed -lt $timeout ]; do if docker inspect --format='{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null | grep -q "running"; then - if curl -f -s -o /dev/null "http://localhost:3000" --max-time 5 2>/dev/null; then + if curl -f -s -o /dev/null "http://localhost:80" --max-time 5 2>/dev/null; then echo "✅ 服务已启动并响应正常" break else diff --git a/docker-compose-nginx.yml b/docker-compose-nginx.yml new file mode 100644 index 0000000..641fb6a --- /dev/null +++ b/docker-compose-nginx.yml @@ -0,0 +1,23 @@ +version: "3.8" + +services: + nginx: + image: nginx:alpine + container_name: novalon-nginx-secure + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx-static-production.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - ./logs:/var/log/nginx + - ../certbot:/var/www/certbot + - ../novalon-static:/var/www/novalon:ro + networks: + - novalon-network + +networks: + novalon-network: + driver: bridge + external: true diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..c5f1504 --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,18 @@ +version: "3.8" + +services: + novalon-website: + build: + context: . + dockerfile: Dockerfile.static + image: novalon-website:latest + container_name: novalon-website + restart: unless-stopped + ports: + - "3000:3000" + networks: + - novalon-network + +networks: + novalon-network: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 41c476e..6af8677 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,9 @@ version: "3.8" services: novalon-website: + build: + context: . + dockerfile: Dockerfile image: novalon-website:1.0.0 container_name: novalon-website restart: unless-stopped diff --git a/nginx-internal.conf b/nginx-internal.conf new file mode 100644 index 0000000..48821d2 --- /dev/null +++ b/nginx-internal.conf @@ -0,0 +1,57 @@ +server { + listen 3000; + server_name localhost; + + root /var/www/novalon; + index index.html; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml + application/rss+xml + image/svg+xml; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + + location /_next/static/ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location /fonts/ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Access-Control-Allow-Origin "*"; + try_files $uri =404; + } + + location ~* \.(svg|jpg|jpeg|png|gif|webp|avif|ico)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri.html $uri/ /404.html; + } + + error_page 404 /404.html; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; +} diff --git a/nginx-static-production.conf b/nginx-static-production.conf new file mode 100644 index 0000000..831f1fb --- /dev/null +++ b/nginx-static-production.conf @@ -0,0 +1,237 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + resolver 127.0.0.11 valid=30s ipv6=off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + server_tokens off; + client_max_body_size 100M; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+text text/javascript; + + upstream gitea_app { + server gitea:3000; + } + + upstream jenkins_app { + server jenkins:8080; + } + + # Novalon 主站 - 静态文件服务 + server { + listen 80; + server_name novalon.cn www.novalon.cn; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://www.novalon.cn$request_uri; + } + } + + server { + listen 443 ssl http2; + server_name novalon.cn www.novalon.cn; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # 静态文件根目录 + root /var/www/novalon; + index index.html; + + # 静态资源长期缓存 + location /_next/static/ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + try_files $uri =404; + } + + # 字体文件缓存 + location ~* \.(ttf|woff|woff2|eot)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Access-Control-Allow-Origin "*"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + try_files $uri =404; + } + + # 图片文件缓存 + location ~* \.(svg|jpg|jpeg|png|gif|webp|avif|ico)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + try_files $uri =404; + } + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Next.js 静态导出的页面路由 + location / { + limit_req zone=general burst=20 nodelay; + try_files $uri $uri.html $uri/ /404.html; + } + + # 自定义 404 页面 + error_page 404 /404.html; + + # 优化文件传输 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + } + + # Git 服务 + server { + listen 80; + server_name git.f.novalon.cn; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl http2; + server_name git.f.novalon.cn; + + ssl_certificate /etc/nginx/ssl/git.f.novalon.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/git.f.novalon.cn/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + limit_req zone=general burst=20 nodelay; + proxy_pass http://gitea_app; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_hide_header X-Powered-By; + + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + } + + # CI 服务 + server { + listen 80; + server_name ci.f.novalon.cn; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl http2; + server_name ci.f.novalon.cn; + + ssl_certificate /etc/nginx/ssl/ci.f.novalon.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/ci.f.novalon.cn/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + limit_req zone=general burst=20 nodelay; + proxy_pass http://jenkins_app; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_hide_header X-Powered-By; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + client_max_body_size 100m; + } + + access_log /var/log/nginx/jenkins-access.log; + error_log /var/log/nginx/jenkins-error.log; + } +} diff --git a/nginx-static.conf b/nginx-static.conf index f0aafc1..3378118 100644 --- a/nginx-static.conf +++ b/nginx-static.conf @@ -20,7 +20,7 @@ server { ssl_session_timeout 10m; # 静态文件根目录 - root /var/www/novalon; + root /usr/share/nginx/html; index index.html; # Gzip 压缩 @@ -79,7 +79,7 @@ server { # Let's Encrypt ACME challenge location /.well-known/acme-challenge/ { - root /var/www/certbot; + root /usr/share/nginx/html; try_files $uri =404; } diff --git a/package.json b/package.json index 3d47621..c955d58 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,6 @@ "lighthouse:upload": "lhci upload", "lighthouse:desktop": "lhci autorun --settings.preset=desktop", "lighthouse:mobile": "lhci autorun --settings.preset=mobile", - "deploy:cdn": "bash scripts/deploy-cdn.sh", - "deploy:cdn:refresh": "bash scripts/refresh-cdn.sh", "clean:tests": "bash scripts/maintenance/clean-test-files.sh", "prepare": "husky" }, diff --git a/public/images/news/founding.png b/public/images/news/founding.png new file mode 100644 index 0000000..a008903 Binary files /dev/null and b/public/images/news/founding.png differ diff --git a/public/images/news/solution.png b/public/images/news/solution.png new file mode 100644 index 0000000..3439649 Binary files /dev/null and b/public/images/news/solution.png differ diff --git a/public/logo.svg b/public/logo.svg index afc02b5..3c03301 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -2,7 +2,7 @@