前回の記事では、GASで取得したデータをWordPressへ自動投稿する仕組みをどのように実現するかを検討し、最終的に 「GAS → JSON → WordPress側のプラグインで受け取って投稿」 という方式を採用する方針としました。
(→「WordPressに自動投稿する仕組み ― しずかなニュースの開発記録」)
この方式のメリットは、WordPress側で複雑な認証設定を行う必要がなく、サーバーによっては海外からのアクセス制限を回避できる点にあります。また、WordPress側で投稿処理を行うことで、投稿ページ/固定ページの追加・更新、ドラフト保存、即時公開 など、柔軟なワークフローを実現できることも大きな利点です。
本記事では、WordPress初心者でも実践できるよう、シンプルな自作プラグインを作成し、GASから渡されたJSONデータを投稿として保存する手順 を紹介します。
全体の作業の流れ
今回の流れは、大きく次の 4 ステップです。
- WordPressプラグインの作成
GASから送られてきたJSONを受け取り、投稿として保存するプラグインを作成します - プラグインの有効化
作成したプラグインを指定の場所に格納し、利用できる状態にします - GASコードの整備
投稿データをJSONとして返すコードを作成します - Webアプリとしてデプロイ
URLを発行し、WordPressプラグインから参照できるようにします。
この流れで構築することで、GAS → WordPress の自動連携が可能になります。
プラグイン作成
ここでは、GASから取得したJSONデータを受け取り、WordPressの投稿として登録するためのプラグインを作成します。WordPressのプラグインは、基本的に PHP ファイルを所定のフォルダへ配置すれば動作します。
✅ 作成するファイル名
- フォルダ名:
gas-pull-auto-publisher - プラグイン本体:
gas-pull-auto.php
プラグインの役割は次のとおりです。
- 指定のURL(GAS Webアプリ)へアクセスしてJSONを取得
- JSONを解析し、記事情報(タイトル・本文など)を抽出
- WordPressに投稿を作成(または更新)
- 処理ログを残す(任意)
gas-pull-auto.php(クリックして開く)
<?php
/**
* Plugin Name: GAS Pull Auto Publisher (Draft Only, Minimal)
*/
if (!defined('ABSPATH')) exit;
/** ===== 設定 ===== */
define('GAS_PULL_FEED', 'https://script.google.com/macros/s/*******/exec?token=*******'); //
define('GAS_PULL_DEFAULT_ID', '1'); // ← 投稿設定シートの既定ID列(B列=1)
define('GAS_PULL_META_KEY', '_external_id');
define('GAS_LOG_ENDPOINT', 'https://script.google.com/macros/s/*******/exec'); // ← /exec に修正
define('GAS_TOKEN', 'tkn_5zX8hJq2LmR9vTnY');
/** ===== ロギング(GASのスプレシートへ) ===== */
function gas_log($level, $message, $context = []) {
$payload = [
'token' => GAS_TOKEN,
'level' => $level,
'message' => $message,
'context' => $context,
];
wp_remote_post(GAS_LOG_ENDPOINT, [
'timeout' => 10,
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode($payload),
]);
}
/** ===== 取得URL作成(?id= の付与) ===== */
function gas_build_feed_url($id_param = null) {
$id = $id_param !== null && $id_param !== '' ? $id_param : GAS_PULL_DEFAULT_ID;
// すでに token は入っている前提。id を追加する
return add_query_arg(['id' => $id], GAS_PULL_FEED);
}
/** ===== 取得&反映ロジック(下書き既定) ===== */
function gas_pull_run_once($override_id = null) {
$feed_url = gas_build_feed_url($override_id);
$res = wp_remote_get($feed_url, ['timeout' => 20]);
if (is_wp_error($res)) { gas_log('ERROR', 'wp_remote_get error', ['err'=>$res->get_error_message(), 'url'=>$feed_url]); return; }
$items = json_decode(wp_remote_retrieve_body($res), true);
if (!is_array($items)) { gas_log('ERROR','invalid json', ['body'=>wp_remote_retrieve_body($res)]); return; }
foreach ($items as $it) {
$ext_id = isset($it['id']) ? sanitize_text_field($it['id']) : '';
if (!$ext_id) { gas_log('WARN','skip: no id'); continue; }
// ====== 本文整形(保険つき)======
$raw = isset($it['content']) ? (string) $it['content'] : '';
$clean_content = wp_kses_post($raw);
// ====== 本文整形ここまで ======
// 受け取り可能な追加フィールド
$incoming_status = isset($it['status']) ? sanitize_key($it['status']) : '';
$incoming_type = isset($it['type']) ? sanitize_key($it['type']) : 'post'; // ← post/page を想定
$incoming_slug = isset($it['slug']) ? sanitize_title($it['slug']) : '';
$allowed_status = ['draft','publish','pending','future','private'];
if (!in_array($incoming_status, $allowed_status, true)) {
$incoming_status = 'draft';
}
if ($incoming_type !== 'page' && $incoming_type !== 'post') {
$incoming_type = 'post';
}
// 既存検索(post/page両方見に行く)
$q = new WP_Query([
'post_type' => 'any',
'meta_key' => GAS_PULL_META_KEY,
'meta_value' => $ext_id,
'post_status' => ['draft','publish','future','pending','private']
]);
$postarr = [
'post_title' => wp_kses_post($it['title'] ?? '(no title)'),
'post_content' => $clean_content,
'post_status' => $incoming_status,
'post_type' => $incoming_type,
];
if ($q->have_posts()) {
// 既存更新
$postarr['ID'] = $q->posts[0]->ID;
// スラッグが来ていれば更新(不要ならこの行をコメントアウト)
if ($incoming_slug) {
$postarr['post_name'] = $incoming_slug;
}
$post_id = wp_update_post($postarr, true);
if (is_wp_error($post_id)) {
gas_log('ERROR','update failed', ['err'=>$post_id->get_error_message(), 'ext_id'=>$ext_id]);
} else {
gas_log('INFO','updated', ['id'=>$post_id, 'ext_id'=>$ext_id, 'type'=>$incoming_type, 'status'=>$incoming_status]);
}
} else {
// 新規挿入
if ($incoming_slug) {
$postarr['post_name'] = $incoming_slug;
}
$post_id = wp_insert_post($postarr, true);
if (!is_wp_error($post_id) && $post_id) {
update_post_meta($post_id, GAS_PULL_META_KEY, $ext_id);
gas_log('INFO','inserted', ['id'=>$post_id, 'ext_id'=>$ext_id, 'type'=>$incoming_type, 'status'=>$incoming_status]);
} else {
gas_log('ERROR','insert failed', ['err'=>is_wp_error($post_id)?$post_id->get_error_message():$post_id, 'ext_id'=>$ext_id]);
}
}
}
}
/** ===== WP-Cron (有効化時: 5分後に1回、以後60分ごと) ===== */
register_activation_hook(__FILE__, function(){
if (!wp_next_scheduled('gas_pull_cron_hook')) {
add_filter('cron_schedules', function($s){ $s['hourly_custom'] = ['interval'=>3600,'display'=>__('Every 60 Minutes')]; return $s; });
wp_schedule_event(time()+300, 'hourly_custom', 'gas_pull_cron_hook');
}
});
register_deactivation_hook(__FILE__, function(){
$ts = wp_next_scheduled('gas_pull_cron_hook');
if ($ts) wp_unschedule_event($ts, 'gas_pull_cron_hook');
});
add_filter('cron_schedules', function($s){ $s['hourly_custom'] = ['interval'=>3600,'display'=>__('Every 60 Minutes')]; return $s; });
add_action('gas_pull_cron_hook', function(){ gas_pull_run_once(null); });
/** ===== 手動実行(URLで叩ける) ===== */
/* 例:
https://example.com/?gas_pull_run=1&key=tkn_5zX8hJq2LmR9vTnY ← 既定ID
https://example.com/?gas_pull_run=1&key=tkn_5zX8hJq2LmR9vTnY&id=2 ← この回だけID=2
*/
add_action('init', function(){
if (isset($_GET['gas_pull_run']) && isset($_GET['key']) && $_GET['key'] === GAS_TOKEN) {
$id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : null;
gas_pull_run_once($id);
nocache_headers();
header('Content-Type: text/plain; charset=utf-8');
echo 'GAS pull executed. (id=' . ($id ?: GAS_PULL_DEFAULT_ID) . ')';
exit;
}
});
プラグイン導入方法
ここでは、作成したプラグインをWordPressに配置し、有効化する手順を解説します。
1.サーバーへ格納
/wp-content/plugins/
- 作成したプラグインフォルダ(例:
gas-auto-post)を用意 - サーバー上の以下のディレクトリにアップロード
/wp-content/plugins/
└── gas-auto-publisher/
└── gas-pull-auto.php
アップロード後、WordPress側で認識される状態になります。
2. プラグインの有効化
- WordPress管理画面へログイン
- 左メニュー「プラグイン」→「インストール済みプラグイン」
- 作成したプラグインが一覧に表示されるので「有効化」をクリック
以上で、WordPress側の準備は完了です。
GASコード作成
GAS 側では、(A) HTML本文を生成する関数 と (B) doGet() でJSONを返すエンドポイント の2つを用意します。GASの役割は「投稿に必要な最小情報(タイトル・本文など)をJSONで返す」ことです。
(A) HTML本文を生成する関数(クリックして開く)
/**
* exportArticlesHtml(kwd対応版)
* - 投稿シートを読み込み、WP本文HTMLを生成
*
* 投稿シート列
* B:タイトル
* C:要約
* D:媒体
* E:URL
* F:kwd_en
* G:kwd_jp
* H:kwd_desc
* I:title_en
*/
function exportArticlesHtml() {
const SHEET_NAME = '投稿';
const HEADER_ROW = 1;
const START_ROW = HEADER_ROW + 1;
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(SHEET_NAME);
if (!sh) throw new Error(`シートが見つかりません: ${SHEET_NAME}`);
const lastRow = sh.getLastRow();
if (lastRow < START_ROW) return '';
// B〜I(8列)を取得(列B=2)
const values = sh.getRange(START_ROW, 2, lastRow - HEADER_ROW, 8).getValues();
// 整形
const items = values
.map(([title, summary, source, url, kw_en, kw_jp, kw_desc,title_en]) => ({
title: String(title || '').trim(),
summary: String(summary || '').trim(),
source: String(source || '').trim(),
url: String(url || '').trim(),
kw_en: String(kw_en || '').trim(),
kw_jp: String(kw_jp || '').trim(),
kw_desc: String(kw_desc || '').trim(),
title_en: String(title_en || '').trim(),
}))
.filter(r => r.title && r.url);
// HTML エスケープ
const esc = s => String(s)
.replaceAll('&','&')
.replaceAll('<','<')
.replaceAll('>','>')
.replaceAll('"','"')
.replaceAll("'",''');
// 件数
const header = `<p class="news-count">件数:${items.length}</p>`;
// 記事単位HTML
const rows = items.map((it, idx) => {
const title = esc(it.title);
const source = esc(it.source);
const url = esc(it.url);
const title_en = esc(it.title_en);
const summaryHtml = esc(it.summary).replace(/\n/g, '<br>');
// --- ✅キーワード表示 ---
let kwHtml = '';
if (it.kw_en || it.kw_jp || it.kw_desc) {
kwHtml = `
<div class="news-keywords">
${it.kw_en ? `<p><strong>Keyword(EN):</strong> ${esc(it.kw_en)} (${esc(it.kw_jp)})</p>` : ''}
${it.kw_desc ? `<p><small>${esc(it.kw_desc)}</small></p>` : ''}
</div>`;
}
return `
<article class="news-card" aria-label="記事 ${idx + 1}">
<h2 class="news-title">${title}</h2>
<b>${title_en}</b>
${source ? `<div class="news-meta">引用元:<a href="${url}" target="_blank" rel="noopener noreferrer">${esc(source)}</a></div>` : ''}
${summaryHtml ? `<p class="news-summary">${summaryHtml}</p>` : ''}
${kwHtml}
</article>`;
}).join('\n');
const content = header + '\n' + rows;
Logger.log(content);
return content;
}
(B) doGet() でJSONを返すエンドポイント(クリックして開く)
// 設定 /
var FEED_TOKEN = ‘xxxx’; // テスト用トークン
var SHEET_ID = ‘yyyyy’; // 投稿設定/ログ格納先SS
/ 共通: 投稿設定ロード /
function getPostConfig_(postId) {
var SHEET_NAME = ‘投稿設定’;
var sh = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
if (!sh) throw new Error(‘シート「投稿設定」が見つかりません’);
var values = sh.getDataRange().getValues();
if (!values.length) throw new Error(‘投稿設定が空です’);
// 1行目:ID行(例: A1=「投稿パターン」, B1=1, C1=2 …)
var headerRow = values[0];
// 指定IDの列インデックスを探す(なければB列=1にフォールバック)
var wanted = (postId != null && postId !== ”) ? String(postId).trim() : ”;
var colIndex = -1;
for (var c = 1; c < headerRow.length; c++) {
if (String(headerRow).trim() === wanted) { colIndex = c; break; }
}
if (colIndex === -1) colIndex = 1; // 既定はB列(ID=1)
// A列のキー → 対応列の値 でオブジェクト化(2行目以降)
var cfg = {};
for (var r = 1; r < values.length; r++) {
var key = String(values[r][0]).trim();
if (!key) continue;
cfg[key] = values[r][colIndex];
}
return cfg;
}
/ JSONフィード(WPが取りに来る) /
// 例: https://script.google.com/…/exec?token=…&id=1
function doGet(e) {
if (!e || e.parameter.token !== FEED_TOKEN) {
return ContentService.createTextOutput(‘Unauthorized’)
.setMimeType(ContentService.MimeType.TEXT);
}
// どの投稿パターンIDの列を使うか(?id=1 等)
var postId = e.parameter.id; // 未指定OK(B列=1を使う)
// 投稿設定を取得
var cfg = getPostConfig_(postId);
// 本文生成(あなたの既存関数)。{content, count} 返却にも耐える
var content1 = exportArticlesHtml();
if (typeof content1 === ‘object’ && content1 && typeof content1.content === ‘string’) {
content1 = content1.content;
}
content1 = String(content1 || ”);
// 不要タグを排除
content1 = content1
.replace(/?<\/style>/gi, ”) .replace(/?<\/script>/gi, ”)
.replace(/]+rel=[“‘]stylesheet[“‘][^>]*>/gi, ”)
.trim();
// 投稿設定の項目を反映
var titleBase = cfg[‘タイトル’] ? String(cfg[‘タイトル’]).trim() : ‘無題’;
var statusRaw = (cfg[‘Status’] || ”).toString().toLowerCase(); // publish / draft など
var status = (statusRaw === ‘publish’ || statusRaw === ‘draft’) ? statusRaw : ‘draft’;
var slug = (cfg[‘slug’] || ”).toString().trim(); // 任意
var headerText = (cfg[‘ヘッダー文’] || ”).toString();
var footerText = (cfg[‘フッター文’] || ”).toString();
var htmlPattern = (cfg[‘htmlパターン’] || ”).toString(); // 任意(必要ならcontent生成側で利用)
// ヘッダー/フッターを本文へ(必要な場合だけ)
var finalContent = content1;
if (headerText) finalContent = headerText + ‘\n\n’ + finalContent;
if (footerText) finalContent = finalContent + ‘\n\n’ + footerText;
// タイトルに時刻を付す(従来仕様)
var d = new Date();
var formatted = d.getFullYear() + ‘/’ + (d.getMonth() + 1) + ‘/’ + d.getDate() + ‘ ‘ + d.getHours() + ‘時’;
var title = titleBase + ‘(’ + formatted + ‘現在)’;
// ID の決め方:
// 既存の “同じIDなら更新扱い” 仕様に合わせ、slug があれば slug を、なければ titleBase を使用
// 必要なら別ルールに変更してください
var feedId = slug || titleBase;
var item = {
id: feedId,
title: title,
content: finalContent,
status: status,
// 形式(Post/Page)を渡す必要があるなら、WP側の取り込みで解釈できるよう
// 任意フィールドとして追加(WPプラグイン側対応が必要)
type: String(cfg[‘形式(Post/Page)’] || ”).toLowerCase(), // ‘post’ | ‘page’
slug: slug
// , html_pattern: htmlPattern
};
return ContentService.createTextOutput(JSON.stringify([item]))
.setMimeType(ContentService.MimeType.JSON);
}
/ WP→GAS ログ受け(POST) /
function doPost(e) {
var SHEET_NAME = ‘logs’;
try {
var data = JSON.parse(e.postData.contents || ‘{}’);
if (data.token !== FEED_TOKEN) {
return ContentService.createTextOutput(‘Unauthorized’).setMimeType(ContentService.MimeType.TEXT);
}
var sh = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME) ||
SpreadsheetApp.openById(SHEET_ID).insertSheet(SHEET_NAME);
sh.appendRow([
new Date(),
data.level || ‘INFO’,
data.message || ”,
(typeof data.context === ‘string’) ? data.context : JSON.stringify(data.context || {})
]);
return ContentService.createTextOutput(‘ok’).setMimeType(ContentService.MimeType.TEXT);
} catch (err) {
return ContentService.createTextOutput(‘error’).setMimeType(ContentService.MimeType.TEXT);
}
}
レスポンス仕様
今回の doGet(e) は、配列(Array)で1件以上の投稿オブジェクトを返す 形になっています。WordPress 側のプラグインは、この配列をそのまま json_decode してループ処理します。
返却オブジェクトの主なフィールドは次のとおりです(コード準拠)。
id… 重複判定用の一意キー。本実装では「slugがあればslug、なければtitleBase」を使用します。title… タイトル。タイトル設定値にYYYY/M/D H時** の時刻を付与** して生成します。content… 記事本文の HTML 文字列。exportArticlesHtml()の結果に、必要に応じて ヘッダー/フッター文 を前後に連結し、<style> / <script> / stylesheet <link>を除去してから返します。status… 投稿ステータス。投稿設定のStatus(publish/draft)を元に、不正値はdraft** にフォールバック**。type… 投稿タイプ。形式(Post/Page)を小文字化して設定(post/page)。不正値はpost。slug… 任意。指定がある場合は WP 側でスラッグに反映されます。
返却例
[
{
"id": "silentnews",
"title": "しずかなニュース(2025/11/10 10時現在)",
"content": "<p class=\"news-count\">件数:20</p>
<article class=\"news-card\" ...>…</article>",
"status": "draft",
"type": "post",
"slug": "silentnews"
}
]
備考
- 重複回避:WP プラグインは
idを_external_idメタに保存し、同一idが来た場合は 更新モード に切り替えます。 - スラッグの扱い:
slugが渡っている場合のみ、新規挿入時/更新時ともにpost_name** を更新**します(コード上で明示)。 - 安全化:本文は WordPress 側で「許可されたタグだけ」に整えて保存され、GAS 側でも
<style>や<script>、<link rel="stylesheet">などを事前に取り除いています。
デプロイ
GAS を Webアプリ として公開し、取得用のURLを発行します。再デプロイするとURLが変わる 点に注意してください(WordPress側の設定も更新が必要)。
1. 新規デプロイ
- GASエディタ右上 [デプロイ] → [新しいデプロイ]
- 種類:ウェブアプリ を選択
- 説明:任意(例:WP自動投稿API v1)
- 実行するユーザー:自分(自分の権限でシート等へアクセス)
- アクセスできるユーザー:全員 または リンクを知っている全員(WPサーバーから取得できるように)
- デプロイ を押す → Web アプリのURL を控える

2. URLをWordPress側へ設定
- 取得した WebアプリURL を、プラグインの設定値(例:
GAS_PULL_FEED)に貼り付けます。 - 簡易トークンを使う場合:
https://script.google.com/.../exec?token=xxxxのようにプラグイン側と合わせておく。
3. 再デプロイ時の注意
- コードを修正して 再デプロイ すると、URLが変わる 場合があります(GAS側の仕様)。
- 運用上の負担を減らすには、同じデプロイを更新する運用、または リバースプロキシ/中継URL を用意して、WP側は固定URLを見る構成も検討できます。
動作確認
ここまで準備ができたら、実際に WordPress 側で投稿が作成されるかを確認します。
1. 手動で実行
下記URLにアクセスすると、GAS 側のデータを即時に取得できます。
https://example.com/?gas_pull_run=1&key=xxxx
example.com→ あなたの WordPress ドメインkey=xxxx→GAS_TOKENに設定した値
アクセスしてしばらくすると、
GAS pull executed. (id=1)
のようなテキストが返り、処理が完了します。
2. WordPress 管理画面で確認
- WordPress 管理画面にログイン
- 左メニュー → 投稿(または固定ページ)
- 新しい投稿が作成されているか確認
※ステータスdraftの場合は 下書き として保存されています。
問題なく作成されていれば成功です。
まとめ
本記事では、GASで生成したJSONをWordPress側のプラグインで受け取り、自動投稿する仕組みを解説しました。この方式を採用することで、WordPress側での認証設定が簡略化され、構造がシンプルになるだけでなく、投稿・更新の制御をすべてWordPress側で行える点が大きな利点です。また、GAS側で取得したデータを自由に加工し、HTMLとして整形してから返すため、用途に応じた柔軟な表現が可能になります。
さらに、この仕組みは 「定期的な情報の更新」や「外部コンテンツの自動収集・公開」 といったユースケースに向いており、ニュースまとめ、スクレイピング結果の公開、社内データの定期反映など、幅広く応用できます。
次回は、今回構築した仕組みを定期的に実行し、完全に自動化する方法を紹介します。運用をよりスムーズにしたい方は、ぜひ続けてご覧ください。

