本地服务企业网站的内容工程: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

更多推荐