Add static deployment build and guide

This commit is contained in:
Jacky Ser
2026-08-11 17:37:11 +08:00
parent 775ccf82ab
commit 42c21dd024
11 changed files with 168 additions and 2 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.git
.gitignore
node_modules
dist
dist-static
.next
.vinext
.wrangler
*.log

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ yarn-error.log*
.vercel .vercel
/dist/ /dist/
/dist-static/
/.wrangler/ /.wrangler/
/outputs/ /outputs/
/work/ /work/

75
DEPLOY_STATIC.md Normal file
View File

@@ -0,0 +1,75 @@
# 高徳乐 AI 舵机选型助手:静态部署说明
本仓库已经提供纯静态构建方式。页面的产品数据、图片、选型计算、筛选和弹窗交互都在浏览器中运行,不依赖 Node.js、数据库、API 或 Cloudflare Worker 的运行时。
## 给 Git AI 的部署任务
可以直接把下面这段话发给仓库里的 AI
> 请将本仓库部署为纯静态网站,不要启动 Node.js 后端、Vinext Server 或 Cloudflare Worker。先执行 `npm ci`,再执行 `npm run build:static`。把生成的 `dist-static/` 目录作为网站根目录发布到当前云服务器,并配置 Nginx 将未知路径回退到 `index.html`。如果服务器使用 Docker请使用仓库中的 `Dockerfile.static` 构建并运行,容器端口为 80。部署完成后检查首页、`#selector`、`#products` 和 `#precision-gears` 页面区域,以及产品图片是否能正常加载。
## 方式一Nginx / 静态文件服务器
在服务器项目目录执行:
```bash
git clone https://git.databerrypi.com/MJ-IT/gdm-Servo-model.git
cd gdm-Servo-model
npm ci
npm run build:static
```
`dist-static/` 下的全部文件复制到 Nginx 网站根目录,例如:
```bash
sudo mkdir -p /var/www/gdm-servo
sudo rsync -a --delete dist-static/ /var/www/gdm-servo/
```
Nginx 的站点配置可以参考 [`deploy/nginx-static.conf`](deploy/nginx-static.conf)。其中最关键的是:
```nginx
root /var/www/gdm-servo;
location / {
try_files $uri $uri/ /index.html;
}
```
以后更新时,在仓库目录执行:
```bash
git pull --ff-only origin main
npm ci
npm run build:static
sudo rsync -a --delete dist-static/ /var/www/gdm-servo/
```
运行时只需要 Nginx不需要保留 Node.js 进程。
## 方式二Docker
仓库已经提供 [`Dockerfile.static`](Dockerfile.static)
```bash
docker build -f Dockerfile.static -t gdm-servo-static:latest .
docker run -d --name gdm-servo-static --restart unless-stopped -p 80:80 gdm-servo-static:latest
```
如果服务器已有 80 端口服务,可以改成 `-p 8080:80`,再由现有 Nginx 反向代理到 `127.0.0.1:8080`
## 本地检查
```bash
npm run build:static
python3 -m http.server 8080 --directory dist-static
```
然后打开 `http://服务器IP:8080`。不要直接双击 `index.html`,因为浏览器会限制本地文件加载模块和图片资源。
## 部署注意事项
- 静态版没有服务端接口;当前“申请样品”和“联系我们”表单只在浏览器中生成演示编号,不会把线索写入 CRM 或数据库。
- 如果后续要真正保存客户线索,需要另行接入表单 API、企业微信、CRM 或数据库,不应把密钥写进前端代码。
- 页面使用根路径资源(例如 `/catalog-products/...`),因此建议部署在域名根路径。如果必须部署到 `/servo/` 子目录,需要同步调整 Vite 的 `base` 和图片资源路径。
- 每次产品数据或图片变更后,都要重新执行 `npm run build:static`,再同步整个 `dist-static/` 目录。

12
Dockerfile.static Normal file
View File

@@ -0,0 +1,12 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build:static
FROM nginx:1.27-alpine
COPY deploy/nginx-static.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist-static /usr/share/nginx/html
EXPOSE 80

View File

@@ -16,6 +16,10 @@ npm run dev
npm run build npm run build
``` ```
## Static Deployment
This project also supports a no-backend static build. Run `npm run build:static` and publish the generated `dist-static/` directory to Nginx, object storage, or another static file server. See [DEPLOY_STATIC.md](DEPLOY_STATIC.md) for instructions that can be handed directly to a Git AI deployment agent.
This starter does not use `wrangler.jsonc`. This starter does not use `wrangler.jsonc`.
## Included Shape ## Included Shape

View File

@@ -1200,14 +1200,15 @@ export default function Home() {
<div <div
className="catalog-modal-backdrop" className="catalog-modal-backdrop"
role="presentation" role="presentation"
onMouseDown={() => setSelectedCatalogProduct(null)} onMouseDown={(event) => {
if (event.target === event.currentTarget) setSelectedCatalogProduct(null);
}}
> >
<section <section
className="catalog-modal" className="catalog-modal"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="catalog-modal-title" aria-labelledby="catalog-modal-title"
onMouseDown={(event) => event.stopPropagation()}
> >
<button <button
className="catalog-modal-close" className="catalog-modal-close"

17
deploy/nginx-static.conf Normal file
View File

@@ -0,0 +1,17 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:js|css|png|jpg|jpeg|webp|gif|svg|ico|woff2)$ {
try_files $uri =404;
expires 1d;
add_header Cache-Control "public, max-age=86400";
}
}

View File

@@ -8,6 +8,7 @@
"scripts": { "scripts": {
"dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev", "dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev",
"build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build", "build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build",
"build:static": "vite build --config vite.static.config.ts",
"start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start", "start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
"test": "npm run build && node --test tests/rendered-html.test.mjs", "test": "npm run build && node --test tests/rendered-html.test.mjs",
"lint": "eslint . --ignore-pattern dist --ignore-pattern .next", "lint": "eslint . --ignore-pattern dist --ignore-pattern .next",

16
static/entry.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import Home from "../app/page";
import "../app/globals.css";
const root = document.getElementById("root");
if (!root) {
throw new Error("Static app root was not found.");
}
createRoot(root).render(
<StrictMode>
<Home />
</StrictMode>,
);

14
static/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#185ee8" />
<meta name="description" content="高徳乐 AI 舵机选型助手,支持产品目录浏览、参数筛选和 AI 初选。" />
<title>高徳乐 AI 舵机选型助手</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./entry.tsx"></script>
</body>
</html>

16
vite.static.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
const projectRoot = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
root: resolve(projectRoot, "static"),
plugins: [react()],
publicDir: resolve(projectRoot, "public"),
build: {
outDir: resolve(projectRoot, "dist-static"),
emptyOutDir: true,
},
});