本地服务企业网站的内容工程: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
}
重点不是建多少张表,而是避免把服务、区域和案例全部写成互不关联的文本。
三、不要批量生成只有地名不同的重复页面
本地内容建设最容易出现的问题是:
榆次开荒保洁
太谷开荒保洁
寿阳开荒保洁
三个页面除了地名不同,正文完全一致。
这种页面对用户没有增加新的信息,也不利于长期维护。
更合理的区域页应包含真实差异:
实际覆盖范围
预计到达时间
服务团队安排
常见住宅或商业场景
本地区域案例
交通与加价规则
当地常见问题
系统可以设置发布前校验:
type PublishCheck = {
hasUniqueSummary: boolean;
hasLocalCases: boolean;
hasServiceRelation: boolean;
hasContactInformation: boolean;
contentLength: number;
};
function canPublishAreaPage(
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 示例:
import type {
Metadata,
} from 'next';
export function buildServiceMetadata(
service: Service,
): Metadata {
const canonical =
`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,
},
};
}
同一页面不应同时产生多组可访问地址。
五、让业务信息具备机器可理解的结构
页面正文需要直接回答:
公司是谁
提供什么服务
覆盖哪些地区
服务流程是什么
如何计价
有哪些保障
如何联系
可以建立统一企业配置:
export const organization = {
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:latest
restart: unless-stopped
environment:
DATABASE_URL:
file:/app/data/production.db
volumes:
- ./data:/app/data
- ./uploads:/app/public/uploads
ports:
- "3000:3000"
容器重建后:
代码和依赖可以重新生成
数据库和上传文件必须保留
这是生产部署中非常基础、但又容易遗漏的一点。
七、SQLite备份要同时处理数据库和图片
仅备份数据库,不备份上传图片,恢复后仍然会出现内容缺失。
备份脚本示例:
#!/usr/bin/env bash
set -euo pipefail
BASE_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" \
uploads
find "$BACKUP_DIR" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-mtime +30 \
-exec rm -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是否生成
示例:
function validateArticleForPublish(
article: Article,
): string[] {
const errors: 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');
}
return errors;
}
把基础质量要求写进系统,比完全依赖人工记忆更稳定。
十、GEO的核心是实体、服务和证据关系
对本地企业而言,需要持续建立:
企业
→ 服务
→ 区域
→ 案例
→ 常见问题
→ 联系方式
文章不是独立存在的流量页面,而应连接到真实服务和真实案例。
例如:
新房开荒需要准备什么
→ 关联新房开荒服务
→ 关联榆次服务区域
→ 关联真实案例
→ 关联预约方式
这种信息结构既方便用户阅读,也方便搜索系统理解业务。
十一、总结
企业网站真正的工程价值并不只在首页设计,而在于:
内容能持续发布
数据不会因重建丢失
图片可以长期访问
页面关系清晰
服务信息保持一致
备份能够真正恢复
搜索系统能够理解业务
我在本地家政企业网站实践中形成的主要判断是:
个人作品集适合静态化,企业内容系统需要后台和持久化;页面数量不是目标,真实、唯一、可维护的服务信息才是内容工程的基础。
关于作者
张智博,石家庄邮电职业技术学院学生,主要关注 AI 工具应用、网站建设、产品设计、项目运营与工程化实践,参与过本地服务企业网站与 GEO 内容系统建设。
个人作品集:张智博的思考空间
个人官网:https://www.zzb9.cn
GitHub:https://github.com/zzb99
更多推荐
所有评论(0)