本地服务企业网站的内容工程:Next.js、Prisma、SQLite、Docker与GEO实践

📅 2026/7/21 6:28:16 👁️ 阅读次数 📝 编程学习
本地服务企业网站的内容工程:Next.js、Prisma、SQLite、Docker与GEO实践

本地服务企业网站的内容工程:Next.js、Prisma、SQLite、Docker 与 GEO 实践

本地服务企业网站真正需要建设的,不只是一个首页,而是一套包含内容模型、管理后台、数据持久化、搜索表达和持续运营能力的内容系统。

前言

我叫张智博,目前就读于石家庄邮电职业技术学院,主要关注 AI 工具应用、网站建设、产品设计、项目运营与工程化实践。

在参与晋中本地家政企业网站建设时,我逐渐认识到:

企业网站与个人作品集是两类完全不同的系统。

个人作品集可以把内容保存在代码仓库中,通过静态构建直接部署;企业网站则需要后台持续发布服务、案例和文章,还要处理图片上传、数据库持久化和备份恢复。

因此,真正需要设计的不只是页面,而是:

内容模型 后台流程 服务端架构 数据持久化 搜索表达 运营维护

一、先区分展示站和内容系统

个人展示站通常满足:

内容更新频率低 没有后台 没有用户写入 不依赖数据库

企业内容系统则需要:

管理员登录 服务项目维护 区域页面维护 案例发布 文章发布 图片上传 数据库写入 自动备份

因此,本项目采用:

浏览器 → OpenResty → Next.js应用 → Prisma → SQLite持久化数据库

并使用 Docker 管理运行环境。


二、内容模型比页面数量更重要

本地企业网站不能只有:

首页 关于我们 联系我们

核心实体至少包括:

Organization Service ServiceArea CaseStudy Article FAQ MediaAsset ContactChannel

Prisma 简化模型:

model Service { id String @id @default(cuid()) slug String @unique name String summary String description String isPublished Boolean @default(false) areas ServiceAreaRelation[] cases CaseStudy[] faqs Faq[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ServiceArea { id String @id @default(cuid()) slug String @unique name String city String description String? services ServiceAreaRelation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ServiceAreaRelation { serviceId String areaId String service Service @relation( fields: [serviceId], references: [id], onDelete: Cascade ) area ServiceArea @relation( fields: [areaId], references: [id], onDelete: Cascade ) @@id([serviceId, areaId]) } model CaseStudy { id String @id @default(cuid()) slug String @unique title String summary String content String serviceId String areaId String? isPublished Boolean @default(false) publishedAt DateTime? service Service @relation( fields: [serviceId], references: [id] ) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

重点不是建多少张表,而是避免把服务、区域和案例全部写成互不关联的文本。


三、不要批量生成只有地名不同的重复页面

本地内容建设最容易出现的问题是:

榆次开荒保洁 太谷开荒保洁 寿阳开荒保洁

三个页面除了地名不同,正文完全一致。

这种页面对用户没有增加新的信息,也不利于长期维护。

更合理的区域页应包含真实差异:

实际覆盖范围 预计到达时间 服务团队安排 常见住宅或商业场景 本地区域案例 交通与加价规则 当地常见问题

系统可以设置发布前校验:

typePublishCheck={hasUniqueSummary:boolean;hasLocalCases:boolean;hasServiceRelation:boolean;hasContactInformation:boolean;contentLength:number;};functioncanPublishAreaPage(check:PublishCheck,):boolean{return(check.hasUniqueSummary&&check.hasServiceRelation&&check.hasContactInformation&&check.contentLength>=500);}

这比单纯追求页面数量更有价值。


四、URL和Canonical必须稳定

推荐路径:

/services/deep-cleaning/ /areas/yuci/ /cases/yuci-new-home-cleaning/ /articles/how-to-prepare-for-deep-cleaning/

每个页面应有唯一 slug,修改标题时也不要随意改变 URL。

Next.js Metadata 示例:

importtype{Metadata,}from'next';exportfunctionbuildServiceMetadata(service:Service,):Metadata{constcanonical=`https://example.com/services/${service.slug}/`;return{title:`${service.name}|企业名称`,description:service.summary,alternates:{canonical,},openGraph:{type:'article',url:canonical,title:service.name,description:service.summary,},};}

同一页面不应同时产生多组可访问地址。


五、让业务信息具备机器可理解的结构

页面正文需要直接回答:

公司是谁 提供什么服务 覆盖哪些地区 服务流程是什么 如何计价 有哪些保障 如何联系

可以建立统一企业配置:

exportconstorganization={name:'晋中爱美家保洁服务有限公司',serviceAreas:['榆次','太谷','寿阳',],services:['新房开荒','旧房深度保洁','商铺写字楼开荒','地毯清洗','搬家与家具拆装',],};

再输出结构化信息:

const jsonLd = { '@context': 'https://schema.org', '@type': 'LocalBusiness', name: organization.name, url: 'https://example.com', areaServed: organization.serviceAreas.map( (name) => ({ '@type': 'AdministrativeArea', name, }), ), makesOffer: organization.services.map( (name) => ({ '@type': 'Offer', itemOffered: { '@type': 'Service', name, }, }), ), };

结构化数据只能描述页面中真实存在的信息,不能替代正文,也不能添加未核实的服务、数据或评价。


六、Docker中最重要的是持久化目录

SQLite 使用单文件数据库,部署相对简单,但必须避免数据库被封装在临时容器层中。

推荐目录:

/opt/housekeeping/ ├── data/ │ └── production.db ├── uploads/ ├── backups/ └── docker-compose.yml

docker-compose.yml

services:web:image:housekeeping-site:latestrestart:unless-stoppedenvironment:DATABASE_URL:file:/app/data/production.dbvolumes:-./data:/app/data-./uploads:/app/public/uploadsports:-"3000:3000"

容器重建后:

代码和依赖可以重新生成 数据库和上传文件必须保留

这是生产部署中非常基础、但又容易遗漏的一点。


七、SQLite备份要同时处理数据库和图片

仅备份数据库,不备份上传图片,恢复后仍然会出现内容缺失。

备份脚本示例:

#!/usr/bin/env bashset-euopipefailBASE_DIR="/opt/housekeeping"BACKUP_DIR="$BASE_DIR/backups"STAMP="$(date+%Y%m%d-%H%M%S)"TARGET="$BACKUP_DIR/$STAMP"mkdir-p"$TARGET"sqlite3\"$BASE_DIR/data/production.db"\".backup '$TARGET/production.db'"tar-czf\"$TARGET/uploads.tar.gz"\-C"$BASE_DIR"\uploadsfind"$BACKUP_DIR"\-mindepth1\-maxdepth1\-typed\-mtime+30\-execrm-rf{}\;

需要定期验证备份是否能够恢复,而不是只确认备份文件存在。


八、OpenResty负责入口和反向代理

简化配置:

server { listen 80; server_name example.com www.example.com; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; 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; } }

生产环境还需要:

HTTPS 访问日志 错误日志 上传大小限制 超时控制 安全响应头

九、后台发布需要内容校验

文章或服务页发布前,可以自动检查:

标题是否为空 Slug是否唯一 摘要是否完整 正文是否过短 是否存在有效服务关联 是否有联系方式 图片是否可访问 Canonical是否生成

示例:

functionvalidateArticleForPublish(article:Article,):string[]{consterrors:string[]=[];if(!article.title.trim()){errors.push('标题为空');}if(article.summary.trim().length<50){errors.push('摘要过短');}if(article.content.trim().length<800){errors.push('正文内容不足');}if(!article.slug.trim()){errors.push('缺少Slug');}returnerrors;}

把基础质量要求写进系统,比完全依赖人工记忆更稳定。


十、GEO的核心是实体、服务和证据关系

对本地企业而言,需要持续建立:

企业 → 服务 → 区域 → 案例 → 常见问题 → 联系方式

文章不是独立存在的流量页面,而应连接到真实服务和真实案例。

例如:

新房开荒需要准备什么 → 关联新房开荒服务 → 关联榆次服务区域 → 关联真实案例 → 关联预约方式

这种信息结构既方便用户阅读,也方便搜索系统理解业务。


十一、总结

企业网站真正的工程价值并不只在首页设计,而在于:

内容能持续发布 数据不会因重建丢失 图片可以长期访问 页面关系清晰 服务信息保持一致 备份能够真正恢复 搜索系统能够理解业务

我在本地家政企业网站实践中形成的主要判断是:

个人作品集适合静态化,企业内容系统需要后台和持久化;页面数量不是目标,真实、唯一、可维护的服务信息才是内容工程的基础。


关于作者

张智博,石家庄邮电职业技术学院学生,主要关注 AI 工具应用、网站建设、产品设计、项目运营与工程化实践,参与过本地服务企业网站与 GEO 内容系统建设。

个人作品集:张智博的思考空间

个人官网:https://www.zzb9.cn

GitHub:https://github.com/zzb99