DifyからWordPressに自動投稿するGASの全コード──設計の変遷と詰まったポイントを公開
結論:GASはスプレッドシート管理だけで始めたが、403回避のためにWP投稿まで担うことになった
最初のGASコードはシンプルなGET/POSTだけでした。WordPressへの投稿はDifyから直接REST APIを叩く予定でしたが、エックスサーバーの海外IP制限で403エラーが出たため、GASがWordPress投稿の中継役まで担う構成に変わりました。
この記事では、GASのコード全体と設計が変わっていった経緯、詰まったポイントを順番に書いていきます。
判断基準
GASがスプレッドシートの仲介だけで設計が済んでいる
→ DifyからWP REST APIを直接叩ける環境(海外IP制限なし)であればそのままでよい。エックスサーバーを使っているなら、GASにWP投稿まで寄せる設計に切り替える。
GASをデプロイし直したらDify側のURLが変わってしまった
→ 新規デプロイではなく「既存デプロイの更新」を使う。デプロイ → デプロイを管理 → 編集アイコン → バージョン:新しいバージョン → デプロイの手順で更新すればURLは変わらない。
最初のGASコードはシンプルだった
最初に書いたコードはGETとPOSTだけの構成でした。重複コンテンツを防ぐために、Googleスプレッドシートで投稿済み記事を管理しようと考えたのが出発点です。
function doGet() {
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName("articles_db");
const data = sheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1);
const result = rows.map(row => {
const obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName("articles_db");
const data = JSON.parse(e.postData.contents);
sheet.appendRow([
data.keyword || "",
data.title || "",
data.meta || "",
data.slug || "",
new Date()
]);
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}GETでスプレッドシートの記事一覧を返す。POSTで新しい記事情報を追記する。WordPressへの投稿はDifyのHTTPノードから直接REST APIを叩く予定でした。
403エラーでGASの役割が一気に増えた
ワークフローが完成して最初のテスト実行をしたとき、最初のHTTPリクエストで即座に止まりました。
{ "status_code": 403, "body": "403 Forbidden" }DifyのサーバーIPがエックスサーバーにブロックされていたのが原因です。GoogleのサーバーはブロックされないためGASを中継に使う構成に変え、GASがWordPress投稿まで担うことになりました。
現在のGASコード全体
設計の変遷を経て、現在のコードは以下の構造です。
const WP_BASE_URL = "https://あなたのドメイン";
const WP_AUTH = "Basic 【Base64変換済みの認証文字列】";
const API_TOKEN = "【自分で決めたトークン文字列】";
function isAuthorized(data) {
return (data.token || "") === API_TOKEN;
}
function doPost(e) {
let data = {};
try {
const contentType = e.postData.type || "";
if (contentType.includes("multipart") || contentType.includes("urlencoded")) {
data = e.parameter;
} else {
data = JSON.parse(e.postData.contents);
}
} catch(err) {
return jsonResponse({ ok: false, error: "parse error: " + err });
}
if (!isAuthorized(data)) {
return jsonResponse({ ok: false, error: "Unauthorized" });
}
const action = data.action || "save_article";
if (action === "get_articles") { /* 記事一覧取得 */ }
if (action === "save_article") { /* シートに保存 */ }
if (action === "get_categories") { /* WPカテゴリ取得 */ }
if (action === "post_draft") { /* WP Draft投稿 */ }
return jsonResponse({ ok: false, error: "unknown action: " + action });
}トークン認証
GASのエンドポイントは公開URLのため、トークン認証がないと誰でも叩けてしまいます。リクエストのtokenとGAS側のAPI_TOKENを照合して、一致しなければ処理を拒否します。
action振り分け
1つのエンドポイントがactionパラメータで4種類の処理を振り分けます。DifyのHTTPノードが叩くURLは1本で済むため管理がシンプルになります。
既存記事の取得(重複防止)
if (action === "get_articles") {
let sheetRows = [];
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName("articles_db");
const d = sheet.getDataRange().getValues();
const headers = d[0];
sheetRows = d.slice(1)
.filter(row => row.some(cell => cell !== ""))
.map(row => {
const obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
} catch(e) {}
let wpRows = [];
try {
const res = UrlFetchApp.fetch(
WP_BASE_URL + "/wp-json/wp/v2/posts?per_page=100&_fields=title,slug,link",
{ headers: { "Authorization": WP_AUTH }, muteHttpExceptions: true }
);
if (res.getResponseCode() === 200) {
wpRows = JSON.parse(res.getContentText()).map(p => ({
keyword: p.slug || "",
title: p.title?.rendered || "",
url: p.link || "",
slug: p.slug || ""
}));
}
} catch(e) {}
return jsonResponse([...sheetRows, ...wpRows]);
}スプレッドシートの記録とWordPressの実際の投稿記事、両方を合わせて返します。
WordPress Draft投稿
if (action === "post_draft") {
let categories = [];
if (data.categories) {
const cat = String(data.categories).replace(/[\[\]\s]/g, "");
categories = cat ? [parseInt(cat, 10)] : [];
}
const res = UrlFetchApp.fetch(
WP_BASE_URL + "/wp-json/wp/v2/posts",
{
method: "post",
headers: {
"Authorization": WP_AUTH,
"Content-Type": "application/json"
},
payload: JSON.stringify({
title: data.title || "",
content: data.content || "",
slug: data.slug || "",
status: "draft",
categories: categories,
meta: {
"ssp_meta_title": data.title || "",
"ssp_meta_description": data.meta || "",
"ssp_meta_keyword": data.keywords || ""
}
}),
muteHttpExceptions: true
}
);
return jsonResponse(JSON.parse(res.getContentText()));
}権限承認で詰まった
コードを書いてデプロイしても、最初はUrlFetchAppの権限エラーで動きませんでした。GASが外部URLにアクセスするにはGoogleアカウントの権限承認が必要です。承認ダイアログに「このアプリはGoogleで確認されていません」という警告が出て止まりました。
解決策は「詳細」リンクをクリックして先に進むことです。
警告画面で「詳細」をクリック
→「安全でないページに移動(開発者名)」をクリック
→「許可」をクリック自分が作ったスクリプトへの許可なので安全上の問題はありません。コードを変更して再デプロイするたびに同じ承認が必要になります。「なぜか動かなくなった」というときは権限承認を忘れている可能性があります。
この後どうなったか
この構成が固まってからは、GASが安定して動くようになりました。現在は実際にかねしょう(shibukidai.com)でDify経由の記事自動投稿をテスト運用しています。
ただ、GASを中継に使う設計は設定箇所が多く、購入者に渡すシステムとして複雑さが課題になっています。「動く」ことと「使いやすい」ことの間のギャップを埋めることが現在の課題です。
よくある質問
Q. GASのコードを変更したのに反映されません。なぜですか?
コード変更後は必ず既存デプロイの更新が必要です。更新後に権限承認ダイアログが出た場合は「詳細」→「安全でないページに移動」→「許可」の手順で承認してください。新規デプロイすると別のURLが発行されるので、Dify側のURL設定も変更が必要になります。
Q. API_TOKENはどれくらいの強度にすればいいですか?
英数字16文字以上のランダム文字列を推奨します。パスワード生成ツール(例:1Password、Bitwarden)で生成した文字列が確実です。GASのコードに直書きするため、GitHubなどのパブリックリポジトリには公開しないよう注意してください。
Q. form-dataとJSONの両対応が必要な理由は何ですか?
DifyのHTTPノードはbodyの形式としてJSONとform-dataの両方が選択できます。ノードの設定によってGAS側に届く形式が変わるため、両方に対応しておくと設定ミスによるエラーを防げます。
---
GASのコードはここまでです。ただ、なぜこの設計を選んだのか、この構成が抱える問題は何かという判断の話はツールの実装とは別の問題です。設計の判断軸についてはbizinets.bizの思想設計ページで書いています。
bizinets.biz
記事を読んで「もっと知りたい」と思ったあなたへ
bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。
bizinets.biz へ行く →