Next.js 14 と microCMSで作る俺得RSSリーダー：開発編その3, レイアウト作成・RSS一覧取得編

## 過去の記事

https://qiita.com/unotame/items/87c148154f2f581c980d

https://qiita.com/unotame/items/50a7fedfc0f2c4589aea

https://qiita.com/unotame/items/72669e069741a57385c0

https://qiita.com/unotame/items/dc163f2f2466c207cee3

今回は基本的なレイアウトを作った後、microCMSに登録したRSSから一覧表示までを目標にやっていきます。
## レイアウトの作成

### ヘッダーの作成
```tsx:Header.tsx
import React from 'react'
import styles from './Header.module.scss'
import Link from 'next/link'
import Search from '@/public/components/search/Search'

const Header = () => {
  return (
    <header className={styles['c-header']}>
      <Link href={`/`}>
        <h1>UNORSS</h1>
      </Link>

      <nav className={styles['c-header__nav']}>
        <ul className={styles['c-header__nav__list']}>
          <li className={styles['c-header__nav__list__item']}>
            <Link href={`/bookmark`}>
              ブックマーク
            </Link>
          </li>
          <li className={styles['c-header__nav__list__item']}>
            <Search />
          </li>
        </ul>
      </nav>
    </header>
  )
}

export default Header
```

### フッターの作成
```tsx:Footer.tsx
import React from 'react'
import styles from './Footer.module.scss'

const Footer = () => {
  return (
    <footer className={styles['c-footer']}>
      Footer
    </footer>
  )
}

export default Footer
```

### サイドバーの作成
```tsx:Sidebar.tsx
import React from 'react'
import styles from './Sidebar.module.scss'

const Sidebar = () => {
  return (
    <aside className={styles['c-sidebar']}>
      <ul className={styles['c-sidebar__card']}>
        <p className={styles['c-sidebar__card--title']}>配信元別</p>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
      </ul>
      <ul className={styles['c-sidebar__card']}>
        <p className={styles['c-sidebar__card--title']}>カテゴリ別</p>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
      </ul>
      <ul className={styles['c-sidebar__card']}>
        <p className={styles['c-sidebar__card--title']}>月別</p>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
        <li className={styles['c-sidebar__card__item']}>
          <a href="/">リンク</a>
        </li>
      </ul>
    </aside>
  )
}

export default Sidebar
```

### レイアウトへの組み込み
```tsx:src/app/layout.tsx
import type { Metadata } from "next";
import "@/public/styles/destyle.css"
import "@/public/styles/global.scss"
import Header from "@/public/components/header/Header";
import Footer from "@/public/components/footer/Footer";
import Sidebar from "@/public/components/sidebar/Sidebar";

export const metadata: Metadata = {
  title: "RSS reader",
  description: "ワタシのワタシによるワタシのためのRSSリーダー",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja" id="html">
      <body>
        <Header />
        <div className="l-inner">
          <main>
            {children}
          </main>
          <Sidebar />
        </div>
        <Footer />
      </body>
    </html>
  );
}

```

結果。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2531585/e8ce0562-6842-12fd-7b93-9f67f044aeec.png)

（リンク等全てダミー状態。）

## RSS一覧取得

### rss-parceのインストール
```
$ npm i rss-parser
```

試しにzennのrssを取得・表示
```ts
import Parser from 'rss-parser'

const feed = await new Parser().parseURL("https://zenn.dev/niiyama_k/feed")

console.log(feed.title)

feed.items.map(item => {
console.log(item.title)
console.log(item.link)
})
```

```:出力結果
新山 慶介さんのフィード
MAMPによるWordPress開発環境の最大アップロード容量を変更する
https://zenn.dev/niiyama_k/articles/change-max-upload-size-mamp
【EC-CUBE】ECサイトの移転がうまくいかない
https://zenn.dev/niiyama_k/articles/6774e2befe7a67
【Xserver】phpMyAdminのID・パスワード
https://zenn.dev/niiyama_k/articles/c01c402e3f59c6
【React】useLocation で 今いるページ・前いたページのpathを取得する
https://zenn.dev/niiyama_k/articles/eab421dddf2956
Sass * FROCSSによるサイトコーディングのベストプラクティス
https://zenn.dev/niiyama_k/articles/7b041bc8ee577a
Swagger(OpenAPI)によるAPI設計
https://zenn.dev/niiyama_k/articles/3975164ab205df
gridレイアウトでカレンダーのデザイン（html / sass）
https://zenn.dev/niiyama_k/articles/e83f57ad0b880b
[almalinux9 + laravel] sodiumがなくて怒られる
https://zenn.dev/niiyama_k/articles/c3ec0913a4ba61
ChatGPTでシステム開発の設計書テンプレートを作成する
https://zenn.dev/niiyama_k/articles/7aebf968001607
````

### TOPページで一覧取得
```tsx:src/app/page.tsx
import { client, getList } from '@/libs/client'
import styles from './FrontPage.module.scss'
import Parser from 'rss-parser'

import ArticleCard from '@/public/components/card/ArticleCard'

const FrontPage = async () => {
  const { contents } = await getList();
  const parse = new Parser();

  return (
    <div className='p-front-page'>
      <ul className={styles['p-front-page__list']}>
        {contents.map(async (rss) => {
          const feed = await parse.parseURL(rss.url)
          return feed.items.map((item) => {
            return (
              <ArticleCard
                date={item.pubDate ?? item.date}
                link={item.link}
                title={item.title}
                site_title={rss.title}
              />
            )
          })
        })}
      </ul>

      //後でお気に入りカテゴリのループが入る
    </div>
  )
}

export default FrontPage
```

```tsx
{contents.map(async (rss) => {
  const feed = await parse.parseURL(rss.url)
  return feed.items.map((item) => {
    return (
    )
  })
})}
```
これの理解に結構時間がかかった。

**microCMSに登録してあるRSSのURLを取得 → rss-parserでパース → 並び替え**

という流れが必要だった。

（個人的にmap関数のネストはあまり良くなさそうですがどうなんでしょう...?教えて有識者様）

各記事のカードはコンポーネントにした。

コンポーネントに渡している情報は以下の通り
 - item.pubDate ?? item.date：日付情報
 - item.link：各記事へのリンク
 - item.title：各記事のタイトル
 - rss.title：microCMSで登録したRSSタイトル

### カードコンポーネント
```tsx:ArticleCard.tsx
import Link from 'next/link'
import React from 'react'
import dayjs from 'dayjs';
import styles from './ArticleCard.module.scss';

type Props = {
  date: string | undefined
  link: string | undefined
  title: string | undefined
  site_title: string
}

function getOrder(millis: number, asc = false) {
  return (asc ? 1 : -1) * Math.floor(millis / 1000); // order は昇順なので、降順に見せたい場合は -1 を掛ける
}

const ArticleCard = (props: Props) => {
  return (
    <li style={{ order: getOrder(dayjs(props.date).valueOf()) }} className={styles['c-article-card']}>
      <Link href={`${props.link}`}>
        <img src="" alt="" className={styles['c-article-card--img']} />
        <time className={styles['c-article-card--time']}>{dayjs(props.date).format('YYYY-MM-DD')}</time>
        <h2 className={styles['c-article-card--title']}>{props.title}</h2>
        <span className={styles['c-article-card--site-title']}>{props.site_title}</span>
      </Link>
    </li>
  )
}

export default ArticleCard
```

最初の ```function getOrder```は、取得したカードを昇順で並べるために必要な関数。
コピペしてきたので正直よくわかってない。けど動いてるからとりあえずOK。
（難しいことは後で考える派。）

日付順の並び替えやフォーマットに日付や時間を扱うパッケージが必要だったのでインストールした。

https://github.com/iamkun/dayjs

```
$ npm i dayjs
```

使用用途は次のとおり。
```tsx
//渡された日付情報をYYYY-MM-DD形式でフォーマットしている
{dayjs(props.date).format('YYYY-MM-DD')}
```

```tsx
//渡された日付情報をミリ秒unixtimeに変換（昇順ソートに必要）
(dayjs(props.date).valueOf())
```

ここまで作ってスタイル適用するとこうなる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2531585/f4fcad33-7790-3313-40ac-80aa73bb82a6.png)
ちゃんとリンクも繋がってるし、いい感じ。

時間かかったのでちょっと調整して一旦プッシュ。

https://github.com/mkrms/rss-reader/commits/main/

## 次回予告
次回はリンク別ページ・カテゴリページの作成を行っていく予定。
一気に進めすぎて疲れたのでちょっぴり更新頻度落ちます。

## 案件のご依頼・ご相談について
ワタシが運営するUNOTAMEでは、制作案件やPM業務の外部委託案件などを承っております。
少しでも興味のある方はぜひカジュアルにお話しさせていただけますと嬉しいです。

[Twitter(X)](htpps://x.com/niiyamakeisuke)のDMや下記ポートフォリオサイトからのお問い合わせ、お待ちしております。

https://unotame.com