先日、個人的に関わっているとあるサイト制作(Nuxt.js 製)のプロジェクトで、クライアントが運用している note のアカウントの記事一覧をそのままサイトに掲載するという実装をおこないました。
note を Webサイトのブログ代わりに使う
note は、いまや個人だけでなく企業アカウントも運用する一大プラットフォームです。note を使えば自分でレンタルサーバーを借りてブログを構築することなく、手軽に情報発信ができます。
そうしたなか、企業や個人のWebサイト上に、わざわざブログを構築するのではなく、普段運用している note の記事をそのまま外部リンクとして載せてしまえばいいのではないか?という声も少なからず耳にするようになりました。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/02.png)
たしかに、note はオーサーにとっても読者にとってもユーザビリティが高く、ブログコンテンツをどのメディア(ないしドメイン)で発行するかにこだわりがないユーザにとってはそれも有効な選択肢のひとつかもしれません。
今回はそうした背景もあり、先述のプロジェクトにて、ブログ機能の実装をヘッドレス CMS を使った実装から、 note の記事リンク一覧をそのまま表示する方針に舵を切ることとなりました。
具体的には、Cloud Functions と Express を使って 、note の RSS をもとに別ドメインからクライアントサイドで記事を取得するための仲介サーバを作り、そこから記事を取得し表示する方法を採用しました。
今回はその方法についてシェアしたいと思います。
前提:note の記事一覧はマガジンごとに RSS で配信されている
そもそも前提として、note は各アカウントごとの記事一覧を RSS で配信しており、外部から記事一覧を取得することができます。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-07-22.52.39-1024x624.jpg)
RSS の単位としては、アカウントのすべての記事、もしくはマガジンごとです。以下、私のアカウントの例です。
すべての記事の RSS
https://note.com/shimotsu_/rss
個別のマガジンの RSS
https://note.com/shimotsu_/m/m5774f6fe2c0b/rss
実際にフィードの中身を見てみると、各記事の title
や description
をはじめ、thumbnail
、pubDate
、 link
など、一覧表示に必要な要素はもちろん一通り揃っているので、これを取得すれば問題なさそうです。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-09-11.00.02-1024x258.jpg)
問題:note の RSS は CORS 対応されていない
note で提供されている RSS を使えば、アカウントとマガジンに紐づく記事一覧を取得できることは分かりました。
ただし、ここでひとつ問題なのが、note の RSS が CORS 対応されていないことです。つまり、別ドメインのサイトから RSS を取得するために HTTP 通信を試みると、許可されずエラーとなってしまいます。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-07-23.42.44.jpg)
RSS の配信サーバが CORS 対応されていない場合、クロスオリジンの制約を受けないサーバサイドで取得を実行すればよいのですが、今回のプロジェクトのように Nuxt.js 製の静的サイトだったりするとそう簡単にはいきません。
解決策:Cloud Functions + Express でクロスオリジンの制約を受けない仲介サーバを作る
そこで今回は、ひとつの解決策として、サーバレスのアプリケーションを手軽に作れる Google の Cloud Functions と Node.js のフレームワークである Express を利用し、note の RSS を単に仲介するだけのサーバを作り、そこから記事一覧を取得することでこの問題を対処しました。
要は、ブラウザから直接 RSS を読み込むのではなく、踏み台となるプログラムを作り、ブラウザはその踏み台から記事を取得できるようにするということです。今回はその踏み台のプログラムとして、Cloud Functions を利用します。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/01.png)
Cloud Functions 自体についての説明は割愛しますが、サービスの詳細については公式ドキュメントに記載されているのでご参照ください。
ドキュメントの「使用例」の中に「軽量 API」とありますが、今回はまさしくその例でしょう。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-09-11.18.09-1024x500.jpg)
参照:HTTP トリガー
Firebase のプロジェクトのセットアップ
では、前置きが長くなりましたが、実際に作ってみましょう。
(※一連の操作において、Node.js および npm のインストールがされている必要がありますので、各自ご準備ください)
まずは、Firebase のプロジェクトを作る必要があるのですが、事前に Firebase のコンソールから新規プロジェクトを作成しておいてください。今回は、デモ用に mediation-server
というプロジェクトを作成しました。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット_2021-05-09_11_32_28-1024x392.png)
最初に、Firebase 関連のコマンドを有効にするための firebase-tools
のバージョンを確認します。
$ firebase --version
もしインストールされていなければ、firebase-tools
をグローバルにインストールします。
$ npm install -g firebase-tools
次に、プロジェクトの初期化前に作業用のディレクトリを作り、初期化を実行します。今回は cloudfunctions
というディレクトリを作りました。
$ cd ~/Documents
$ mkdir cloudfunctions
$ cd cloudfunctions
$ firebase init
firebase init
を実行すると、使用する Firebase の機能リストが表示されるので、今回は Functions
にチェックを入れ、Enterを押します。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-9.25.27-1024x627.jpg)
次にオプションの選択が表示されるので、Use an existing project
にチェックを入れて Enter を押します。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-9.25.37-1024x627.jpg)
ここで、さきほど Firebase のコンソールで作成したプロジェクトを選択します。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-9.25.51-1024x627.jpg)
次に言語について聞かれるので、Javascript
を選択して Enter
を押します(もちろん TypeScript
でも OK)。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-9.25.58-1024x627.jpg)
以降は ESLint の使用や、依存モジュールのインストールについて聞かれるので、いずれも Yes
で回答し、Enter
を押してください。
その後、 Firebase initialization complete!
と表示されたらセットアップは完了です。
Express を利用して API を実装する
作業ディレクトリ内に Firebase の各種ファイルが展開されているので、 functions
ディレクトリ内の index.js
を開きます。
const functions = require("firebase-functions");
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// functions.logger.info("Hello logs!", {structuredData: true});
// response.send("Hello from Firebase!");
// });
このファイルをアップデートしていくのですが、まず必要なモジュールをインストールします。
$ yarn add express request-promise-native cors
express
は Node.js のフレームワーク、request-promise-native
は Node.js で Promise を使うために、cors
は仲介サーバ自体の CORS 対応のために、それぞれ入れておきます。
また、.eslint.rc
の parserOptions
も少し書き換えておきます。
module.exports = {
root: true,
env: {
es6: true,
node: true,
},
extends: [
"eslint:recommended",
"google",
],
parserOptions: {
sourceType: "module",
parser: "babel-eslint",
ecmaVersion: 2017,
},
rules: {
quotes: ["error", "double"],
},
};
それでは用意が整ったら、index.js を以下のように修正します。
const functions = require("firebase-functions");
const requestPromise = require("request-promise-native");
const cors = require("cors");
const express = require("express");
const app = express();
// すべての外部からのリクエストを受け付ける
// 必要に応じてアクセス許可するオリジンなどの設定をしてください
// Docs: https://www.npmjs.com/package/cors
app.use(cors());
const BASE_KEY = "https://note.com/shimotsu_";
const KEY_ALL = BASE_KEY + "/rss";
const KEY_MAGAZINE = BASE_KEY + "/m/m5774f6fe2c0b/rss";
const getRSS = async (key) => {
const result = await requestPromise(key);
return result;
};
// エンドポイント設定
app.get("/magazine", async (req, res) => {
res.set("Cache-Control", "public, max-age=300, s-maxage=600");
getRSS(KEY_MAGAZINE).then((response) => res.send(response));
});
app.get("/all", async (req, res) => {
res.set("Cache-Control", "public, max-age=300, s-maxage=600");
getRSS(KEY_ALL).then((response) => res.send(response));
});
const api = functions.https.onRequest(app);
module.exports = { api };
それぞれ見ていきます。
const functions = require("firebase-functions");
const requestPromise = require("request-promise-native");
const cors = require("cors");
const express = require("express");
const app = express();
// すべての外部からのリクエストを受け付ける
// 必要に応じてアクセス許可するオリジンなどの設定をしてください
// Docs: https://www.npmjs.com/package/cors
app.use(cors());
ここではまず必要なモジュールを読み込んでいます。その後、読み込んだ express
モジュールをインスタンス化し、app
に代入します。
また今回、クライアントサイドで HTTP 通信する際に CORS のエラーが発生しないよう、cors
というミドルウェアを入れています。
const BASE_KEY = "https://note.com/shimotsu_";
const KEY_ALL = BASE_KEY + "/rss";
const KEY_MAGAZINE = BASE_KEY + "/m/m5774f6fe2c0b/rss";
ここでは RSS フィードの URL を定数化しています。
const getRSS = async (key) => {
const result = await requestPromise(key);
return result;
};
ここで定義している getRSS
関数は、取得したいフィードの URL を引数に取り、GET した結果を Promise オブジェクトとして返します。
// エンドポイント設定
app.get("/magazine", async (req, res) => {
res.set("Cache-Control", "public, max-age=300, s-maxage=600");
getRSS(KEY_MAGAZINE).then((response) => res.send(response));
});
app.get("/all", async (req, res) => {
res.set("Cache-Control", "public, max-age=300, s-maxage=600");
getRSS(KEY_ALL).then((response) => res.send(response));
});
ここではエンドポイントを設定しています。 /magazine
ではマガジンの記事一覧を、 /all
ではアカウントに紐づくすべての記事一覧を取得します。また、キャッシュの設定もおこなっています。
const api = functions.https.onRequest(app);
module.exports = { api };
最後にモジュールをエクスポートしています。onRequest
の引数に app
を使用すると、この Express アプリ全体を HTTP 関数に渡すことができます。
これで Cloud Functions 自体の実装は完了です。
Cloud Functions アプリケーションをデプロイ
それでは、早速デプロイしてみましょう。デプロイには以下のコマンドを実行します。
$ firebase deploy
【注意】このとき、Firebase のプロジェクトのプランが無料の Spark プランだとデプロイに失敗するので、従量課金の Blaze プランにアップデートしておいてください。
デプロイが完了すると以下のような表示になります。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-10.47.34-1024x586.jpg)
【注意】デプロイにコケる場合は、①料金プランが “Blaze” になっていない、②リンターのリントでエラーが出ている、③Node.js のバージョンが不適切、などいくつか可能性が考えられるので、アラートを読んで対処してください。
実際に Firebase のコンソール画面で Cloud Functions のページを見ると、このように新しい関数が登録され、HTTP リクエストのためのトリガー URL が設定されていることが分かります。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-09-11.41.48-1024x287.jpg)
試しに、 curl コマンドで HTTP 関数に定義されている /all
エンドポイントを叩いてみましょう。
$ curl https://us-central1-mediation-server.cloudfunctions.net/api/all
…すると、記事一覧が XML 形式で取得できています。
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-10.53.18-1024x401.jpg)
クライアント(Nuxt.js)から取得してみる
最後に、今回の目的であるクライアントからの取得を試してみましょう。トップページ(index.vue
)を以下のように編集します。
<template>
<div class="wrapper">
<h1>All</h1>
<ul>
<li v-for="(article, index) in articlesAll" :key="index">
<a :href="article.link">{{ article.title[0] }}</a>
</li>
</ul>
<h1>Magazine</h1>
<ul>
<li v-for="(article, index) in articlesMagazine" :key="index">
<a :href="article.link">{{ article.title[0] }}</a>
</li>
</ul>
</div>
</template>
<script>
import xml2js from 'xml2js'
export const processors = xml2js.processors
export const xmlParser = xml2js.Parser({
tagNameProcessors: [processors.stripPrefix],
})
export const BASE_KEY =
'https://us-central1-mediation-server.cloudfunctions.net/api'
export const KEY_ALL = BASE_KEY + '/all'
export const KEY_MAGAZINE = BASE_KEY + '/magazine'
export default {
data() {
return {
articlesMagazine: [],
articlesAll: [],
}
},
async mounted() {
;[this.articlesMagazine, this.articlesAll] = await Promise.all([
await this.fetchArticles(KEY_ALL),
await this.fetchArticles(KEY_MAGAZINE),
])
},
methods: {
async fetchArticles(KEY) {
let result = []
const articles = await this.$axios.get(KEY).then((response) => {
xmlParser.parseString(response.data, (messages, xmlres) => {
result = xmlres.rss.channel[0].item
})
return result
})
return articles
},
},
}
</script>
<style>
.wrapper {
padding: 20px;
}
h1 {
margin-bottom: 10px;
}
ul {
margin-bottom: 25px;
padding-left: 20px;
}
</style>
今回、データ処理のしやすさのために XML から javascript オブジェクトに変換するモジュール xml2js
を使用しています。
また、note は記事サムネイルやクリエイター名などについて <media:thumbnail>
や <note:creatorName>
といった独自タグを設定しているので、それらを取得するために、22行目で xml2js.Parser()
メソッドを利用しています。
参照:Processing attribute, tag names and values
index.vue
を上記のように編集した結果、以下のようにクライアントサイドで note の記事一覧が取得できました!
![](https://www.to-r.net/admin/media/wp-content/uploads/2021/05/スクリーンショット-2021-05-08-11.14.10-1024x651.jpg)
こうして、Cloud Functions と Express を活用し、クロスオリジンの制約を受けずにクライアントサイドで note の記事一覧が取得できるようになりました。note の記事を自在に取得できるようになると小規模サイト制作の幅も広がるので、必要な方はぜひ試してみてください。
なお、今回デモ用に作成したコードは以下のリポジトリにアップしていますので、ご参考ください。
https://github.com/shimotsu4431/note-rss/
Appendix:Cloud Functions のコールドスタート対応
Cloud Functions には、しばらくトリガーが使われていないとスリープし、その後最初のリクエストで処理に時間がかかる性質があるようです。
参照:コールド スタート
そこで今回はコールドスタートの対策として、定義したエンドポイントを定期的(5分おき)に叩くだけの関数を作成しました。Cloud Functions では、こうした定期実行関数も容易に作れます。
参照:関数のスケジュール設定
const CLOUD_BASE_KEY = "https://us-central1-mediation-server.cloudfunctions.net/api"
const CLOUD_KEY_ALL = CLOUD_BASE_KEY + "/all"
const CLOUD_KEY_MAGAZINE = CLOUD_BASE_KEY + "/magazine"
// Cloud Functions に定義されたエンドポイントを5分おきに叩くだけの関数
const scheduledRun = functions.pubsub.schedule("every 5 minutes").onRun(async () => {
await requestPromise(CLOUD_KEY_ALL)
await requestPromise(CLOUD_KEY_MAGAZINE)
return null;
});
トリガーを実行する回数は増えてしまいますが、これで常にフレッシュな状態を保てるので、スリープ後にリクエストに時間がかかってしまう問題を解決できました。
【謝辞】本件の実装アイデアは、同じくプロジェクトを一緒に進めていた @gonshi_com に提供していただきました。感謝!
フロントエンドエンジニア積極採用中
株式会社トゥーアールでは現在フロントエンドエンジニア積極的に採用中です!
興味がある人は採用ページをチェック!!