Read Notion Page Content
Read page content as structured blocks
Source Code
import fs from "fs";
const [pageId, outputPath = "session/notion-content.json", maxDepth = "2"] =
process.argv.slice(2);
if (!pageId) {
console.error("Error: pageId is required");
process.exit(1);
}
const maxDepthNum = Math.min(Math.max(parseInt(maxDepth) || 2, 1), 3);
const NOTION_API = "https://api.notion.com/v1";
const headers = {
Authorization: "Bearer PLACEHOLDER_TOKEN",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
};
/**
* Extract readable text from rich_text array
*/
function extractText(richText) {
if (!richText || !Array.isArray(richText)) return "";
return richText.map((t) => t.plain_text || "").join("");
}
/**
* Simplify a block for output
*/
function simplifyBlock(block) {
const simplified = {
id: block.id,
type: block.type,
hasChildren: block.has_children,
};
const content = block[block.type];
if (!content) return simplified;
switch (block.type) {
case "paragraph":
case "heading_1":
case "heading_2":
case "heading_3":
case "bulleted_list_item":
case "numbered_list_item":
case "quote":
case "toggle":
simplified.text = extractText(content.rich_text);
break;
case "to_do":
simplified.text = extractText(content.rich_text);
simplified.checked = content.checked;
break;
case "callout":
simplified.text = extractText(content.rich_text);
simplified.icon =
content.icon?.type === "emoji"
? content.icon.emoji
: content.icon?.external?.url;
break;
case "code":
simplified.text = extractText(content.rich_text);
simplified.language = content.language;
break;
case "image":
simplified.url =
content.type === "external" ? content.external?.url : content.file?.url;
simplified.caption = extractText(content.caption);
break;
case "video":
case "file":
case "pdf":
simplified.url =
content.type === "external" ? content.external?.url : content.file?.url;
break;
case "bookmark":
case "embed":
simplified.url = content.url;
break;
case "table":
simplified.tableWidth = content.table_width;
simplified.hasColumnHeader = content.has_column_header;
simplified.hasRowHeader = content.has_row_header;
break;
case "table_row":
simplified.cells = content.cells?.map((cell) => extractText(cell));
break;
case "child_page":
case "child_database":
simplified.title = content.title;
break;
}
return simplified;
}
/**
* Recursively fetch blocks with children
*/
async function fetchBlocksRecursive(blockId, depth = 1) {
const blocks = [];
let cursor = undefined;
while (true) {
const url = new URL(`${NOTION_API}/blocks/${blockId}/children`);
url.searchParams.set("page_size", "100");
if (cursor) url.searchParams.set("start_cursor", cursor);
const res = await fetch(url.toString(), { method: "GET", headers });
if (!res.ok) {
const errorText = await res.text();
console.error(`Failed to fetch blocks: ${res.status}`);
console.error(errorText);
throw new Error(`Failed to fetch blocks: ${res.status}`);
}
const response = await res.json();
for (const block of response.results) {
const simplified = simplifyBlock(block);
if (block.has_children && depth < maxDepthNum) {
simplified.children = await fetchBlocksRecursive(block.id, depth + 1);
}
blocks.push(simplified);
}
if (!response.has_more) break;
cursor = response.next_cursor;
}
return blocks;
}
try {
console.log(`Reading content from page ${pageId}...`);
console.log(` Max depth: ${maxDepthNum}`);
// Get page info
let pageTitle = null;
try {
const pageRes = await fetch(`${NOTION_API}/pages/${pageId}`, {
method: "GET",
headers,
});
if (pageRes.ok) {
const pageInfo = await pageRes.json();
const titleProp = Object.values(pageInfo.properties || {}).find(
(p) => p.type === "title"
);
pageTitle = titleProp?.title?.[0]?.plain_text || "Untitled";
}
} catch {
// Might be a block ID
}
if (pageTitle) {
console.log(` Page: ${pageTitle}`);
}
const blocks = await fetchBlocksRecursive(pageId);
// Ensure output directory exists
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
if (dir) {
fs.mkdirSync(dir, { recursive: true });
}
const output = {
pageId,
pageTitle,
readAt: new Date().toISOString(),
blockCount: blocks.length,
blocks,
};
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
// Count block types
const typeCounts = {};
function countTypes(blockList) {
for (const block of blockList) {
typeCounts[block.type] = (typeCounts[block.type] || 0) + 1;
if (block.children) countTypes(block.children);
}
}
countTypes(blocks);
console.log(`\n✓ Read ${blocks.length} top-level blocks`);
console.log(` Written to: ${outputPath}`);
console.log(
` Block types: ${Object.entries(typeCounts)
.map(([t, c]) => `${t}(${c})`)
.join(", ")}`
);
// Preview
const textBlocks = blocks.filter((b) => b.text);
if (textBlocks.length > 0) {
console.log(`\n Preview:`);
for (const block of textBlocks.slice(0, 3)) {
const preview =
block.text.length > 60
? block.text.substring(0, 60) + "..."
: block.text;
console.log(` [${block.type}] ${preview}`);
}
}
console.log(
JSON.stringify({
success: true,
pageId,
pageTitle,
blockCount: blocks.length,
outputPath,
})
);
} catch (error) {
console.error("Error:", error.message);
throw error;
} import fs from "fs";
const [pageId, outputPath = "session/notion-content.json", maxDepth = "2"] =
process.argv.slice(2);
if (!pageId) {
console.error("Error: pageId is required");
process.exit(1);
}
const maxDepthNum = Math.min(Math.max(parseInt(maxDepth) || 2, 1), 3);
const NOTION_API = "https://api.notion.com/v1";
const headers = {
Authorization: "Bearer PLACEHOLDER_TOKEN",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
};
/**
* Extract readable text from rich_text array
*/
function extractText(richText) {
if (!richText || !Array.isArray(richText)) return "";
return richText.map((t) => t.plain_text || "").join("");
}
/**
* Simplify a block for output
*/
function simplifyBlock(block) {
const simplified = {
id: block.id,
type: block.type,
hasChildren: block.has_children,
};
const content = block[block.type];
if (!content) return simplified;
switch (block.type) {
case "paragraph":
case "heading_1":
case "heading_2":
case "heading_3":
case "bulleted_list_item":
case "numbered_list_item":
case "quote":
case "toggle":
simplified.text = extractText(content.rich_text);
break;
case "to_do":
simplified.text = extractText(content.rich_text);
simplified.checked = content.checked;
break;
case "callout":
simplified.text = extractText(content.rich_text);
simplified.icon =
content.icon?.type === "emoji"
? content.icon.emoji
: content.icon?.external?.url;
break;
case "code":
simplified.text = extractText(content.rich_text);
simplified.language = content.language;
break;
case "image":
simplified.url =
content.type === "external" ? content.external?.url : content.file?.url;
simplified.caption = extractText(content.caption);
break;
case "video":
case "file":
case "pdf":
simplified.url =
content.type === "external" ? content.external?.url : content.file?.url;
break;
case "bookmark":
case "embed":
simplified.url = content.url;
break;
case "table":
simplified.tableWidth = content.table_width;
simplified.hasColumnHeader = content.has_column_header;
simplified.hasRowHeader = content.has_row_header;
break;
case "table_row":
simplified.cells = content.cells?.map((cell) => extractText(cell));
break;
case "child_page":
case "child_database":
simplified.title = content.title;
break;
}
return simplified;
}
/**
* Recursively fetch blocks with children
*/
async function fetchBlocksRecursive(blockId, depth = 1) {
const blocks = [];
let cursor = undefined;
while (true) {
const url = new URL(`${NOTION_API}/blocks/${blockId}/children`);
url.searchParams.set("page_size", "100");
if (cursor) url.searchParams.set("start_cursor", cursor);
const res = await fetch(url.toString(), { method: "GET", headers });
if (!res.ok) {
const errorText = await res.text();
console.error(`Failed to fetch blocks: ${res.status}`);
console.error(errorText);
throw new Error(`Failed to fetch blocks: ${res.status}`);
}
const response = await res.json();
for (const block of response.results) {
const simplified = simplifyBlock(block);
if (block.has_children && depth < maxDepthNum) {
simplified.children = await fetchBlocksRecursive(block.id, depth + 1);
}
blocks.push(simplified);
}
if (!response.has_more) break;
cursor = response.next_cursor;
}
return blocks;
}
try {
console.log(`Reading content from page ${pageId}...`);
console.log(` Max depth: ${maxDepthNum}`);
// Get page info
let pageTitle = null;
try {
const pageRes = await fetch(`${NOTION_API}/pages/${pageId}`, {
method: "GET",
headers,
});
if (pageRes.ok) {
const pageInfo = await pageRes.json();
const titleProp = Object.values(pageInfo.properties || {}).find(
(p) => p.type === "title"
);
pageTitle = titleProp?.title?.[0]?.plain_text || "Untitled";
}
} catch {
// Might be a block ID
}
if (pageTitle) {
console.log(` Page: ${pageTitle}`);
}
const blocks = await fetchBlocksRecursive(pageId);
// Ensure output directory exists
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
if (dir) {
fs.mkdirSync(dir, { recursive: true });
}
const output = {
pageId,
pageTitle,
readAt: new Date().toISOString(),
blockCount: blocks.length,
blocks,
};
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
// Count block types
const typeCounts = {};
function countTypes(blockList) {
for (const block of blockList) {
typeCounts[block.type] = (typeCounts[block.type] || 0) + 1;
if (block.children) countTypes(block.children);
}
}
countTypes(blocks);
console.log(`\n✓ Read ${blocks.length} top-level blocks`);
console.log(` Written to: ${outputPath}`);
console.log(
` Block types: ${Object.entries(typeCounts)
.map(([t, c]) => `${t}(${c})`)
.join(", ")}`
);
// Preview
const textBlocks = blocks.filter((b) => b.text);
if (textBlocks.length > 0) {
console.log(`\n Preview:`);
for (const block of textBlocks.slice(0, 3)) {
const preview =
block.text.length > 60
? block.text.substring(0, 60) + "..."
: block.text;
console.log(` [${block.type}] ${preview}`);
}
}
console.log(
JSON.stringify({
success: true,
pageId,
pageTitle,
blockCount: blocks.length,
outputPath,
})
);
} catch (error) {
console.error("Error:", error.message);
throw error;
}