Next.js App Router 无头 WordPress 实战:cms-wordpress 官方示例全解析
2026/9/7 7:48:24 网站建设 项目流程

Next.js App Router 无头 WordPress 实战:cms-wordpress 官方示例全解析

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

本文以 Next.js 官方示例 examples/cms-wordpress 为主体,完整拆解「用 WordPress 作为数据源、用 Next.js App Router 作为前端渲染层」的无头(Headless)架构落地方案。读完本文,你将能够独立完成 WordPress 侧的 WPGraphQL + JWT 插件配置、Next.js 侧的环境变量与代码生成配置,并理解草稿预览(Draft Mode)、按需缓存重验证(On Demand Revalidation)、中间件重定向与 SEO 自动化这几条关键链路的源码实现原理。

示例定位与核心功能

该示例展示如何构建一个功能丰富的 App Router 项目,以 WordPress 作为内容数据源。README 中声明的核心功能与其对应的源码位置如下:

功能源码位置作用
robots.tssrc/app/robots.ts自动抓取 WordPress 的 robots.txt 内容并在/robots.txt路由上重新解析、吐出
sitemap.tssrc/app/sitemap.ts通过自定义 REST 端点拉取全部路径,动态生成/sitemap.xml
middleware.tssrc/middleware.ts检查每个请求路径在 WordPress Redirection 插件中的重定向记录,命中则执行跳转
[[...slug]]可选通配路由src/app/[[...slug]]/page.tsx渲染所有 WordPress 内容的兜底路由,先查询内容类型再渲染对应模板,不可删除
not-found.tsxsrc/app/not-found.tsx动态 404 页:将其中的 WordPress 数据库 ID 调整为你自己 404 页面的 ID(slug 为404-not-found),即可让 404 页面在 CMS 中可编辑
codegen.tscodegen.ts针对你的 WordPress 安装自动生成 GraphQL 的 TypeScript 类型
Draft Mode 草稿预览src/app/api/preview/route.ts基于 WPGraphQL JWT Authentication 登录 + Next.js Draft Mode,实现无缝的草稿/预览
按需缓存重验证src/app/api/revalidate/route.ts配合一个只含两个文件的最小 WordPress 主题,在内容更新时调用 Next.js 的 revalidation

快速开始

通过 create-next-app 初始化项目

使用create-next-app配合 npm、Yarn、pnpm 或 Bun 任一方式引导该示例:

npx create-next-app --example cms-wordpress cms-wordpress-app
yarn create next-app --example cms-wordpress cms-wordpress-app
pnpm create next-app --example cms-wordpress cms-wordpress-app
bunx create-next-app --example cms-wordpress cms-wordpress-app

本地运行的一个重要细节

查看 package.json 可以看到,devbuild脚本并不是直接执行next dev,而是先跑 Codegen:

"dev": "graphql-codegen --config codegen.ts && node ./add-ts-nocheck.js && next dev"

也就是说,运行npm run dev时会自动根据NEXT_PUBLIC_WORDPRESS_API_URL指向的 WordPress 安装生成 TypeScript 类型(README 中的 NOTE 亦明确说明这一点)。codegen脚本单独暴露,方便你在 WordPress Schema 变更后手动重新生成类型。

WordPress 侧配置(10 个步骤)

README 给出了完整的 WordPress 安装配置流程,这是整个无头方案能跑起来的前提,必须按序完成:

  1. Settings → General中把Site Address (URL)设置为前端 URL,例如本地开发时的https://localhost:3000
  2. 确认Settings → Permalinks中 Permalinks 设置为Post name
  3. Settings → Reading中将Sample page设为Static page
  4. 创建一个名为404 not found的新页面,确保其 slug 为404-not-found
  5. 安装并激活以下插件:
    • Add WPGraphQL SEO
    • Classic Editor
    • Redirection
    • WPGraphQL
    • WPGraphQL JWT Authentication
    • Yoast SEO
    • Advanced Custom Fields PRO(可选)
    • WPGraphQL for ACF(可选)
  6. 完成 Redirection 插件的首次安装,建议开启变更监控(monitor of changes)。
  7. 配置 Yoast SEO:
    • 在 Yoast SEO → Settings 下禁用 XML Sitemaps(因为 sitemap 改由 Next.js 生成);
    • 如果你在安装 Yoast 之前修改过Site Address (URL),它在更改 permalink 后会提示运行 optimize SEO data,照做即可;
    • 在 Yoast SEO → Tools → File Editor 下生成 robots.txt 文件;
    • 将 robots.txt 中的 sitemap 引用从wp-sitemap.xml改为sitemap.xml
  8. GraphQL → Settings下开启Enable Public Introspection(Codegen 依赖内省接口拉取 Schema)。
  9. wp-config.php中添加以下常量:
define('HEADLESS_SECRET', 'INSERT_RANDOM_SECRET_KEY'); define('HEADLESS_URL', 'INSERT_LOCAL_DEVELOPMENT_URL'); // http://localhost:3000 for local development define('GRAPHQL_JWT_AUTH_SECRET_KEY', 'INSERT_RANDOM_SECRET_KEY'); define('GRAPHQL_JWT_AUTH_CORS_ENABLE', true);
  1. 创建一个只含两个文件的最小自定义 WordPress 主题:style.cssfunctions.php(完整functions.php见文末WordPress 主题 functions.php一节)。

Next.js 侧配置

克隆仓库(或通过create-next-app初始化)后:

  1. 执行npm install安装依赖;
  2. 在项目根目录创建.env文件并添加以下变量:
名称取值示例说明
NEXT_PUBLIC_BASE_URL前端基础 URLhttp://localhost:3000用于生成 sitemap、重定向等
NEXT_PUBLIC_WORDPRESS_API_URLWordPress 安装的基础 URLhttp://wp-domain.com请求 WordPress 数据时使用
NEXT_PUBLIC_WORDPRESS_API_HOSTNAME不带协议的 WordPress 主机名wp-domain.com用于动态填充 next.config 的 images remotePatterns
HEADLESS_SECRETwp-config.php中相同的随机密钥INSERT_RANDOM_SECRET_KEY用于前后端之间的公共校验
WP_USER有效的 WordPress 用户名username专门用于与 WordPress 交互的系统账号用户名
WP_APP_PASS应用密码(Application Password)1234 5678 abcd efghWP_USER生成的应用密码

注意:WP_USERWP_APP_PASS是预览(preview)与重定向(redirection)功能正常工作所必需的关键变量。

  1. 调整 not-found.tsx 中的 WordPress 页面 ID(示例默认值为501),使其匹配你自己 404 页面的databaseId
const notFoundPageWordPressId = 501;
  1. 执行npm run dev,即可开始构建基于 WordPress 的应用(类型生成会随 dev 脚本自动完成)。

此外,next.config.js 利用上表中的主机名环境变量动态配置图片远程加载白名单,这正是NEXT_PUBLIC_WORDPRESS_API_HOSTNAME存在的意义:

const nextConfig = { trailingSlash: true, images: { remotePatterns: [ { protocol: "http", hostname: process.env.NEXT_PUBLIC_WORDPRESS_API_HOSTNAME, port: "", }, ], }, };

trailingSlash: true也是有意为之的——WordPress 的 permalink 天然带尾斜杠,与重定向匹配、sitemap 生成的行为保持一致。

路由设计:可选通配段与模板路由

README 的「Template handling」章节说明:示例使用Optional Catch-all Segment[[...slug]])承接所有 WordPress 内容,渲染时先向 GraphQL 提问「这条路由是什么类型的内容」,再取对应模板;每个模板可以拥有自己独立的查询来获取特定内容。

page.tsx 的实现印证了这一点:

export default async function Page({ params }: Props) { const slug = nextSlugToWpSlug(params.slug); const isPreview = slug.includes("preview"); const { contentNode } = await fetchGraphQL<{ contentNode: ContentNode }>( print(ContentInfoQuery), { slug: isPreview ? slug.split("preview/")[1] : slug, idType: isPreview ? "DATABASE_ID" : "URI", }, ); if (!contentNode) return notFound(); switch (contentNode.contentTypeName) { case "page": return <PageTemplate node={contentNode} />; case "post": return <PostTemplate node={contentNode} />; default: return <p>{contentNode.contentTypeName} not implemented</p>; } }

几个值得注意的实现细节:

  • 探针查询 ContentInfoQuery.ts 只取contentTypeNamedatabaseIdstatusuri四个字段,成本极低,先「定类型」再「取内容」,避免每次请求都拉全量大查询;
  • nextSlugToWpSlug.ts 负责把 Next.js 传入的slug(可能是数组)归一化为 WordPress URI,例如["about","us"]about/us
  • 预览路由/preview/{id}会被识别为「假路由」:slugpreview时改用DATABASE_ID而非URI查询。这是因为草稿状态的 post 没有真实 slug;
  • 文件顶部还导出generateStaticParams()返回空数组,表示这些路由不做静态预渲染。

SEO:Yoast 对象 → generateMetadata

README「SEO」章节说明:SEO 数据由 WordPress 中的 Yoast SEO 插件管理,每个路由都会请求 Yoast 的 SEO 对象,并解析到动态generateMetadata()中。在 page.tsx 中可以看到完整链路:SeoQuery取出contentNode.seo,经 utils/seoData.ts 的setSeoData转换后,再拼接基于NEXT_PUBLIC_BASE_URL的 canonical 链接返回给 Next.js 的Metadata类型。404 页面同样走这套流程(见 not-found.tsx),canonical 固定指向/404-not-found/

robots.ts 与 sitemap.ts:SEO 的自动化

robots.ts

robots.ts 在运行时直接fetchWordPress 的/robots.txt原文(cache: "no-store"),逐行解析出User-agentAllowDisallowSitemap字段后,转译为 Next.js 的MetadataRoute.Robots结构返回。也就是说 WordPress(Yoast)里维护的 robots 规则会自动「镜像」到前端域名下,无需二次维护。

sitemap.ts

sitemap.ts 的生成逻辑分三步,全部依赖 WordPress 主题中注册的sitemap/v1REST 端点(对应functions.phpwsra_*系列函数):

  1. GET /wp-json/sitemap/v1/totalpages获取各内容类型的总数;
  2. 对每个类型按perPage = 50计算总页数,并发请求/wp-json/sitemap/v1/posts?pageNo=&postType=&perPage=分页拉取 URL 与lastModified
  3. 汇总后拼上NEXT_PUBLIC_BASE_URL前缀输出为MetadataRoute.Sitemap

两者都声明export const revalidate = 0,即每次访问都回源 WordPress,保证站点地图与 robots 规则始终与 CMS 状态一致。

中间件:把 WordPress Redirection 插件的能力接到前端

README「Redirection setup」章节说明:示例支持 WordPress 的 Redirection 插件,必须配置WP_USERWP_APP_PASS,这样重定向就可以完全在 WordPress CMS 里管理。

middleware.ts 的完整链路是:

  1. 若缺少WP_USER/WP_APP_PASS直接放行(NextResponse.next()),保证未配置时不破坏站点;
  2. user:applicationPassword做 Basic 认证,调用 Redirection 插件的 REST 端点/wp-json/redirection/v1/redirect/,按「去尾斜杠后的路径」精确匹配(filterBy[url-match]=plain);
  3. 命中记录后,用NEXT_PUBLIC_BASE_URL拼接目标地址,并根据原状态码做语义保持的转换:301 → 308,302 → 307(保留请求方法与体的重定向码):
return NextResponse.redirect(newUrl, { status: redirect.action_code === 301 ? 308 : 307, });

草稿 / 预览模式:Draft Mode + WPGraphQL JWT

README「Draft / Preview support」章节解释了预览机制:在 api/preview/route.ts 中启用draftMode时,路由会用WP_USER+WP_APP_PASS登录 WordPress,以认证用户身份请求 GraphQL,从而让草稿与预览可用;草稿状态的文章没有真实 slug,因此示例重定向到「假路由」/preview/${id},用 ID 直接取数。

对照 route.ts 源码,完整时序为:

  1. 密钥校验?secret=必须等于HEADLESS_SECRET且带id参数,否则 401。这个 URL 正是 WordPress 后台预览按钮经过set_headless_preview_link过滤器改写后生成的(见functions.phpadd_query_arg(['secret' => HEADLESS_SECRET, 'id' => $post->ID], .../api/preview));
  2. GraphQL 登录:执行LoginUsermutation 换取 JWTauthToken(依赖 WPGraphQL JWT Authentication 插件);
  3. 开启 Draft ModedraftMode().enable(),此后该请求链路中的页面组件都能感知预览状态;
  4. 按 ID 查内容节点:携带Authorization: Bearer ${authToken}请求contentNode(id: $id, idType: DATABASE_ID),拿到statusuri
  5. 智能跳转status === "draft"时跳/preview/${databaseId},否则跳真实uri,并通过Set-Cookie: wp_jwt=...把 JWT 存入 Cookie。

Cookie 的消费方是 utils/fetchGraphQL.ts:它读取draftMode()状态,预览开启时从 Cookie 取wp_jwt组装 Bearer 头,并把请求缓存策略切为no-cache;退出预览则由 api/exit-preview/route.ts 调用draftMode().disable()并清空wp_jwtCookie,再按?path=跳回原页面。

按需缓存重验证(On Demand Revalidation)

README「Cache Revalidation」章节的机制是:所有 GraphQL 请求统一打上缓存标签wordpress;WordPress 中任何内容更新都会调用前端的/api/revalidate路由,重验证wordpress标签——既保证内容始终最新,又只在实际有更新时才回源。

三个文件共同构成这条链路:

  • 打标签:fetchGraphQL.ts 中每次fetch都附带next: { tags: ["wordpress"] },预览态时改为no-cache
  • 接收端:revalidate/route.ts 处理PUT请求,先校验请求头X-Headless-Secret-Key是否等于HEADLESS_SECRET(不符返回 401),然后对请求体中的paths逐条调用revalidatePath、对tags逐条调用revalidateTag,最后返回本次重验证的结果;
  • 触发端:WordPress 主题中通过transition_post_status钩子在文章状态变更时向$frontendUrl/api/revalidate/发 PUT 请求,body 为{"tags": ["wordpress"]},并跳过自动保存(DOING_AUTOSAVE)、计划任务(DOING_CRON)、纯草稿状态迁移与inherit状态。

GraphQL Codegen 与 TypeScript 类型

README「GraphQL and typescript types」章节说明:类型由 Codegen 从 WordPress 提供的 Schema 生成。codegen.ts 的配置要点:

  • schema指向NEXT_PUBLIC_WORDPRESS_API_URL+/graphql,即 WordPress 的 GraphQL 端点(依赖第 8 步开启的 Public Introspection);
  • generates有两个产物:src/gql/下用clientpreset 生成各查询的类型,src/gql/schema.gqlschema-ast插件落盘完整 Schema AST。

为 GraphQL 查询开启自动补全

README 建议安装 VS Code 的 "Apollo GraphQL" 扩展,并在next.config.js旁创建apollo.config.js

module.exports = { client: { service: { name: "WordPress", localSchemaFile: "./src/gql/schema.gql", }, }, };

注意这里引用的./src/gql/schema.gql正是 Codegen 生成的产物——先跑一次npm run codegen,IDE 的查询补全即可工作。

Advanced Custom Fields PRO(可选但推荐)

README 推荐用 ACF Pro 的Flexible Content数据类型来构建页面内容:它既能在编辑后台提供「块构建器」式的编辑体验,又能让所有内容自动完成类型生成、以结构化数据送达前端。相比之下,默认 Gutenberg 编辑器会返回大量 HTML,损失了「GraphQL + 类型生成」的大部分优势。

WordPress 主题 functions.php

README 说明,最小主题的functions.php实现了对 Next.js 集成至关重要的四项能力:

  • 注册主导航菜单(供前端的Navigation组件抓取);
  • 将预览链接与 REST 链接重写为指向前端域名而非 WordPress 安装地址;
  • 在每次更新文章时实现缓存标签重验证;
  • 为 sitemap 生成实现 REST 端点。

完整代码如下(README 原文收录,建议原样放入主题目录):

<?php /** * Registers new menus * * @return void */ add_action('init', 'register_new_menu'); function register_new_menu() { register_nav_menus( array( 'primary-menu' => __('Primary menu') ) ); } /** * Changes the REST API root URL to use the home URL as the base. * * @param string $url The complete URL including scheme and path. * @return string The REST API root URL. */ add_filter('rest_url', 'home_url_as_api_url'); function home_url_as_api_url($url) { $url = str_replace(home_url(), site_url(), $url); return $url; } /** * Customize the preview button in the WordPress admin. * * This function modifies the preview link for a post to point to a headless client setup. * * @param string $link Original WordPress preview link. * @param WP_Post $post Current post object. * @return string Modified headless preview link. */ add_filter( 'preview_post_link', 'set_headless_preview_link', 10, 2 ); function set_headless_preview_link( string $link, WP_Post $post ): string { // Set the front-end preview route. $frontendUrl = HEADLESS_URL; // Update the preview link in WordPress. return add_query_arg( [ 'secret' => HEADLESS_SECRET, 'id' => $post->ID, ], esc_url_raw( esc_url_raw( "$frontendUrl/api/preview" )) ); } add_filter( 'rest_prepare_page', 'set_headless_rest_preview_link', 10, 2 ); add_filter( 'rest_prepare_post', 'set_headless_rest_preview_link' , 10, 2 ); function set_headless_rest_preview_link( WP_REST_Response $response, WP_Post $post ): WP_REST_Response { // Check if the post status is 'draft' and set the preview link accordingly. if ( 'draft' === $post->post_status ) { $response->data['link'] = get_preview_post_link( $post ); return $response; } // For published posts, modify the permalink to point to the frontend. if ( 'publish' === $post->post_status ) { // Get the post permalink. $permalink = get_permalink( $post ); // Check if the permalink contains the site URL. if ( false !== stristr( $permalink, get_site_url() ) ) { $frontendUrl = HEADLESS_URL; // Replace the site URL with the frontend URL. $response->data['link'] = str_ireplace( get_site_url(), $frontendUrl, $permalink ); } } return $response; } /** * Adds the headless_revalidate function to the save_post action hook. * This function makes a PUT request to the headless site' api/revalidate endpoint with JSON body: paths = ['/path/to/page', '/path/to/another/page'] * Requires HEADLESS_URL and HEADLESS_SECRET to be defined in wp-config.php * * @param int $post_ID The ID of the post being saved. * @return void */ add_action('transition_post_status', 'headless_revalidate', 10, 3); function headless_revalidate(string $new_status, string $old_status, object $post ): void { if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return; } // Ignore drafts and inherited posts. if ( ( 'draft' === $new_status && 'draft' === $old_status ) || 'inherit' === $new_status ) { return; } $frontendUrl = HEADLESS_URL; $headlessSecret = HEADLESS_SECRET; $data = json_encode([ 'tags' => ['wordpress'], ]); $response = wp_remote_request("$frontendUrl/api/revalidate/", [ 'method' => 'PUT', 'body' => $data, 'headers' => [ 'X-Headless-Secret-Key' => $headlessSecret, 'Content-Type' => 'application/json', ], ]); // Check if the request was successful if (is_wp_error($response)) { // Handle error error_log($response->get_error_message()); } } function wsra_get_user_inputs() { $pageNo = sprintf("%d", $_GET['pageNo']); $perPage = sprintf("%d", $_GET['perPage']); // Check for array key taxonomyType if (array_key_exists('taxonomyType', $_GET)) { $taxonomy = $_GET['taxonomyType']; } else { $taxonomy = 'category'; } $postType = $_GET['postType']; $paged = $pageNo ? $pageNo : 1; $perPage = $perPage ? $perPage : 100; $offset = ($paged - 1) * $perPage; $args = array( 'number' => $perPage, 'offset' => $offset, ); $postArgs = array( 'posts_per_page' => $perPage, 'post_type' => strval($postType ? $postType : 'post'), 'paged' => $paged, ); return [$args, $postArgs, $taxonomy]; } function wsra_generate_author_api() { [$args] = wsra_get_user_inputs(); $author_urls = array(); $authors = get_users($args); foreach ($authors as $author) { $fullUrl = esc_url(get_author_posts_url($author->ID)); $url = str_replace(home_url(), '', $fullUrl); $tempArray = [ 'url' => $url, ]; array_push($author_urls, $tempArray); } return array_merge($author_urls); } function wsra_generate_taxonomy_api() { [$args,, $taxonomy] = wsra_get_user_inputs(); $taxonomy_urls = array(); $taxonomys = $taxonomy == 'tag' ? get_tags($args) : get_categories($args); foreach ($taxonomys as $taxonomy) { $fullUrl = esc_url(get_category_link($taxonomy->term_id)); $url = str_replace(home_url(), '', $fullUrl); $tempArray = [ 'url' => $url, ]; array_push($taxonomy_urls, $tempArray); } return array_merge($taxonomy_urls); } function wsra_generate_posts_api() { [, $postArgs] = wsra_get_user_inputs(); $postUrls = array(); $query = new WP_Query($postArgs); while ($query->have_posts()) { $query->the_post(); $uri = str_replace(home_url(), '', get_permalink()); $tempArray = [ 'url' => $uri, 'post_modified_date' => get_the_modified_date(), ]; array_push($postUrls, $tempArray); } wp_reset_postdata(); return array_merge($postUrls); } function wsra_generate_totalpages_api() { $args = array( 'exclude_from_search' => false ); $argsTwo = array( 'publicly_queryable' => true ); $post_types = get_post_types($args, 'names'); $post_typesTwo = get_post_types($argsTwo, 'names'); $post_types = array_merge($post_types, $post_typesTwo); unset($post_types['attachment']); $defaultArray = [ 'category' => count(get_categories()), 'tag' => count(get_tags()), 'user' => (int)count_users()['total_users'], ]; $tempValueHolder = array(); foreach ($post_types as $postType) { $tempValueHolder[$postType] = (int)wp_count_posts($postType)->publish; } return array_merge($defaultArray, $tempValueHolder); } add_action('rest_api_init', function () { register_rest_route('sitemap/v1', '/posts', array( 'methods' => 'GET', 'callback' => 'wsra_generate_posts_api', )); }); add_action('rest_api_init', function () { register_rest_route('sitemap/v1', '/taxonomy', array( 'methods' => 'GET', 'callback' => 'wsra_generate_taxonomy_api', )); }); add_action('rest_api_init', function () { register_rest_route('sitemap/v1', '/author', array( 'methods' => 'GET', 'callback' => 'wsra_generate_author_api', )); }); add_action('rest_api_init', function () { register_rest_route('sitemap/v1', '/totalpages', array( 'methods' => 'GET', 'callback' => 'wsra_generate_totalpages_api', )); });

从源码结构看,functions.php中注册的四个sitemap/v1REST 端点(/posts/taxonomy/author/totalpages)与 sitemap.ts 的请求一一对应,其中当前示例实际消费了/totalpages/posts两个端点;/taxonomy/author端点是为分类、标签、作者归档页预留的扩展位。

文件夹结构

README 描述的目录约定与实际src/目录完全对应:

  • app:应用的路由与页面——src/app 下含[[...slug]]/page.tsxapi/previewapi/exit-previewapi/revalidaterobots.tssitemap.tsnot-found.tsx等;
  • assets:存放变量等实用样式(本示例中以globals.css与 CSS Modules 形式存在);
  • components:应用组件——src/components 下分Globals(Navigation、PreviewNotice)与Templates(Page、Post 两套模板,各自携带独立的查询文件);
  • gql:GraphQL CodeGen 自动生成的类型(src/gql/,运行 dev/build 时生成);
  • queries:可复用的 GraphQL 数据请求——src/queries/general 下的ContentInfoQuery.tsSeoQuery.ts
  • utils:跨应用使用的工具函数——src/utils 下的fetchGraphQL.tsnextSlugToWpSlug.tsseoData.ts

小结:这套无头架构的分工

综合以上各节,该示例的架构分工可以归纳为:

  • WordPress 负责「内容与元数据管理」:文章/页面编辑、Yoast SEO、Redirection 重定向、应用密码认证、JWT 签发;
  • Next.js 负责「渲染与边缘能力」:App Router 的可选通配路由 + 内容类型模板分发、generateMetadata输出 SEO、middleware 拦截重定向、revalidateTag按需回源、robots.ts/sitemap.ts动态 SEO 文件;
  • 两端通过三根「线缆」连接:GraphQL(数据)、HEADLESS_SECRET(重验证与预览鉴权)、WP_USER+WP_APP_PASS应用密码(认证登录与 Redirection API 访问)。

如果你想继续参考同类集成,仓库的 examples 目录下还收录了 AgilityCMS、Builder.io、Contentful、Ghost、Prismic、Sanity 等多个 CMS 官方示例,可以横向对比不同数据源的接入方式;而本示例独有的「JWT 草稿预览 + Redirection 中间件 + 标签级重验证」组合,是 WordPress 无头场景下较为完整的工程化参考实现。

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询