Skip to content

Instantly share code, notes, and snippets.

@TakamiChie
Created July 23, 2025 10:30
Show Gist options
  • Select an option

  • Save TakamiChie/c8975ece2af2d090fc83c165fb442746 to your computer and use it in GitHub Desktop.

Select an option

Save TakamiChie/c8975ece2af2d090fc83c165fb442746 to your computer and use it in GitHub Desktop.
NotionページをPDFとして取得するGoogle Apps Script
const NOTION_API_URL = "https://api.notion.com/v1/";
function doGet(e) {
return ContentService.createTextOutput("It works!");
}
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
const pageId = payload.data?.id || payload.pageId;
const title = payload.data.properties["名前"].title[0].plain_text;
workPageID(pageId, title);
}
function workPageID(pageId, documentTitle){
try {
const { NOTION_API_KEY, TO_EMAIL } = getConfig();
if (!pageId || !NOTION_API_KEY || !TO_EMAIL) {
throw new Error("Missing required properties or page ID");
}
Logger.log(`Request PageID:${pageId}`);
const blocks = fetchAllBlocks(pageId, NOTION_API_KEY);
const doc = DocumentApp.create(documentTitle ? documentTitle : "Notion Export");
// ✅ 余白を狭く設定
const body = doc.getBody();
doc.getBody().setPageWidth(595); // 約A4横幅
doc.getBody().setPageHeight(842); // 約A4高さ
doc.getBody().setMarginTop(36);
doc.getBody().setMarginBottom(36);
doc.getBody().setMarginLeft(36);
doc.getBody().setMarginRight(36);
for (const block of blocks) {
renderBlockToDoc(body, block, NOTION_API_KEY);
}
Logger.log(`ページ構築完了`);
doc.saveAndClose();
const docFile = DriveApp.getFileById(doc.getId());
const pdfBlob = docFile.getAs("application/pdf");
Logger.log(`PDF作成完了 ID${doc.getId()}`);
MailApp.sendEmail({
to: TO_EMAIL,
subject: "NotionページのPDF変換",
body: "PDFを添付します。",
attachments: [pdfBlob]
});
Logger.log(`PDF送信完了`);
docFile.setTrashed(true);
Logger.log(`送信済みPDF削除完了`);
Logger.log(`処理終了`);
return ContentService
.createTextOutput(JSON.stringify({ status: "ok" }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
Logger.log(err.toString());
return ContentService
.createTextOutput(JSON.stringify({ error: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function getConfig() {
const props = PropertiesService.getScriptProperties();
return {
NOTION_API_KEY: props.getProperty('NOTION_API_KEY'),
TO_EMAIL: props.getProperty('TO_EMAIL')
};
}
function testPageID() {
workPageID("22460d1e-6e79-8021-81e9-f88d05bc8315", "テスト");
}
function testSendEmail() {
const sampleHtml = "<h1>テスト</h1><p>これはテストPDFです。</p>";
const pdf = createPdfFromHtml(sampleHtml);
const { TO_EMAIL } = getConfig();
sendPdfByEmail(pdf, "テストPDF送信", TO_EMAIL);
}
function fetchAllBlocks(blockId, apiKey) {
const blocks = [];
let cursor = null;
do {
const response = UrlFetchApp.fetch(`${NOTION_API_URL}blocks/${blockId}/children?page_size=100${cursor ? "&start_cursor=" + cursor : ""}`, {
method: "get",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
},
muteHttpExceptions: true
});
const json = JSON.parse(response.getContentText());
if (json.results) {
blocks.push(...json.results);
}
cursor = json.has_more ? json.next_cursor : null;
} while (cursor);
return blocks;
}
function renderBlockToDoc(container, block, apiKey) {
const type = block.type;
const content = block[type];
Logger.log(type);
Logger.log(content);
switch (type) {
case "paragraph":
container.appendParagraph(richTextToPlain(content.rich_text));
break;
case "heading_1":
case "heading_2":
case "heading_3":
const heading = {
heading_1: DocumentApp.ParagraphHeading.HEADING1,
heading_2: DocumentApp.ParagraphHeading.HEADING2,
heading_3: DocumentApp.ParagraphHeading.HEADING3
};
container.appendParagraph(richTextToPlain(content.rich_text)).setHeading(heading[type]);
break;
case "code":
if (content.language === "mermaid") {
const mermaidText = richTextToPlain(content.rich_text);
const imgBlob = renderMermaidImage(mermaidText);
if (imgBlob) {
const img = container.appendImage(imgBlob);
scaleImageToPageWidth(img);
}
} else {
container.appendParagraph(richTextToPlain(content.rich_text));
}
break;
case "image":
const imageBlob = downloadImageFromNotion(content);
if (imageBlob) {
const img = container.appendImage(imageBlob);
scaleImageToPageWidth(img);
}
break;
case "table":
renderNotionTable(container, block.id);
break;
case "table_row":
// テーブル内から呼び出される。ここでは無視。
break;
case "column_list":
const columnBlocks = fetchAllBlocks(block.id, apiKey);
Logger.log(`column_list id ${block.id}`);
const table = container.appendTable();
const row = table.appendTableRow();
columnBlocks.forEach(column => {
if (column.type === "column") {
const cell = row.appendTableCell();
const children = fetchAllBlocks(column.id, apiKey);
Logger.log(`column id ${column.id}`);
children.forEach(childBlock => renderBlockToDoc(cell, childBlock));
}
});
break;
case "bulleted_list_item":
case "numbered_list_item":
container.appendListItem(richTextToPlain(content.rich_text));
break;
case "divider":
// ドキュメントの全段落数から相対位置を算出
const totalParagraphs = container.getNumChildren();
const isNearTop = totalParagraphs <= 10 || totalParagraphs <= Math.floor(getEstimatedParagraphsPerPage() / 4);
if (!isNearTop) {
container.appendPageBreak();
}
break;
default:
container.appendParagraph(`[未対応のブロック: ${type}]`);
}
}
function richTextToPlain(richTextArray) {
return richTextArray.map(rt => rt.plain_text).join("");
}
function getEstimatedParagraphsPerPage() {
// A4サイズで余白を狭くしている前提で、おおよそ30段落程度が1ページに収まる
return 30;
}
function renderMermaidImage(code) {
try {
const url = "https://mermaid.ink/img/";
const encoded = Utilities.base64EncodeWebSafe(Utilities.newBlob(code).getBytes());
const response = UrlFetchApp.fetch(url + encoded);
return response.getBlob().setName("mermaid.png");
} catch (e) {
Logger.log("Mermaid変換失敗: " + e);
return null;
}
}
function downloadImageFromNotion(content) {
if (content.type !== "file" || !content.file?.url) return null;
const url = content.file.url;
const response = UrlFetchApp.fetch(url);
return response.getBlob().setName("notion_image.jpg");
}
function scaleImageToPageWidth(image) {
const doc = DocumentApp.getActiveDocument();
const pageWidth = 595; // A4 width in points (about 21cm)
const margin = 36; // 0.5 inch ≒ 36pt
const maxWidth = pageWidth - margin * 2;
if (image.getWidth() > maxWidth) {
const scale = maxWidth / image.getWidth();
image.setWidth(maxWidth);
image.setHeight(image.getHeight() * scale);
}
}
function renderNotionTable(body, tableBlockId) {
const rows = fetchAllBlocks(tableBlockId).filter(b => b.type === "table_row");
if (rows.length === 0) return;
const table = body.appendTable();
rows.forEach(row => {
const rowContent = row.table_row.cells;
const rowElement = table.appendTableRow();
rowContent.forEach(cell => {
const text = cell.map(rt => rt.plain_text).join("");
rowElement.appendTableCell(text);
});
});
}
@TakamiChie
Copy link
Author

TakamiChie commented Jul 23, 2025

Notionのデータベースページの内容をPDF化してメール送信するツール

Google Apps Scriptにこのコードをデプロイし、そのURLをNotionのデータベースページのカラムにボタンを作ってそこのwebhookとして設定することで、ページの内容をPDF化としてメール送信するツール。以下の処理を行います。

  • ページ内にMermaid記法のコードブロックがあればそれを画像に変換する
  • ページ内の区切り線を改ページとみなす。なお、その改ページがページ冒頭にあった場合は、改ページしない

なおNotion APIの仕様で、あらかじめデータベースをスクリプトに接続していないと、存在するページIDを送ってもAPIからIDを認識することができません。
必ずデータベースページの「接続」よりスクリプトを接続するようにしてください(一度接続してあればそのページ配下のページは接続済みになるようです)
2025-07-23_19h38_41

Notion側の準備

  1. データベースのカラムに「ボタン」を作成し、Webhookを設定してGoogle Apps ScriptのWebアプリURLを貼り付けます
  2. 「コンテンツ」として「名前」を設定する
2025-07-23_21h04_29

必要なスクリプトプロパティ

以下の内容をあらかじめスクリプトプロパティに設定してください。

  • NOTION_API_KEY: NotionのAPIキー
  • TO_EMAIL: 送信先メールアドレス

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment