headless-wordpress-astro-setup.html
< BACK 温暖金色光线下的老式书桌,摆着手稿页面和录音机

Headless WordPress + Astro:内容密集型网站的实际可用方案

一位客户在 2023 年初联系我——一家媒体发行商在 WordPress 上运行约 14,000 篇文章,主题是由六年来四个开发者拼凑而成,Core Web Vitals 评分真的令人尴尬。他们的 LCP 在移动设备上达到了 7.2 秒。他们试过 WP Rocket,试过 CDN,甚至删除了一半的插件。还是很慢。问题不在 WordPress 本身。问题在于每一个页面渲染都要经过 PHP、臃肿的主题和一个从 2018 年以来就没人管过的数据库查询链。

那一刻我真正决定采用 headless 架构,用 Astro 作为前端。不是因为它很时髦——而是因为对于一个有数千篇文章和大量编辑内容的网站来说,这是唯一有意义的架构。Astro as the frontend. Not because it's fashionable — because for a site pushing thousands of posts with heavy editorial content, it was the only architecture that made sense.

这就是我具体如何设置的。权衡、真实配置,以及那些给我教训的部分。

---

为什么选 Astro 而不是 Next.js

我经常被问这个问题。如果你的团队已经是React重度使用者,或者你需要复杂的客户端交互,Next.js是显而易见的答案。但对于内容密集型网站呢?Astro胜出,而且没有特别接近的竞争对手。

Astro默认不传输任何JavaScript。对于博客、新闻网站或文档门户,这是正确的默认设置。你只在需要JavaScript的地方启用它,而不是被迫排除一个你大部分时候都不想要的200kb的React包。Astro文档中关于部分水合——他们称之为Islands架构——的说明比我一句话能解释的要好得多,但简而言之就是:只有交互部分才会使用JS。文章正文、页头、侧边栏?都是静态HTML。zero JavaScript by default. For a blog, a news site, or a documentation portal, that's the correct default. You opt into JavaScript where you need it, rather than opting out of a 200kb React bundle you mostly don't want. The Astro docs on partial hydration — they call it the Islands architecture — explain it better than I can in a sentence, but the short version is: only the interactive bits get JS. The article body, the header, the sidebar? Static HTML.

我在2022年末用Next.js和WordPress构建了一个法律内容网站。速度足够快,但客户一直在问,当"现在应该速度快"时,他们的Lighthouse移动端得分为什么是74。水合开销。用Astro的话,同样类型的网站现在经常能达到95–98。不是在吹牛——那只是这个架构免费给你的。

Astro不擅长的地方

得老实说。如果你的网站需要实时个性化、重型购物车,或任何真正依赖跨多个组件的客户端状态的东西,Astro就会开始显得很别扭。它不是React应用。Islands模式很强大,但它的思维模式与构建SPA不同。我在2023年中期试图在Astro项目中硬塞一个客户端仪表板,两周内就恢复到Next.js了。知道你在构建什么。

---

将WordPress设置为无头CMS

WordPress是一个真正好用的无头后端。WP REST API在核心中就有,文档完善,你的编辑团队不需要学习任何新东西。这最后一点比开发人员通常承认的要重要得多。WP REST API ships in core, it's well-documented, and your editorial team doesn't have to learn anything new. That last point matters more than developers usually admit.

这是我使用的设置:

  1. 在子域名上安装WordPress——我使用cms.yourdomain.com或api.yourdomain.com。将其放在基本身份验证后面,或至少限制直接公共流量。前端是yourdomain.com。两个独立的部署。 — I use cms.yourdomain.com or api.yourdomain.com. Keep it behind basic auth or at minimum restrict direct public traffic. The frontend is yourdomain.com. Two separate deployments.
  2. 安装 [WPGraphQL 插件](https://www.wpgraphql.com/) — 对于内容网站,我倾向于使用 GraphQL 而不是 REST,因为你可以将查询与组件并置,并且只获取你需要的字段。没有过度获取。REST API 也不错,但当你的每个文章类型有 15+ 个自定义字段时,GraphQL 方法明显更简洁。 — I prefer GraphQL over REST for content sites because you can co-locate your queries with your components and fetch exactly the fields you need. No over-fetching. The REST API is fine, but once you've got 15+ custom fields per post type, the GraphQL approach is noticeably cleaner.
  3. 安装 Advanced Custom Fields (ACF) 和 WPGraphQL for ACF 扩展。这个组合是让 WordPress 作为无头内容模型真正灵活的关键 — 你可以为每个文章类型定义结构化数据,通过 GraphQL 公开它,Astro 可以干净地使用它。 and the WPGraphQL for ACF extension. This combo is what makes WordPress genuinely flexible as a headless content model — you can define structured data per post type, expose it through GraphQL, and Astro consumes it cleanly.
  4. 禁用评论、表情符号和默认的 XML-RPC(如果你还没有的话)。这些会增加额外的开销和不必要的攻击面。 if you haven't already. These add overhead and attack surface you don't need.
  5. 在开始构建之前,将固定链接设置为合理的格式。当你的 Astro 路由已经设置好后再更改它们真的很麻烦。 to something sensible before you start building. Changing them mid-project when your Astro routes are already set is a genuine pain.

有一点会让人困惑:CORS。默认情况下,WordPress 不会让你的 Astro 开发服务器(运行在 localhost:4321 上)向你的 WP 安装发送请求。在开发期间,把这段代码放到你的主题 functions.php 或一个小实用插件中:localhost:4321) make requests to your WP install. Drop this into your theme's functions.php or a small utility plugin during development:

`` add_action('init', function() { header("Access-Control-Allow-Origin: *"); }); `` add_action('init', function() { header("Access-Control-Allow-Origin: *"); }); ``

在生产环境中将其限制为特定来源。显然。

---

Astro 项目结构

我在各个项目中保持这个结构的固定性和一致性。经过十多个无头构建项目后,这是行之有效的方法:

`` src/ components/ layouts/ pages/ index.astro blog/ [slug].astro lib/ wpgraphql.ts ← 所有 WP 查询逻辑都在这里 styles/ `` src/ components/ layouts/ pages/ index.astro blog/ [slug].astro lib/ wpgraphql.ts ← all WP query logic lives here styles/ ``

lib/wpgraphql.ts 文件是我集中处理每一个 GraphQL 获取的地方。没有散落在页面文件中的内联获取调用。每个查询都是一个命名的、导出的异步函数。在凌晨两点某个东西出错时,跨 14,000 篇文章调试这个——你以后会感谢自己。lib/wpgraphql.ts file is where I centralise every GraphQL fetch. No inline fetch calls scattered across page files. Every query is a named, exported async function. Debugging this across 14,000 posts when something breaks at 2am — you'll thank yourself later.

在构建时获取文章

Astro 的 getStaticPaths 在这里是你的核心工具。对于有数千篇文章的博客:getStaticPaths is your bread and butter here. For a blog with thousands of posts:

`` export async function getStaticPaths() { const posts = await getAllPostSlugs(); // 调用 WPGraphQL return posts.map(post => ({ params: { slug: post.slug }, })); } `` export async function getStaticPaths() { const posts = await getAllPostSlugs(); // calls WPGraphQL return posts.map(post => ({ params: { slug: post.slug }, })); } ``

getAllPostSlugs 使用 after 游标分页遍历 WPGraphQL——WordPress 的 GraphQL 层默认每次请求返回 100 篇文章,所以对于 14,000 篇文章,你在构建时要发起 140 个请求。这听起来很可怕。实际上,在一个还不错的服务器上,完整的构建耗时约 4-5 分钟。对于一天重建几次的网站来说完全可以接受。 paginates through WPGraphQL using after cursors — WordPress's GraphQL layer returns 100 posts per request by default, so for 14,000 posts you're making 140 requests at build time. That sounds scary. In practice, on a decent server, the full build runs in about 4–5 minutes. Perfectly acceptable for a site that rebuilds a few times per day.

---

处理图片而不失去理智

这是没人充分谈论的部分。WordPress 存储指向你的 CMS 子域的图片 URL。当 Astro 进行静态构建时,那些图片仍然存放在 cms.yourdomain.com 上——这意味着访客的浏览器从你的 WordPress 服务器获取图片,可能绕过了你的 CDN。cms.yourdomain.com — which means your visitors' browsers are fetching images from your WordPress server, potentially bypassing your CDN.

我处理这个的几种方式:

  • Cloudflare 放在两个域名前面。最简单的方案。通过 Cloudflare 代理 yourdomain.com 和 cms.yourdomain.com,在 /wp-content/uploads/* 上配置激进缓存,基本上就没问题了。 Simplest option. Proxy both yourdomain.com and cms.yourdomain.com through Cloudflare, configure aggressive caching on /wp-content/uploads/*, and you're mostly fine.
  • 使用媒体离卸插件。我喜欢 WP Offload Media — 它将上传文件移到 S3(或兼容的存储)并自动重写 URL。这是我对任何预期有大流量的网站采用的方案。你的 WordPress 服务器完全不再提供图像。 I like WP Offload Media — it moves uploads to S3 (or compatible storage) and rewrites URLs automatically. This is the approach I use for any site expecting serious traffic. Your WordPress server stops serving images entirely.
  • Astro 的 Image 组件。对于你在构建时控制的图像(通过 GraphQL 获取的特色图像),你可以将远程 URL 传入 Astro 的 <Image> 组件,它会优化、调整大小,然后从你的构建输出中提供服务。效果很棒。但不适用于嵌入在文章正文 HTML 中的图像 — 那需要另外处理。 For images you control at build time (featured images pulled via GraphQL), you can pass the remote URL into Astro's <Image> component and it'll optimise, resize, and serve them from your build output. Works brilliantly. Does not work for images embedded in post body HTML — that requires a different pass.

Seahawk 去年有个旅游内容客户 — 大约 8,000 篇文章,图像非常密集,平均每篇 12 张图像。即使采用了无头设置,他们的 WordPress 服务器仍然被图像请求彻底压垮。迁移到 S3 + CloudFront 后,他们的源站带宽下降了 94%。对他们的托管账单来说真的是变革性的。

---

增量构建和重建问题

大规模静态生成的一个真实问题:你的编辑在下午 3 点发布了一个文章更正,却要等 5 分钟来完成一次完整重建。这在新闻编辑室是不可接受的。

我用过的几种方法:

选项 1:Netlify 或 Vercel 的按需 ISR。Astro 支持带适配器的服务端渲染 — 你可以在混合模式下运行 Astro,其中大多数页面是静态的,但特定路由按需渲染。对于新闻网站,我通常会静态预渲染最近 30 天的文章(高流量,需要速度),并设置较旧的归档页面按需服务端渲染。两全其美。 Astro supports server-side rendering with adapters — you can run Astro in hybrid mode where most pages are static but specific routes are rendered on-demand. For a news site, I'll often statically pre-render the last 30 days of posts (high traffic, needs speed) and set older archive pages to server-render on demand. Best of both worlds.

选项 2:Webhook 触发的部分构建。WordPress 在文章保存时触发 webhook(使用 WP Webhooks 插件很容易)。Webhook 击中 Netlify 或 Vercel 部署钩子。构建运行,它只获取更改的内容。这不是真正的部分构建 — Astro 仍然重建所有内容 — 但如果你的构建速度够快,4 分钟是可以接受的。 WordPress fires a webhook on post save (easy with the WP Webhooks plugin). That webhook hits a Netlify or Vercel deploy hook. The build runs, it fetches only what's changed. It's not truly partial — Astro still rebuilds everything — but if you keep your build fast, 4 minutes is workable.

选项 3:直接对整个项目使用 SSR。用 Node 适配器部署 Astro 到 VPS(我用 Hetzner——便宜、快速、可靠)。每个页面都在请求时渲染,你在 Nginx 或 Cloudflare 层面进行激进缓存,这样就能实现即时文章更新。如果你要运营一个拥有 50,000 多篇文章的真正的发布平台,我会这样做。 Deploy Astro with the Node adapter to a VPS (I use Hetzner for this — cheap, fast, reliable). Every page renders on request, you cache aggressively at the Nginx or Cloudflare level, and you have instant post updates. This is what I'd do for a proper publishing operation over 50,000 posts.

说实话?大多数网站不需要选项 1 或 3 的复杂性。被 webhook 触发的 4 分钟重建对 90% 的内容网站来说已经足够了。

---

性能:你实际能获得的

从开头提到的发布项目来看——Astro 迁移后发生的情况:

  • 移动端 LCP 从 7.2 秒降到 1.1 秒(用 WebPageTest 从伦敦节点测试)1.1s on mobile (tested with WebPageTest from a London node)
  • 总阻塞时间从约 800 毫秒降到 0 毫秒(记住,默认零 JS)0ms (zero JS by default, remember)
  • 他们的 Google Search Console Core Web Vitals 报告在部署后六周内从 3% "良好" URL 增至 91% "良好"91% "Good" within six weeks of deployment
  • 托管成本下降了,因为他们的 WordPress 服务器不再供应页面,只提供 API 响应

这都不是魔法。当你把 PHP 渲染从关键路径中移除,不再向每个读者推送 400kb 的主题 JavaScript 包时,就会发生这样的事。

---

那些会让你踩坑的细节

说实话——这些是我在实际项目中必须调试的问题:

  • 草稿文章预览。在无头 WordPress 的设置中,这确实很烦人。WordPress 原生预览依赖前端渲染。你需要在 Astro 中构建一个自定义预览端点,接收 WordPress 预览 nonce,然后通过 WPGraphQL 获取草稿。不难,但要做到位需要花一天时间。 This is genuinely annoying in a headless setup. WordPress's native preview relies on front-end rendering. You need to build a custom preview endpoint in Astro that accepts a WordPress preview nonce and fetches the draft via WPGraphQL. Not hard, but it takes a day to do properly.
  • 重定向。如果旧网站在 .htaccess 中有几百个重定向,那么现在它们位于 WordPress 服务器上。你要么需要在 Astro 的配置中复制它们,要么让 WordPress 保持可访问并代理特定路径。我两种方法都试过。在 Astro 中复制从长期来看更整洁。 If the old site had hundreds of redirects in .htaccess, those live on the WordPress server now. You need to either replicate them in Astro's config, or keep WordPress accessible and proxy specific paths. I've done both. Replicating in Astro is cleaner long-term.
  • 搜索。在无头设置中,WordPress 内置搜索基本没用。我使用 Algolia 和 WP Search with Algolia 插件。在 WP 中索引文章,从 Astro Island 组件查询 Algolia。效果很好。 WordPress's built-in search is useless in a headless setup. I use Algolia with the WP Search with Algolia plugin. Index your posts in WP, query Algolia from an Astro Island component. Works well.
  • 菜单和导航。WordPress 菜单通过 WPGraphQL 暴露出来的方式很繁琐。用 wpgraphql-acf 的方式通常更整洁——直接把导航建模为 ACF repeater 就完事了。 WordPress menus are weirdly fiddly to expose through WPGraphQL. The wpgraphql-acf route often ends up being cleaner — just model your nav as an ACF repeater and call it done.

---

常见问题

我需要 WPGraphQL 还是可以直接用 REST API?

你完全可以使用 REST API——它内置在 WordPress 核心中,不需要额外插件。对于具有标准文章类型和最少自定义字段的简单站点,它是可以的。GraphQL 的价值在于你拥有复杂的内容模型,每个类型有许多自定义字段的时候。能够在单个请求中获取所需的确切字段,而不用费力处理 _embed 参数和嵌套的 REST 调用,可以为你编写的每个查询节省时间。选择权在你。我只是发现在超过一定复杂度阈值后,GraphQL 更简洁。_embed parameters and nested REST calls, saves time on every query you write. Up to you. I just find GraphQL cleaner past a certain complexity threshold.

我如何为仅限成员的内容处理 WordPress 身份验证?

JWT 身份验证是标准方法。安装 JWT Authentication for WP REST API 插件,在登录时签发令牌,在你的 GraphQL 请求头中传递它们。在 Astro 端,你需要使用 SSR 路由(而不是静态)来处理这个问题,这样特定用户的内容在每个请求中都在服务器端获取。不要尝试静态做这个——那样会出大问题。JWT Authentication for WP REST API plugin, issue tokens on login, pass them in your GraphQL request headers. On the Astro side, you'd handle this with an SSR route (not static) so the user-specific content is fetched server-side per request. Don't try to do this statically — that way lies madness.

对于一个小博客来说,这是不是过度设计了?

是的,可能是。如果你有少于 500 篇文章和一个编辑,维护无头架构的开销就不值得了。只需使用一个好的 WordPress 主题,优化你的图像,然后继续你的工作。当你拥有大量内容、编辑复杂性或流量级别真正影响收入的时候,这种架构才能发挥作用。

生产环境中的托管设置是什么样的?

WordPress(仅限 CMS)托管在小型 VPS 或托管 WordPress 主机上——我使用 Kinsta 或 Cloudways。Astro 前端部署在 Vercel、Netlify 或装有 Nginx 的 Hetzner VPS 上,具体取决于项目。所有服务前面都有 Cloudflare。中等规模内容站点的月度总成本通常在 £60–£120 之间,这通常比客户之前为勉强支撑负载的一体化 WordPress 主机支付的费用要少。less than what clients were paying for an all-in-one WordPress host that was struggling under the load.

---

诚实的总结是这样的:用 Astro 的无头 WordPress 是近年来内容站点最好的事情之一。不是因为它新,而是因为工具终于跟上了这个想法。WPGraphQL 已经稳定,Astro 的构建系统很快,性能提升是真实的和可测量的。

尽早正确设计架构——尤其是你的图像策略和重建方法——你以后花在救火上的时间会少得多。就这样。

< BACK