このブログは「Movable Type Advent Calendar 2024」23日目の記事です。

「GoogleスプレッドシートからMovable TypeのData APIを使って記事を投稿できないか」

ある日、「GoogleスプレッドシートからMovable TypeのData APIを使って記事を投稿できないか」という相談を受けました。

Googleフォームから社員に記事を投稿してもらい、その内容を自動POSTしたい

さらに要望として「Googleフォームを使って社員が書いた記事を、そのままブログ投稿にしたい」という案も出てきました。フォームの回答はスプレッドシートに集約されるので、それを自動でMTに送れたら便利ですよね。社内情報やちょっとしたニュースをすぐ共有できる仕組みになりそうです。

直接記事化できれば、効率がかなり上がりそうだということで、まずは試験的に取り組んでみることにしました。

MTのData APIでSpreadSheetのAppScriptからPOSTしても、うまく受け取れない

しかし、実際にAppScriptでPOSTしてみるとエラーが出たり、うまく受信できなかったり。いろいろ調べるうちに、送信データとMT側の受け取り方に微妙な差があったようです。「同じJSONのつもりでも、なぜ通らない?」と首をかしげながら試行錯誤が続きました。

MT側で受け取れる構成にPOST値を調整するプラグイン「SpreadsheetFriendly」を開発してみた

デバッグしてみると、MT側でSpreadSheetのPOST値が、POSTDATAというキーに集約されていることがわかりました。

そこで「SpreadsheetFriendly」というプラグインを開発しました。これは、スプレッドシートから送られるデータを、MTで正しく扱いやすい形に調整してくれます。データ形式の変換を代行し、投稿までのハードルを下げるのが狙いです。

SpreadsheetFriendlyの使い方

プラグインをダウンロードして、MTにインストール

まずはプラグインを入手し、MTのプラグインフォルダに配置します。通常のプラグイン追加と同じ手順で、有効化すれば準備完了です。(ダウンロードは記事の最下部にあります)

フォームと回答を管理するSpreadSheetを用意

Googleフォームの回答先として使うスプレッドシートを用意し、そこにタイトルや本文など必要なカラムを作成しておきます。社員が簡単に入力できるよう調整しましょう。

SpreadSheetのAppScriptを記述

AppScriptのエディタを開き、POST先URLやパラメータの設定を行います。シートが更新されたタイミングでData APIにデータを飛ばすように設定しておくと便利です。検証用に作成したスクリプトをサンプルで記載します。

// 開発環境に基本認証をかけていたので、認証情報を記載
const username = '[[基本認証ユーザー名]]';
const password = '[[基本認証パスワード]]';
const basicAuth = Utilities.base64Encode(`${username}:${password}`);
function myFunction(e) {
  // Movable Type DataAPIのエンドポイントとアクセストークン
  const apiEndpoint = '[[DataAPIのエンドポイント]]';
  const accessToken = '[[APIトークン]]';
  const siteId = '[[投稿先のblog_id]]';
  const catId = '[[投稿先のcategory_id]]';
  const params = {
    username: '[[ログインユーザー名]]',
    password: '[[ログインパスワード]]',
    clientId: '[[任意のアプリID]]'
  }
  let options = {
    method: 'POST',
    'muteHttpExceptions': true,
    'validateHttpsCertificates': false,
    'followRedirects': false,
    headers: {
      'Authorization': 'Basic ' + basicAuth,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    payload: params
  };
  var sessionId;
  try {
    // Movable Type DataAPIにデータを送信
    const response = UrlFetchApp.fetch(apiEndpoint + '/authentication', options);
    const responseCode = response.getResponseCode();
    const responseText = response.getContentText();
    // ログ出力(エラーがあれば確認)
    Logger.log(response);
    Logger.log('Response Code: ' + responseCode);
    Logger.log('Response Body: ' + responseText);
    const json = JSON.parse(responseText);
    if ( json.accessToken ) {
      sessionId = json.accessToken;
    }
  } catch (error) {
    Logger.log('Error: ' + error.message);
  }
  // フォーム送信イベントからデータを取得
  //const sheet = e.source.getActiveSheet();
  //const lastRow = sheet.getLastRow();
  //const rowData = sheet.getRange(lastRow, 1, 1, sheet.getLastColumn()).getValues()[0];
  // スプレッドシートの列に対応するデータを取得(カスタマイズが必要)
  //const title = rowData[2]; // 例: タイトル列
  //const body = rowData[3];  // 例: 本文列
  //const category = rowData[4]; // 例: カテゴリ列
  const title = '開発テストタイトル';
  const body = '開発テスト本文';
  const categories = [ { id: catId } ];
  // Movable Typeに送信するデータ
  const payload = {
    site_id: siteId,
    entry: {
      title: title,
      body: body,
      categories: categories,
      status: 'publish',
    },
    status: 2
  };
  // HTTPリクエストのオプション
  options = {
    method: 'post',
    headers: {
      'Authorization': `Basic ${basicAuth}`,
      'X-MT-Authorization': 'MTAuth accessToken=' + sessionId,
      //'Bearer': 'Bearer ${accessToken}',
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  Logger.log(options);
  try {
    // Movable Type DataAPIにデータを送信
    const response = UrlFetchApp.fetch(apiEndpoint + '/sites/' + siteId '/entries', options);
    const responseCode = response.getResponseCode();
    const responseText = response.getContentText();
    // ログ出力(エラーがあれば確認)
    Logger.log(response);
    Logger.log('Response Code: ' + responseCode);
    Logger.log('Response Body: ' + responseText);
    // スプレッドシートに結果を記録(例: 最終列に投稿結果を記録)
    const resultColumn = sheet.getLastColumn() + 1;
    sheet.getRange(lastRow, resultColumn).setValue(responseCode === 200 ? 'Success' : 'Failed');
  } catch (error) {
    Logger.log('Error: ' + error.message);
    sheet.getRange(lastRow, sheet.getLastColumn() + 1).setValue('Error: ' + error.message);
  }
}
function uploadAsset(apiEndpoint, accessToken, filePath) {
  // ファイルをGoogle Driveから取得
  const file = DriveApp.getFileById(filePath);
  const blob = file.getBlob();
  const options = {
    method: 'POST',
    headers: {
      'Authorization': `MTAuth accessToken=${accessToken}`,
      'Authorization': `Basic ${basicAuth}`,
      'Content-Type': 'application/json'
    },
    payload: {
      file: blob,
      path: '/',
      site_id: 65
    },
    muteHttpExceptions: true
  };
  const response = UrlFetchApp.fetch(`${apiEndpoint}/assets/upload`, options);
  const responseData = JSON.parse(response.getContentText());
  if (response.getResponseCode() === 200) {
    return responseData.id; // アップロードされたアセットのIDを返す
  } else {
    Logger.log('Asset upload failed: ' + response.getContentText());
    return null;
  }
}

各APIで使うには作り込みが必要だけど道は開けそう

現時点では、記事投稿の基本的な動作確認だけにとどまっています。他のAPI機能(タグやカテゴリー、ユーザー関連など)は未検証ですが、作り込めば色々と活用ができそうなので、今後はより幅広い機能を試してみたいと思います。

開発ご要望があればお気軽にご相談ください

画像やファイルをアセットとしてアップロードする部分は手をつけられていませんが、ここが実現すれば、フォームやシートから画像も含めて記事投稿が完結できるようになります。もしご興味がある方は、お気軽にご相談ください。

SpreadsheetFriendlyのダウンロードはこちら

無償ソフトウェア利用規約」をご一読の上、ご利用ください。