きっかけ
いつものようにクラスメソッドさんのブログを見ていた時、何気なく執筆者さんのリンクを押下しました。
↑のページにある、各執筆者様たちのリンクをクリックします。するとこのように投稿数やシェア数と…
投稿した記事の一覧が簡単なプロフィールと共に表示されています。
これを見たワイ。
「かっけぇぇぇぇぇぇぇぇぇぇぇ〜〜〜〜!!自分のポートフォリオでもやりてぇぇぇぇぇぇぇ〜〜〜🤯」
冷静になって仕様を決める
「さてどういう仕様にするか?」を考え始めたところで、自分が技術記事を書いている媒体はZennとQiitaであることを思い出します。
これまでそれぞれへの導線はポートフォリオのトップに置いていました。
が、さっきのクラスメソッドさんのページをヒントにこう思い始めます。
「そもそも、記事の特性によって投稿する媒体を変えているけど、書いてるものは"技術記事"なんだから、プラットフォーム関係なく一覧に出しておきたいなぁ🤔」
できたもの
というわけでいきなりですが完成したブツです。完全に自画自賛ですがめちゃくちゃ気に入っています。
Rechartsを使って年間の投稿数を出したり、媒体別の月別記事数を出したり…
年間のいいね数を出したり、媒体別の月別いいね数を出したりしていて…
ZennとQiitaの両方の記事を投稿日時の降順にページングしながら表示しています。
ちなみに実際にポートフォリオを見ていただけるとわかると思いますが、2年近いデータを取得しているにもかかわらず非常に高速です。一切のラグがなく表示されているはずです。
そして気づかれた方もいらっしゃるかもしれません。開発者ツールからネットワーク状況を確認すると、データ取得のためのfetch
が走っていません。
では、ここからはお待ちかね?の「どう作ったか」の話です。
要件
ゴールとしては先に話した通り、「グラフを使って投稿数やいいね数が見えること」「ZennとQiitaの記事を一括で一覧表示できること」でした。
が、非機能的な要件として以下を考えていました。
とにかくお金をかけない
まず、そもそもポートフォリオのホスティング先はFirebase
でReact×Viteでクライアントサイドで動いています。
つまり、APIを実行するために必要なトークンを秘匿して保持することができません。よって、アクセストークンが必要なQiita APIを実行できません。
トークンを秘匿しながらAPI実行するにはCloud Functions
などを利用する必要がありますが、1円たりともお金は払いたくありません。
最新の情報を取得していきたい
当たり前ですが、最新の情報を表示してほしいです。
が、ポートフォリオにアクセスがある度に数年単位の記事情報を取得して表示するのはナンセンスでしょう。APIサーバーへ余計な負荷をかけることにもなるため、避けたいところです。
そのために、キャッシュを作りたいところです。
実装
以上の要件を満たすべく実装を進めていきます。
Recharts
まずは機能要件から。本筋の目的であるグラフ表示をするために、ライブラリの導入を検討しました。
今回は表題の通り、Recharts
を使うことにしました。理由は特にないのですが、強いて言えば割と頻繁にリリースがされていることと、実装が簡単そうだったからです。
実際、実装はとてもシンプルです。以下のようにimport
するだけで簡単に使い始めることができます。
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { TechArticleData } from '../data/TechArticleData';
export const TechArticlesGraph = () => {
return (
<div className="grid grid-cols-1 items-center justify-center gap-4">
<div className="flex justify-center items-center gap-4">
<h1 className="text-center text-3xl font-extrabold text-gray-600 underline">Article Posts</h1>
</div>
<div className="flex justify-center items-center gap-4 mb-4">
<ResponsiveContainer width="100%" height={300}>
<BarChart
width={500}
height={300}
data={TechArticleData.yearArticleCounts}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="articles" stackId="a" fill="#FBBC05" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)
}
Rechartsのグラフに渡すデータ形式は以下のようになっている必要があります。
const data = [
{
name: 'Page A',
uv: 4000,
pv: 2400,
amt: 2400,
},
{
name: 'Page B',
uv: 3000,
pv: 1398,
amt: 2210,
},
{
name: 'Page C',
uv: 2000,
pv: 9800,
amt: 2290,
},
];
ということでZennとQiitaのデータを取得して、このオブジェクトのようなデータを作成する必要があります。
データ取得
記事情報のデータ取得には、Zenn・QiitaともにAPIを使うことができます。
Zennは簡単で、ただのGETリクエストを送るだけで記事一覧の取得が可能です。エンドポイントは以下の通りです。
https://zenn.dev/api/articles?username=ユーザー名&order=latest&count=取得件数
一方で一手間必要なのがQiitaです。リクエストを送信するためにアクセストークンの取得が必要になります。
トークンの取得方法・リクエストの送信方法はこちらをご参照ください。
これでデータの取得ができるようになりました。しかしQiitaAPIのトークンを保存する先が問題になります。
このポートフォリオはクライアントサイドで動くため、素直にトークンを隠匿することができません。
キャッシュの作成と保存先
先の課題を解決するために、GitHub Actions
で作成したプログラムを実行しデータを取得します。トークンなどはリポジトリのSecrets
に持たせることで、外部に晒されることはありません。
今回はnode.js
でJavaScriptを実行し、取得したそれぞれの媒体の情報を整形したうえでRecharts
やページング表示がしやすい形でTSファイルとして出力することでした。
const chartDataJson = JSON.stringify(chartData, null, 2);
const articleListJson = JSON.stringify(pagingArticles, null, 2);
const jsonContent = `export const TechArticleData = ${chartDataJson};\nexport const TechArticleList = ${articleListJson};`
fs.writeFile(FILE_PATH, jsonContent, 'utf8', (err) => {
if (err) {
throw err;
}
console.log('#### Print Succeeded!! ####');
});
その結果以下のようなTSファイルを作成し、このオブジェクトをグラフやページング表示で利用しています。
export const TechArticleData = {
"articlesCounts": [
{
"yearMonth": "2023/04",
"zenn": 8,
"qiita": 0
},
{
"yearMonth": "2023/05",
"zenn": 10,
"qiita": 0
},
{
"yearMonth": "2023/06",
"zenn": 2,
"qiita": 0
},
],
"yearArticleCounts": [
{
"year": "2023",
"articles": 54
},
{
"year": "2024",
"articles": 13
}
],
"favoritesCounts": [
{
"yearMonth": "2023/04",
"zenn": 315,
"qiita": 0
},
{
"yearMonth": "2023/05",
"zenn": 1152,
"qiita": 0
},
{
"yearMonth": "2023/06",
"zenn": 79,
"qiita": 0
},
],
"yearFavoritesCounts": [
{
"year": "2023",
"favorites": 2265
},
{
"year": "2024",
"favorites": 443
}
]
};
export const TechArticleList = [
[
{
"treeType": "🖋",
"img": "qiita",
"year": "2024/04/20",
"title": "Postmanを使い始めた時に知っておきたかった地味に便利な機能10選",
"url": "https://qiita.com/ysknsid25/items/86fa54eca58edefe156d",
"content": "❤️ 152"
},
{
"treeType": "🖋",
"img": "qiita",
"year": "2024/04/18",
"title": "[TIPS]MutableSetのaddメソッドを使って、先勝ちの処理をスリムに書く",
"url": "https://qiita.com/ysknsid25/items/fa3c1d43c77f3a164a42",
"content": "❤️ 6"
},
{
"treeType": "🖋",
"img": "zenn",
"year": "2024/04/17",
"title": "テックカンファレンスに「なんとなく」や「ただ楽しいから」で参加してない?",
"url": "https://zenn.dev/bs_kansai/articles/4a8d9afc534d18",
"content": "❤️ 50"
},
],
[
{
"treeType": "🖋",
"img": "zenn",
"year": "2024/04/03",
"title": "テストコード品質を高めるためにJS向けMutation Testingライブラリ・Strykerを実戦導入してみた",
"url": "https://zenn.dev/hitocolor/articles/3b6792cc9887df",
"content": "❤️ 25"
},
{
"treeType": "🖋",
"img": "zenn",
"year": "2024/03/04",
"title": "Laravel(Pest)でInfectionを利用したMutation Testingを試してみる",
"url": "https://zenn.dev/bs_kansai/articles/3a198f77e60d40",
"content": "❤️ 5"
},
{
"treeType": "🖋",
"img": "zenn",
"year": "2024/02/24",
"title": "Re: WebサーバーアーキテクチャとPHP実行方式の理解から始めるphp-fpmとはなにか?",
"url": "https://zenn.dev/bs_kansai/articles/3706c12408160c",
"content": "❤️ 107"
},
],
];
キャッシュの更新
キャッシュの更新はGitHub Actions
の定期実行で1日に一度行います。これで毎日最新の記事投稿状態でいいね数なども表示されます。
そこまで件数がないため更新時には先ほどのTSファイルをまるっと置き換えます。ちなみにファイル更新にかかっている時間は5秒でした。
今後の方針としては、あまり昔のデータを出力しても仕方がないと思っているので、直近5年くらいのデータを保持しようと考えています。
1年で50記事程度の増加具合なので、5年でも250件ほど。大した件数にはならなそうです。
そしてキャッシュを更新したタイミングで、ポートフォリオもデプロイを行います。
GitHub Actions
からのデプロイ方法は、これらの記事が参考になりました。
おわりに
ZennとQiitaの記事をいい感じにポートフォリオに出したいという人は、結構いらっしゃいそうです。
結構ポストに、いいねで反響があったり、「真似したい」という声も。
ということで、やってみたい方の参考になれば幸いです!めちゃ簡単にカッコ良くできますよ!!