Qiigleという、Qiitaの記事を検索するサービスを作りました。
サイトURL: https://qiigle.com/
GitHub: https://github.com/nyshk97/qiigle
何ができるか(仕様)
- Qiitaの記事を「ユーザー名」「タイトル」「本文」「タグ」で絞り込んで表示します
- 複数入力した場合、AND条件になります
- ユーザー名とタグは完全一致、タイトルと本文は含むです
なぜ作ったか
開発中に過去自分が書いた記事を参照することがよくあるのですが、数が多くなってきて探すのが大変になってきました。(ブックマークで管理するのツラい)
ユーザー名 + 記事の内容で簡単に記事を検索する方法がありそうでなかった(どの方法もちょっと面倒だった)ので作ってみました。
使用技術
- ConohaWing
- PHP
- Tailwind CSS(CDN)
- HTML
やったことまとめ
- GoogleDomainsでドメインを取得
- レンタルサーバー(ConohaWING)のコンパネからサイトを追加
- DNSの設定(ConohaWingのネームサーバーをGoogleDomainsに登録)
- QiitaAPIのアクセストークンを取得
- POSTMANでAPIを叩く
- HTMLでフォームを作成
- フォームから受け取ったデータを使いPHPでAPIを叩く
- 取得したデータを整形して表示
- Tailwind CSSでデザインを整える
- SSL有効化
- URL正規化
- ファビコンを作成→設置
- TwitterOGPの設定
- GoogleAnalyticsの設定
実装はシンプルでもやることが意外と多くて、結局半日かかりました。
超久々にPHPを書きました。(普段はRubyを書いています)
ソースコード
Formから受け取った値を使ってQiitaのAPIを叩くだけのシンプルな実装です。
Tailwind CSSをCDNから読み込んでいます。
画像ファイル等も含めたソース全体はGitHubからご確認ください。
index.php
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@d0ne1s" />
<meta property="og:url" content="https://qiigle.com" />
<meta property="og:title" content="Qiigle - qiita記事検索サービス" />
<meta property="og:description" content="Qiitaの記事を検索するサービスです。ユーザー名、タイトル、本文、タグの複数条件で検索することができます。" />
<meta property="og:image" content="https://qiigle.com/ogp.png" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<title>Qiigle - qiita記事検索サービス</title>
</head>
<body>
<style>
.green {color: #5bad10;}
.light-green {color: #54c000;}
.blue {color: #4092d4;}
.pink {color: #eadad1;}
.black {color: #3b3e3e;}
</style>
<div class='max-w-3xl mx-auto px-4'>
<div class='text-center mb-4'>
<a href='/'>
<h1>
<span class='green text-6xl'>Q</span>
<span class='blue text-6xl'>i</span>
<span class='pink text-6xl'>i</span>
<span class='green text-6xl'>g</span>
<span class='black text-6xl'>l</span>
<span class='blue text-6xl'>e</span>
<span class='text-xs text-gray-800'>by <a href='https://twitter.com/d0ne1s' class='text-gray-600 text-sm'>d0ne1s</a></span>
</h1>
</a>
</div>
<div class='mb-6'>
<p class='text-sm text-center text-gray-800'>Qiitaの記事を検索します</p>
</div>
<form action='search_result.php'>
<div class="bg-white rounded-lg">
<div class="grid lg:grid-cols-2 gap-6">
<div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
<div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
<p>
<label for="user" class="bg-white text-gray-600 px-1">ユーザー名</label>
</p>
</div>
<p>
<input id="user" name='user' autocomplete="false" tabindex="0" type="text" placeholder='d0ne1s' class="py-1 px-1 text-gray-900 outline-none block h-full w-full">
</p>
</div>
<div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
<div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
<p>
<label for="title" class="bg-white text-gray-600 px-1">タイトル</label>
</p>
</div>
<p>
<input id="title" name='title' autocomplete="false" tabindex="0" type="text" placeholder='Rubocopを使ってみた' class="py-1 px-1 outline-none block h-full w-full">
</p>
</div>
<div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
<div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
<p>
<label for="body" class="bg-white text-gray-600 px-1">本文</label>
</p>
</div>
<p>
<input id="body" name='body' autocomplete="false" tabindex="0" type="text" placeholder='.rubocop.yml' class="py-1 px-1 outline-none block h-full w-full">
</p>
</div>
<div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
<div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
<p>
<label for="tag" class="bg-white text-gray-600 px-1">タグ</label>
</p>
</div>
<p>
<input id="tag" name='tag' autocomplete="false" tabindex="0" type="text" placeholder='Ruby' class="py-1 px-1 outline-none block h-full w-full">
</p>
</div>
</div>
<div class="mt-3 pt-3 text-center">
<button class="rounded text-gray-100 px-8 py-2 hover:shadow-inner hover:bg-blue-700 transition-all duration-300 hover:opacity-75" style='background: #54c000'>
検索
</button>
</div>
</div>
</form>
</div>
</body>
</html>
search_result.php
<?php
if($_GET['user']) {
$q_user = urlencode("user:{$_GET['user']} ");
}
if($_GET['title']) {
$q_title = urlencode("title:{$_GET['title']} ");
}
if($_GET['body']) {
$q_body = urlencode("body:{$_GET['body']} ");
}
if($_GET['tag']) {
$q_tag = urlencode("tag:{$_GET['tag']} ");
}
function esc($s){
return htmlspecialchars($s, ENT_QUOTES, 'utf-8');
}
$base_url = "https://qiita.com/api/v2/items";
$url = "{$base_url}?per_page=100&query={$q_user}{$q_title}{$q_body}{$q_tag}";
$curl = curl_init();
$option = [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer MY_ACCESS_TOKEN'],
];
curl_setopt_array($curl, $option);
$response = curl_exec($curl);
$articles = json_decode($response, true);
curl_close($curl);
?>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@d0ne1s" />
<meta property="og:url" content="https://qiigle.com" />
<meta property="og:title" content="Qiigle - qiita記事検索サービス" />
<meta property="og:description" content="Qiitaの記事を検索するサービスです。ユーザー名、タイトル、本文、タグの複数条件で検索することができます。" />
<meta property="og:image" content="https://qiigle.com/ogp.png" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
<title>検索結果 | Qiigle - qiita記事検索サービス</title>
</head>
<body>
<style>
.green {color: #5bad10;}
.light-green {color: #54c000;}
.blue {color: #4092d4;}
.pink {color: #eadad1;}
.black {color: #3b3e3e;}
</style>
<div class='max-w-3xl mx-auto px-4 pb-8'>
<div class='text-center mb-4'>
<a href='/'>
<h1>
<span class='green text-6xl'>Q</span>
<span class='blue text-6xl'>i</span>
<span class='pink text-6xl'>i</span>
<span class='green text-6xl'>g</span>
<span class='black text-6xl'>l</span>
<span class='blue text-6xl'>e</span>
<span class='text-xs text-gray-800'>by <a href='https://twitter.com/d0ne1s' class='text-gray-600 text-sm'>d0ne1s</a></span>
</h1>
</a>
</div>
<div>
<p>
<span class='mr-1'>検索結果</span>
<span class='text-gray-800 text-xs'>(ユーザー名: <?= esc($_GET['user']); ?>, タイトル: <?= esc($_GET['title']); ?>, 本文: <?= esc($_GET['body']); ?>, タグ: <?= esc($_GET['tag']); ?>)
</span>
</p>
</div>
<div>
<?php foreach($articles as $a){ ?>
<div class='py-4'>
<a href='https://qiita.com/<?= esc($a['user']['id']); ?>' class='block text-xs' target='_blank'><?= esc($a['user']['name']); ?> (@<?= esc($a['user']['id']); ?>)</a>
<a href='<?= esc($a['url']); ?>' class='block hover:underline' target='_blank'>
<h3 class='text-lg' style='color: #1a0dab;'><?= esc($a['title']); ?></h3>
<p class='break-all text-xs text-gray-800'><?= substr(esc($a['body']), 0, 200); ?></p>
</a>
</div>
<?php } ?>
</div>
</div>
</body>
</html>
機能追加(08/03)
- 検索結果画面にLGTM、投稿日、更新日を追加
- ユーザー名を記憶する機能を実装