このブログは「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機能(タグやカテゴリー、ユーザー関連など)は未検証ですが、作り込めば色々と活用ができそうなので、今後はより幅広い機能を試してみたいと思います。
開発ご要望があればお気軽にご相談ください
画像やファイルをアセットとしてアップロードする部分は手をつけられていませんが、ここが実現すれば、フォームやシートから画像も含めて記事投稿が完結できるようになります。もしご興味がある方は、お気軽にご相談ください。