ChatGPTからBloggerへ下書き保存する方法|Apps Script+Custom GPT Actionの構成
この記事では、ChatGPTのCustom GPT ActionからGoogle Apps Scriptを経由し、Google Bloggerへ記事を下書きとしてのみ保存する構成をまとめます。
ここで紹介する方法は自分専用GPT向けです。複数人での利用や一般公開用途には推奨しません。また、Google Apps Script、Blogger API、ChatGPT Actionの仕様は変更される可能性があるため、以下は執筆時点で動作確認した構成としてご覧ください。
構成の概要
全体の流れは、Custom GPTからGoogle Apps ScriptのWebアプリへJSONを送信し、Apps Script側でBlogger APIを呼び出して下書きを作成する形です。
- ChatGPT Actionからタイトル、HTML本文、ラベルを送信
- Apps Scriptで固定のactionKeyを照合
- Blogger APIへ
isDraft=trueでPOST - 公開・更新・削除は行わず、下書き作成のみ実行
1. Google Apps Script:Code.gs
次のコードをCode.gsへ貼り付けます。実際のブログIDや秘密キーはコードへ直書きせず、スクリプトプロパティから取得します。
/**
* ChatGPTからBloggerへ下書きだけを作成するApps Script Webアプリ。
*
* スクリプトプロパティ:
* - BLOGGER_BLOG_ID
* - CHATGPT_ACTION_KEY
*/
var BLOGGER_POSTS_URL =
'https://www.googleapis.com/blogger/v3/blogs/';
var MAX_HTML_LENGTH = 200000;
/**
* Blogger APIの利用権限を承認するため、一度だけ手動実行する。
*/
function authorizeBlogger() {
var response = UrlFetchApp.fetch(
'https://www.googleapis.com/blogger/v3/users/self/blogs',
{
headers: {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
},
muteHttpExceptions: true
}
);
if (response.getResponseCode() !== 200) {
throw new Error(
'Blogger API authorization failed: ' +
response.getContentText()
);
}
console.log(response.getContentText());
}
/**
* ウェブアプリの稼働確認。
*/
function doGet() {
return jsonOutput_({
status: 'ok',
mode: 'draft-only'
});
}
/**
* ChatGPT Actionから記事を受信し、Bloggerへ下書き保存する。
*/
function doPost(e) {
var config = getConfig_();
var input;
try {
if (!e || !e.postData || !e.postData.contents) {
throw new Error('POST body is empty.');
}
input = JSON.parse(e.postData.contents);
} catch (error) {
return jsonOutput_({
error: 'Request body must be valid JSON.'
});
}
/*
* ChatGPT ActionのAPIキー認証はHTTPヘッダーを使用するが、
* Apps ScriptのdoPost(e)では任意の受信ヘッダーを取得できない。
*
* また、/exec/秘密キーへのPOSTはGoogle側で401になったため、
* 自分専用GPTに限定してJSON本文のactionKeyを照合する。
*/
if (input.actionKey !== config.actionKey) {
return jsonOutput_({
error: 'Unauthorized.'
});
}
var validation = validateInput_(input);
if (validation.error) {
return jsonOutput_({
error: validation.error
});
}
try {
var accessToken = ScriptApp.getOAuthToken();
var requestBody = {
kind: 'blogger#post',
title: validation.value.title,
content: validation.value.html
};
if (validation.value.labels.length > 0) {
requestBody.labels = validation.value.labels;
}
var response = UrlFetchApp.fetch(
BLOGGER_POSTS_URL +
encodeURIComponent(config.blogId) +
'/posts?isDraft=true',
{
method: 'post',
contentType: 'application/json',
headers: {
Authorization: 'Bearer ' + accessToken
},
payload: JSON.stringify(requestBody),
muteHttpExceptions: true
}
);
var statusCode = response.getResponseCode();
var responseText = response.getContentText();
var responseBody;
try {
responseBody = JSON.parse(responseText || '{}');
} catch (error) {
responseBody = {};
}
if (statusCode < 200 || statusCode >= 300) {
console.log(
'Blogger API error: ' + responseText
);
return jsonOutput_({
error:
'Blogger draft could not be created. ' +
'Check the Apps Script execution log.'
});
}
return jsonOutput_({
status: 'draft_created',
post: {
id: responseBody.id,
title: responseBody.title,
url: responseBody.url,
published: responseBody.published
}
});
} catch (error) {
console.log(
'Unexpected error: ' + error.message
);
return jsonOutput_({
error:
'Blogger draft could not be created. ' +
'Check the Apps Script execution log.'
});
}
}
/**
* スクリプトプロパティを取得する。
*/
function getConfig_() {
var properties =
PropertiesService.getScriptProperties();
var blogId =
properties.getProperty('BLOGGER_BLOG_ID');
var actionKey =
properties.getProperty('CHATGPT_ACTION_KEY');
if (!blogId || !actionKey) {
throw new Error(
'Set BLOGGER_BLOG_ID and ' +
'CHATGPT_ACTION_KEY in Script Properties.'
);
}
return {
blogId: blogId,
actionKey: actionKey
};
}
/**
* ChatGPTから受信した記事を検証する。
*/
function validateInput_(input) {
if (
!input ||
Object.prototype.toString.call(input) !==
'[object Object]'
) {
return {
error: 'JSON object is required.'
};
}
if (
typeof input.title !== 'string' ||
!input.title.trim()
) {
return {
error: 'title is required.'
};
}
if (
typeof input.html !== 'string' ||
!input.html.trim()
) {
return {
error: 'html is required.'
};
}
if (input.html.length > MAX_HTML_LENGTH) {
return {
error:
'html must be 200000 characters or fewer.'
};
}
if (
/<\/?(?:html|head|body)\b/i.test(input.html)
) {
return {
error:
'html must be a Blogger post fragment; ' +
'do not include html, head, or body tags.'
};
}
if (/<script\b/i.test(input.html)) {
return {
error:
'script tags are not permitted in ' +
'Blogger post content.'
};
}
if (
input.labels !== undefined &&
(
!Array.isArray(input.labels) ||
input.labels.some(function (label) {
return (
typeof label !== 'string' ||
!label.trim()
);
})
)
) {
return {
error:
'labels must be an array of ' +
'non-empty strings.'
};
}
return {
value: {
title: input.title.trim(),
html: input.html.trim(),
labels: (input.labels || []).map(
function (label) {
return label.trim();
}
)
}
};
}
/**
* JSON形式のレスポンスを返す。
*/
function jsonOutput_(value) {
return ContentService
.createTextOutput(JSON.stringify(value))
.setMimeType(ContentService.MimeType.JSON);
}
2. Apps Script:appsscript.json
{
"timeZone": "Asia/Tokyo",
"runtimeVersion": "V8",
"exceptionLogging": "STACKDRIVER",
"oauthScopes": [
"https://www.googleapis.com/auth/blogger",
"https://www.googleapis.com/auth/script.external_request"
]
}
runtimeVersionにはV8を指定します。Blogger APIを呼び出すためにauth/blogger、UrlFetchAppを使うためにscript.external_requestが必要です。マニフェストを変更した場合は、再承認が必要になる場合があります。
3. ChatGPT Action:openapi.yaml
openapi: 3.1.0
info:
title: Blogger Draft Action
version: 1.0.0
description: >-
Creates Blogger drafts through a private
Google Apps Script deployment.
It never publishes, updates, or deletes posts.
servers:
- url: https://script.google.com
paths:
/macros/s/YOUR_DEPLOYMENT_ID/exec:
post:
operationId: createBloggerDraft
summary: Create a Blogger draft
description: >-
Creates a Blogger draft from a title,
Blogger-safe HTML fragment, and labels.
This operation never publishes the post.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- actionKey
- title
- html
properties:
actionKey:
type: string
enum:
- YOUR_PRIVATE_ACTION_KEY
description: >-
Fixed private key for this
owner's private GPT.
title:
type: string
description: Article title.
html:
type: string
description: >-
Blogger post HTML fragment.
Do not include html, head,
body, or script tags.
labels:
type: array
description: Optional Blogger labels.
items:
type: string
responses:
"200":
description: Draft creation result
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: >-
Returns draft_created
when creation succeeds.
post:
type: object
properties:
id:
type: string
title:
type: string
url:
type: string
published:
type: string
required:
- id
- title
error:
type: string
description: >-
Error message when
the operation fails.
YOUR_DEPLOYMENT_IDはApps ScriptウェブアプリURLの/s/と/execの間にある値へ置換します。YOUR_PRIVATE_ACTION_KEYは、スクリプトプロパティのCHATGPT_ACTION_KEYと同じ値へ置換します。
記事やスクリーンショットには、実際のデプロイIDや秘密キーを掲載しないでください。Actionの認証タイプは「なし」、カスタムGPTの公開範囲は「自分のみ」に設定します。
4. 秘密キーを生成する
Windows PowerShell 5.1でも動作する生成例です。
$rng = New-Object `
System.Security.Cryptography.RNGCryptoServiceProvider
$bytes = New-Object byte[] 32
$rng.GetBytes($bytes)
$actionKey = -join (
$bytes |
ForEach-Object {
$_.ToString('x2')
}
)
$rng.Dispose()
$actionKey
出力される64文字を秘密キーとして使用します。チャット、記事、スクリーンショットへ掲載せず、Apps Scriptのスクリプトプロパティへ保存します。OpenAPI定義の固定値にも同じキーを設定します。キーを公開してしまった場合は、必ず再生成してください。
5. スクリプトプロパティ
| プロパティ名 | 設定する値 |
|---|---|
| BLOGGER_BLOG_ID | 投稿先ブログの数字ID |
| CHATGPT_ACTION_KEY | PowerShellで生成した秘密キー |
実際の値は記事や公開場所へ掲載しません。
6. Apps Scriptの初回承認
- Apps Scriptで
authorizeBloggerを選択する - 「実行」を押す
- Googleアカウントを選択する
- Bloggerへのアクセスを許可する
- 実行ログにブログ一覧が表示されることを確認する
7. ウェブアプリをデプロイする
- 種類:ウェブアプリ
- 次のユーザーとして実行:自分
- アクセスできるユーザー:ChatGPT Actionから到達できる公開設定
- 使用するURL:末尾が
/execのもの /devは開発テスト用なのでActionには使わない
コード変更後は、「デプロイ」→「デプロイを管理」→対象デプロイを編集→「新バージョン」を選択→再デプロイ、の順で更新します。デプロイIDが変わった場合はOpenAPI側も更新します。
8. カスタムGPTの指示例
あなたはGoogle Blogger向けの記事作成を支援する編集アシスタントです。
記事本文はBlogger用のHTML断片として作成してください。
html、head、body、meta、title、scriptタグは含めないでください。
ユーザーが明確に下書き保存を承認した場合だけ、
createBloggerDraftを実行してください。
単に記事作成や修正を依頼された場合は、
Bloggerへ送信しないでください。
Actionは下書き作成にのみ使用してください。
公開、更新、削除は行わないでください。
Action実行前に、タイトル、ラベル、記事概要を提示し、
ユーザーへ確認してください。
Action実行後にstatusがdraft_createdの場合は、
Bloggerへ下書き保存したことを伝え、
Bloggerのプレビュー確認を促してください。
errorが返った場合は、成功したと説明せず、
Apps Scriptの実行履歴、デプロイURL、
Blogger APIの設定を確認するよう案内してください。
秘密キー、デプロイURL、ブログIDなどの
認証情報を会話へ表示しないでください。
9. 動作確認用の記事
最初は短い記事でテストします。
タイトル
ChatGPTからBloggerへ下書きを作成するテスト
HTML本文
<p>
これはChatGPTからBloggerへ作成した
テスト用の下書きです。
</p>
<p>
Bloggerの管理画面で内容と表示を確認してから、
必要に応じて編集・公開してください。
</p>
ラベル
ChatGPT
Blogger
テスト
10. 成功時のレスポンス例
{
"status": "draft_created",
"post": {
"id": "1234567890123456789",
"title": "テスト記事",
"url": "https://example.blogspot.com/example",
"published": "2026-08-13T00:00:00Z"
}
}
IDやURLはダミー値です。実際の値を記事へ掲載しないようにします。
11. 失敗時のレスポンス例
認証失敗
{
"error": "Unauthorized."
}
入力内容の不備
{
"error": "title is required."
}
Blogger API呼び出し失敗
{
"error": "Blogger draft could not be created. Check the Apps Script execution log."
}
まとめ
この構成では、ChatGPT ActionからGoogle Apps ScriptへJSON本文でactionKeyを送信し、Apps Script側で照合したうえでBlogger APIへ下書きを作成します。
ポイントは、秘密パス方式ではなく、最終的に動作したJSON本文のactionKey方式を使うことです。実際の秘密キー、ブログID、デプロイID、Apps Script URLは記事へ掲載せず、必ずプレースホルダーで管理してください。
また、この構成は自分専用GPTを前提としています。複数人利用や一般公開用途では、より適切な認証方式やアクセス制御を検討してください。
コメント
コメントを投稿