
現在運用している「ぼくのノートブック」と「TRAVeeeNET」のブログ記事が2,000本を超えたあたりから、何か面白いこと出来ないかなと考えていました。
「せっかく書いた記事をLINE公式アカウントで検索出来るようにできれば面白いのではないか?」
公式LINEのAIチャット機能にFAQを投げて作ろうかと思ったのですが、登録出来るFAQが100個のみと記事紹介BOTには少なすぎました。
そこで、LINEでキーワードを送るだけで、関連記事を最大5件までカード形式でレコメンドしてくれるBOTを、Google Apps Script(GAS)だけを使って無料で作ってみました。
この記事では、実際に動かすまでの手順とハマったポイントを、これから同じものを作りたい人向けに全部書き残しておきます。
\ お友達追加してチェック /

この記事でわかること
- Google Apps ScriptだけでLINE公式アカウントの自動応答BOTを作る全体像
- キーワードに応じて記事をレコメンドする「カルーセルメッセージ」の実装方法
- 「はい/いいえ」で分岐する会話フロー(クイックリプライ)の作り方
- 記事の追加・更新をスプレッドシートだけで完結させる仕組み
- 実際にハマったエラーと解決方法(302エラー・スプレッドシートID・応答メッセージ設定など)
完成イメージ
やりたかったことはシンプルです。
- ユーザーが「タイ eSIM」のようにキーワードを送る
- BOTが関連記事を検索し、カード型のメッセージ(カルーセル)で最大5件表示
- 「気になる記事は見つかった?」とはい/いいえボタン付きで質問
- 「はい」→ お礼を言って終了
- 「いいえ」→ 「こんな記事が欲しい」をリクエストできるフォームのURLを送信
読者が求めている情報にすぐたどり着けて、なおかつ見つからなかった場合のフィードバックも自動で集められる、という設計です。
なぜGoogle Apps Scriptを選んだのか

LINE BotのバックエンドというとNode.jsやPythonでサーバーを立てるイメージが強いかもしれませんが、今回は次の理由からGASを選びました。
- サーバー代がかからない(GASは無料枠で十分動く)
- 常時稼働のサーバー管理が不要(Webhookが届いた時だけ動く)
- データベースの代わりにスプレッドシートが使える(SQLを書かなくていい)
- ブログ記事の管理をすでにスプレッドシートでやっている人なら、学習コストがほぼゼロ
特に最後のポイントが大きくて、「記事タイトルと回答文とURLを一覧にしたスプレッドシート」さえあれば、それがそのままBOTのデータベースになります。
記事を追加したいときは、コードを一切触らずスプレッドシートに1行足すだけでBOTの回答が増えるのは、運用面でかなり楽です。
必要なもの
- LINE Developersアカウント(無料)
- Googleアカウント(スプレッドシート・Apps Script用)
- 紹介したい記事の「質問文・回答文・URL・カテゴリ」を整理したデータ
全体構成

[ユーザー] --メッセージ送信--> [LINE Messaging API]
|
| Webhook (POST)
v
[Google Apps Script (doPost)]
|
+----------------+----------------+
| |
[スプレッドシートを検索] [ユーザーの状態を保存/取得]
| (PropertiesService)
v
[カルーセルメッセージを生成]
|
v
[LINE Messaging API へ返信]
ポイントは、GASのdoPost関数がWebhookの受け口になるという点です。
LINEからメッセージが届くたびにこの関数が呼ばれ、スプレッドシートを検索してカードを組み立て、返信する、という流れです。
Step1: 記事データをスプレッドシートに用意する
まず、以下の5列を持つスプレッドシートを作ります。シート名は記事一覧にしました。
| No | カテゴリ | 質問 | 回答 | URL |
|---|---|---|---|---|
| 1 | 通信(eSIM/SIM) | タイ旅行で使えるおすすめのeSIMを知りたい | タイ旅行で使えるおすすめのeSIMを比較してご紹介しています。 | https://example.com/xxx |
| 2 | 観光 | バンコクのおすすめ観光スポットを知りたい | バンコクの見どころ・観光スポットについて詳しくご紹介しています。 | https://example.com/yyy |
検索は「カテゴリ」「質問」「回答」の文字列に対する部分一致で行うので、読者が打ちそうなキーワード(国名・地名・サービス名など)が「質問」や「回答」の文章に自然に含まれているかがヒット率を左右します。
Step2: LINE Developersでチャネルを作成する
- LINE Developersにログインし、Messaging APIのチャネルを作成
- 「Messaging API設定」タブでチャネルアクセストークン(長期)を発行してコピー
- この段階では「Webhook URL」はまだ空欄でOK(GAS側の準備が終わってから設定します)
Step3: Google Apps Scriptでコードを書く
スプレッドシートのメニューから「拡張機能」→「Apps Script」を開き、以下のようなコードを書きます(全文はこの記事末尾にまとめて掲載しています)。
Webhookの受け口
js
function doPost(e) {
try {
const body = JSON.parse(e.postData.contents);
const events = body.events || [];
events.forEach(function (event) {
handleEvent(event);
});
} catch (err) {
Logger.log('doPost error: ' + err);
}
return ContentService.createTextOutput(JSON.stringify({ status: 'ok' }))
.setMimeType(ContentService.MimeType.JSON);
}
doPostは「LINEから実際にPOSTリクエストが届いた時」だけ動く特別な関数です。エディタの「実行」ボタンで直接動かそうとすると、必要な引数eが渡されずエラーになります(私も最初これでハマりました)。テストする時は、ダミーのeを作って渡す専用のテスト関数を用意すると安全です。
キーワード検索ロジック
js
function searchArticles(userText) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
const data = sheet.getDataRange().getValues();
const rows = data.slice(1);
const keywords = userText.replace(/ /g, ' ').split(' ')
.map(function (k) { return k.trim(); })
.filter(function (k) { return k.length > 0; });
const scored = rows.map(function (row) {
const category = String(row[1] || '');
const question = String(row[2] || '');
const answer = String(row[3] || '');
const url = String(row[4] || '');
const searchTarget = (category + ' ' + question + ' ' + answer).toLowerCase();
let score = 0;
keywords.forEach(function (kw) {
const kwLower = kw.toLowerCase();
if (searchTarget.indexOf(kwLower) !== -1) score += 1;
if (category.toLowerCase().indexOf(kwLower) !== -1) score += 1;
});
return { category, question, answer, url, score };
});
return scored
.filter(function (r) { return r.score >= CONFIG.MIN_SCORE; })
.sort(function (a, b) { return b.score - a.score; })
.slice(0, CONFIG.MAX_RESULTS);
}
スペース区切りで複数キーワード(例:「タイ 観光」)にも対応させています。スコアが高い順に並び替えて上位5件を返すだけのシンプルなロジックですが、これで十分実用的に動きました。
カルーセル(カード型)メッセージの生成
js
function buildCarouselMessage(results) {
const columns = results.map(function (r) {
return {
title: truncate(r.question, 40),
text: truncate(r.answer, 60),
actions: [{ type: 'uri', label: '記事を見る', uri: r.url }],
};
});
return {
type: 'template',
altText: 'おすすめ記事のご紹介',
template: { type: 'carousel', columns: columns },
};
}
LINEのカルーセルテンプレートは、タイトル40文字・本文60文字までという制限があるので、truncate関数で自動的に切り詰めています。
「はい/いいえ」の会話フロー
js
function buildFeedbackQuickReply() {
return {
type: 'text',
text: '気になる記事は見つかった?',
quickReply: {
items: [
{ type: 'action', action: { type: 'message', label: 'はい', text: 'はい' } },
{ type: 'action', action: { type: 'message', label: 'いいえ', text: 'いいえ' } },
],
},
};
}
「はい/いいえ」の状態管理には、GASのPropertiesServiceを使いました。データベースを立てなくても、ユーザーIDごとに「今、はい/いいえの返事待ちかどうか」を簡単に記憶できます。
js
function setUserState(userId, state) {
PropertiesService.getScriptProperties().setProperty('state_' + userId, state);
}
function getUserState(userId) {
return PropertiesService.getScriptProperties().getProperty('state_' + userId);
}
function clearUserState(userId) {
PropertiesService.getScriptProperties().deleteProperty('state_' + userId);
}
Step4: Webアプリとしてデプロイする
- 右上の「デプロイ」→「新しいデプロイ」
- 種類は「ウェブアプリ」を選択
- 実行するユーザー: 自分
- アクセスできるユーザー: 全員 ← ここを間違えると詰みます(後述)
- デプロイすると、
https://script.google.com/macros/s/xxxxx/execというURLが発行される
Step5: LINE DevelopersにWebhook URLを設定する
- LINE Developersの「Messaging API設定」→「Webhook URL」に、Step4で発行されたURLを貼り付け
- 「Webhookの利用」をON
- 「検証」ボタンを押して、200が返ってくることを確認
- 「応答メッセージ」はOFFにする(ここがONだと、LINE公式アカウントマネージャーのデフォルト応答とBOTの返信が二重に動いてしまいます)
実際にハマったエラーと解決策
ここからは実際に構築中にぶつかった問題です。同じ症状で困っている人の参考になれば幸いです。
① 検証ボタンで「302 Found」が返ってくる
原因は2つ考えられます。
- デプロイURLがテスト用の
/devになっていた(本番用の/execを使う必要がある) - デプロイ時の「アクセスできるユーザー」が「全員」になっておらず、Googleのログイン画面にリダイレクトされていた
「デプロイを管理」から設定を見直し、「全員」+ /exec のURLに変更したところ解決しました。
② doPost error: TypeError: Cannot read properties of undefined (reading 'postData')
これはdoPostをエディタの「実行」ボタンで直接動かした時に必ず起きるエラーでした。eという引数はLINEからのリクエスト時にしか渡されないため、単体実行では想定通りエラーになります。テスト専用にダミーのeを渡す関数を別途用意することで解決しました。
js
function testDoPost() {
const dummyEvent = {
postData: {
contents: JSON.stringify({
events: [{
type: 'message',
message: { type: 'text', text: 'タイ eSIM' },
source: { type: 'user', userId: 'test-user' },
replyToken: 'dummy-reply-token',
}],
}),
},
};
const result = doPost(dummyEvent);
Logger.log(result.getContent());
}
③ Illegal spreadsheet id or key
スプレッドシートのURLからIDをコピーする際、余分な文字まで含めてコピーしてしまっていたのが原因でした。正しいIDは/d/と/editの間の文字列だけです。
https://docs.google.com/spreadsheets/d/【ここだけ】/edit
スプレッドシートに紐づいた形でApps Scriptを作成している場合は、SpreadsheetApp.getActiveSpreadsheet()を使えばID指定自体が不要になり、このミスを根本的に防げるのでおすすめです。
運用してみて感じたメリット
実際に運用を始めて感じたのは、記事の追加・修正がスプレッドシートを触るだけで完結するという手軽さです。
エンジニアでなくても、行を1つ追加するだけでBOTの「知識」が増えていきます。デプロイし直す必要もありません。
今後の改善予定
- スプレッドシートに「キーワード」列を追加して、検索の精度を手動でチューニングできるようにする
- カードにサムネイル画像を追加してタップ率を上げる
- 「いいえ」を選んだ回数が多いキーワードを集計して、記事ネタの発掘に活用する
まとめ
LINE公式アカウントの自動応答は、専門のツールを契約しなくても、Google Apps Scriptとスプレッドシートだけで十分実用的なものが作れます。
特に「記事をレコメンドしたいけど、検索の手間はかけさせたくない」というブログ・メディア運営者には、今回のようなキーワード反応型のカルーセルBOTはかなり相性が良いと感じました。
同じように自作を検討している方の参考になれば嬉しいです。

