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/bloggerUrlFetchAppを使うために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_KEYPowerShellで生成した秘密キー

実際の値は記事や公開場所へ掲載しません。

6. Apps Scriptの初回承認

  1. Apps ScriptでauthorizeBloggerを選択する
  2. 「実行」を押す
  3. Googleアカウントを選択する
  4. Bloggerへのアクセスを許可する
  5. 実行ログにブログ一覧が表示されることを確認する

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を前提としています。複数人利用や一般公開用途では、より適切な認証方式やアクセス制御を検討してください。

コメント

このブログの人気の投稿

初心者でもわかるMFCとVC++の基本:Visual Studio開発入門

フォントサイズ単位の比較と換算表【Windows・Android】

プロジェクト参画初日の効率的な情報整理と2週間でキャッチアップするための方法