Created
July 23, 2025 10:30
-
-
Save TakamiChie/c8975ece2af2d090fc83c165fb442746 to your computer and use it in GitHub Desktop.
NotionページをPDFとして取得するGoogle Apps Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| }); | |
| }); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Notionのデータベースページの内容をPDF化してメール送信するツール
Google Apps Scriptにこのコードをデプロイし、そのURLをNotionのデータベースページのカラムにボタンを作ってそこのwebhookとして設定することで、ページの内容をPDF化としてメール送信するツール。以下の処理を行います。
なおNotion APIの仕様で、あらかじめデータベースをスクリプトに接続していないと、存在するページIDを送ってもAPIからIDを認識することができません。

必ずデータベースページの「接続」よりスクリプトを接続するようにしてください(一度接続してあればそのページ配下のページは接続済みになるようです)
Notion側の準備
必要なスクリプトプロパティ
以下の内容をあらかじめスクリプトプロパティに設定してください。