Skip to content

Commit b21a2f9

Browse files
hkiratclaude
andcommitted
fix: fetch missing Notion descendants after normalizing nested shape
notion-client's getPage walks the raw recordMap to find missing descendants, but Notion's API now wraps blocks in an extra value.value layer — so the walk reads block.value.content (undefined) and stops at the root. Toggle children and other nested blocks were never fetched, which is why toggles expanded to empty content. Extract a fetchNotionPage helper that disables the broken traversal, normalizes the shape, then manually walks the tree and fetches missing ids via getBlocks until complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b633c9b commit b21a2f9

File tree

3 files changed

+62
-22
lines changed

3 files changed

+62
-22
lines changed

apps/web/app/pdf/[...pdfId]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getProblem, getTrack } from "../../../components/utils";
66
import { LessonView } from "../../../components/LessonView";
77
import { getServerSession } from "next-auth";
88
import { authOptions } from "../../../lib/auth";
9+
import { fetchNotionPage } from "../../../lib/notion";
910

1011
const notion = new NotionAPI();
1112

@@ -24,10 +25,9 @@ export default async function TrackComponent({ params }: { params: { pdfId: stri
2425
}
2526

2627
if (problemDetails?.notionDocId && trackDetails?.problems) {
27-
// notionRecordMaps = await notion.getPage(problemDetails.notionDocId);
2828
notionRecordMaps = await Promise.all(
2929
trackDetails.problems.map(
30-
async (problem: any) => await notion.getPage((await getProblem(problem.id))?.notionDocId!)
30+
async (problem: any) => fetchNotionPage(notion, (await getProblem(problem.id))?.notionDocId!)
3131
)
3232
);
3333
}

apps/web/app/tracks/[...trackIds]/page.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,11 @@ import { NotionAPI } from "notion-client";
33
import { redirect, notFound } from "next/navigation";
44
import { getProblem, getTrack } from "../../../components/utils";
55
import { LessonView } from "../../../components/LessonView";
6+
import { fetchNotionPage } from "../../../lib/notion";
67

78
const notion = new NotionAPI();
89
export const dynamic = "force-dynamic";
910

10-
// Normalize Notion record map to handle nested value.value structure
11-
// and remove blocks with no actual data (role-only entries)
12-
function normalizeRecordMap(recordMap: any) {
13-
if (!recordMap?.block) return recordMap;
14-
const normalizedBlock: any = {};
15-
for (const [key, block] of Object.entries(recordMap.block) as any) {
16-
if (block?.value?.value) {
17-
// Fix double-nested value.value structure
18-
normalizedBlock[key] = { ...block, value: block.value.value };
19-
} else if (block?.value?.type) {
20-
// Normal block with type - keep as is
21-
normalizedBlock[key] = block;
22-
}
23-
// Skip blocks with no type (role-only entries like { value: { role: "none" } })
24-
}
25-
return { ...recordMap, block: normalizedBlock };
26-
}
27-
2811
// Dynamic Metadata
2912
export async function generateMetadata({ params }: { params: { trackIds: string[] } }) {
3013
const trackId = params.trackIds[0] || "";
@@ -78,8 +61,7 @@ export default async function TrackComponent({ params }: { params: { trackIds: s
7861
}
7962

8063
if (problemDetails?.notionDocId) {
81-
const rawRecordMap = await notion.getPage(problemDetails.notionDocId);
82-
notionRecordMap = normalizeRecordMap(rawRecordMap);
64+
notionRecordMap = await fetchNotionPage(notion, problemDetails.notionDocId);
8365
}
8466

8567
if (trackDetails && problemDetails) {

apps/web/lib/notion.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NotionAPI } from "notion-client";
2+
3+
function normalizeRecordMap(recordMap: any) {
4+
if (!recordMap?.block) return recordMap;
5+
const normalizedBlock: any = {};
6+
for (const [key, block] of Object.entries(recordMap.block) as any) {
7+
if (!block?.value) continue;
8+
const value = block.value;
9+
if (!value.type && value.value?.type) {
10+
normalizedBlock[key] = { ...block, value: value.value };
11+
} else if (value.type) {
12+
normalizedBlock[key] = block;
13+
}
14+
}
15+
return { ...recordMap, block: normalizedBlock };
16+
}
17+
18+
function collectContentBlockIds(recordMap: any): string[] {
19+
const blocks = recordMap?.block;
20+
if (!blocks) return [];
21+
const rootId = Object.keys(blocks)[0];
22+
if (!rootId) return [];
23+
24+
const seen = new Set<string>();
25+
const walk = (id: string) => {
26+
if (seen.has(id)) return;
27+
seen.add(id);
28+
const value = blocks[id]?.value;
29+
if (!value) return;
30+
if (id !== rootId && (value.type === "page" || value.type === "collection_view_page")) return;
31+
if (Array.isArray(value.content)) {
32+
for (const childId of value.content) walk(childId);
33+
}
34+
const refId = value.format?.transclusion_reference_pointer?.id;
35+
if (refId) walk(refId);
36+
};
37+
walk(rootId);
38+
return Array.from(seen);
39+
}
40+
41+
// Notion's API now returns blocks in a nested `value.value` shape. notion-client's
42+
// built-in missing-block traversal walks the raw map and can't see past that nesting,
43+
// so toggle children (and other nested descendants) never get fetched. We disable its
44+
// traversal, normalize the shape, then manually fetch descendants until the tree is
45+
// complete.
46+
export async function fetchNotionPage(notion: NotionAPI, pageId: string): Promise<any> {
47+
let recordMap: any = await notion.getPage(pageId, { fetchMissingBlocks: false });
48+
recordMap = normalizeRecordMap(recordMap);
49+
50+
for (let i = 0; i < 10; i++) {
51+
const missing = collectContentBlockIds(recordMap).filter((id) => !recordMap.block[id]);
52+
if (!missing.length) break;
53+
const fetched = await notion.getBlocks(missing).then((r: any) => r.recordMap.block);
54+
recordMap = normalizeRecordMap({ ...recordMap, block: { ...recordMap.block, ...fetched } });
55+
}
56+
57+
return recordMap;
58+
}

0 commit comments

Comments
 (0)