Skip to content

Commit c208e72

Browse files
authored
Merge branch 'tangly1024:main' into main
2 parents be07302 + 4f6e0a4 commit c208e72

10 files changed

Lines changed: 220 additions & 12 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 每次向 main 推送(含合并 PR)后,自动将 package.json 最后一位小版本 +1。
2+
# 自动提交信息含 [skip-version],避免无限循环。发正式版或大版本时请人工改版本号后再合并。
3+
4+
name: Bump package version on main
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: write
14+
15+
concurrency:
16+
group: bump-package-version-main
17+
cancel-in-progress: false
18+
19+
jobs:
20+
bump-patch:
21+
runs-on: ubuntu-latest
22+
# 跳过自动发版机器人自己的提交,防止循环触发
23+
if: >-
24+
github.event_name == 'workflow_dispatch' ||
25+
(github.event.head_commit != null &&
26+
!contains(github.event.head_commit.message, '[skip-version]'))
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Bump package.json patch (last segment)
35+
run: node scripts/bump-package-patch-version.js
36+
37+
- name: Commit and push if changed
38+
run: |
39+
git config user.name "github-actions[bot]"
40+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
41+
if git diff --quiet package.json; then
42+
echo "No version change (unexpected)."
43+
exit 0
44+
fi
45+
NEW_VER=$(node -p "require('./package.json').version")
46+
git add package.json
47+
git commit -m "chore(release): bump package.json to ${NEW_VER} [skip-version]"
48+
git push

__tests__/lib/notion-data-format.test.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import getAllPageIds from '@/lib/db/notion/getAllPageIds'
2-
import { adapterNotionBlockMap } from '@/lib/utils/notion.util'
2+
import {
3+
adapterNotionBlockMap,
4+
normalizeNotionBlockType
5+
} from '@/lib/utils/notion.util'
36

47
jest.mock('p-limit', () => ({
58
__esModule: true,
69
default: jest.fn(() => fn => fn())
710
}))
811

12+
jest.mock('notion-utils', () => ({
13+
getTextContent: jest.fn(value => {
14+
if (!Array.isArray(value)) return ''
15+
return value.map(item => item?.[0] || '').join('')
16+
})
17+
}))
18+
919
jest.mock('@/lib/cache/cache_manager', () => ({
1020
getDataFromCache: jest.fn(),
1121
getOrSetDataWithCache: jest.fn(),
@@ -21,6 +31,7 @@ jest.mock('@/lib/db/notion/getNotionAPI', () => ({
2131
}))
2232

2333
const { formatNotionBlock } = require('@/lib/db/notion/getPostBlocks')
34+
const { getPageTableOfContents } = require('@/lib/db/notion/getPageTableOfContents')
2435

2536
describe('Notion data format compatibility', () => {
2637
it('unwraps nested block values returned by newer Notion payloads', () => {
@@ -121,4 +132,58 @@ describe('Notion data format compatibility', () => {
121132
'https://example.com/image.png'
122133
)
123134
})
135+
136+
it('downgrades newer heading block types for older renderers', () => {
137+
expect(normalizeNotionBlockType('heading_1')).toBe('header')
138+
expect(normalizeNotionBlockType('heading_2')).toBe('sub_header')
139+
expect(normalizeNotionBlockType('heading_3')).toBe('sub_sub_header')
140+
expect(normalizeNotionBlockType('heading_4')).toBe('sub_sub_header')
141+
expect(normalizeNotionBlockType('header_4')).toBe('sub_sub_header')
142+
})
143+
144+
it('formats header_4 blocks into a renderable fallback heading type', () => {
145+
const formatted = formatNotionBlock({
146+
page_1: {
147+
value: {
148+
id: 'page_1',
149+
type: 'header_4',
150+
properties: {
151+
title: [['Section 4']]
152+
}
153+
}
154+
}
155+
})
156+
157+
expect(formatted.page_1.value.type).toBe('sub_sub_header')
158+
})
159+
160+
it('builds a stable toc for newer heading block types', () => {
161+
const page = {
162+
id: 'page_root',
163+
content: ['h1', 'h4']
164+
}
165+
const recordMap = {
166+
block: {
167+
h1: {
168+
value: {
169+
id: 'h1',
170+
type: 'header',
171+
properties: { title: [['Top']] }
172+
}
173+
},
174+
h4: {
175+
value: {
176+
id: 'h4',
177+
type: 'header_4',
178+
properties: { title: [['Deep']] }
179+
}
180+
}
181+
}
182+
}
183+
184+
expect(getPageTableOfContents(page, recordMap)).toEqual([
185+
expect.objectContaining({ id: 'h1', indentLevel: 0 }),
186+
expect.objectContaining({ id: 'h4', indentLevel: 1 })
187+
])
188+
})
124189
})

lib/db/notion/getNotionPost.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import BLOG from '@/blog.config'
22
import { idToUuid } from 'notion-utils'
33
import ReactNotionX from 'react-notion-x'
44
import formatDate from '../../utils/formatDate'
5-
import { fetchNotionPageBlocks } from './getPostBlocks'
5+
import { fetchNotionPageBlocks, formatNotionBlock } from './getPostBlocks'
66
import { checkStrIsNotionId, checkStrIsUuid } from '@/lib/utils'
7+
import { adapterNotionBlockMap } from '@/lib/utils/notion.util'
78

89
/**
910
* 根据页面ID获取文章,同时打印获取耗时
@@ -14,14 +15,17 @@ export async function fetchPageFromNotion(pageId) {
1415
const start = Date.now() // 开始时间
1516

1617
// 获取页面内容块
17-
const blockMap = await fetchNotionPageBlocks(pageId, 'slug')
18+
const rawBlockMap = await fetchNotionPageBlocks(pageId, 'slug')
1819
const fetchEnd = Date.now() // fetchNotionPageBlocks 耗时
1920
console.log(`⏱ [Notion] pageId: ${pageId} fetch blocks耗时: ${fetchEnd - start}ms`)
2021

21-
if (!blockMap) {
22+
if (!rawBlockMap) {
2223
return null
2324
}
24-
25+
const blockMap = adapterNotionBlockMap(rawBlockMap)
26+
if (blockMap?.block) {
27+
blockMap.block = formatNotionBlock(blockMap.block)
28+
}
2529
if (checkStrIsNotionId(pageId)) {
2630
pageId = idToUuid(pageId)
2731
}

lib/db/notion/getPageTableOfContents.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ const indentLevels = {
66
sub_sub_header: 2,
77
heading_1: 0,
88
heading_2: 1,
9-
heading_3: 2
9+
heading_3: 2,
10+
heading_4: 3,
11+
header_4: 3
1012
}
1113

1214
const unknownHeadingStats = new Map()
@@ -95,7 +97,7 @@ function getBlockHeader(contents, recordMap, toc, pageId) {
9597
const { type } = block
9698
const isHeading =
9799
typeof type === 'string' &&
98-
(type.indexOf('header') >= 0 || /^heading_[123]$/.test(type))
100+
(type.indexOf('header') >= 0 || /^heading_[1234]$/.test(type))
99101

100102
if (block.content?.length > 0) {
101103
getBlockHeader(block.content, recordMap, toc, pageId)

lib/db/notion/getPostBlocks.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import {
66
import { deepClone, delay } from '../../utils'
77
import notionAPI from '@/lib/db/notion/getNotionAPI'
88
import pLimit from 'p-limit'
9+
import { normalizeNotionBlockType } from '@/lib/utils/notion.util'
910

1011
// ⚠️ 全局限流器(非常关键)
1112
const limit = pLimit(15)
1213

1314
// ⚠️ 每个请求之间的间隔(防 burst)
1415
const REQUEST_INTERVAL = 50 // ms
1516

16-
1717
/**
1818
* 获取文章内容块
1919
* @param {string} id
@@ -109,6 +109,7 @@ export function formatNotionBlock(block) {
109109
if (b?.value) {
110110
delete b.value.crdt_data
111111
delete b.value.crdt_format_version
112+
b.value.type = normalizeNotionBlockType(b.value.type)
112113
}
113114

114115
// 原有逻辑不变 ↓↓↓
@@ -269,4 +270,4 @@ function sanitizeBlockUrls(blockValue) {
269270

270271
// 4. 处理其他可能的 URL 字段(可选扩展)
271272
// 例如:video、audio 的 source 可能也走 properties.source,已覆盖
272-
}
273+
}

lib/utils/notion.util.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ export function adapterNotionBlockMap(blockMap) {
2828
};
2929
}
3030

31+
export function normalizeNotionBlockType(type) {
32+
switch (type) {
33+
case 'heading_1':
34+
return 'header'
35+
case 'heading_2':
36+
return 'sub_header'
37+
case 'heading_3':
38+
case 'heading_4':
39+
case 'header_4':
40+
return 'sub_sub_header'
41+
default:
42+
return type
43+
}
44+
}
45+
3146

3247
function unwrapValue(obj) {
3348
if (!obj) return obj
@@ -50,4 +65,4 @@ function unwrapValue(obj) {
5065

5166
// 兜底:原样返回
5267
return obj?.value ?? obj
53-
}
68+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"bundle-report": "cross-env ANALYZE=true next build",
2626
"build-all-in-dev": "cross-env VERCEL_ENV=production next build",
2727
"version": "echo $npm_package_version",
28+
"bump-patch-version": "node scripts/bump-package-patch-version.js",
2829
"lint": "next lint",
2930
"lint:fix": "next lint --fix",
3031
"type-check": "tsc --noEmit",

pages/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import BLOG from '@/blog.config'
22
import { siteConfig } from '@/lib/config'
33
import { fetchGlobalAllData, getPostBlocks } from '@/lib/db/SiteDataApi'
4+
import { formatNotionBlock } from '@/lib/db/notion/getPostBlocks'
45
import { generateRobotsTxt } from '@/lib/utils/robots.txt'
56
import { generateRss } from '@/lib/utils/rss'
67
import { generateSitemapXml } from '@/lib/utils/sitemap.xml'
78
import { DynamicLayout } from '@/themes/theme'
89
import { generateRedirectJson } from '@/lib/utils/redirect'
910
import { checkDataFromAlgolia } from '@/lib/plugins/algolia'
1011
import pLimit from 'p-limit'
12+
import { adapterNotionBlockMap } from '@/lib/utils/notion.util'
1113

1214
/**
1315
* 首页布局
@@ -78,7 +80,11 @@ export async function getStaticProps(req) {
7880
await Promise.all(
7981
previewTargets.map(post =>
8082
previewLimit(async () => {
81-
post.blockMap = await getPostBlocks(post.id, 'slug', POST_PREVIEW_LINES)
83+
const rawBlockMap = await getPostBlocks(post.id, 'slug', POST_PREVIEW_LINES)
84+
post.blockMap = adapterNotionBlockMap(rawBlockMap)
85+
if (post.blockMap?.block) {
86+
post.blockMap.block = formatNotionBlock(post.blockMap.block)
87+
}
8288
})
8389
)
8490
)

pages/page/[page].js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import BLOG from '@/blog.config'
22
import { siteConfig } from '@/lib/config'
33
import { fetchGlobalAllData, getPostBlocks } from '@/lib/db/SiteDataApi'
4+
import { formatNotionBlock } from '@/lib/db/notion/getPostBlocks'
5+
import { adapterNotionBlockMap } from '@/lib/utils/notion.util'
46
import { DynamicLayout } from '@/themes/theme'
57

68
/**
@@ -56,7 +58,11 @@ export async function getStaticProps({ params: { page }, locale }) {
5658
if (post.password && post.password !== '') {
5759
continue
5860
}
59-
post.blockMap = await getPostBlocks(post.id, 'slug', POST_PREVIEW_LINES)
61+
const rawBlockMap = await getPostBlocks(post.id, 'slug', POST_PREVIEW_LINES)
62+
post.blockMap = adapterNotionBlockMap(rawBlockMap)
63+
if (post.blockMap?.block) {
64+
post.blockMap.block = formatNotionBlock(post.blockMap.block)
65+
}
6066
}
6167
}
6268

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Increment the last numeric segment of package.json "version".
4+
* Supports 4-part versions (e.g. 4.9.5.2 -> 4.9.5.3) used by this repo,
5+
* and falls back to semver patch for 3-part (x.y.z -> x.y.(z+1)).
6+
*
7+
* Usage: node scripts/bump-package-patch-version.js [--dry-run]
8+
*/
9+
10+
const fs = require('fs')
11+
const path = require('path')
12+
13+
const root = path.join(__dirname, '..')
14+
const pkgPath = path.join(root, 'package.json')
15+
16+
const dryRun = process.argv.includes('--dry-run')
17+
18+
function bumpVersionString(current) {
19+
const parts = current.split('.').map(p => {
20+
const n = parseInt(p, 10)
21+
if (Number.isNaN(n)) {
22+
throw new Error(`Invalid version segment in "${current}"`)
23+
}
24+
return n
25+
})
26+
27+
if (parts.length < 3) {
28+
throw new Error(`Expected at least semver x.y.z, got "${current}"`)
29+
}
30+
31+
parts[parts.length - 1] += 1
32+
return parts.join('.')
33+
}
34+
35+
function main() {
36+
const raw = fs.readFileSync(pkgPath, 'utf8')
37+
const match = raw.match(/"version"\s*:\s*"([^"]+)"/)
38+
if (!match) {
39+
console.error('Could not find "version" field in package.json')
40+
process.exit(1)
41+
}
42+
43+
const current = match[1]
44+
const next = bumpVersionString(current)
45+
46+
if (dryRun) {
47+
console.log(`[dry-run] ${current} -> ${next}`)
48+
return
49+
}
50+
51+
const updated = raw.replace(
52+
/("version"\s*:\s*")([^"]+)(")/,
53+
(_, a, __, c) => `${a}${next}${c}`
54+
)
55+
56+
fs.writeFileSync(pkgPath, updated)
57+
console.log(`Bumped package.json version: ${current} -> ${next}`)
58+
}
59+
60+
main()

0 commit comments

Comments
 (0)