🐣

Nextjs,Panda CSSで APP Router使ったニュースアプリを作成

2023/08/10に公開

はじめに

Nextjs v13.4 から新しく App Router 機能が実装されました。
ニュースアプリを作り APP Router を学んでいきましょう!

間違っている箇所がありましたらご指摘ください。

デモサイトです
デモサイト

準備

まずは環境構築です。

// npmの場合
npx create-next-app --ts

// yarnの場合
yarn create next-app --typescript

いくつか対話式の質問がありますが、任意の回答をしてください。
Would you like to use App Router? (recommended)
… No / Yes
と App Router 使いますか?と質問されるので
その質問は Yes と回答してください。

Would you like to use Tailwind CSS? › No / Yes
Tailwind CSS を使いますかと質問されるので。
そこは No と言ってください。

続いて Panda CSS をインストールします。

// npmの場合
npm install -D @pandacss/dev

// yarnの場合
yarn add --dev @pandacss/dev

続いて設定ファイルを作成します。

npx panda init --postcss

package.json を更新します。

    "scripts": {
+   "prepare": "panda codegen",
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },

"prepare": "panda codegen"を追加することにより
新しくパッケージを追加した際に
自動的にstyled-stystemフォルダに
各ファイル設定が作成されます。

次にsrc/appフォルダにあるglobal.cssを開いて
全て消してから次のコードを入力します。

global.css

@layer reset, base, tokens, recipes, utilities;

これで pandaCSS を使えるようになりました。

NewsAPI の取得

まずは NewsAPI の取得を行います。

公式サイト
https://newsapi.org/

Get API Key から名前,Email,パスワードを入力し、
You are...の部分は個人開発の場合は上の
I am an individual にチェックを入れましょう。

アカウントを作成し、APIKEY をゲットしたらまず
プロジェクトのディレクトリ直下に.env.localファイルを作成し、
そちらに入力します。

.env.local
API_KEY=あなたのAPIをここに入れる

記事を出力

app ファルダ直下にある page.tsx に
データを取得するコードを書いていきます。

page Router では getStaticProps を用いて
データを取得してきましたが、
App Router ではasync/awaitを用いて
データを取得します。

page.tsx
export default function Home() {
  const API = process.env.API_KEY;
  const res = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
  );
  const json = await res.json();
  const articles = json?.articles;

  console.log(articles);
  return <p>News</p>;
}

従来の Page Router の方法ですと

page.tsx
export default async function Home(props) {
  console.log(props.articles);
 return <p>News</p>;
}

export const getStaticProps = async () => {
  const API = process.env.API_KEY;
  const res = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
  );
  const json = await res.json();
  const articles = json?.articles;

  return {
    props: {
      articles,
    }
  }
}

となるのでコードの記述量がだいぶ減りました。
軽く解説すると
res関数に非同期として fetch を用いてデータを取得します。
json関数では非同期で取得したデータを json 形式に変換しています。
最後にarticles関数で json データにある記事データを
articles だけで出力できるようにしています。

動作の確認の為ローカル環境を立ち上げます。

// npmの場合
npm run dev

// yarnの場合
yarn dev

ターミナルに記事が表示されれば成功です
記事

続いて API で取得した記事を表示します。
app ディレクトリ内に component フォルダを作成し、
article ファイルを作成します。

app/components/articles.tsx
import { FC } from "react";

type Props = {
  articles: [
    article: {
      author: string;
      title: string;
      publishedAt: string;
      url: string;
      urlToImage: string;
    }
  ];
};

export const Articles: FC<Props> = ({ articles }) => {
  return (
    <section>
      <div>
        <h1>News App</h1>
      </div>
      {articles.map((article) => {
        const time = new Date(article.publishedAt).toLocaleString();
        return (
          <a href={article.url} key={article.title}>
            <article>
              <div>
                <h2>{article.title}</h2>
                <p>{time}</p>
              </div>
              {article.urlToImage && (
                // eslint-disable-next-line @next/next/no-img-element
                <img
                  key={article.title}
                  src={article.urlToImage}
                  alt={`${article.title} image`}
                />
              )}
            </article>
          </a>
        );
      })}
    </section>
  );
};

解説していきます。

type Props = {
  articles: [
    article: {
      author: string;
      title: string;
      publishedAt: string;
      url: string;
      urlToImage: string;
    }
  ];
};

まずはPropsとして必要な型を定義していきます。
型を定義するのになにが必要かは先程
ターミナルに出力された部分を参考に必要な
部分の型を定義していきます。

型

とても字が汚いです。ごめんなさい。

export const Articles: FC<Props> = ({ articles }) => {
  return (
    <section>
      <div>
        <h1>News App</h1>
      </div>
      {articles.map((article) => {
        const time = new Date(article.publishedAt).toLocaleString();
        return (
          ...
        )
      })}
      </section>
      )
}

Articles という関数コンポーネントを定義しており
React の FC をコンポーネントの型とし
プロパティとして先程のPropsの型を入れます。
分割代入として props からarticlesを抽出します。
section の中に map 関数をに用いて
引数はarticleとして記事を1つ1つ表示します。

const time = new Date(article.publishedAt).toLocaleString();

こちらのコードは記事の中にある投稿時間を日本時間に置き換えています。

{
  article.urlToImage && (
    // eslint-disable-next-line @next/next/no-img-element
    <img
      key={article.title}
      src={article.urlToImage}
      alt={`${article.title} image`}
    />
  );
}

このコードは記事の画像がある場合のみ
表示されるようにしています。
通常 Nextjs ではimgタグを使用すると
Imageタグを推奨してるよ!と警告が出ます

今回なぜimgタグなのかというと
Nextjs の Image タグは外部の URL から画像を読み込む場合
そのドメインをnext.config.jsに記入しなくてはなりません。
今回の News API は世界中で 100 サイト以上の外部サイトから
ニュースを取得してきている為、
日本の必要なサイトだけでもいくつドメインを
記入しなくてはならないのかわからない為
今回はimgタグを使用しています。

作成した Article コンポーネントを page.tsx に反映させます。

app/page.tsx
+ import { Articles } from "./components/articles";

export default function Home() {
  const API = process.env.API_KEY;
  const res = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
  );
  const json = await res.json();
  const articles = json?.articles;

-  console.log(articles);
-  return <p>News</p>;
+  return <Articles articles={articles} />;
}

これでローカル環境を立ち上げると
記事一覧が表示されます。

記事一覧

スタイルを設定していないので不格好です、
これからスタイルを書いていきます。

スタイルを書く

Panda CSS を用いてスタイルを書いていきます

Panda CSS とは
公式サイト

上記公式サイトでも書いてある通り
ゼロランタイム CSS-in-JS ライブラリとなっております。

従来のようにランタイム CSS in JS を利用することが
推奨されなくなってきている中、
ゼロインタイムを実現した Panda CSS が
リリースされました。

articles.tsx
 <section>
      <div
+       className={css({
+         display: "flex",
+         justifyContent: "space-between",
+         margin: "5",
+       })}
      >
        <h1
+         className={css({
+           fontSize: "3xl",
+           fontWeight: "bold",
+         })}
        >
          News App
        </h1>
      </div>
      {articles.map((article) => {
        const time = new Date(article.publishedAt).toLocaleString();
        return (
          <a href={article.url} key={article.title}>
            <article
+             className={css({
+               display: "flex",
+               flexDirection: "row-reverse",
+               justifyContent: "start",
+               padding: "12px",
+               borderTop: "1px solid #f0f0f0",
+             })}
            >
              <div
+               className={css({
+                 paddingLeft: "20px",
+               })}
              >
                <h2>{article.title}</h2>
                <p>{time}</p>
              </div>
              {article.urlToImage && (
                // eslint-disable-next-line @next/next/no-img-element
                <img
+                 className={css({
+                   width: "80px",
+                   height: "80px",
+                 })}
                  key={article.title}
                  src={article.urlToImage}
                  alt={`${article.title} image`}
                />
              )}
            </article>
          </a>
        );
      })}
    </section>

簡易的ではありますが、スタイルを当てました。

Header作成

続いてヘッダーを作成して、ニュースのカテゴリ毎に
切り替えられるようにします。

componentsフォルダーの中にheader.tsxファイルを作成します。
中身を書いていきます。

components/header.tsx

import Link from "next/link";
import { FC } from "react";

const TOPICS = [
  {
    path: "/",
    title: "Top stories",
  },
  {
    path: "/topics/business",
    title: "Business",
  },
  {
    path: "/topics/technology",
    title: "Techonology",
  },
  {
    path: "/topics/entertainment",
    title: "Entertainment",
  },
  {
    path: "/topics/sports",
    title: "Sports",
  },
  {
    path: "/topics/science",
    title: "Science",
  },
  {
    path: "/topics/general",
    title: "general",
  },
];

export const Header: FC = () => {
  return (
    <section>
      <ul>
        {TOPICS.map((topic, index) => (
          <li key={index}>
            <Link href={`${topic.path}`}>
              <span>{topic.title}</span>
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
};

TOPICSという関数にそれぞれカテゴリーとページリンクを
記述し、map関数を用いて展開しています。

こちらもスタイルを整えていきます。

components/header.tsx
import Link from "next/link";
import { FC } from "react";
+ import { css } from "../../../styled-system/css";

...


export const Header: FC = () => {
  return (
-    <section>
+    <section className={css({
+      height:"56px",
+      backgroundColor: "#93C5FD",
+    })}>
-       <ul>
+       <ul className={css({
+        display: "flex",
+        justifyContent: "center",
+        alignItems: "center",
+        gap: "32px",
+        height: "100%",
+      })}>
        {TOPICS.map((topic, index) => (
          <li key={index}>
            <Link href={`${topic.path}`}>
              <span>{topic.title}</span>
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
};

appディレクトリ直下のlayout.tsxにHeaderを追加します。

app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
+        <Header/>
+        <main>
        {children}
+        </main>
        </body>
    </html>
  )
}

動的ルーティング

この状態だとまだそれぞれの
カテゴリーに飛ぶことができません。
動的ルーティングを用いてページを飛ばします。

動的ページを作る際はPage Routerと
同じく[]の中にページ名を入れることで
動的ルーティングになります。

appディレクトリ直下にtopicsフォルダを作成し、
さらにその中に[id]フォルダを作成します。
page.tsxファイルを作成しコードを書いていきます。

app/topics/[id]/page.tsx

import { Article } from "@/app/_components/article";

type Props = {
  id: string;
}

async function getPost(params: Props) {
  const API = process.env.NEWS_API;
  const topicRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&category=${params.id}&country=jp&apiKey=${API}`
  );
  const topicJson = await topicRes.json();
  const topicArticles = await topicJson.articles;

  return topicArticles;
}

async function Topic({ params }: {params:Props}) {
  const title = await getPost(params);
  return (
    <div>
      <Article  articles={title} />
    </div>
  );
}

export default Topic;

解説していきます。

本来動的ルーティングを行う時は
getStaticPathgetStaticProps
必要でしたが、
App Routerからは不必要になります。

async function getPost(params: Props) {
  const API = process.env.NEWS_API;
  const topicRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&category=${params.id}&country=jp&apiKey=${API}`
  );
  const topicJson = await topicRes.json();
  const topicArticles = await topicJson.articles;

  return topicArticles;
}

getPost関数では引数にparamsを指定し、
newsAPIから記事を取得しています。
params.idという情報を使って特定のカテゴリーの
ニュースを取得しています。

async function Topic({ params }: {params:Props}) {
  const title = await getPost(params);
  return (
    <div>
      <Article articles={title} />
    </div>
  );
}

最後にTopic関数ですが、
先程のgetPost関数をtitleという関数に入れます。

Article関数にタイトルと記事一覧を表示させます。
先程のtitleを指定することにより、
カテゴリー毎の記事一覧を取得することができています。

最後に

おつかれさまでした!
これにてニュースアプリの完成です!

APP Routerを用いたアプリ開発の
お役に立てれば幸いです!

最後までお読みいただきありがとうございました。

Discussion