先日、フロントエンド開発に関連する技術系記事をまとめてチェックできる 「TechDeck」 という Web サイトを Next.js + Tailwind CSS + いくつかの API + Vercel という組み合わせで作ってみましたという記事を書きました。
TechDeck では、下記の技術系記事がまとまっている Web サイトから記事データを取得させていただいてるんですけども、それぞれ、Qiita API (Qiita)、Feedly API (Zenn と はてなブックマーク)、DEV API (DEV Community)、という 3つの API を使用して記事を取得しています。
- Zenn から 「Tech」 カテゴリに分類される人気記事
- Qiita から過去 10 日間の人気記事 (20 ストック以上されている記事)
- はてなブックマーク 「テクノロジー」 カテゴリの人気記事
- DEV Community から、フロントエンド開発に関連しそうなタグごとの人気記事
ひとつ前の記事では、Qiita API を使用して、Next.js から 「過去 10 日間の人気記事 (20 ストック以上されている記事)」 を取得する方法について書いたのですが、今回は、Feedly API を使用して、登録したフィード単位で記事データを取得する方法について簡単にメモしておこうと思います。
Feedly とは
Feedly は所謂 「フィードリーダー (RSS リーダー)」 ですね。ある程度歴の長いインターネットユーザーはご存じだと思いますが、その昔、Google リーダーというフィードリーダーを多くの人が使っていましたけども、残念ながらサービス終了となり、その代替サービスとして注目されたのが Feedly でした (Feedly 自体は Google リーダーが正式リリースされた翌年の 2008 年にサービス開始されています)。
個人的には Google リーダーが終わるらしいよという話が出たあたりで一時的に livedoor Reader (これももうサービス終了してますね) に移行したんですけども、それと並行して Feedly も試しつつ、使い勝手いいなということで、以降は Feedly を愛用しています。
Feedly API
Feedly API の公式ドキュメントは下記にあります。
今回は Feedly に登録済みのフィードを個別に指定して記事データを取得したいため、API 群の中から、「Streams API」 を使用します。
そもそもなぜ Feedly を使うのかなのですが、TechDeck を作るに際して Zenn のトレンド記事データを取得したいと思ったとき、Zenn では API が公式に提供されておらず、トレンドの RSS フィードが提供されているという状態でした (Zenn で提供されている RSS フィードに関しては下記参照)。
で、まぁ RSS フィードがあるなら Next.js で直接 RSS フィードを読み込んでパースしちゃえばいいかと思ったんですけども、RSS フィードだと最新 24 件分しかデータが取得できないので、今回のようにもう少し多くの記事を掲載したい場合、何らかの方法で取得したデータを保存しておく場所と仕組みを作らなければなりません。
単純に面倒くさかったので、どこかにそれをやってくれる先はないかなと思ってたところ、Feedly に RSS フィードを登録すれば過去の記事データも Feedly に保存されるので、API を利用してそこから取得すればいいじゃんってことになりました。
アクセストークン
Feedly API のアクセストークンはちょっと特殊なのですが、まず、非営利 (non-commercial) 目的で利用可能な 「Developer Access Tokens」 と、ビジネス用途で使用可能な 「Enterprise Access Tokens」 の 2 種類のアクセストークンが存在します。
今回は個人利用なので 「Enterprise Access Tokens」 のことはひとまず置いておいて、「Developer Access Tokens」 だけに触れます。以降、「アクセストークン」 と言った場合は 「Developer Access Tokens」 のこととして話を進めますね。
次に、Feedly には無料で利用できる Free アカウントと、有料の Pro / Pro+ アカウントがあります (Enterprise アカウントっていうのもあるんですが企業向けなので今回は無視)。普通にフィードリーダーとして使用する分には Free アカウントでも十分だと思いますが、登録するフィードの数が多い場合などは Pro アカウント ($6 / 月) を使用するとハッピーになれます。
で、アクセストークンに話を戻しますが、Feedly API のアクセストークンと、それによって可能な API へのアクセスには下記のような制約があります。
- アクセストークンの有効期限は 30 日間
- 1 日あたり 25 件までのリクエストを送信可能
アクセストークンの発行は開発者向けの認証ページにアクセスし、Feedly アカウントでログインすると、登録してあるメールアドレスにアクセストークン生成用の URL リンクが届きます。リンク先に移動するとアクセストークンが表示されますので保存しましょう。
この時、Pro / Pro+ アカウントでログインした場合は、「リフレッシュトークン (Refresh Token)」 と呼ばれる、アクセストークン更新用のトークンが別途発行されますので、そちらも保存しておきます。
Pro / Pro+ アカウントのユーザーは、このリフレッシュトークンを使用して、アクセストークンを API から更新可能になります。つまり、API へのアクセスの度にリフレッシュトークンを使用して新しいアクセストークンを発行してあげることで、アクセストークンの有効期限を気にせず開発ができるということになります。
一方で Free アカウントのユーザーは、30 日ごとに、新しいアクセストークンを手動で発行して差し替えないといけませんので、その辺は結構面倒かもしれません。
なお、アクセストークンの有効期限が切れている場合、API からは
HTTP/401 ("token expired")
というレスポンスが帰りますので、アクセストークンの有効期限切れに気がつくことはできますが、Free アカウントの場合はそれを自動で更新する術がありません。
私は Pro アカウントなのと、今回の TechDeck でもリフレッシュトークンを使用したアクセストークンの更新を行っているため、その前提で以降の話を進めたいと思います。
ちなみにですが、Feedly API のリクエスト制限を超えた、あるいはアクセス頻度が高すぎる場合は、429 Too Many Requests
ステータスコードとともにアクセスが一時的に拒否され、下記のようなレスポンスが帰ってきます。
X-Ratelimit-Count: 17 X-Ratelimit-Reset: 23841
X-Ratelimit-Count
は現在までにリクエストされた回数、X-Ratelimit-Reset
はリミットが解除されるまでの時間 (秒数) になります。上の例だと、23,841 秒なので、「6 時間 37 分 21 秒後に制限が解除されますよ」 という返答になります。
例えば Next.js で SSG している場合などは next build && next export
したときくらいで、そんなに頻繁に API へのリクエストが発生することはないと思いますが、開発中や SSR の場合などは API へのリクエストが想定以上に行われてしまう可能性もあると思いますので、その辺は注意が必要かと思います。
リフレッシュトークンを使用したアクセストークンの更新方法
前述したとおり、リフレッシュトークンを使用したアクセストークンの自動更新を行うには Feedly の Pro アカウントが必要ですので、有料アカウントでない場合は使用できません。
アクセストークンの使用方法について、詳しくは下記に公式ドキュメントがあります。
具体的には /v3/auth/token
に対して、下記の各情報を POST
すれば、更新されたアクセストークンが帰ってきます。
- refresh_token
事前に取得したリフレッシュトークン - client_id
「feedlydev
」 で固定 - client_secret
「feedlydev
」 で固定 - grant_type
「refresh_token
」 で固定
レスポンスとしては下記のような感じで JSON データが帰ってきますので、
{ "id": "c805fcbf-3acf-4302-a97e-d82f9d7c897f", "access_token": "AQAAEg_icFaeyekDi7gKtCL9O1jh...", "expires_in": 3920, "token_type": "Bearer", "plan": "pro" }
access_token
を取得して使用する形になります。
Next.js における具体的な実装例
Next.js から具体的にどういう風にリクエストを投げるかですが、方法はいくつかあると思いますけども、今回は getStaticProps
で記事データを取得する際に、同時にアクセストークンの更新も行う感じで実装してみました。
まず、必要な情報を .env
ファイルにまとめておきます。
// .env.local
FEEDLY_API_URL=https://cloud.feedly.com/v3
FEEDLY_REFRESH_TOKEN=[リフレッシュトークン]
次に lib/api.js
を作成します。
// lib/api.js
const FEEDLYURL = process.env.FEEDLY_API_URL
const FEEDLYKEY = process.env.FEEDLY_REFRESH_TOKEN
export const getFeedlyAccessToken = async () => {
const data = {
client_id: 'feedlydev',
client_secret: 'feedlydev',
grant_type: 'refresh_token',
refresh_token: FEEDLYKEY
}
const key = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
}
const res = await fetch(`${FEEDLYURL}/auth/token`, key)
.catch((err) => {
console.error(err)
})
const json = await res.json()
if (json.errorMessage) {
console.error(json.errorMessage)
throw new Error('Failed to fetch API')
}
return json.access_token
}
まず、リクエストに必要な情報をセットします。
const data = {
client_id: 'feedlydev',
client_secret: 'feedlydev',
grant_type: 'refresh_token',
refresh_token: FEEDLYKEY
}
次にその情報を使って API にリクエストを送ります。
const key = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
}
const res = await fetch(`${FEEDLYURL}/auth/token`, key)
.catch((err) => {
console.error(err)
})
const json = await res.json()
Feedly API はリクエストに問題があった場合、下記のような JSON を返してきますので、
{"errorMessage":"Invalid JSON", "errorId":"xyz.2018021111.12345"}
その場合はエラーとして処理を中断するように、一応対策しておきます。
if (json.errorMessage) {
console.error(json.errorMessage)
throw new Error('Failed to fetch API')
}
JSON データに問題がなければ、access_token
を返します。
return json.access_token
あとは、getStaticProps
で記事データを取得する際、最初にこの処理を呼び出して、アクセストークンを取得します。
// pages/index.js
import { getFeedlyAccessToken } from '../lib/api'
export const getStaticProps = async () => {
const feedlyKey = await getFeedlyAccessToken()
}
Feedly API から記事データを取得
アクセストークン取得の準備ができたら、あとは Feedly API から記事データを取得します。
先ほど作成した lib/api.js
に下記を追加します。
// lib/api.js
const FEEDLYURL = process.env.FEEDLY_API_URL
const FEEDLYKEY = process.env.FEEDLY_REFRESH_TOKEN
export const getFeedlyAccessToken = async () => {
... 略 ...
}
// ↓ここから追加↓
export const getFeedlyPosts = async (token) => {
const key = {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
}
const res = await fetch(`${FEEDLYURL}/streams/contents?streamId=feed%2Fhttps%3A%2F%2Fzenn.dev%2Ffeed&count=50`, key)
.catch((err) => {
console.error(err)
})
const json = await res.json()
if (json.errorMessage) {
console.error(json.errorMessage)
throw new Error('Failed to fetch API')
}
return json
}
アクセストークンは下記のようにセット。
const key = {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
}
フィードごとの記事データは、「Streams API」 を使用して取得します。
/v3/streams/contents?streamId=:streamId
の形式でリクエストを送りますが、:streamId
には、Feedly における各 RSS フィードの ID をしています。これは feed/
のあとに RSS フィードの URL を続けたものです。
Zenn トレンド記事の RSS フィードは、
https://zenn.dev/feed
ですので、:streamId
は、
feed/https://zenn.dev/feed
となります。実際にはこれを URL エンコードした状態で指定しますので、具体的なリクエスト URL としては下記のようになります。
/streams/contents?streamId=feed%2Fhttps%3A%2F%2Fzenn.dev%2Ffeed
また、いくつかのパラメータが指定可能ですが、今回は取得する記事件数を指定する count
のみ使用しました。デフォルト値は 20 件、最大で 1,000 件まで取得できますが、今回は 50 件分取得する設定にしています。
const res = await fetch(`${FEEDLYURL}/streams/contents?streamId=feed%2Fhttps%3A%2F%2Fzenn.dev%2Ffeed&count=50`, key)
.catch((err) => {
console.error(err)
})
const json = await res.json()
return json
先に用意したアクセストークンの取得部分と組み合わせて、getStaticProps
で記事データを取得し、実際のページに反映すれば完了です。
// pages/index.js
import { getFeedlyAccessToken, getFeedlyPosts } from '../lib/api'
export const getStaticProps = async () => {
const feedlyKey = await getFeedlyAccessToken()
const feedly = await getFeedlyPosts(feedlyKey)
return {
props: {
feedlypost: feedly,
}
}
}