企业官网正式上线
基于 Strapi v5 + Nuxt 4 的三语企业官网正式发布。涵盖 CMS 内容架构、Admin 后台定制、三语前端页面、认证文件网关、缓存策略与安全加固,并集成微信公众号文章导入与 TLS 证书自动化。
概览
#企业官网是双仓库项目——Strapi v5 CMS 管内容,Nuxt 4 前端管渲染。编辑人员在多语言 Admin 后台维护产品、新闻和资源,前端在 bioer.com / en.bioer.com / jp.bioer.com 下按域名渲染中英日三种语言,集成认证、搜索、文件门控交付,以及微信公众号文章导入和 TLS 证书自动签发。
架构示意:
用户浏览器
|
v
Nginx 80/443
|-- bioer.com / en.bioer.com / jp.bioer.com -> Nuxt Node 127.0.0.1:3000
|-- cms.bioer.com -> Strapi 127.0.0.1:1337
|-- media.bioer.com -> 媒体 CDN(Strapi /uploads 渲染域名)
中英日三语共用同一份 Nuxt 构建产物和一个 Node 进程,语言由请求 Host 决定。
CMS — Strapi 后端
#内容架构
十二个 Collection Type 构成内容骨架:产品带多级分类树、新闻带分类、资源带分类文件库,about/product/article 三种 Banner,首页 home_module(轮播 / 产品展示)模块,联系表单收件箱(生命周期钩子发邮件通知),以及 components/search/seo 组件。
内容组(Content Group)描述字段与翻译键管理:编辑者指定内容所属分组,前端按组渲染布局;多语言字段空值时显示翻译占位符而非空白。
自研插件
两个本地插件扩展 Content Manager:
product-category-filter— 列表页按分层分类树筛选产品、文章和资源product-category-sort— 分类树拖拽排序(基于 vendoredsortable-entries),排序结果用于前端侧边栏
第三方插件:strapi-plugin-img-convert(上传时生成响应式 WebP/JPEG/PNG 变体)、@_sh/strapi-plugin-ckeditor(富文本)、strapi-soft-delete-plugin(软删除 + 可选定时清理)、@sensinum/strapi-table-field(产品规格表格)。
// 图片响应式配置 — 四个尺寸,webp 优先,保留 JPEG/PNG 兜底
breakpoints: {
xlarge: { breakpoint: 1566, formats: ['webp', 'jpeg', 'png'] },
large: { breakpoint: 1280, formats: ['webp', 'jpeg', 'png'] },
medium: { breakpoint: 768, formats: ['webp', 'jpeg', 'png'] },
small: { breakpoint: 640, formats: ['webp', 'jpeg', 'png'] },
},
Admin 后台定制
品牌化:自定义 Logo/Favicon,一套蓝色主色调贯穿按钮、链接、边框、底色。多语言覆盖完整:
- Admin 界面文本经自定义 locale 文件翻译,硬编码英文在运行时替换
- 内容模型字段标签跟随 Admin 语言
- 翻译缺失收集器(
report:i18n-nonkey)按需生成审计报告
Content Manager 导航按业务域分组,顺序在 src/admin/app.ts 集中管理。DOM 观察器保证 Vite/HMR 热更新后标题替换、按钮标签、分组标签依然正确。产品/文章编辑页有"前端查看"按钮,一键新标签打开线上页面。
内容工具链
- 微信公众号导入:
scrape-wechat-articles.py抓取 +import-wechat-articles.cjs导入,配套purge-soft-deleted.cjs清理 - 日文站迁移:
migrate:ja/publish:ja/validate:ja-plan脚本链,从中文内容迁移、发布并校验日文站点 - 首页支持
home_module切换轮播/产品展示,编辑者不改代码即可通过 Strapi API 实时同步首页展示
Web — Nuxt 前端
#技术栈
Nuxt 4 + @nuxt/ui 4 + Tailwind CSS 4 + daisyUI 5 + @nuxtjs/i18n 10。en.json/zh.json/ja.json 三语,按域名切换(en.bioer.com / bioer.com / jp.bioer.com),中文站含 www、.com.cn 别名。自托管 Inter 字体,WOFF2 覆盖 400–900 字重。媒体资源经 media.bioer.com CDN 域名渲染。
公共页面
| 页面 | 功能 |
|---|---|
| 首页 | 轮播 / 产品展示模块,推荐产品与新闻摘要 |
| 产品 | 分类侧边栏、卡片列表、详情页(概览/规格/图库/下载 Tab)、响应式画廊、Markdown 渲染 |
| 新闻 | 按分类筛选列表、CKEditor 富文本详情、相关文章 |
| 资源 | 可筛选资源库,登录门控的文件预览与下载;hide 模式下非公开资源不出现在列表/搜索 |
| 关于 | 公司介绍,荣誉墙卡片、触摸滚动和键盘导航 |
| 联系 | 联系表单,客户端校验 + 服务端提交 |
| 搜索 | 客户端全文搜索(产品/新闻/资源),多词匹配、加权评分、类型/分类筛选、分页、URL 查询状态 |
| 认证 | 登录、注册、隐私政策、服务条款 |
产品/新闻正文支持受限 Markdown:miniMarkdown 先转义再渲染安全子集,sanitizeHtml 做白名单消毒。
安全架构
认证用 httpOnly JWT Cookie(HTTPS 下加 secure)。会话刷新走 /api/auth/session,未登录返回 { user: null };登录、注册、登出、当前用户端点基于同一套 Cookie 流程。
# 安全响应头中间件(实际生效)
Content-Security-Policy: base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), geolocation=(), microphone=()
Strict-Transport-Security: max-age=31536000; includeSubDomains # HTTPS 时
requestSecurity 统一处理跨站防护:数据接收端点按 Origin/Fetch Metadata 拒绝跨站 POST,限制请求体大小,只接受 application/json。CKEditor 产出的 HTML 渲染前经属性白名单消毒:移除脚本、事件处理器、不安全 URL、任意 CSS 函数、嵌入内容和表单控件。
文件网关
产品/资源文件预览与下载经 Nitro 路由流式传输,浏览器接触不到原始 Strapi 媒体 URL。预览仅限浏览器安全类型(PDF/PNG/JPG/JPEG/WebP)。门控检查登录态、资源可见性和可选邮箱白名单。
基础设施
#服务端 Nitro 层
| 端点 | 功能 |
|---|---|
/api/strapi/** | 公共 Strapi 内容代理,限制已知 Collection 根路径,含限流(默认 2400 req/60s) |
/api/auth/** | 登录、注册、登出、会话刷新、当前用户 |
/api/contact-message | 联系表单提交 + 速率限制 + SMTP 通知 |
/api/resources/** | 资源文件下载/预览网关 |
/api/products/[id]/files/** | 产品文件下载/预览网关 |
/sitemap.xml | 动态产品/新闻 URL + 静态回落 |
/robots.txt | 爬虫指令 |
缓存策略
page-cache-control 中间件按路径分类下发 Cache-Control,TTL 由环境变量控制:
| 路径 | 响应头 | 默认 s-maxage |
|---|---|---|
| 动态 HTML | public, max-age=0, s-maxage=N | 300 |
/privacy-policy、/terms-of-use | 同上,更长 | 3600 |
/login、/register、/api/** | no-store / private | — |
NUXT_PUBLIC_HTML_CACHE_MAX_AGE=300 # 动态 HTML
NUXT_PUBLIC_STRAPI_API_CACHE_MAX_AGE=300 # /api/strapi/** 代理
NUXT_PUBLIC_STATIC_PAGE_CACHE_MAX_AGE=3600 # 静态页
运维
- TLS 自动化:
scripts/tls-automation/bioer-tls.mjs走 DNS-01 自动签发/续期证书,按域名分发,先等 Nginx TLS 重载再验证 - 备份与日志:
backup-operations.md、logging-operations.md固化数据库备份与日志采集流程 - 部署:PM2 守护 + systemd Nginx 反向代理
# 构建与启动(三语共用一个进程,按 Host 分流)
pnpm typecheck && pnpm build
PORT=3000 pm2 start .output/server/index.mjs --name bioer-web --cwd /var/www/site/web --time
pm2 save && pm2 startup
# 更新
cd /var/www/site && git pull && pnpm install && pnpm typecheck && pnpm build
pm2 restart bioer-web
改进
#- Strapi 代理查询参数白名单(
fields/filters/locale/pagination/populate/publicationState/sort),分页上限每页 100,公共代理限流 - 文件下载路由流式传输,不整文件读入内存
- 搜索在客户端本地索引,不依赖外部搜索服务
- Sitemap 从 Strapi 取动态 URL,后端不可达时回落静态条目并返回
x-sitemap-status: static-fallback - 服务启动前
scripts/validate-config.cjs校验环境变量 - 会话端点对游客返回
{ user: null }而非 401 - 软删除默认关闭自动清理,需显式环境变量启用
- 资源访问
hide模式:非公开资源不出现在列表与搜索结果,减少暴露面 - 三语按域名共享一个构建产物与 Node 进程,语言由
Host决定,无重复部署
修复
#- Strapi Admin Vite 开发 host 显式白名单,防隧道服务 EPERM / host-check
- 本地化报告按需生成,不再以仓库快照提交
- 未到发布日期的草稿不再出现在公共 API(代理显式
publicationState: live) - CKEditor 过宽表格换行处理,防移动端横向溢出
- 软删除条目默认不出现在公共 API
- Vite HMR 后 Admin 标题替换/按钮标签丢失 — DOM 观察器兜底
已知问题
#- Strapi Admin 构建链以 CommonJS 加载 Vite Node API,输出
CJS deprecation提示,不影响功能 - 内存速率限制器在 PM2 集群/多节点下失效,届时需切换 Redis 共享存储
升级说明
#从 v0.2.0 升级:
cd /var/www/site
git pull && pnpm install && pnpm typecheck && pnpm build
pm2 restart bioer-web
新增环境变量(可选,不设置则用默认值):
NUXT_PUBLIC_HTML_CACHE_MAX_AGE=300
NUXT_PUBLIC_STRAPI_API_CACHE_MAX_AGE=300
NUXT_PUBLIC_STATIC_PAGE_CACHE_MAX_AGE=3600
NUXT_PUBLIC_SITE_DOMAIN_JA=jp.bioer.com
NUXT_PUBLIC_MEDIA_URL=https://media.bioer.com
NUXT_PUBLIC_RESOURCES_ACCESS_MODE=hide
SOFT_DELETE_AUTO_PURGE_ENABLED=false
SOFT_DELETE_AUTO_PURGE_TTL_DAYS=30