「A4」1枚販促アンケート広告作成アドバイザー協会

  • お問い合わせはこちら
  • 24時間以内に
    ご返信いたします
    *土日祝は除く
メニューを開く

第4回:Googleフォームで集めた「A4」1枚アンケートの回答をAIで分類し、タグ付きでWordPressに自動投稿する方法

投稿日:2026.08.05

「A4」1枚アンケート広告作成アドバイザー東京支部の比嘉と申します。
普段はホームページ制作やシステム制作などを行ってます。
中でも、WordPressを使ったホームページ構築や、販促のシステム構築などを得意としており、当「A4」1枚販促アンケート広告作成アドバイザー協会の方でも「販促アンケートPOP自動作成システム」や「オンライン広告原稿作成システム」なども制作させて頂いてます。

はじめに

前回は、Googleフォームで集めた「A4」1枚アンケートの回答から、

  • WordPressの投稿タイトル
  • 投稿の抜粋文

をAIに作ってもらい、自動でWordPressへ投稿する方法を紹介しました。

しかし、投稿が増えてくると、次のような問題が出てきます。

どのお客様が、どのようなことで悩んでいたのか分からない
何が購入の決め手になったのか探しにくい
同じような悩みを持つお客様の声をまとめて見せられない

お客様の声が数件のうちは問題ありませんが、数十件、数百件と増えると、必要な事例を探すのが難しくなります。

そこで今回は、「A4」1枚アンケートのQ1・Q4・Q5をAIに読み取ってもらい、回答内容に合ったタグを自動で付けます。

具体的には、

  • Q1から「購入前の悩み」
  • Q4から「購入の決め手」
  • Q5から「購入後の変化」

をAIに判断してもらいます。

そして、それぞれの回答に合ったタグを、WordPressの投稿へ自動で設定します。

第3回までは、お客様の声を「自動で投稿する」ところまででした。

今回は、お客様の声をあとから探しやすくし、ホームページの中で活用しやすい形に整理していきます。

今回できるようになること

この記事の手順どおりに設定すると、次のことができるようになります。

  • GoogleフォームのQ1・Q4・Q5をAIに送る
  • Q5から投稿タイトルを作る
  • Q1と掲載名から抜粋文を作る
  • Q1から「購入前の悩み」を分類する
  • Q4から「購入の決め手」を分類する
  • Q5から「購入後の変化」を分類する
  • AIが選んだタグをWordPressへ自動で設定する
  • 登録されていないタグがあれば自動で作成する
  • 登録済みのタグがあれば、そのタグを再利用する
  • タグの設定に失敗しても、投稿自体は止めずに続ける
  • Q5の回答は、お客様が書いた文章のまま掲載する

難しそうに見えるかもしれませんが、コードの仕組みをすべて理解する必要はありません。

基本的には、この記事に掲載しているコードを貼り付けて、

  • WordPressサイトの情報
  • Gemini APIキー
  • 使用したいタグの候補
  • Googleフォームの質問名

を自分の環境に合わせて変更します。

第3回との違い

第3回では、次の内容をWordPressへ自動投稿しました。

タイトル:
AIがQ5の回答から作成

抜粋文:
AIがQ1の回答と掲載名から作成

本文:
Q5の回答原文

カテゴリー:
お客様の声

今回の第4回では、ここにタグが追加されます。

タイトル:
AIがQ5の回答から作成

抜粋文:
AIがQ1の回答と掲載名から作成

本文:
Q5の回答原文

カテゴリー:
お客様の声

購入前の悩みタグ:
AIがQ1から選ぶ

購入の決め手タグ:
AIがQ4から選ぶ

購入後の変化タグ:
AIがQ5から選ぶ

カテゴリーは、第3回までと同じく「お客様の声」で固定します。

そのうえで、一つひとつのお客様の声に、回答内容を表すタグを付けていきます。

カテゴリーとタグの違い

WordPressには、投稿を整理するための「カテゴリー」と「タグ」があります。

カテゴリー

カテゴリーは、投稿を大きなグループに分けるために使います。

今回は、すべての投稿を「お客様の声」というカテゴリーに入れます。

タグ

タグは、その投稿にどのような内容が含まれているのかを表すために使います。

たとえば、同じ「お客様の声」カテゴリーの中でも、

  • 集客に悩んでいたお客様
  • 分かりやすい説明が決め手になったお客様
  • 利用後に不安が解消されたお客様

というように、回答内容に合わせて整理できます。

カテゴリーを本棚にたとえるなら、タグは本に付ける目印のようなものです。

すべてのお客様の声を同じ本棚に入れながら、内容に応じた目印を付けておくイメージです。

今回の全体の流れ

今回作る仕組みは、次のような流れです。

Googleフォームからアンケートを送信
↓
Googleスプレッドシートに回答を保存
↓
回答をAIへ送る
↓
AIがタイトルと抜粋文を作る
↓
AIが回答に合ったタグを選ぶ
↓
タグ付きのお客様の声としてWordPressへ自動投稿

AIが選んだタグがWordPressにまだ登録されていない場合は、コードが自動で作成します。

そのため、あらかじめWordPressの管理画面でタグを一つずつ作る必要はありません。

今回の完成イメージ

たとえば、Googleフォームに次のような回答が入ったとします。

掲載名

K.S

Q1

ホームページからの問い合わせが少なく、
どこを改善すればよいのか分からず悩んでいました。

Q4

こちらの悩みを丁寧に聞いてくれて、
専門用語を使わず分かりやすく説明してくれたことが
決め手でした。

Q5

ホームページの見せ方を改善でき、
問い合わせにつながる流れが分かるようになりました。

この回答から、AIが次のような内容を作ります。

投稿タイトル:
問い合わせにつながる流れが分かりました

抜粋文:
ホームページからの問い合わせが少なく
悩んでいたK.S様のご感想です。

購入前の悩み:
集客

購入の決め手:
分かりやすい説明

購入後の変化:
理解向上

WordPressには、次のタグが設定されます。

集客

分かりやすい説明

理解向上

AIにタグ名を自由に考えさせない

今回の仕組みで大切なのは、AIにタグ名を自由に考えさせないことです。

AIに自由にタグを作らせると、同じような意味でも、次のように少しずつ違うタグが作られる可能性があります。

ホームページ集客

Web集客

ネット集客

集客改善

問い合わせを増やす

どれも似たような内容ですが、それぞれ別のタグとして登録されると、かえって投稿を探しにくくなります。

そこで今回は、あらかじめ使用するタグの候補を決めておきます。

そして、AIにはその候補の中から、回答に最も合うものを選んでもらいます。

たとえば、Q1の回答が、

ホームページからの問い合わせが少なくて困っていました。

であれば、AIが「集客」というタグを選びます。

Q4の回答が、

説明が分かりやすかったことが決め手でした。

であれば、「分かりやすい説明」というタグを選びます。

このように、AIが勝手に新しいタグを作るのではなく、用意された選択肢の中から答えを選ぶ仕組みにします。

Geminiには、文章をあらかじめ決めた候補へ振り分け、指定した形で回答させる機能があります。今回のコードでは、その機能を使ってタグを選ばせています。

今回使用するタグ候補

今回のコードでは、次のタグ候補を用意しています。

Q1から選ぶ「購入前の悩み」

集客

売上

費用

時間

操作方法

品質

信頼

その他の悩み

Q4から選ぶ「購入の決め手」

丁寧な対応

分かりやすい説明

実績

価格

対応の早さ

提案力

専門性

安心感

その他の決め手

Q5から選ぶ「購入後の変化」

問い合わせ改善

売上向上

時間短縮

不安解消

理解向上

品質向上

満足

その他の変化

「その他の悩み」「その他の決め手」「その他の変化」は、用意した候補のどれにも当てはまらない回答に使用します。

今回のタグ候補はサンプルです。

実際に使用するときは、自社の商品やサービス、お客様から多く寄せられる回答に合わせて変更してください。

今回使うもの

今回使用するものは、次のとおりです。

  • Googleフォーム
  • Googleスプレッドシート
  • Google Apps Script
  • WordPress
  • WordPressのアプリケーションパスワード
  • Gemini API
  • 第3回で作成した自動投稿の仕組み

まだ第3回の設定ができていない場合は、先に第3回の仕組みが正常に動いているか確認してください。

ステップ1:GoogleフォームのQ4を確認する

第3回では、次の3項目を使用しました。

掲載名

Q1

Q5

今回は、ここにQ4を追加します。

掲載名

Q1

Q4

Q5

コードでは、次の部分でQ4を取得します。

const q4 = getFirstValue_(namedValues['Q4']);

Googleフォームの質問名が「Q4」ではなく、質問文全体になっている場合は、コード内のQ4を実際の質問名に変更してください。

たとえば、Googleフォームの質問名が、

Q4:他にもいろいろと商品がある中で、
何が決め手となって当社の商品を購入しましたか?

となっている場合は、次のように変更します。

const q4 = getFirstValue_(
  namedValues[
    'Q4:他にもいろいろと商品がある中で、何が決め手となって当社の商品を購入しましたか?'
  ]
);

Q1やQ5についても同様です。

Googleフォームに表示されている質問名と、コードに書かれている質問名を同じにしてください。

ステップ2:AIにお願いする内容を増やす

第3回では、AIに次の2つを作ってもらいました。

投稿タイトル

抜粋文

今回は、さらに3つの分類を追加します。

投稿タイトル

抜粋文

購入前の悩み

購入の決め手

購入後の変化

つまり、AIには次のような仕事をしてもらいます。

Q5から投稿タイトルを作る

お客様の感想が伝わる、分かりやすいタイトルを作ります。

Q1と掲載名から抜粋文を作る

どのようなことで悩んでいたお客様なのかが分かる、短い紹介文を作ります。

Q1から購入前の悩みを選ぶ

用意した候補の中から、Q1の回答に最も近いタグを選びます。

Q4から購入の決め手を選ぶ

用意した候補の中から、Q4の回答に最も近いタグを選びます。

Q5から購入後の変化を選ぶ

用意した候補の中から、Q5の回答に最も近いタグを選びます。

ステップ3:AIが選べるタグをコードに登録する

今回のコードには、AIが選べるタグの一覧をあらかじめ登録しておきます。

たとえば、購入前の悩みについては、次の候補を登録します。

concern: [
  { name: '集客', slug: 'concern-attracting-customers' },
  { name: '売上', slug: 'concern-sales' },
  { name: '費用', slug: 'concern-cost' },
  { name: '時間', slug: 'concern-time' },
  { name: '操作方法', slug: 'concern-operation' },
  { name: '品質', slug: 'concern-quality' },
  { name: '信頼', slug: 'concern-trust' },
  { name: 'その他の悩み', slug: 'concern-other' }
]

コードの中には、AIが決められた候補の中から答えを選ぶための設定も含まれています。

見慣れない英単語が並んでいる部分もありますが、すべての意味を理解する必要はありません。

今回掲載しているコードをそのまま使用する場合は、この部分も変更せずに使えます。

自社に合わせてタグを変更したい場合は、タグの名前と、その横に書かれている半角英数字を変更します。

AIが選んだタグがWordPressに登録されていない場合は、コードが自動で作成します。

そのため、WordPressの管理画面で事前にタグを作成する必要はありません。

ステップ4:Apps Scriptのコードを変更する

ここから、実際のコードを設定します。

第3回で使用しているコードを、次のコードへ差し替えてください。

コードが長く見えますが、主に変更するのは、コードの最初にある設定部分です。

// ====== 自分の環境に合わせて変更する設定 ======
const WP_SITE_URL = 'https://example.com'; // 最後の / は不要です
const WP_USERNAME = 'wordpress_user_name';
const WP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx';
const CATEGORY_ID = 12; // 「お客様の声」カテゴリーのID

const GEMINI_API_KEY = 'ここにGemini APIキーを入れます';
const GEMINI_MODEL = 'gemini-2.5-flash';
// ============================================


// AIに選ばせるタグ候補です。
// 自社の商品やサービスに合わせて変更してください。
const TAG_SETTINGS = {
  concern: [
    {
      name: '集客',
      slug: 'concern-attracting-customers'
    },
    {
      name: '売上',
      slug: 'concern-sales'
    },
    {
      name: '費用',
      slug: 'concern-cost'
    },
    {
      name: '時間',
      slug: 'concern-time'
    },
    {
      name: '操作方法',
      slug: 'concern-operation'
    },
    {
      name: '品質',
      slug: 'concern-quality'
    },
    {
      name: '信頼',
      slug: 'concern-trust'
    },
    {
      name: 'その他の悩み',
      slug: 'concern-other'
    }
  ],

  decision: [
    {
      name: '丁寧な対応',
      slug: 'decision-careful-response'
    },
    {
      name: '分かりやすい説明',
      slug: 'decision-clear-explanation'
    },
    {
      name: '実績',
      slug: 'decision-track-record'
    },
    {
      name: '価格',
      slug: 'decision-price'
    },
    {
      name: '対応の早さ',
      slug: 'decision-speed'
    },
    {
      name: '提案力',
      slug: 'decision-proposal'
    },
    {
      name: '専門性',
      slug: 'decision-expertise'
    },
    {
      name: '安心感',
      slug: 'decision-reassurance'
    },
    {
      name: 'その他の決め手',
      slug: 'decision-other'
    }
  ],

  result: [
    {
      name: '問い合わせ改善',
      slug: 'result-inquiries'
    },
    {
      name: '売上向上',
      slug: 'result-sales'
    },
    {
      name: '時間短縮',
      slug: 'result-time-saving'
    },
    {
      name: '不安解消',
      slug: 'result-relief'
    },
    {
      name: '理解向上',
      slug: 'result-understanding'
    },
    {
      name: '品質向上',
      slug: 'result-quality'
    },
    {
      name: '満足',
      slug: 'result-satisfaction'
    },
    {
      name: 'その他の変化',
      slug: 'result-other'
    }
  ]
};


// Googleフォームが送信されたときに実行されます。
function onFormSubmit(e) {
  if (!e || !e.namedValues) {
    throw new Error(
      'フォームの回答を取得できませんでした。' +
      'Apps Scriptの画面から直接実行せず、' +
      'Googleフォームから回答を送信してください。'
    );
  }

  const namedValues = e.namedValues;

  // Googleフォームの質問名が異なる場合は、
  // [掲載名][Q1][Q4][Q5]を変更してください。
  const displayName =
    getFirstValue_(namedValues['掲載名']) || '匿名';

  const q1 =
    getFirstValue_(namedValues['Q1']);

  const q4 =
    getFirstValue_(namedValues['Q4']);

  const q5 =
    getFirstValue_(namedValues['Q5']);

  if (!q5) {
    throw new Error(
      'Q5の回答が空欄のため、投稿を中止しました。'
    );
  }

  let aiResult;
  let aiSucceeded = true;

  try {
    aiResult = createVoicePostInfoWithGemini_(
      GEMINI_API_KEY,
      GEMINI_MODEL,
      displayName,
      q1,
      q4,
      q5
    );

  } catch (error) {
    aiSucceeded = false;

    Logger.log(
      'AIによる文章作成と分類に失敗しました:' +
      error.message
    );

    aiResult = createFallbackResult_(
      displayName,
      q1
    );
  }

  const authToken =
    createWordPressAuthToken_(
      WP_USERNAME,
      WP_APP_PASSWORD
    );

  // タグの処理だけ失敗した場合は、
  // タグなしで投稿を続けます。
  const tagIds = getTagIdsSafely_(
    WP_SITE_URL,
    authToken,
    [
      aiResult.concernTag,
      aiResult.decisionTag,
      aiResult.resultTag
    ]
  );

  const payload = {
    title: aiResult.title,

    content: buildPostContent_(
      aiResult.excerpt,
      q5,
      aiSucceeded
    ),

    excerpt: aiResult.excerpt,
    status: 'publish',
    categories: [CATEGORY_ID],
    tags: tagIds,

    // アイキャッチ画像を
    // 明示的に「なし」にします。
    featured_media: 0
  };

  const response = UrlFetchApp.fetch(
    WP_SITE_URL + '/wp-json/wp/v2/posts',
    {
      method: 'post',
      contentType: 'application/json',

      headers: {
        Authorization: 'Basic ' + authToken
      },

      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    }
  );

  assertSuccessResponse_(
    response,
    'WordPressへの投稿'
  );

  Logger.log(
    'WordPressへの投稿が完了しました。'
  );
}


// AIにタイトル、抜粋文、
// 3種類のタグを作ってもらいます。
function createVoicePostInfoWithGemini_(
  apiKey,
  model,
  displayName,
  q1,
  q4,
  q5
) {
  const prompt = `
以下の「A4」1枚アンケートの回答をもとに、
WordPressへ投稿する「お客様の声」の情報を
作成してください。

条件:
・Q5の内容から分かりやすい投稿タイトルを作る
・タイトルは30文字以内にする
・大げさな表現にしない
・回答に書かれていないことは加えない
・抜粋文はQ1の悩みと掲載名が自然に伝わる一文にする
・抜粋文は「〇〇で悩んでいた〇〇様のご感想です。」
  に近い形にする
・Q1から購入前の悩みを1つ選ぶ
・Q4から購入の決め手を1つ選ぶ
・Q5から購入後の変化を1つ選ぶ
・複数の候補がある場合も、
  最も中心的なものを1つだけ選ぶ
・どの候補にも合わない場合は、
  それぞれ「その他の悩み」
  「その他の決め手」
  「その他の変化」を選ぶ
・Q5の本文は作り変えない

掲載名:
${displayName}

Q1:
${q1 || '回答なし'}

Q4:
${q4 || '回答なし'}

Q5:
${q5}
`;

  // AIがどのような形で答えるかを
  // コード側で決めています。
  // この部分は基本的に変更不要です。
  const answerFormat = {
    type: 'object',
    additionalProperties: false,

    properties: {
      title: {
        type: 'string',
        description:
          'Q5の内容に基づく30文字以内の投稿タイトル'
      },

      excerpt: {
        type: 'string',
        description:
          'Q1の悩みと掲載名が自然に伝わる抜粋文'
      },

      concernTag: {
        type: 'string',
        description:
          'Q1から選ぶ購入前の悩み',
        enum:
          getAllowedTagNames_('concern')
      },

      decisionTag: {
        type: 'string',
        description:
          'Q4から選ぶ購入の決め手',
        enum:
          getAllowedTagNames_('decision')
      },

      resultTag: {
        type: 'string',
        description:
          'Q5から選ぶ購入後の変化',
        enum:
          getAllowedTagNames_('result')
      }
    },

    required: [
      'title',
      'excerpt',
      'concernTag',
      'decisionTag',
      'resultTag'
    ]
  };

  const endpoint =
    'https://generativelanguage.googleapis.com/' +
    'v1beta/models/' +
    encodeURIComponent(model) +
    ':generateContent';

  const requestBody = {
    contents: [
      {
        parts: [
          {
            text: prompt
          }
        ]
      }
    ],

    generationConfig: {
      responseMimeType: 'application/json',
      responseJsonSchema: answerFormat,

      // 今回は短い文章作成と分類のため、
      // 追加の思考処理を使わない設定にします。
      thinkingConfig: {
        thinkingBudget: 0
      }
    }
  };

  const response = fetchGeminiWithRetry_(
    endpoint,
    {
      method: 'post',
      contentType: 'application/json',

      headers: {
        'x-goog-api-key': apiKey
      },

      payload: JSON.stringify(requestBody),
      muteHttpExceptions: true
    }
  );

  assertSuccessResponse_(
    response,
    'AIへのリクエスト'
  );

  const responseJson =
    JSON.parse(response.getContentText());

  const text =
    getGeminiResponseText_(responseJson);

  const rawResult =
    JSON.parse(stripCodeFence_(text));

  return validateAiResult_(
    rawResult,
    displayName,
    q1
  );
}


// AIが一時的に混み合っている場合などに
// 時間を空けて再試行します。
function fetchGeminiWithRetry_(
  endpoint,
  options
) {
  const retryWaitTimes = [
    10000,
    30000,
    60000
  ];

  let response;

  for (
    let attempt = 0;
    attempt <= retryWaitTimes.length;
    attempt++
  ) {
    response = UrlFetchApp.fetch(
      endpoint,
      options
    );

    const responseCode =
      response.getResponseCode();

    if (
      responseCode >= 200 &&
      responseCode < 300
    ) {
      return response;
    }

    const canRetry = [
      429,
      500,
      502,
      503,
      504
    ].indexOf(responseCode) !== -1;

    if (
      !canRetry ||
      attempt === retryWaitTimes.length
    ) {
      return response;
    }

    Utilities.sleep(
      retryWaitTimes[attempt]
    );
  }

  return response;
}


// AIから返された文章を取り出します。
function getGeminiResponseText_(
  responseJson
) {
  if (
    !responseJson ||
    !responseJson.candidates ||
    !responseJson.candidates[0] ||
    !responseJson.candidates[0].content ||
    !responseJson.candidates[0].content.parts ||
    !responseJson.candidates[0].content.parts[0] ||
    !responseJson.candidates[0].content.parts[0].text
  ) {
    throw new Error(
      'AIから回答を取得できませんでした。'
    );
  }

  return responseJson
    .candidates[0]
    .content
    .parts[0]
    .text;
}


// AIが返した内容を確認します。
function validateAiResult_(
  rawResult,
  displayName,
  q1
) {
  const fallback =
    createFallbackResult_(
      displayName,
      q1
    );

  return {
    title:
      truncateText_(
        cleanSingleLine_(rawResult.title),
        30
      ) || fallback.title,

    excerpt:
      cleanSingleLine_(rawResult.excerpt) ||
      fallback.excerpt,

    concernTag:
      validateTagName_(
        rawResult.concernTag,
        'concern'
      ),

    decisionTag:
      validateTagName_(
        rawResult.decisionTag,
        'decision'
      ),

    resultTag:
      validateTagName_(
        rawResult.resultTag,
        'result'
      )
  };
}


// AIが候補にない言葉を返した場合は、
// 「その他」を使用します。
function validateTagName_(
  tagName,
  groupName
) {
  const allowedNames =
    getAllowedTagNames_(groupName);

  if (
    allowedNames.indexOf(tagName) !== -1
  ) {
    return tagName;
  }

  return getOtherTagName_(groupName);
}


// そのグループのタグ名を一覧で返します。
function getAllowedTagNames_(
  groupName
) {
  return TAG_SETTINGS[groupName].map(
    function(tag) {
      return tag.name;
    }
  );
}


// 「その他」のタグ名を取得します。
function getOtherTagName_(
  groupName
) {
  const tags =
    TAG_SETTINGS[groupName];

  return tags[tags.length - 1].name;
}


// AIの処理が失敗した場合に
// 使用する内容です。
function createFallbackResult_(
  displayName,
  q1
) {
  return {
    title:
      'お客様の声:' +
      displayName +
      '様',

    excerpt:
      '〖お悩み内容〗\n' +
      (q1 || '回答なし'),

    concernTag: '',
    decisionTag: '',
    resultTag: ''
  };
}


// AIが選んだタグを
// WordPressのタグIDへ変換します。
function getTagIdsSafely_(
  siteUrl,
  authToken,
  tagNames
) {
  const uniqueNames = tagNames.filter(
    function(tagName, index, names) {
      return (
        tagName &&
        names.indexOf(tagName) === index
      );
    }
  );

  const tagIds = [];

  uniqueNames.forEach(
    function(tagName) {
      const definition =
        findTagDefinition_(tagName);

      if (!definition) {
        Logger.log(
          'タグの設定が見つかりません:' +
          tagName
        );

        return;
      }

      try {
        const tagId =
          getOrCreateTagId_(
            siteUrl,
            authToken,
            definition
          );

        if (
          tagId &&
          tagIds.indexOf(tagId) === -1
        ) {
          tagIds.push(tagId);
        }

      } catch (error) {
        // タグの処理だけ失敗しても、
        // 投稿は続けます。
        Logger.log(
          'タグの設定に失敗しました:' +
          tagName +
          ' / ' +
          error.message
        );
      }
    }
  );

  return tagIds;
}


// タグ名に合う設定を探します。
function findTagDefinition_(
  tagName
) {
  const groupNames =
    Object.keys(TAG_SETTINGS);

  for (
    let i = 0;
    i < groupNames.length;
    i++
  ) {
    const tags =
      TAG_SETTINGS[groupNames[i]];

    for (
      let j = 0;
      j < tags.length;
      j++
    ) {
      if (tags[j].name === tagName) {
        return tags[j];
      }
    }
  }

  return null;
}


// WordPressにタグがあれば使用し、
// なければ新しく作成します。
function getOrCreateTagId_(
  siteUrl,
  authToken,
  definition
) {
  const searchEndpoint =
    siteUrl +
    '/wp-json/wp/v2/tags?slug=' +
    encodeURIComponent(definition.slug) +
    '&per_page=100';

  const searchResponse =
    UrlFetchApp.fetch(
      searchEndpoint,
      {
        method: 'get',

        headers: {
          Authorization:
            'Basic ' + authToken
        },

        muteHttpExceptions: true
      }
    );

  assertSuccessResponse_(
    searchResponse,
    'WordPressタグの確認'
  );

  const foundTags =
    JSON.parse(
      searchResponse.getContentText()
    );

  if (foundTags.length > 0) {
    return Number(foundTags[0].id);
  }

  const createResponse =
    UrlFetchApp.fetch(
      siteUrl + '/wp-json/wp/v2/tags',
      {
        method: 'post',
        contentType: 'application/json',

        headers: {
          Authorization:
            'Basic ' + authToken
        },

        payload: JSON.stringify({
          name: definition.name,
          slug: definition.slug
        }),

        muteHttpExceptions: true
      }
    );

  const createCode =
    createResponse.getResponseCode();

  const createText =
    createResponse.getContentText();

  if (
    createCode >= 200 &&
    createCode < 300
  ) {
    return Number(
      JSON.parse(createText).id
    );
  }

  // 同じタグがすでにある場合は、
  // そのタグを使用します。
  try {
    const errorJson =
      JSON.parse(createText);

    if (
      errorJson.code === 'term_exists' &&
      errorJson.data &&
      errorJson.data.term_id
    ) {
      return Number(
        errorJson.data.term_id
      );
    }

  } catch (ignore) {
    // 読み取れない場合は、
    // 下のエラーとして処理します。
  }

  throw new Error(
    'タグを作成できませんでした。' +
    'ステータスコード:' +
    createCode +
    ' / 内容:' +
    createText
  );
}


// WordPressへ接続するための
// 認証情報を作ります。
function createWordPressAuthToken_(
  username,
  appPassword
) {
  const passwordWithoutSpaces =
    appPassword.replace(/\s/g, '');

  return Utilities.base64Encode(
    username +
    ':' +
    passwordWithoutSpaces
  );
}


// WordPressの投稿本文を作ります。
function buildPostContent_(
  excerpt,
  q5,
  aiSucceeded
) {
  let content = '';

  if (aiSucceeded) {
    content +=
      '<p>' +
      escapeHtml_(excerpt)
        .replace(/\n/g, '<br>') +
      '</p>' +
      '<hr>';
  }

  return (
    content +
    '<p><strong>' +
    '〖利用後の感想〗' +
    '</strong></p>' +
    '<p>' +
    escapeHtml_(q5)
      .replace(/\n/g, '<br>') +
    '</p>'
  );
}


// 正常に処理されたか確認します。
function assertSuccessResponse_(
  response,
  processName
) {
  const responseCode =
    response.getResponseCode();

  if (
    responseCode < 200 ||
    responseCode >= 300
  ) {
    throw new Error(
      processName +
      'に失敗しました。' +
      'ステータスコード:' +
      responseCode +
      ' / 内容:' +
      response.getContentText()
    );
  }
}


// Googleフォームの回答から
// 最初の値を取り出します。
function getFirstValue_(
  value
) {
  if (
    !value ||
    value.length === 0
  ) {
    return '';
  }

  return String(value[0]).trim();
}


// 改行を削除して
// 一行の文章にします。
function cleanSingleLine_(
  text
) {
  if (!text) {
    return '';
  }

  return String(text)
    .replace(/[\r\n]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}


// タイトルが長すぎる場合に
// 指定した文字数で切ります。
function truncateText_(
  text,
  maxLength
) {
  if (
    !text ||
    text.length <= maxLength
  ) {
    return text;
  }

  return text.substring(
    0,
    maxLength
  );
}


// AIの回答に余分な記号が付いた場合に
// 取り除きます。
function stripCodeFence_(
  text
) {
  return String(text)
    .replace(/```json/gi, '')
    .replace(/```/g, '')
    .trim();
}


// 投稿した文章が崩れないように
// 特殊な文字を変換します。
function escapeHtml_(
  text
) {
  if (!text) {
    return '';
  }

  return String(text)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

今回使用するAIのモデルは、第3回と同じgemini-2.5-flashです。

const GEMINI_MODEL = 'gemini-2.5-flash';

今回の処理では、アンケートの回答から、

  • 投稿タイトルを作る
  • 抜粋文を作る
  • 用意した候補からタグを選ぶ

という作業をAIに行ってもらいます。

Gemini 2.5 Flashは、Googleの公式情報でも、処理速度と価格のバランスに優れたモデルとして案内されています。また、今回使用する、決められた候補から回答させる機能にも対応しています。

第3回と同じモデルを使用するため、第3回の設定を引き継いでいる場合は、モデル名を変更する必要はありません。

コード内で変更する設定部分

コードを貼り付けたら、最初に次の部分を変更します。

const WP_SITE_URL = 'https://example.com';
const WP_USERNAME = 'wordpress_user_name';
const WP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx';
const CATEGORY_ID = 12;

const GEMINI_API_KEY = 'ここにGemini APIキーを入れます';
const GEMINI_MODEL = 'gemini-2.5-flash';

WP_SITE_URL

WordPressサイトのURLを入力します。

const WP_SITE_URL = 'https://example.com';

最後に/は付けません。

WP_USERNAME

WordPressのユーザー名を入力します。

const WP_USERNAME = 'admin';

WP_APP_PASSWORD

WordPressで作成したアプリケーションパスワードを入力します。

const WP_APP_PASSWORD =
  'abcd efgh ijkl mnop qrst uvwx';

コードでは、アプリケーションパスワードに含まれている空白を自動で削除して使用します。

CATEGORY_ID

「お客様の声」カテゴリーのIDを入力します。

const CATEGORY_ID = 12;

GEMINI_API_KEY

Google AI Studioで作成したGemini APIキーを入力します。

const GEMINI_API_KEY = 'AIza...';

APIキーは、パスワードと同じように大切な情報です。

自分のAPIキーをブログ記事や公開ページへ、そのまま掲載しないようにしてください。

GEMINI_MODEL

使用するAIのモデル名を指定しています。

const GEMINI_MODEL = 'gemini-2.5-flash';

第3回と同じモデルを使用するため、第3回の設定を引き継いでいる場合は変更する必要はありません。

タグ候補を自社用に変更する

タグ候補は、コード内のTAG_SETTINGSという部分で設定しています。

たとえば、購入前の悩みは次の部分です。

concern: [
  {
    name: '集客',
    slug: 'concern-attracting-customers'
  },
  {
    name: '売上',
    slug: 'concern-sales'
  }
]

nameには、WordPressの画面やホームページに表示したいタグ名を入力します。

name: '集客'

slugには、タグを見分けるための半角英数字を入力します。

slug: 'concern-attracting-customers'

この半角英数字は、タグを区別するためにWordPressの中で使用されます。

タグを追加したい場合は、次のように追加します。

{
  name: '人手不足',
  slug: 'concern-staff-shortage'
}

タグを削除したい場合は、そのまとまりを削除します。

どのようなタグを用意すればよいか分からない場合は、最初から細かく分けすぎないことをおすすめします。

まずは5個から8個程度で始め、アンケートの回答が増えてから見直した方が管理しやすくなります。

タグは事前にWordPressへ登録しなくてもよい

今回のコードでは、AIが選んだタグがWordPressに登録されているか、自動で確認します。

すでに登録されていれば、そのタグを使用します。

登録されていなければ、コードが新しいタグを作ります。

そのため、コードで指定したタグを、事前に一つずつWordPressの管理画面へ登録する必要はありません。

ただし、すでに同じ意味のタグを使用している場合は、既存のタグ名と半角英数字に合わせてコードを変更してください。

ステップ5:トリガーを確認する

第3回ですでにトリガーを設定している場合は、そのまま使用できます。

Apps Scriptのトリガー画面で、次の設定になっているか確認してください。

実行する関数:
onFormSubmit

イベントのソース:
スプレッドシートから

イベントの種類:
フォーム送信時

この設定になっていれば、Googleフォームが送信されたときに、自動でコードが実行されます。

Apps Scriptの画面からonFormSubmitを直接実行すると、Googleフォームの回答が渡されないため、エラーになります。

テストするときは、Googleフォームから実際に回答を送信してください。

ステップ6:テスト送信する

ここまで設定できたら、Googleフォームからテスト回答を送信します。

今回は、次の回答例を使います。

掲載名

K.S

Q1

ホームページからの問い合わせが少なく、
どこを改善すればよいのか分からず悩んでいました。

広告を出しても反応があまりなく、
このまま続けてよいのか不安に感じていました。

Q2

知人から紹介されて知りました。

その後、ホームページを見て、
相談してみようと思いました。

Q3

すぐには相談しませんでした。

費用がどれくらいかかるのか、
自分たちのような小さな会社でも
対応してもらえるのかが分からず、
少し迷っていました。

Q4

こちらの悩みを丁寧に聞いてくれて、
専門用語を使わずに分かりやすく
説明してくれたことが決め手でした。

無理に売り込まれる感じがなかったので、
安心してお願いできました。

Q5

相談してみると、
自分たちでは気づかなかった問題点を
分かりやすく教えてもらえました。

ホームページの見せ方も改善でき、
問い合わせにつながる流れが
分かるようになりました。

この回答では、Q4に、

  • 丁寧な対応
  • 分かりやすい説明
  • 安心感

という複数の要素が含まれています。

今回のコードでは、その中から最も中心的なものを1つだけ選ぶようにAIへお願いしています。

想定される結果は、次のとおりです。

購入前の悩み:
集客

購入の決め手:
分かりやすい説明

購入後の変化:
理解向上

ただし、AIによる判断のため、回答の表現によっては別の候補が選ばれる場合があります。

分類結果が自社の考え方と合わない場合は、タグ候補を見直してください。

テスト後に確認すること

Googleフォームを送信したら、次の点を確認します。

  • スプレッドシートに回答が追加されている
  • Apps Scriptの処理が正常に終了している
  • WordPressに新しい投稿が作られている
  • 投稿タイトルが作られている
  • 抜粋文が作られている
  • Q5の原文が本文に掲載されている
  • 「お客様の声」カテゴリーが設定されている
  • 最大3つのタグが設定されている
  • アイキャッチ画像が設定されていない

エラーが起きた場合の動き

今回のコードでは、一部の処理が失敗しても、できるだけ投稿を止めないようにしています。

AIの処理が失敗した場合

AIとの接続に問題が起きた場合は、次の内容で投稿します。

タイトル:
お客様の声:掲載名様

抜粋文:
Q1の回答

本文:
Q5の回答

タグ:
なし

一時的な混雑が原因の場合に備えて、時間を空けて自動で再試行します。

それでも処理できなかった場合に、AIを使用しない投稿へ切り替えます。

タグの設定だけが失敗した場合

タグの設定だけが失敗した場合は、

  • AIが作ったタイトル
  • AIが作った抜粋文
  • Q5の回答本文

を使用し、タグなしで投稿します。

タグを付けられなかったことが原因で、お客様の声そのものが投稿されなくなることを防ぐためです。

WordPressへの投稿が失敗した場合

WordPressへの投稿自体が失敗した場合は、投稿は作成されません。

Apps Scriptの左側にある「実行数」から、エラー内容を確認してください。

よくあるつまずき

タグが付かない

次の点を確認してください。

  • Q1・Q4・Q5に回答が入っているか
  • Gemini APIキーが正しいか
  • WordPressのユーザー名が正しいか
  • アプリケーションパスワードが正しいか
  • WordPressユーザーに投稿やタグを作る権限があるか

Apps Scriptの記録に、

タグの設定に失敗しました

と表示されている場合は、その後に表示されている内容も確認してください。

同じようなタグが複数できる

コード内のタグ名や半角英数字を途中で変更すると、以前とは別のタグとして作られる場合があります。

運用を始めたあとにタグ名を変更するときは、WordPressに登録済みのタグも確認してください。

AIが想定と違うタグを選ぶ

意味の似たタグが複数あると、AIの判断が分かれることがあります。

たとえば、

丁寧な対応

安心感

は、回答内容によっては両方に当てはまります。

想定と違う分類が続く場合は、

  • 似ているタグを一つにまとめる
  • タグ名をより分かりやすくする
  • タグの候補を減らす

といった調整を行います。

「その他」が多く選ばれる

「その他の悩み」「その他の決め手」「その他の変化」が多く選ばれる場合は、現在のタグ候補がお客様の回答に合っていない可能性があります。

実際の回答を確認して、よく出てくる内容を新しいタグとして追加してください。

Googleフォームの回答を取得できない

コードでは、次の質問名を使用しています。

namedValues['掲載名']
namedValues['Q1']
namedValues['Q4']
namedValues['Q5']

Googleフォームの実際の質問名と一致していないと、回答を取得できません。

全角・半角、空白、記号なども含めて、同じ文字にしてください。

Gemini APIでエラーが出る

次の点を確認してください。

  • Gemini APIキーが正しいか
  • APIキーをコピーし間違えていないか
  • APIの利用上限に達していないか
  • gemini-2.5-flashが利用できる状態か

AIのサービスは、使用できるモデル名や接続方法が変更されることがあります。

時間がたってからこの記事を試す場合は、現在の公式情報も確認してください。

今回できたこと

今回の設定で、Googleフォームから送信された「A4」1枚アンケートの回答をAIで分類し、タグ付きでWordPressへ自動投稿できるようになりました。

今回自動化した流れは、次のとおりです。

アンケートの回答を集める
↓
AIが投稿タイトルを作る
↓
AIが抜粋文を作る
↓
AIがQ1・Q4・Q5の内容を分類する
↓
回答に合ったタグを選ぶ
↓
タグ付きのお客様の声として投稿する

AIが選んだタグがWordPressに登録されていない場合は、コードが自動で作成します。

利用者がWordPressの管理画面で、毎回タグを作成する必要はありません。

第3回までは、お客様の声を「自動で投稿する」ところまででした。

今回からは、お客様の声を「あとから探して使える情報」として整理できるようになります。

タグを付けると、お客様の声を再利用しやすくなる

タグを設定しておくと、将来的に次のような見せ方ができます。

集客に悩んでいたお客様の声だけを表示する

価格が決め手になったお客様の声だけを表示する

利用後に不安が解消されたお客様の声だけを表示する

さらに、商品ページやサービスページに、

同じ悩みを持っていたお客様の声

を表示することもできます。

お客様の声を単に掲載するだけでなく、必要な場所で見せられるようになることが、タグを設定する大きなメリットです。

まとめ

今回は、Googleフォームで集めた「A4」1枚アンケートのQ1・Q4・Q5をAIで分類し、タグ付きでWordPressへ自動投稿する方法を紹介しました。

今回のポイントは、AIに自由にタグを作らせるのではなく、あらかじめ用意した候補の中から選ばせることです。

これにより、

  • 同じようなタグが大量に増える
  • タグの表記がバラバラになる
  • 投稿をうまく整理できない

といった問題を減らせます。

用意した候補に当てはまらない回答には、

  • その他の悩み
  • その他の決め手
  • その他の変化

のいずれかが設定されます。

AIやタグ設定が失敗しても、お客様の声の投稿自体はできるだけ止めない仕組みにしています。

お客様の声は、数が増えてから整理しようとすると大変です。

Googleフォームから自動投稿する段階でタグも設定しておけば、投稿が増えても活用しやすくなります。

次回予告

次回は、今回設定したタグを使って、

悩み別に「お客様の声」を探せるWordPress一覧ページを作る方法

を紹介する予定です。

たとえば、

  • 集客に悩んでいたお客様の声
  • 費用に不安を感じていたお客様の声
  • 分かりやすい説明が決め手になったお客様の声

というように、タグごとのお客様の声を一覧で表示できるようにします。

自分では設定が難しい方は、設定のお手伝いも可能です。こちらのお問い合わせフォームよりお気軽にご相談ください。

※ご案内※

◆新刊発売記念|客単価アップ大事典「つい買ってしまう」販促の仕掛け75|限定プレゼント

◆毎月 販促成功事例が届くメルマガ

  • 全国の開催日程を確認をする
  • 広告作成アドバイザーの派遣を依頼する
  • 近くの広告作成アドバイザーを探す
  • 認定資格の取得を目指す

お問い合わせ窓口

ページTOPに戻る

©2017 「A4」1枚販促アンケート広告作成アドバイザー協会

お問い合わせ窓口