我们是如何构建一个在 WordPress 上服务 80K+ 页面且不使用 wp_posts 的程序化 SEO 引擎
Source: Dev.to
当我们决定打造 startup‑cost.com 时,就已经知道传统的 WordPress 方案根本不够用。我们需要提供 79 000+ 个独立页面——每个页面对应 479 个城市和 167 种业务类型的组合——并且要展示真实的成本数据、实时计算以及可靠的性能。
大多数人听到 “WordPress 上的 80 K 页面” 就会觉得我们疯了。WordPress 不是博客平台吗?的确如此——但在底层它其实是一个灵活的 PHP 框架,拥有强大的重写引擎。我们只需要抛弃那些无法扩展的部分,自己动手构建即可。
下面就是我们 在 wp_posts 中没有任何一行记录 的实现过程。
大规模使用 wp_posts 的问题
WordPress 将所有内容存储在名为 wp_posts 的单一表中。对于拥有几百个页面的博客或小型企业站点,这种方式运行良好。但当你向该表中插入数万行数据时,问题会迅速显现:
| 问题 | 为什么会受影响 |
|---|---|
| 查询性能下降 | WordPress 几乎在每个查询中都要将 wp_posts 与 wp_postmeta 进行关联。拥有 80 K 篇文章且每篇文章有 10+ 个元字段时,wp_postmeta 中的记录会超过 800 K 行。原本约 5 ms 的查询现在需要约 500 ms。 |
| 后台管理面板变得难以使用 | 加载包含 80 K 条目的 “全部文章” 页面是一场噩梦——虽然有分页,但统计总数的过程会耗费极长时间。 |
| 修订历史占用磁盘空间 | 自动保存会在修订记录中生成实际内容的 3‑4 倍数据,尤其是内容通过程序生成时更是如此。 |
| XML 站点地图卡死 | 常用的站点地图插件尝试一次性查询所有文章;当表中有 80 K 行时,它们要么超时,要么耗尽内存。 |
| 导入/导出失败 | WordPress 导出会生成单个 XML 文件。一个包含 80 K 篇文章的 WXR 文件几乎不可能正常处理。 |
我们需要一种根本不同的解决方案。
为什么不使用静态站点生成器或其他 CMS?
Fair question. We evaluated Hugo, Next.js, and a custom Node.js app. WordPress still gave us specific advantages:
- 托管费用极低 – 共享的 WordPress 主机每月只需几美元,且能够轻松应对我们的流量。
- 插件生态系统 – 我们仍然使用实用插件来进行 SEO、缓存以及其他任务。
- 熟悉的部署方式 – 我们的团队对 WordPress 了如指掌。无需学习曲线,也不需要新的 CI/CD 流水线。
- PHP 已足够快 – 在使用 OpCache 和清晰的查询模式后,PHP 8 能在两位数毫秒内响应页面。
The key insight: we don’t have to use WordPress the way it was designed. We can treat it as a routing and rendering framework while storing our data however we want.
架构 – 高层概览
我们构建了一个自定义的 WordPress 插件,完全绕过 wp_posts。该概念基于三大支柱:
自定义数据库表
我们没有把所有数据塞进 posts 和 postmeta,而是创建了专用表,拥有合适的模式、数据类型和索引。可以把它看作是运行在 WordPress MySQL 实例中的一个小型应用数据库。
- 每个实体对应一张表(城市、业务类型、成本指标)。
- 列与实际数据模型相匹配——不使用通用的键值对。
- 采用恰当的索引、规范化,以及只检索所需数据的查询。
结果:对 80 K 条帖子的基于 meta 的查找可能需要数百毫秒;而对专门构建的表进行直接索引查询则能在个位数毫秒内返回。
虚拟 URL 路由
WordPress 的 rewrite API 功能强大,却常被低估。大多数开发者只通过永久链接设置界面稍作触碰。实际上,你可以在底层注册完全自定义的 URL 模式,并将其映射到自己的渲染逻辑。
- 定义 WordPress 能识别的 URL 模式。
- 当请求到达时,WordPress 匹配 URL、提取 slug,并将控制权交给我们的插件。
- 不会创建帖子,也不会触碰
wp_posts中的任何行——该 URL 之所以存在,仅因为我们让 WordPress 认识了这个模式。
这样既能完全掌控 URL 结构,又能受益于 WordPress 的请求生命周期、缓存钩子以及插件兼容性。
动态内容生成
程序化 SEO 只有在每个页面提供真实价值时才有效。我们并非仅在模板中替换城市名称;每个页面都会展示 真实的成本数据、真实的计算结果以及真实的对比。
- 渲染层从我们的自定义表中提取数据。
- 对每个城市‑业务组合执行特定计算。
- 输出完整的 HTML,包含唯一的标题、meta 描述、结构化数据(schema)标记等。
Google 能识别薄弱、模板化的内容。每页都具备数据驱动的真实价值,才是程序化 SEO 能长期奏效的关键。
性能结果
基准测试在相同的服务器上运行(共享主机,PHP 8.1,MariaDB 10.6)。
| 指标 | wp_posts 方法 | 自定义方法 |
|---|---|---|
| 平均页面加载时间(TTFB) | ~800 毫秒 | ~120 毫秒 |
| 数据库查询时间 | ~200 毫秒(元数据联接) | ~15 毫秒(索引查找) |
| 后台仪表盘加载 | 30+ 秒 | 即时(无后台开销) |
| 站点地图生成 | 超时(60 秒) | ~2 秒 |
| 每个请求的内存使用量 | ~64 MB | ~12 MB |
差异显著。使用适当索引的自定义表使 WordPress 的性能如同专门构建的应用程序。
网站地图策略
Google 的限制:每个文件 50 000 个 URL 且 未压缩大小不超过 50 MB。对于 80 K 页面,我们需要多个站点地图以及一个索引。
- 以编程方式生成站点地图,将 URL 切分为可管理的文件。
- 创建一个站点地图索引,引用所有切片。
- 在每周的 cron 任务中运行生成过程。
- Google Search Console 能够顺利抓取这些文件,我们还能按站点地图切片跟踪索引进度。
内部链接
有 80 K 页面,内部链接对 SEO 和帮助用户导航都至关重要:
- 每个城市页面链接到该城市中所有可用的业务类型。
- 每个业务页面链接到该业务类型的热门城市。
- 每个详情页面根据地理和主题的接近性链接到相关页面。
- 密集的内部链接图有助于搜索引擎发现并理解站点结构。
缓存
- 并非所有 80 K 页面都拥有相同的流量。
- 热门的城市/业务组合会预缓存,并使用更长的 TTL。
- 长尾页面按需缓存,使用较短的 TTL。
- 我们还在数据库查询层面缓存经常访问的聚合。
我们学到了什么
- WordPress 能够处理大规模 – 但前提是你要跳出其默认的内容模型。该平台比人们想象的更灵活。
- 自定义表不是“投机取巧” – 当你的数据不符合文章/元数据模式时,它们是正确的工具。WordPress 本身就为评论、用户和选项使用自定义表。
- 虚拟路由功能强大 – WordPress 的重写规则可以处理复杂的 URL 模式。仅仅因为你的 URL 与文章不对应,并不意味着需要自定义框架。
- 程序化 SEO 需要真实价值 – Google 能识别薄弱、模板化的内容。每个页面都必须拥有真正不同且有用的数据。这就是垃圾信息与真实产品的区别。
- 从数据模型开始 – 我们花在设计数据库模式上的时间比写渲染代码更多。良好的数据建模在各层面都能带来回报。
- 先监控,再优化 – 我们最初没有使用缓存,只在监控显示瓶颈的地方才添加。过早的优化会带来不必要的复杂性。
查看结果:startup-cost.com – 为全球 479 个城市的 167 种业务类型提供创业成本估算。
由 Kavela Ltd 构建 – 我们在规模化下构建数据驱动的网络工具。