Skip to content

Commit 2651700

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 6b08954 commit 2651700

3 files changed

Lines changed: 63 additions & 35 deletions

File tree

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

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,10 @@ 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

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) {
17-
continue;
18-
}
19-
if (block.value.value?.type) {
20-
normalizedBlock[key] = { ...block, value: block.value.value };
21-
} else {
22-
normalizedBlock[key] = block;
23-
}
24-
}
25-
return { ...recordMap, block: normalizedBlock };
26-
}
27-
2813
export default async function TrackComponent({ params }: { params: { pdfId: string[] } }) {
2914
const trackId: string = params.pdfId[0] || "";
3015
const problemId = params.pdfId[1];
@@ -40,10 +25,9 @@ export default async function TrackComponent({ params }: { params: { pdfId: stri
4025
}
4126

4227
if (problemDetails?.notionDocId && trackDetails?.problems) {
43-
// notionRecordMaps = await notion.getPage(problemDetails.notionDocId);
4428
notionRecordMaps = await Promise.all(
4529
trackDetails.problems.map(
46-
async (problem: any) => normalizeRecordMap(await notion.getPage((await getProblem(problem.id))?.notionDocId!))
30+
async (problem: any) => fetchNotionPage(notion, (await getProblem(problem.id))?.notionDocId!)
4731
)
4832
);
4933
}

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

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,10 @@ import { redirect, notFound } from "next/navigation";
44
import { getAllTracks, getProblem, getTrack } from "../../../components/utils";
55
import { cache } from "react";
66
import { LessonView } from "../../../components/LessonView";
7+
import { fetchNotionPage } from "../../../lib/notion";
78

89
const notion = new NotionAPI();
910

10-
function normalizeRecordMap(recordMap: any) {
11-
if (!recordMap?.block) return recordMap;
12-
const normalizedBlock: any = {};
13-
for (const [key, block] of Object.entries(recordMap.block) as any) {
14-
if (!block?.value) {
15-
continue;
16-
}
17-
if (block.value.value?.type) {
18-
normalizedBlock[key] = { ...block, value: block.value.value };
19-
} else {
20-
normalizedBlock[key] = block;
21-
}
22-
}
23-
return { ...recordMap, block: normalizedBlock };
24-
}
2511
export const dynamic = "auto";
2612
// Dynamic Metadata
2713
export async function generateMetadata({ params }: { params: { trackIds: string[] } }) {
@@ -49,7 +35,7 @@ export async function generateMetadata({ params }: { params: { trackIds: string[
4935
description: "The track you are looking for does not exist.",
5036
openGraph: {
5137
title: "Track Not Found",
52-
description: "The track you are looking for does not exist.",
38+
description: "Track Not Found",
5339
images: [
5440
{
5541
url: "/default-thumbnail.jpg", // Use a default image if the track is not found
@@ -96,7 +82,7 @@ export default async function TrackComponent({ params }: { params: { trackIds: s
9682
}
9783

9884
if (problemDetails?.notionDocId) {
99-
notionRecordMap = normalizeRecordMap(await notion.getPage(problemDetails.notionDocId));
85+
notionRecordMap = await fetchNotionPage(notion, problemDetails.notionDocId);
10086
}
10187

10288
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 {
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)