もちっとメモ

もちっとメモ

もぐりのエンジニアが日々の中で試してみたことを気が向いたときに書き連ねていきます

やっぱりはてなブログを自分のホームページに埋め込みたい

※2021/1/18に複数のサイトからRSSを取得したい場合のやり方を末尾に追記しました。

経緯

長年お世話になっていたYQLが残念ながら2019年1月に無期限のサービス停止を発表されました。Yahooさん、今までありがとうございました。

したがって、再び新しい方法を考える必要が出てきました。
私情ではありますが、最近ようやくReactを勉強し始めましたので、今回はReactを使ったRSSフィードの取得・表示方法を考えてみました。
着手当初はどうしようと戦々恐々だったのですが、比較的簡単に行けそうだったのでご紹介してみます。
やり方さえ分かれば、30分もかからずにできてしまうと思います。(私は調べる過程を含めると半日くらいかかりました。)
※Reactのチュートリアルすらまともに理解しているか怪しい初心者がトライ&エラーで考えた方法なので、ベテランの方からすれば滅茶苦茶な実装になっているかもしれません。ご容赦ください。

とりあえずRSS情報を取得して表示する

何はともあれ、まずは完成形をお見せします。無事、ブログのタイトルとリンクを取得して飛べるようになりました。
見てくれ部分は私のUIセンスの問題なので、これなんかよりいくらでも格好よくできると思うので、ぜひ各々アレンジしてみてください。
(というか、格好いいデザインができたら教えてください)

f:id:T-N-Clark:20190530004403g:plain
完成図

環境は

$ create-react-app -V
2.1.1

を使っています。 で、肝心の実装ですが、あれこれ調べてみたところ、手っ取り早く実現できそうなものにRSSParserというライブラリがありました。
ということで今回は、このRSSParserを使います。 www.npmjs.com

ライブラリのインストールは

npm i rss-parser

とかで出来ます。

ひとまず、RSS情報を取ってきてコンソールに表示させてみます。
取り急ぎ動作確認したいならcreate-react-appでプロジェクト?を作ってApp.jsに書き込めば動作確認は出来ます。

import React from 'react';
//中略
const RssParser = require('rss-parser'); //importではなくrequireで呼びます
const url = 'http://t-n-clark.hatenadiary.jp/feed'
const rssParser = new RssParser();
rssParser.parseURL(url)
      .then((feed) => {
        console.log(feed);
      });
      .catch((error) => {
        console.error('Get Failed', error);
      });
class App extends Component {
  render(props) {
    const classes = this.props.classes;
//...後略

Chromeデベロッパーツールで確認すると、ちゃんとコンソールに取得したRSS情報が出ています。

f:id:T-N-Clark:20190530004713j:plainf:id:T-N-Clark:20190530004716j:plainf:id:T-N-Clark:20190530004723j:plain

ざっと見たところ、記事のタイトルとかの情報はこのitemsに入っているみたいです。

ちなみに、はてなブログRSS情報は

各ブログのトップURL/rss

もしくは

各ブログのトップ/feed

で取得出来ます。 例えば、このブログのRSS
https://t-n-clark.hatenadiary.jp/feed
で取れます。 ※HTTPでも取れそうな気がするのですが、HTTPだとスマホではちゃんと取れなかった表示されなかったので、HTTPSをおすすめします。 (端末ではなく、ブラウザの問題かもしれませんが... これが関係するのでしょうか?) bookmark.hatenastaff.com

Qiitaであれば、

https://qiita.com/アカウント名/feed.atom

で取得出来ます。 例えば、こんな感じ
https://qiita.com/Sashimimochi/feed.atom
ただ、HTTPSの場合はクロスドメイン制約があるので、URLの前にCORS(cross-origin resource sharing)PROXYをつけてあげる必要があります。
上記のRSSParserのページを読んだところ、こんな感じで行けそうです。

const CORS_PROXY = "https://cors-anywhere.herokuapp.com/"
//はてなブログならこっち
const url = CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed'
//Qiitaならこっち
const url = CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom'

あとは取得した情報をstateに格納してJSX内で呼び出してみます。 先ほどデバッグ用に出していたConsoleへの出力はもういらないので消しても大丈夫です。

import React from 'react';
import { Component } from 'react';
import PropTypes from 'prop-types';
import './App.css';

/*RSSParserのインスタンス生成*/
const RssParser = require('rss-parser');
const url = "http://t-n-clark.hatenadiary.jp/feed"
const rssParser = new RssParser();

class App extends Component {
  /*コンストラクターの定義*/
  constructor(props) {
    super(props);
    this.state = {
      contents: [],
    };
    this.componentDidMount = this.componentDidMount.bind(this)
  }
  /*stateにrssParserの結果をバインド*/
  componentDidMount() {
    rssParser.parseURL(url)
      .then((feed) => {
        const data = feed.items;
        console.log(data);
        this.setState({
          contents: [...data]
        });
      })
      .catch((error) => {
        console.error('Get Failed', error);
      })
  }
  render(props) {
    const classes = this.props.classes;
    /*表示するコンテンツの形に合わせてstateの中身を書きだす(mapで拡張forループみたいに使う)*/
    const contents = this.state.contents.map(content => {
      return <div>
        {content.title}
        /*{content.pubDate}  公開日(Qiitaなら{content.published})*/
       /*{content.link}  ページリンク*/
      </div>
    });
    return (
      <div className="App">
        <header className="App-header">
          {contents} /*ここに出力*/
        </header>
      </div>
    );
  }
}

App.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default App;

これで無事RSS情報を取得して表示できるようになりました。

f:id:T-N-Clark:20190530004740j:plain

あとは、これを自分なりに装飾していくことになります。

見てくれを整える

参考になるかはわかりませんが、冒頭にあった私なりの装飾を紹介してみます。 主にMaterial-Ul(のCard)とreact-moment(※momentではありません)を使っています。 material-ui.com www.npmjs.com

×こっちではありません。 www.npmjs.com

それぞれ

# Material-UI (@4.0.1)
$ npm install @materail-ui/core
# react-moment (@0.9.2)
$ npm i react-moment

でインストールできます。 まずは、ほぼほぼMaterial-Ulの公式ドキュメントにあるCardのサンプルに先程のRSSデータを流し込みます。

import React from 'react';
import { Component } from 'react';
import PropTypes from 'prop-types';
import Card from '@material-ui/core/Card';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import red from '@material-ui/core/colors/red';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import './App.css';
import { CardHeader, CardContent } from '@material-ui/core';

const RssParser = require('rss-parser');
const url = "http://t-n-clark.hatenadiary.jp/feed"
const rssParser = new RssParser();

const styles = theme => ({
  blog: {
    margin: 20,
    padding: 20,
    marginTop: 10,
  },
  blogcontent: {
    overflowY: 'scroll',
    height: '200px',
  },
  avatar: {
    backgroundColor: red[500],
  },
});

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      contents: [],
    };
    this.componentDidMount = this.componentDidMount.bind(this)
  }
  componentDidMount() {
    rssParser.parseURL(url)
      .then((feed) => {
        const data = feed.items;
        console.log(data);
        this.setState({
          contents: [...data]
        });
      })
      .catch((error) => {
        console.error('Get Failed', error);
      })
  }
  render(props) {
    const classes = this.props.classes;
    const contents = this.state.contents.map(content => {
      return <Card>
        <CardHeader
          avatar={<Avatar arial-label="Blog" className={classes.avatar}>も</Avatar>}
          subheader={content.pubDate}
          title={content.title}
        />
        <CardContent key={content.id}>
          <Button size="small" color="primary" href={content.link}>Read More</Button>
        </CardContent>
      </Card>
    });
    return (
      <div className="App">
        <header className="App-header">
          <Grid container>
            <Grid item xs={12} sm={7}>
              <div className={classes.blog}>
                <h3>はてなブログ更新情報</h3>
                <div className={classes.blogcontent}>{contents}</div>
              </div>
            </Grid>
          </Grid>
        </header>
      </div>
    );
  }
}

App.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(App);

今回はタイトル(title)と日付(pubDate)とリンク(link)のみを使っていますが、
スペースとの相談次第では、サマリー(contentSnippet)とか入れてもいいかもしれません。
(RSSParserってサムネイル画像も取れているのでしょうか?もし取れているならサムネイル画像を使うのもありだと思います。)

はてブのfeed側の仕様なのか、RSSParser側の仕様なのか確かめてませんが、デフォルトだと最新30件?分のRSS情報が取れているみたいです。
なので、それをそのまま表示すると中々悲惨なことになります。
Material-UIにもbootstrapのようなグリッドシステム的なものがありますので、Gridで幅調整をすることが出来ます。
Gridを使えば多少のレスポンシブデザインに対応出来ます。
あとはoverflowY: 'scroll'で表示枠のサイズを指定したりしてみました。

f:id:T-N-Clark:20190530004747j:plainf:id:T-N-Clark:20190530004757j:plain
左:Gridなし,右:Gridあり

これで個人的には、おおよその見た目は及第点くらいにはなったのですが、時刻表示が世界標準のままで見にくいです。
そこで、react-momentというライブラリを使ってこれを整形します。
RSSParserで取ってきた日付をフォーマットを指定してMomentタグで挟んであげればいいみたいです。
例えば、YYYY/MM/DDにしたければ、

import Moment from 'react-moment'
<Moment format="YYYY/MM/DD">{content.pubDate}</Moment>

とやれば、

# 変換前
2019-03-01T02:30:00.000Z
# 変換後
2019/03/01

に出来ました。

f:id:T-N-Clark:20190530004802j:plain
こんな感じ

他にもいろいろあるみたいなので、公式のサンプルを参考にお好みのフォーマットにしてみてください。

ちなみにですが、Twitterの埋め込みなら、専用のライブラリ(私はreact-twitter-widgetsを使っています)を使えばもっとお手軽にできてしまいます。 www.npmjs.com

f:id:T-N-Clark:20190530004811j:plain

あとがき

以上で、React限定&初心者によるメチャメチャ実装ではありますが、ひとまず再びブログの更新情報を埋め込めるようになりました。
この方法はあと何年使えるんだろう…?

先日、Googleから発表されたiframeに代わるPortalsタグを使えば、現在稼働中のクラシカルなページでもSPA並みにサクサク遷移できるようになるみたいです。
まあ、まだearly stageみたいなので私は試していませんが… docs.google.com blog.uskay.io

私事ですが、今回RSSフィードの実現方法を変える必要があったので、この機にいっそのことホームページの方も一新しようかなと思っています。
今見返すと一昔前のような見た目ですし。無事、一新したら紹介するかもしれません。 暫定版→http://clarkphys.html.xdomain.jp/index.htmlclarkphys.html.xdomain.jp

それではまた。

2021/1/18追記

効率が良いかは自信がありませんが、複数のサイトからRSS情報を取得する場合は以下のようにurlcomponentDidMount()を書き換えればできます。 ポイントは次の2つです。

  1. setStateで情報を更新するときに、既存のstateと新規取得情報を結合しておく
  2. サイト内では順番通りになっているが結合したときに日付がバラバラなので、sortしておく
const RssParser = require('rss-parser');
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/"
// 取得したいURLをListで定義しておく
const urls = [
    CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed',
    CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom'
]
const rssParser = new RssParser();

class Profile extends Component {
    constructor(props) {
        super(props);
        this.state = {
            contents: [],
        };
        this.componentDidMount = this.componentDidMount.bind(this)
    }
    componentDidMount() {
        urls.map((url) => {
            rssParser.parseURL(url)
                .then((feed) => {
                    // 新たに取得したfeedを取り出す
                    const data = feed.items;
                    // 既存のstateを取り出しておく
                    var feeds = this.state.contents
                    // 既存のstateに新規で取得したfeedを1つずつ追加する
                    data.map((d) => {
                        feeds.push(d)
                    })
                    // 日付でsortする
                    feeds.sort(function (a, b) {
                        if (a.pubDate < b.pubDate) {
                            return 1;
                        }
                        if (a.pubDate > b.pubDate) {
                            return -1;
                        }
                        return 0
                    });
                    // stateをupdateする
                    this.setState({
                        contents: feeds
                    });
                })
                .catch((error) => {
                    console.error('Get Failed', error);
                })
        })
    }

次回

t-n-clark.hatenadiary.jp