九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、最近のマーケティングでは欠かせない存在と言えば、やはり、
SNS(YouTube、Instagram、X など)
ですよね。
というのも、もしSNSにフォロワーさんが多ければ、
良いことばかりだからです。
例えば、
- 広告費なしで集客できる
- ファン化しているので成約率が高い
- 同じ理由から、高単価のものが売れる
- 同じ理由から、他の商品も買ってもらえる
- 事前に理念を共有できるので、質のいい採用ができる
なので、企業さんもSNSに力を入れているようです。
(ただし、属人化してしまうのが悩みだそうです…担当者がやめちゃうので😅)
そして、この流れを考えると私も本格的にSNSの勉強をするべき
という結論になり、尊敬する社長さんの有料教材を購入してみました。
📝 参考ページ(PR): Twitterマーケティングマスター講座
やはり有料ということで、これまで全く想像していなかったアプローチ方法
を学ぶことができて、とても満足してます。😊✨
そして、その教材の中ではXで良ポストを探す方法を勉強するのですが、
毎回検索コマンドを入力するのが面倒でした。

じゃあ、使いやすい検索フォームをパパッと作っちゃおう!
というのが今回の趣旨です。
※ なお、Xの検索コマンドは公開されている情報を元にしてつくってます。念のため!
今回はシンプルなので、ぜひLaravelの勉強をしたい方も
真似して作ってもらったらスキルアップになると思います!
ぜひ何かの参考になりましたら嬉しいです。😊✨
「有料の情報って
想像してたのと全然
違ってたなぁ(感心)」
開発環境: Laravel + Inertia + Vue 3(Composition API)
環境を整える
Laravel + Inertia + Vue 3が使えるようにするために
一番手っ取り早いのがLaravel breezeをインストールすることです。
Laravel breezeはログイン周りのパッケージですが、今回の環境をすべて
満たしているので先にこれをインストールしておきましょう。
以下のコマンドを実行して下さい。
composer require laravel/breeze --dev
するとパッケージがインストールされるので、
続けて以下のコマンドを実行して下さい。
php artisan breeze:install
すると、どのモードでインストールするか聞かれるので、
今回はVue with Inertiaを選択。

続くオプションは何も選択せずエンターキーを押します。

テストもそのまま(そのうちPestも勉強しなきゃ…😅)

これでインストールが完了しました。
あとは、以下のコマンドを実行すれば完了です。
php artisan migrate npm install npm run dev
ちなみに表示URLは.env内のAPP_URLが適用されます。
.env
APP_URL=(ここにあなたの環境のURL。localhostとかl11x.testとかですね)
JS パッケージをインストールする
今回以下2つのパッケージを使うので、先にインストールしておきましょう。
- dayjs: 日付を管理する
- Lodash: 便利機能が満載なパッケージ
以下のコマンドを実行して下さい。
npm i lodash --save-dev npm i dayjs --save-dev
すると、パッケージがインストールされますが
まだこの状態ではVueから呼び出すことができません。
以下に登録をしておきましょう。
resources/js/bootstrap.js
// 省略
// dayjs
import dayjs from "dayjs";
import 'dayjs/locale/ja';
window.dayjs = dayjs;
dayjs.locale('ja');
// Lodash
import _ from 'lodash';
window._ = _;
これで、Vue側からもdayjsと_にアクセスができるようになります。
Vue ファイルをつくる
では、今回はシンプルな構造なのでコントローラーは作らず
いきなりVueファイルを作ります。
※ もし実際にサイトをつくる場合はコントローラーを作ったほうがいいです。
resources/js/Pages/UsefulForm.vue
<script setup>
import {computed, onBeforeMount, onMounted, ref} from 'vue';
import { Head } from '@inertiajs/vue3';
// Form
const keyword = ref('');
const params = ref({
from: '',
min_faves: 100,
min_retweets: 100,
min_replies: 100,
since: '',
until: ''
});
const toggle = ref({});
onBeforeMount(() => {
let toggleValues = {};
for(let key in params.value) {
toggleValues[key] = false;
}
toggle.value = toggleValues;
});
// Inputs
const onInputChange = (key) => {
toggle.value[key] = params.value[key] !== '';
};
const onClearKeywordClick = () => {
keyword.value = '';
document.getElementById('keyword-input').focus();
};
const setDate = (paramKey, type) => {
let dt = dayjs();
if (type === 'year') {
dt = dt.subtract(1, 'year');
} else if (type === 'month') {
dt = dt.subtract(1, 'month');
} else if (type === 'week') {
dt = dt.subtract(1, 'week');
} else if (type === 'yesterday') {
dt = dt.subtract(1, 'day');
}
params.value[paramKey] = dt.format('YYYY-MM-DD');
toggle.value[paramKey] = true;
};
// Search
const searchUrl = computed(() => {
let searchParams = [];
if (keyword.value) {
searchParams.push(keyword.value);
}
for (const [key, value] of Object.entries(params.value)) {
if (toggle.value[key] && value) {
searchParams.push(`${key}:${value}`);
}
}
let searchUrl = `https://twitter.com/search?q=${searchParams.join(' ')}`;
if(currentIncludeItems.value.length > 0) {
searchUrl += ' filter:';
for(let i = 0; i < currentIncludeItems.value.length; i++) {
searchUrl += currentIncludeItems.value[i];
if(i < currentIncludeItems.value.length - 1) {
searchUrl += ' ';
}
}
}
return searchUrl;
});
const currentIncludeItems = ref([]);
const includingItems = ref([
{ value: 'images', label: '画像を含む' },
{ value: 'videos', label: '動画を含む' },
{ value: 'links', label: 'リンクを含む' },
]);
// DataList
const dataList = ref({
keyword: [],
from: [],
});
const onSearchButtonClick = () => {
setDataList('keyword', keyword.value);
setDataList('from', params.value.from);
localStorage.setItem('dataList', JSON.stringify(dataList.value));
};
const setDataList = (dataListKey, text) => {
if(text.length > 0) {
const targetDataList = dataList.value[dataListKey];
let hasDataListItem = false;
for(let i = 0; i < targetDataList.length; i++) {
let dataListItem = targetDataList[i];
if(dataListItem.text === text) {
dataListItem.count++;
hasDataListItem = true;
break;
}
}
if(! hasDataListItem) {
targetDataList.push({
text: text,
count: 1
});
}
}
};
const dataListKeywords = computed(() => {
return _.sortBy(dataList.value.keyword, 'count').reverse(); // 検索が多い順に並び替え
});
const dataListFroms = computed(() => {
return _.sortBy(dataList.value.from, 'count').reverse(); // 検索が多い順に並び替え
});
onMounted(() => {
const storage = localStorage.getItem('dataList');
if (storage) {
try {
dataList.value = JSON.parse(storage);
} catch (e) {}
}
});
</script>
<template>
<Head title="X ポスト / リサーチ" />
<div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8 bg-gray-200">
<div class="text-center text-2xl mb-6 font-bold">
X ポスト / リサーチ
</div>
<table class="mx-auto table-auto">
<tbody>
<tr>
<td colspan="3" class="pb-1">
<input id="keyword-input" list="keyword" v-model="keyword" type="text" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="キーワード">
<datalist id="keyword">
<option v-for="keyword in dataListKeywords" :value="keyword.text"></option>
</datalist>
<div class="text-right px-2 py-1 text-sm">
<a href="#" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer" @click.prevent="onClearKeywordClick">クリア</a>
</div>
</td>
</tr>
<tr>
<td class="w-10 align-top">
<input type="checkbox" v-model="toggle.from" id="toggleFrom" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td class="w-40 align-top">
<label for="toggleFrom" class="text-sm font-medium text-gray-700 text-nowrap">アカウントID:</label>
</td>
<td class="w-64">
<input v-model="params.from" list="from" type="text" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('from')">
<datalist id="from">
<option v-for="from in dataListFroms" :value="from.text"></option>
</datalist>
<div class="pt-0.5 p-1 text-gray-600">
<small>例:elonmusk</small>
</div>
</td>
</tr>
<tr>
<td>
<input type="checkbox" v-model="toggle.min_faves" id="toggleMinFavorites" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td>
<label for="toggleMinFavorites" class="text-sm font-medium text-gray-700 text-nowrap">最小いいね数:</label>
</td>
<td>
<input v-model="params.min_faves" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_faves')">
</td>
</tr>
<tr>
<td>
<input type="checkbox" v-model="toggle.min_retweets" id="toggleMinRetweets" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td>
<label for="toggleMinRetweets" class="text-sm font-medium text-gray-700 text-nowrap">最小リツイート数:</label>
</td>
<td>
<input v-model="params.min_retweets" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_retweets')">
</td>
</tr>
<tr>
<td>
<input type="checkbox" v-model="toggle.min_replies" id="toggleMinReplies" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td>
<label for="toggleMinReplies" class="text-sm font-medium text-gray-700 text-nowrap">最小リプライ数:</label>
</td>
<td>
<input v-model="params.min_replies" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_replies')">
</td>
</tr>
<tr>
<td class="align-top">
<input type="checkbox" v-model="toggle.since" id="toggleSince" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td class="align-top">
<label for="toggleSince" class="text-sm font-medium text-gray-700 text-nowrap">開始日:</label>
</td>
<td>
<input v-model="params.since" :max="params.until" type="date" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('since')">
</td>
</tr>
<tr>
<td colspan="3">
<div class="text-right p-1 text-gray-400">
<a href="#" @click="setDate('since', 'year')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1年前</a> /
<a href="#" @click="setDate('since', 'month')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1ヶ月前</a> /
<a href="#" @click="setDate('since', 'week')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1週間前</a> /
<a href="#" @click="setDate('since', 'yesterday')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">昨日</a> /
<a href="#" @click="setDate('since', 'today')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">今日</a>
</div>
</td>
</tr>
<tr>
<td class="align-top">
<input type="checkbox" v-model="toggle.until" id="toggleUntil" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
</td>
<td class="align-top">
<label for="toggleUntil" class="text-sm font-medium text-gray-700 text-nowrap">終了日:</label>
</td>
<td>
<input v-model="params.until" :min="params.since" type="date" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('until')">
</td>
</tr>
<tr>
<td colspan="3">
<div class="text-right p-1 text-gray-400">
<a href="#" @click="setDate('until', 'year')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1年前</a> /
<a href="#" @click="setDate('until', 'month')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1ヶ月前</a> /
<a href="#" @click="setDate('until', 'week')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1週間前</a> /
<a href="#" @click="setDate('until', 'yesterday')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">昨日</a> /
<a href="#" @click="setDate('until', 'today')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">今日</a>
</div>
</td>
</tr>
<tr>
<td colspan="3" class="pt-4">
<div class="text-sm font-medium text-gray-700 mb-2">含むアイテム:</div>
<div class="flex flex-wrap">
<div class="mr-2" v-for="item in includingItems">
<label class="text-sm font-medium text-gray-700 ml-1 flex items-center">
<input type="checkbox" v-model="currentIncludeItems" :value="item.value" class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500 mr-1">
{{ item.label }}
</label>
</div>
</div>
</td>
</tr>
<tr>
<td colspan="3" class="pt-3 text-right">
<a :href="searchUrl" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" target="_blank" @click="onSearchButtonClick">
検索
</a>
</td>
</tr>
</tbody>
</table>
</div>
</template>
ちなみに、今回実装している内容は次のとおりです。
- 検索したキーワードが LocalStorage に保存されて、次回から入力補完ができる
- 検索回数が多い順に自動で並べ替える
- アカウントIDも同じ形で入力補完できる
- チェックボックスで検索したい内容を選択できる
- 日付は選択が面倒なのでワンクリックで完了できるようにする
- その他オプションも選択可能にする
ちなみに、その他のコマンドはこちらがわかりやすいです。
📝 参考ページ: 【Twitterユーザー必見】便利な検索コマンド25選!いいね数や特定ユーザーで検索
そして、LocalStorageは、以下の2パターンのみです。
- ページ読み込み時に取得
- 検索ボタンを押したら保存
ルートをつくる
今回ルートはとてもシンプルになっています。
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('x_useful_form', fn() => Inertia::render('UsefulForm'));
※ なお、Laravel Breezeをインストールするといくつかルートが追加されていますが、今回は不要なので削除しています。
さぁ、これで作業は完了です。
お疲れ様でした。😊
テストしてみる
では、実際のテストしてみましょう❗
ブラウザで「https://******/x_useful_form」へアクセスします。
すると・・・・・・

はい❗
検索用のフォームが表示されました。
では、キーワードに「カレーうどん」と入力し、
最小いいね数にチェックを入れて「検索」ボタンをクリックしてみます。
どうなるでしょうか・・・・・・

はい!
ページ移動した先の検索ワードが想定通りになっていました。
では、次に入力補完のチェックです。
「豚骨ラーメン」で2回チェックして一番上に来ているか
確認しておきましょう。
うまくいくでしょうか・・・・・・

はい❗
豚骨ラーメンは2回検索されたので、一番上に表示されています。
すべて成功です。😊✨
実際に公開しました
今回のコードを実際に体験できるように
ページを公開しました。
ぜひ以下から試してみて下さい。
📝 公開ページ: https://social-digger.capilano-fw.com/
企業様へのご提案
昨今、Xを始めとするSNSの重要性は
より大きくなってきています。
しかし、それらの運用はなかなか時間がかかってしまします。
そのため、今回のように運用を支援するツールを
ご用意になってみてはいかがでしょうか。
貴社やその業界にカスタマイズした専用リサーチツールを
つくっておくと集客の効率化に役に立つことが期待できます。
ぜひそういったツールをご希望でしたら、
いつでもお気軽にご相談下さい。
お待ちしております。😊✨
おわりに
ということで、今回はSNSマーケティングを支援するツールとして
Xの便利な検索フォームをつくってみました。
常に、優秀なものに触れるのはスキルアップにつながると思いますので、
ぜひ皆さんも今回のフォームを活用していただけると嬉しいです。
また、ご自身の業界や状況によって必要な項目も出てくるかと思うので、
その場合は拡張して入力項目を増やしてみてはいかがでしょうか。
私もまだまだ勉強します!
ではでは〜❗
「ついでに、
(PR) ビジネスモデルマスター講座
も買っちゃった 😊👍」





