code icon Code

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;
}