Repository: notionnext-org/NotionNext (2026-07-10T09:36:44Z)

Issue:
Notice同步区块部分部署到网站上仍然无法显示
<!--
  !!! 重要 !!!
  请遵守这个模板的格式填写，否则你的Issue将被关闭
-->

**描述bug**
继上次补丁修复后，公告同步区块仍然部分无法显示。
Notion页面，从“欢迎来到我的博客”部分开始是同步区块【https://app.notion.com/p/230d34c70fe5815cb6facea0d70cacba】（非原始页面。原始页面的公告正常显示）

<img width="1140" height="687" alt="Image" src="https://github.com/user-attachments/assets/dd604968-6f82-488b-90eb-7a54cbdd22af" />

对应部署后的网页：公告的同步区块部分消失【https://blog.happinessrailway.com/】

<img width="2559" height="1287" alt="Image" src="https://github.com/user-attachments/assets/6d5d6066-14b4-4b30-b55d-a680171c0d19" />

**期望的正常结果**
公告Notice同步区块部分正常显示

**复现步骤**
Update Branch

**环境**

- 【必填】NotionNext版本 [4.10.2]
- 【必填】主题 [hexo]
- 【必填】部署方案 [vercel]
- 【可选】操作系统: [windows]
- 【可选】浏览器 [chrome]

**补充说明**
暂无


Discussion:
已在 #4253 修复并合并。

根因是同步块 `sync_block` 在展开时只生成了子块，但没有同步更新父级页面的 `content` 引用，也没有把提升出来的子块重新挂回原父级。公告页后续会做根节点裁剪，所以这些同步块子内容会被裁掉，表现为 Notice 公告中的同步区块部分消失。

本次修复做了两点：

- 展开 `sync_block` 时替换父级 `content` 里的原同步块 ID，改为新的子块 ID；
- 重新设置同步块子内容的 `parent_id`，避免公告内容清洗时被误删。

另外，当前主线依赖已要求 Node 22（`@ai-sdk/google` 需要 `node >=22`）。如果站点部署在 Netlify / Cloudflare Pages / VPS / Docker，请确认构建环境也切到 Node 22；继续使用 Node 20 会在依赖安装阶段失败。

请同步最新主线后重新部署验证。如果仍有个别同步块不显示，可以把对应 Notion 页面结构或最小复现再贴出来。




Patch:
diff --git a/.node-version b/.node-version
index 2a393af592b..2bd5a0a98a3 100644
--- a/.node-version
+++ b/.node-version
@@ -1,1 +1,1 @@
-20.18.0
+22
diff --git a/.nvmrc b/.nvmrc
index 2a393af592b..2bd5a0a98a3 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1,1 +1,1 @@
-20.18.0
+22
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index a7bdc573703..32b458d9a57 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -11,7 +11,7 @@ NotionNext 支持多种部署方式，本指南将详细介绍各种部署选项
 为提升跨平台兼容性（Vercel / Cloudflare Pages / Netlify / EdgeOne Pages 等），本项目建议统一使用以下最简流程：
 
 ```bash
-# Node 20（版本以仓库根目录 `.nvmrc` 为准，便于与各平台预装列表对齐）
+# Node 22（版本以仓库根目录 `.nvmrc` 为准，便于与各平台预装列表对齐）
 nvm use || nvm install
 
 # Yarn
@@ -197,23 +197,23 @@ netlify deploy --prod --dir=out
 
 ## 腾讯云 EdgeOne Pages
 
-EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制台报错类似 **Failed to switch to Node.js x.y.z**，通常是因为平台上 **未提供该补丁版本**（例如仅有 `20.18.0`，而旧版本 `.nvmrc` 写了 `20.20.0`）。
+EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制台报错类似 **Failed to switch to Node.js x.y.z**，通常是因为平台上 **未提供该版本**。
 
 处理方式：
 
-1. **保持上游**：拉取最新 NotionNext，确认 `.nvmrc` 已与 EdgeOne「项目设置 → Node.js 版本」下拉列表中可选版本一致（一般为当前文件中的 `20.18.x`）。
+1. **保持上游**：拉取最新 NotionNext，确认 `.nvmrc` 已与 EdgeOne「项目设置 → Node.js 版本」下拉列表中可选版本一致。
 2. **自建仓库**：在 EdgeOne 控制台选择与 `.nvmrc` **完全一致**的 Node 版本；仍失败时检查构建日志是否仍在读取旧的 `.nvmrc`（需推送后再构建）。
-3. `package.json` 中 `engines.node` 为 `>=20 <25`，在 Node 20 系列内均可构建；关键是 **构建环境实际安装的版本**能解析 `.nvmrc`。
+3. `package.json` 中 `engines.node` 为 `>=22 <25`；关键是 **构建环境实际安装的版本**能解析 `.nvmrc`。
 
 **兼容尝试（按顺序，任选其一即可，不必全做）：**
 
 | 尝试 | 做法 |
 |------|------|
-| A. 对齐补丁号 | 打开 EdgeOne **项目设置 → Node.js 版本**，选择与仓库根目录 **`.nvmrc` 完全一致**的一项（例如均为 `20.18.0`），保存后 **重新部署**。 |
-| B. 改自建 fork | 若平台下拉列表**没有**当前 `.nvmrc` 里的补丁号：把 fork 里的 `.nvmrc` 改成平台**已有**的某一档（仍为 Node 20 即可），推送后再构建。 |
+| A. 对齐版本 | 打开 EdgeOne **项目设置 → Node.js 版本**，选择与仓库根目录 **`.nvmrc` 完全一致**的一项，保存后 **重新部署**。 |
+| B. 改自建 fork | 若平台下拉列表**没有**当前 `.nvmrc` 里的版本：把 fork 里的 `.nvmrc` 改成平台**已有**的 Node 22 版本，推送后再构建。 |
 | C. 清缓存 / 换分支 | 确认构建日志里 **Switching node** 读到的版本已更新；关闭「使用构建缓存」或触发一次无缓存构建，排除旧 `.nvmrc` 缓存。 |
 | D. `engines` 报错时 | 若日志是 **Yarn does not satisfy engine** 而非 **Switching node**：在构建环境设 `YARN_IGNORE_ENGINES=1`（仅当确为 engines 校验问题时使用）。 |
-| E. 不要用「只写 major」** | 少数平台不支持 `.nvmrc` 仅写 `20`；若遇解析错误，改为 **`20.18.0`** 这类完整补丁号。 |
+| E. 按平台要求写版本 | 若平台不支持 `.nvmrc` 仅写 `22`，改为平台支持的 Node 22 完整版本号。 |
 
 说明：Next.js 14 与当前依赖不要求锁在某一补丁版本；**兼容的核心是「平台实际能装上的 Node」与「`.nvmrc` / 控制台选择」一致**。
 
@@ -221,7 +221,7 @@ EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制
 
 ### `yarn install` 报 `ENOSPC: no space left on device`
 
-与 **Node 版本** 无关（日志里已出现 `Now, we're on node version v20.18.0` 即表示切换成功）。错误 **`ENOSPC`** 表示构建机 **磁盘或 `/dev/shm`（内存盘）空间不足**，在安装依赖从 cache 拷贝到 `node_modules` 时写满。
+与 **Node 版本** 无关（日志里已出现正确 Node 版本即表示切换成功）。错误 **`ENOSPC`** 表示构建机 **磁盘或 `/dev/shm`（内存盘）空间不足**，在安装依赖从 cache 拷贝到 `node_modules` 时写满。
 
 **可尝试：**
 
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 12751e4c6b9..ebc045c849b 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -4,7 +4,7 @@
 
 ### 环境要求
 
-- Node.js 20 LTS（具体补丁版本以仓库根目录 `.nvmrc` 为准，与各托管平台预装 Node 对齐）
+- Node.js 22 LTS（具体版本以仓库根目录 `.nvmrc` 为准，与各托管平台预装 Node 对齐）
 - Yarn = 1.22.22（使用 `package.json#packageManager` 保持一致）
 - Git
 
diff --git a/Dockerfile b/Dockerfile
index c8db90bfb3d..86c6b6f793c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 ARG NOTION_PAGE_ID
 ARG NEXT_PUBLIC_THEME
 
-FROM node:20-alpine AS base
+FROM node:22-alpine AS base
 
 # 1. Install dependencies only when needed
 FROM base AS deps
diff --git a/README.md b/README.md
index 6a4b811ab47..c268913e545 100644
--- a/README.md
+++ b/README.md
@@ -94,10 +94,10 @@ NotionNext 是一个基于 **Next.js + Notion API** 的开源站点系统。你
 
 ## 本地开发
 
-推荐使用 Node 20 和 Yarn 1。
+推荐使用 Node 22 和 Yarn 1。Node 20 已无法安装当前依赖（`@ai-sdk/google` 要求 Node >=22），部署平台也需要同步设置为 Node 22。
 
 ```bash
-# 1. 使用 Node 20
+# 1. 使用 Node 22
 nvm use || nvm install
 
 # 2. 安装 Yarn
diff --git a/README_EN.md b/README_EN.md
index 62bfc3dfa03..a123b33bc6f 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -49,7 +49,7 @@ A static blog system built with NextJS and Notion API, deployed on Vercel. Desig
 ## 3-Minute Quick Start
 
 ```bash
-# 1) Use Node 20 (nvm recommended)
+# 1) Use Node 22 (nvm recommended; Node 20 cannot install current dependencies)
 nvm use || nvm install
 
 # 2) Install Yarn if needed
diff --git a/docs/user-guide/deploy/cloudflare-pages-docs.md b/docs/user-guide/deploy/cloudflare-pages-docs.md
index 1c0c6e1dfc1..d9b0078b403 100644
--- a/docs/user-guide/deploy/cloudflare-pages-docs.md
+++ b/docs/user-guide/deploy/cloudflare-pages-docs.md
@@ -119,7 +119,7 @@ npx wrangler pages deploy .vitepress/dist --project-name=notionnext-docs
 | --- | --- |
 | 构建命令 | `yarn install && yarn docs:site:build` |
 | 输出目录 | `.vitepress/dist` |
-| Node | `20` |
+| Node | `22` |
 
 组织仓库更建议用上文 **Actions** 流程。
 
diff --git a/docs/user-guide/deploy/cloudflare-pages.md b/docs/user-guide/deploy/cloudflare-pages.md
index cb9180555f4..7f1176c18c7 100644
--- a/docs/user-guide/deploy/cloudflare-pages.md
+++ b/docs/user-guide/deploy/cloudflare-pages.md
@@ -104,7 +104,7 @@ Cloudflare Pages 提供 V1、V2、V3 三个版本的构建镜像。**V1 将于 2
 
 | 变量名 | 值 | 说明 |
 |--------|-----|------|
-| `NODE_VERSION` | `20` | Node.js 版本（V3 默认 22，项目使用 20） |
+| `NODE_VERSION` | `22` | Node.js 版本（项目使用 22） |
 | `YARN_VERSION` | `1.22.22` | **必需**：V3 默认 Yarn 4.x，必须指定 Yarn 1.x |
 | `BUILD_MODE` | `true` | 启用构建模式 |
 | `EXPORT` | `true` | 启用静态导出 |
@@ -147,7 +147,7 @@ V3 还支持通过文件检测 Node.js 版本（`.nvmrc` 和 `.node-version`）
 2. 前往 **Settings** > **Build** > **Build image version**
 3. 为 **Production** 和 **Preview** 环境都选择 **v3**
 4. 在 **Settings** > **Environment variables** 中添加 `YARN_VERSION` = `1.22.22`
-5. 确保 `NODE_VERSION` 设置为 `20`（或依赖 `.nvmrc` / `.node-version` 文件自动检测）
+5. 确保 `NODE_VERSION` 设置为 `22`（或依赖 `.nvmrc` / `.node-version` 文件自动检测）
 6. 保存设置，触发一次新的部署验证构建成功
 
 ### V3 重要限制
@@ -167,12 +167,12 @@ V3 构建镜像与 V1/V2 有以下行为差异，需要注意：
 | Build command | 构建设置 | `yarn export` | 静态导出命令 |
 | Output directory | 构建设置 | `out` | 静态文件输出目录 |
 | Build image version | 构建设置 | **v3** | 生产和预览环境都选 v3 |
-| `NODE_VERSION` | 环境变量 | `20` | Node.js 20.x |
+| `NODE_VERSION` | 环境变量 | `22` | Node.js 22.x |
 | `YARN_VERSION` | 环境变量 | `1.22.22` | **必需** — 保持 Yarn Classic |
 | `BUILD_MODE` | 环境变量 | `true` | 构建模式 |
 | `EXPORT` | 环境变量 | `true` | 静态导出模式 |
-| `.nvmrc` | 项目文件 | `20.18.0` | Node 版本文件（V3 支持） |
-| `.node-version` | 项目文件 | `20.18.0` | Node 版本文件（V3 支持） |
+| `.nvmrc` | 项目文件 | `22` | Node 版本文件（V3 支持） |
+| `.node-version` | 项目文件 | `22` | Node 版本文件（V3 支持） |
 
 ### 版本固定最佳实践
 
diff --git a/docs/user-guide/deploy/index.md b/docs/user-guide/deploy/index.md
index a5ebbc43c20..4c5f1184dcb 100644
--- a/docs/user-guide/deploy/index.md
+++ b/docs/user-guide/deploy/index.md
@@ -21,7 +21,7 @@
 | [cloudflare-pages.md](./cloudflare-pages.md) | Cloudflare 静态 |
 | [edgeone-pages.md](./edgeone-pages.md) | **腾讯云 EdgeOne**（Node 版本、ENOSPC、Next SSG 预设） |
 | [build-tuning.md](./build-tuning.md) | **构建超时 / Notion 预热与限流**（环境变量） |
-| [vps.md](./vps.md) | VPS / Docker（Node 20+） |
+| [vps.md](./vps.md) | VPS / Docker（Node 22+） |
 
 ## 构建超时排错
 
@@ -29,7 +29,7 @@
 
 ## 环境要求（2026）
 
-- **Node.js 20**（见 `.nvmrc`）
+- **Node.js 22**（见 `.nvmrc` / `.node-version`）。Node 20 已无法安装当前依赖，Cloudflare、Netlify、VPS、Docker 等构建环境都应同步使用 Node 22。
 - **Yarn**：`yarn` → `yarn build` / `yarn export`
 - 必配：`NOTION_PAGE_ID`
 
diff --git a/docs/user-guide/deploy/netlify.md b/docs/user-guide/deploy/netlify.md
index c0d030883ee..63e910bb124 100644
--- a/docs/user-guide/deploy/netlify.md
+++ b/docs/user-guide/deploy/netlify.md
@@ -8,7 +8,7 @@
 ::: tip 提示
 从NotionNext 4.0.9 开始 支持 Netlify部署，Netlify相比Vercel有更充足的免费额度，而且中国大陆访问的速度很不错。
 
-构建环境请使用 Node 20（与官方仓库 README 一致），本地开发与构建命令以 Yarn 为准，例如 &lt;code&gt;yarn&lt;/code&gt;、&lt;code&gt;yarn build&lt;/code&gt;。
+构建环境请使用 Node 22（与官方仓库 README 一致），本地开发与构建命令以 Yarn 为准，例如 &lt;code&gt;yarn&lt;/code&gt;、&lt;code&gt;yarn build&lt;/code&gt;。
 :::
 
 
diff --git a/docs/user-guide/deploy/vps.md b/docs/user-guide/deploy/vps.md
index b2d53c041d5..322f77d1bfc 100644
--- a/docs/user-guide/deploy/vps.md
+++ b/docs/user-guide/deploy/vps.md
@@ -156,7 +156,7 @@ git clone https://github.com/tangly1024/NotionNext &&
 
 #### 1. Docker运行
 
-原先文档是推荐在服务器上使用NodeJS环境，奈何在NotionNext V4.0 之后的新版要用到 Node 20 及以上的环境，这里有部分服务器无法兼容安装。同时Docker本身的稳定性也更加适合在生产环境使用，因此我在2025年7月8日更新了这部分手册，加入了Docker运行方案。
+原先文档是推荐在服务器上使用NodeJS环境，奈何当前新版要用到 Node 22 及以上的环境，这里有部分服务器无法兼容安装。同时Docker本身的稳定性也更加适合在生产环境使用，因此我在2025年7月8日更新了这部分手册，加入了Docker运行方案。
 
 1. 安装Docker最新版(CentOS为例)
 ```Bash
@@ -193,7 +193,7 @@ cd NotionNext
 # 2.创建构建文件
 cat &gt; Dockerfile &lt;&lt;EOF
 # 第一阶段：基础镜像
-FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:20-alpine AS base
+FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine AS base
 
 # 第二阶段：依赖安装
 FROM base AS deps
@@ -311,8 +311,8 @@ echo "source ~/.nvm/nvm.sh" &gt;&gt; ~/.bashrc
 # 2. 更新变量环境
 source ~/.bashrc
 
-# 3. 安装 nodejs ， 这里举例使用 Node 20（与官方 README 要求一致），其它版本请自行按需选择
-nvm install 20
+# 3. 安装 nodejs ， 这里举例使用 Node 22（与官方 README 要求一致），其它版本请自行按需选择
+nvm install 22
 ```
 ::: tip 提示
 题外话：
diff --git a/docs/user-guide/development/getting-started.md b/docs/user-guide/development/getting-started.md
index 2c09575b47e..9321e480425 100644
--- a/docs/user-guide/development/getting-started.md
+++ b/docs/user-guide/development/getting-started.md
@@ -105,7 +105,7 @@ github 会临时分配给您一个临时的域名，用于开发调试
 
 Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
 ::: tip 提示
-建议使用 **Node 20**（与仓库 README、`.nvmrc` 一致，例如 `20.20.0`）；若暂时仍在 18 系，请至少使用 **≥ 18.17.0**（如 18.20.4）。安装后执行 `node -v` 自检。
+建议使用 **Node 22**（与仓库 README、`.nvmrc` 一致）。安装后执行 `node -v` 自检。
 
 个别小版本若遇构建问题，请以 GitHub 仓库 README 与 Issues 的最新说明为准。
       - 安装成功检验
diff --git a/lib/db/notion/getPostBlocks.js b/lib/db/notion/getPostBlocks.js
index 0f6bedd2e8a..93c307b266a 100644
--- a/lib/db/notion/getPostBlocks.js
+++ b/lib/db/notion/getPostBlocks.js
@@ -236,29 +236,33 @@ export function formatNotionBlock(block) {
 
       // Case 1: inline children (original format)
       if (Array.isArray(b.value.children) && b.value.children.length > 0) {
-        delete clonedBlock[blockId]
         b.value.children.forEach((childBlock, index) => {
           const newBlockId = `${blockId}_child_${index}`
           clonedBlock[newBlockId] = childBlock
+          reparentBlock(clonedBlock[newBlockId], newBlockId, b.value.parent_id)
           childBlockIds.push(newBlockId)
         })
+        replaceContentReference(clonedBlock, blockId, childBlockIds)
+        delete clonedBlock[blockId]
         blocksToProcess.splice(i, 1, ...childBlockIds)
         continue
       }
 
       // Case 2: content array with child block IDs (some Notion API responses)
       if (Array.isArray(b.value.content) && b.value.content.length > 0) {
-        delete clonedBlock[blockId]
         b.value.content.forEach((childId, index) => {
           const childBlock = clonedBlock[childId]
           if (childBlock) {
             const newBlockId = `${blockId}_child_${index}`
             clonedBlock[newBlockId] = childBlock
+            reparentBlock(clonedBlock[newBlockId], newBlockId, b.value.parent_id)
             childBlockIds.push(newBlockId)
             delete clonedBlock[childId]
           }
         })
         if (childBlockIds.length > 0) {
+          replaceContentReference(clonedBlock, blockId, childBlockIds)
+          delete clonedBlock[blockId]
           blocksToProcess.splice(i, 1, ...childBlockIds)
           continue
         }
@@ -296,6 +300,21 @@ export function formatNotionBlock(block) {
   return clonedBlock
 }
 
+function replaceContentReference(blockMap, oldId, newIds) {
+  Object.values(blockMap || {}).forEach(entry => {
+    const block = entry?.value || entry
+    if (!Array.isArray(block?.content)) return
+    block.content = block.content.flatMap(id => id === oldId ? newIds : [id])
+  })
+}
+
+function reparentBlock(entry, id, parentId) {
+  const block = entry?.value || entry
+  if (!block) return
+  block.id = id
+  if (parentId) block.parent_id = parentId
+}
+
 /**
  * 根据[]ids，批量抓取blocks
  * 在获取数据库文章列表时，超过一定数量的block会被丢弃，因此根据pageId批量抓取block
diff --git a/netlify.toml b/netlify.toml
index 70ba3726b77..2ebcbcca87c 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -3,7 +3,7 @@
   publish = ".next"
 
 [build.environment]
-  NODE_VERSION = "20"
+  NODE_VERSION = "22"
 
 [[plugins]]
   package = "@netlify/plugin-nextjs"
diff --git a/package.json b/package.json
index 3710f627052..114a04d38e3 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "homepage": "https://github.com/tangly1024/NotionNext.git",
   "license": "MIT",
   "engines": {
-    "node": ">=20 <25"
+    "node": ">=22 <25"
   },
   "repository": {
     "type": "git",


Test patch:
diff --git a/__tests__/lib/db/notion/getPostBlocks.test.js b/__tests__/lib/db/notion/getPostBlocks.test.js
index ec8a1c72a2b..e4c45bd2c01 100644
--- a/__tests__/lib/db/notion/getPostBlocks.test.js
+++ b/__tests__/lib/db/notion/getPostBlocks.test.js
@@ -71,6 +71,78 @@ describe('formatNotionBlock', () => {
     expect(formatted['apple-music-song'].value.type).toBe('embed')
   })
 
+  it('relinks synced block content children to the original parent', () => {
+    const formatted = formatNotionBlock({
+      page: {
+        value: {
+          id: 'page',
+          type: 'page',
+          content: ['sync']
+        }
+      },
+      sync: {
+        value: {
+          id: 'sync',
+          type: 'sync_block',
+          parent_id: 'page',
+          content: ['notice-line']
+        }
+      },
+      'notice-line': {
+        value: {
+          id: 'notice-line',
+          type: 'text',
+          parent_id: 'sync',
+          properties: {
+            title: [['Notice']]
+          }
+        }
+      }
+    })
+
+    expect(formatted.page.value.content).toEqual(['sync_child_0'])
+    expect(formatted.sync).toBeUndefined()
+    expect(formatted['notice-line']).toBeUndefined()
+    expect(formatted.sync_child_0.value.id).toBe('sync_child_0')
+    expect(formatted.sync_child_0.value.parent_id).toBe('page')
+  })
+
+  it('relinks synced block inline children to the original parent', () => {
+    const formatted = formatNotionBlock({
+      page: {
+        value: {
+          id: 'page',
+          type: 'page',
+          content: ['sync']
+        }
+      },
+      sync: {
+        value: {
+          id: 'sync',
+          type: 'sync_block',
+          parent_id: 'page',
+          children: [
+            {
+              value: {
+                id: 'inline-child',
+                type: 'text',
+                parent_id: 'sync',
+                properties: {
+                  title: [['Inline notice']]
+                }
+              }
+            }
+          ]
+        }
+      }
+    })
+
+    expect(formatted.page.value.content).toEqual(['sync_child_0'])
+    expect(formatted.sync).toBeUndefined()
+    expect(formatted.sync_child_0.value.id).toBe('sync_child_0')
+    expect(formatted.sync_child_0.value.parent_id).toBe('page')
+  })
+
   it('keeps regular hosted videos as video blocks', () => {
     const formatted = formatNotionBlock({
       'hosted-video': {