Next.js+TypeScriptでシンプルなニュースサイトを作ってみた。

[NewsAPI]:https://newsapi.org/
[openWeatherMap]:https://openweathermap.org/
[Google News]:https://news.google.com/topstories?gl=US&hl=en-US&ceid=US:en
[Simple News]:https://github.com/kinpatwo-github/simple-news
[public]:https://github.com/kinpatwo-github/google-news-clone/tree/main/public
[localhost]:https://localhost:3000
[flaticon]:https://www.flaticon.com/
[Next.js]:https://nextjs.org/learn/basics/data-fetching/implement-getstaticprops
[Twitter]:https://twitter.com/harashojan
[Vercel]:https://vercel.com/
[Site]:https://simple-news-oiyuyixjo.vercel.app/


# 作ったもの

![スクリーンショット 2021-02-10 0.27.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/88c128f8-be12-5698-210d-6ae96bd6c4dc.png)


タイトルはSimple Newsです。
サイトURLは、 **[こちら][Site]**です。
ソースコードは **[こちら][Simple News]**です。

*[Google News][Google News]* を参考にしました。

## どんな機能があるの？

- NewsAPIからニュースサイトの記事のデータを取得し、タイトルと画像を表示
    - [NewsAPI][NewsAPI]は、世界中のニュースサイトから検索をかけて、情報を取得できるAPI ( Application Programming Interface )
- OpenWeatherMapから取得した、現在の天気情報と5日間の天気予報を表示
    - [OpenWeatherMap][openWeatherMap]は、現在の気象やある期間の気候の予測データを取得できるAPI

- ページ遷移が高速

![ezgif-2-ad5e5eae6103.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/1599cec0-c5e6-fe41-30d9-f5668581d18c.gif)
記事をクリックすると、記事が掲載されているサイトへ飛びます。
こんな感じでとてもシンプルで高速に動くニュースサイトです。

# こんな人に読んでほしい
- Next.jsを使ってみたい初心者
- 外部APIを叩きたい人
- React,TypeScriptの基本は理解できている人

# 何が学べる？

- Next.jsの基本的な扱い方 (getStaticPropsを使った外部データの取得と表示の仕方やLinkコンポーネントを使ったページ遷移の仕方)
- Vercelを使ったデプロイの仕方 
- APIの叩き方 ([NewsAPI][NewsAPI], [OpenWeatherMap][openWeatherMap])


#プロジェクトを作成する前に
必要なAPIKeyを取得します。
※ NewsAPIとOpenWeatherMapのAPIKeyの取得の仕方が分かる方は、読み飛ばして構いません。
## NewsAPIのAPIKeyを取得する
[NewsAPI][NewsAPI]にログインします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/f6d5c385-274b-0309-53ad-fe2a0900094f.png" />

アカウントのメールアドレスとパスワードを設定します。
![スクリーンショット 2021-02-09 23.06.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/43a9130a-a652-a742-5794-8ede8e66526c.png)

これでNewsAPIのAPI Keyを取得することができたので、メモを取っておいて下さい。
![スクリーンショット 2021-02-09 23.06.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/30a1601a-074b-afd0-89f6-87d53051d75a.png)


##OpenWeatherMapのAPIKeyを取得する
[OpenWeatherMap][openWeatherMap]にログインします。
![スクリーンショット 2021-02-09 22.46.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/7e046538-a7b6-bc81-2a40-b706a65c2066.png)
アカウントのメールアドレスとパスワードを設定します。
![スクリーンショット 2021-02-09 22.47.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/7ba59624-d300-8258-3cb2-b2093085be47.png)
ユーザーネームをクリックし、My API Keysを選択します。
![スクリーンショット 2021-02-09 22.55.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/7e8c6b2a-6c28-af1e-63e6-b59141729296.png)
Keyの下に、API Keyが表示されているはずなので、メモを取っておいて下さい。
![スクリーンショット 2021-02-09 22.57.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/68e6ec33-c687-7a9d-83f7-b115da0a9796.png)

# 環境構築

## create-next-appの実行、必要なパッケージのインストール

まず初めに、`create-next-app`でプロジェクトを作成します。

```
$ npx create-next-app <プロジェクト名>
```

その後に、必要なパッケージをインストールしていく。

`npm`なら

```
$ npm install sass moment
```
```
$ npm install --dev @types/node @types/react typescript
```

`yarn`なら

```
$ yarn add sass moment
```
```
$ yarn add -D @types/node @types/react typescript
```

## TypeScriptとSass(scss)を導入

`tsconfig.json`と`next-env.d.ts`ファイルをルートディレクトリに作成します。


```tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

```next-env.d.ts
/// <reference types="next" />
/// <reference types="next/types/global" />

```

これでTypeScriptとSass(scss)を導入することができました。
pageディレクトリのapiファイルは今回削除します。

`_app.js => _app.tsx` 
`index.js => index.tsx`
`globals.css => globals.scss`
`Home.module.css => Home.module.scss`

importの部分も変更して下さい。

それぞれの拡張子を`.tsx,.scss`にしたら、

```
$ npm run dev
```

を実行して動作確認をします。( [localhost:3000][localhost] を開く)

![スクリーンショット 2021-02-10 9.52.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/a605cbe1-bb94-5fcf-2bec-d54258195af3.png)

多分こんな感じになります。

## 必要なファイルとフォルダを作成
ルートディレクトリに`src`フォルダを作成し、下のフォルダを入れます。
`pages,layouts,styles,components`

## globals.scssの設定
```src/styles/globals.scss
* {
  background-color: rgb(31, 31, 31);
  color: white;
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  box-sizing: border-box;
}

a {
  color: inherit;
  text-decoration: none;
}
```

## publicフォルダ

publicフォルダはルートディレクトリ直下のままで大丈夫です。

中身はナビのアイコンや天気のアイコンです。必要でしたら[こちら][public]からダウンロードして下さい。
iconは[flaticon][flaticon]というフリーアイコンサイトからダウンロードしました。

# レイアウト

全体のレイアウトを整えるためにレイアウトコンポーネントを作成します。
layoutsフォルダに`index.tsx`と`index.module.scss`を作成します。

```src/layouts/index.tsx
import Header from "../components/header"
import styles from "./index.module.scss";

type LayoutProps = {
  children: React.ReactNode;
};

function MainLayout({ children }: LayoutProps): JSX.Element {
  return (
    <>
      <Header />
      <main className={styles.main}>{children}</main>
    </>
  );
}

export default MainLayout;
```

```src/layouts/index.module.scss
// Hederコンポーネントの分上にmarginをとります。
.main {
  margin-top: 60px;
}
```

これでメインのレイアウトが決まりました。

#Headerコンポーネント

次にヘッダーを作るためにHeaderコンポーネントを作成します。

```src/components/header/index.tsx
import styles from "./index.module.scss";
import Image from "next/image";
import Link from "next/link";

function Header() {
  return (
    <section className={styles.container}>
      <header className={styles.header}>
        <div className={styles.header__icon} >
          <Image
            src="/img/headerIcon/menu.png"
            alt="menu icon"
            loading="eager"
            width={35}
            height={35}
            priority
          />
        </div>
        <h1 style={{ letterSpacing: "1px", textAlign: "left" }}>
          <Link href="/">
            <a>
              <span style={{ fontWeight: 250 }}>Simple</span>
              <span style={{ fontWeight: 100 }}>News</span>
            </a>
          </Link>
        </h1>
      </header>
    </section>
  );
}

export default Header;
```

```src/components/header/index.module.scss
.container {
  padding: 10px;
  height: 60px;
  position: fixed;
  top: 0;
  width: 100%;
  color: white;
  z-index: 1;
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}

.header {
  height: 53px;
  vertical-align: middle;
  white-space: nowrap;
  align-items: center;
  display: flex;
}

.header__icon {
  display: inline-block;
  vertical-align: middle;
  height: 50px;
  width: 50px;
  flex: 0 0 auto;
  border-radius: 50%;
  padding: 14px;
  &:hover {
    background: rgba(77, 77, 77, 0.6);
  }
}

```

ここまで来たらもう一度[localhost:3000][localhost] を立ち上げます。

```
$ npm run dev
```

そうするとこのような画面になると思います。
![スクリーンショット 2021-02-10 10.27.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/d3c69355-cbe2-0252-1e0d-832d0202476b.png)
まだヘッダーだけなので寂しいですよね（笑)
ここからどんどんコンポーネントを書いていきます。

# Articleコンポーネント
NewsAPIで取得した記事たちを表示するコンポーネント

## Next.jsのデータフェッチについて
Next.jsでは`getStaticProps`を使うことで、ビルド時に外部からのデータなどを事前にフェッチすることができます。これが`SSG (Static Site Generations)`です。正直自分にとって理解するのに難しいと思いました。
`revalidate`とは、設定した時間(ここでは60×10秒)が経ち、リクエストががあって初めて再生成される。これがいわゆる`ISR (Incremental Static Regeneration)`です。これでSSGのページを更新することができます。
>![スクリーンショット 2021-02-10 18.53.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/011536af-3413-e552-d868-9880399710a9.png)

> 参考文献：[Next.js公式チュートリアル][Next.js]より

## NewsAPIで記事を取得
まずはgetStaticPropsで記事を取得し、propsに返り値としていきます。

```src/pages/index.tsx
import Head from 'next/head'
import MainLayout from '../layouts'
import styles from '../styles/Home.module.scss'

export default function Home(props) {
  // 記事を取得できているか確認
  console.log(props.topArticles)
  return (
    <MainLayout>
      <Head>
        <title>Simple News</title>
      </Head>
    </MainLayout>
  )
}


export const getStaticProps = async () => {
  // NewsAPIのトップ記事の情報を取得
  const pageSize = 10   // 取得したい記事の数
  const topRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=${pageSize}&apiKey=あなたのNewsAPIのAPIKey`
  );
  const topJson = await topRes.json();
  const topArticles = topJson?.articles;

  return {
    props: {
      topArticles,
    },
    revalidate: 60 * 10,
  };
};

```

ChomeDevToolsを開くと、配列で10個のオブジェクトが取得できていると思います。

![スクリーンショット 2021-02-10 10.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/1476f21e-9e7d-ee96-b4aa-bf033ea9cd54.png)

※できなかった場合は、もう一度コンソールで `npm run dev` を実行してみたら、取得できるかもしれません

## コンポーネントを作成する前に
typeを作成します。

```src/components/types.ts
type Props = {
  articles?: [
    article: {
      author: string;
      title: string;
      publishedAt: string;
      url: string;
      urlToImage: string;
    }
  ];
  title?: string;
  weatherNews?: {
    current: {
      temp: number;
      clouds: number;
      weather: [
        conditions: {
          main: string;
          icon: string;
        }
      ];
    };
    daily: [
      date: {
        dt: number;
        clouds: number;
        temp: {
          min: number;
          max: number;
        };
        weather: [
          conditions: {
            id: number;
            icon: string;
          }
        ];
      }
    ];
  };
};

export default Props
```

## Articleコンポーネントの作成

```src/components/article/index.tsx
import styles from "./index.module.scss";
import moment from "moment";
import Props from "../types";

const Article: React.FC<Props> = ({ articles, title }) => {
  return (
    <section className={styles.article}>
      <div className={styles.article__heading}>
        <h1>{title.charAt(0).toUpperCase() + title.slice(1).toLowerCase()}</h1>
      </div>
      {articles.map((article, index) => {
        const time = moment(article.publishedAt || moment.now())
          .fromNow()
          .slice(0, 1);
        return (
          <a href={article.url} key={index} target="_blank" rel="noopener">
            <article className={styles.article__main}>
              <div className={styles.article__title}>
                <p>{article.title}</p>
                <p className={styles.article__time}>
                  {time}
                  時間前
                </p>
              </div>
              {article.urlToImage && (
                <img
                  key={index}
                  src={article.urlToImage}
                  className={styles.article__img}
                  alt={`${article.title} image`}
                />
              )}
            </article>
          </a>
        );
      })}
    </section>
  );
};

export default Article;
```

```src/components/article/index.module.scss
.article {
  margin: 10px auto;
}

.article__heading {
  display: flex;
  justify-content: space-between;
  margin: 20px;
}

.article__main {
  display: flex;
  border: 1.2px solid rgba(135, 135, 135, 0.5);
  margin: 0 20px 20px 20px;
  padding: 15px;
  border-radius: 10px;
}
.article__title {
  flex: 4;
  margin-right: 5px;
}

.article__time {
  opacity: 0.5;
}

.article__img {
  object-fit: contain;
  width: 100%;
  border-radius: 5px;
  max-height: 100px;
  margin-right: 10px;
  flex: 1;
}
```

## Articleコンポーネントの表示

`src/pages/index.tsx`にArticleコンポーネントを加えます。

```src/pages/index.tsx
import Head from 'next/head'
import MainLayout from '../layouts'
import styles from '../styles/Home.module.scss'
import Article from '../components/article'

export default function Home(props) {
  console.log(props.topArticles)
  return (
    <MainLayout>
      <Head>
        <title>Simple News</title>
      </Head>

      // Articleコンポーネントを追加
      <div className={styles.main}>
        <Article title="headlines" articles={props.topArticles} />
      </div>
    </MainLayout>
  )
}


export const getStaticProps = async () => {
  // NewsAPIのトップ記事の情報を取得
  const pageSize = 10;
  const topRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=${pageSize}&apiKey=あなたのNewsAPIのAPIKey`
  );
  const topJson = await topRes.json();
  const topArticles = topJson?.articles;

  return {
    props: {
      topArticles,
    },
    revalidate: 60 * 10,
  };
};

```

これで

![スクリーンショット 2021-02-10 11.00.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/84c85b06-5497-cf2c-2885-7bfe3e58997f.png)

取得した記事を一覧として表示できたかと思います。


# Navコンポーネント
トピックごとのページに飛ぶためのナビバーを作成していきます。

## `<Link>` について

`<Link>`はNext.jsでルーティング用のコンポーネントです。クライアントサイドでルーティングを可能にします。


## Navコンポーネントの作成

```src/components/nav/index.tsx
import Link from "next/link";
import styles from "./index.module.scss";
import Image from "next/image";

const TOPICS = [
  {
    icon: "01",
    path: "/",
    title: "Top stories",
  },
  {
    icon: "03",
    path: "/topics/business",
    title: "Business",
  },
  {
    icon: "04",
    path: "/topics/technology",
    title: "Technology",
  },
  {
    icon: "05",
    path: "/topics/entertainment",
    title: "Entertainment",
  },
  {
    icon: "06",
    path: "/topics/sports",
    title: "Sports",
  },
];

const Nav: React.FC = () => {
  return (
    <section className={styles.container}>
      <ul className={styles.contents}>
        {TOPICS.map((topic, index) => {
          return (
            <li key={index} >
              <Link href={`${topic.path}`}>
                <a>
                  <span>
                    <Image
                      src={`/img/navIcons/${topic.icon}.png`}
                      alt={`${topic.title} icon`}
                      loading="eager"
                      width={33}
                      height={33}
                      priority
                    />
                  </span>
                  <span>{topic.title}</span>
                </a>
              </Link>
            </li>
          );
        })}
      </ul>
    </section>
  );
};

export default Nav;

```

```src/components/nav/index.module.scss
.container {
  width: 100%;
  padding: 20px 0 0 20px;
}

.contents {
  list-style-type: none;
  >li {
    margin-bottom: 15px;
    opacity: 0.7;
    color: rgb(196, 196, 196);
    >a {
      display: flex;
      >span {
        margin-left: 15px;
        line-height: 36px;
      }
    }
    &:hover {
      opacity: 1.0;
    }
  }
}
```

Next.jsの`<Link>`を使うことで、ページの高速移動を実現させます。

## Navコンポーネントの表示

```src/pages/index.tsx
import Head from "next/head";
import MainLayout from "../layouts";
import styles from "../styles/Home.module.scss";
import Article from "../components/article";
import Nav from "../components/nav";

export default function Home(props) {
  return (
    <MainLayout>
      <Head>
        <title>Simple News</title>
      </Head>
      <div className={styles.contents}>
        <div className={styles.nav}>
          <nav>
            <Nav />
          </nav>
        </div>
        <div className={styles.blank} />
        <div className={styles.main} >
          <Article title="headline" articles={props.topArticles} />
        </div>
      </div>
    </MainLayout>
  );
}

export const getStaticProps = async () => {
  // NewsAPIのトップ記事の情報を取得
  const pageSize = 10;
  const topRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=${pageSize}&apiKey=あなたのNewsAPIのAPIKey`
  );
  const topJson = await topRes.json();
  const topArticles = topJson?.articles;

  return {
    props: {
      topArticles,
    },
    revalidate: 60 * 10,
  };
};
```

```src/styles/Home.module.scss
.contents {
  display: flex;
  margin: 0 auto;
}

.main {
  flex: 6;
}

.nav {
  flex: 2;
  position: fixed;
}

.blank {
  flex: 2;
}
```

結果、こんな感じになると思います。

![スクリーンショット 2021-02-10 11.36.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/0b86bdcd-250c-aab5-ae3b-d30189be9d6f.png)

まだBusinessやTechnologyを押しても、ページは表示されません。

# 動的ルート (トピックス)
Next.jsでは[id].jsで、動的ルートを作成します。( [ ]鍵カッコの中身は任意です。)

```src/pages/topics/[id].tsx
import Head from "next/head";
import { useRouter } from "next/router";
import Article from '../../components/article'
import Nav from '../../components/nav'
import MainLayout from "../../layouts/index";
import styles from "../../styles/Home.module.scss";

function Topic(props) {
  const router = useRouter();
  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <MainLayout>
      <Head>
        <title>Simple News - {props.title.toUpperCase()}</title>
      </Head>
      <div className={styles.contents}>
        <div className={styles.nav} >
          <nav>
            <Nav />
          </nav>
        </div>
        <div className={styles.blank} />
        <div className={styles.main} style={{marginRight:"10%"}}>
          <Article title={props.title} articles={props.topicArticles} />
        </div>
      </div>
    </MainLayout>
  );
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: true,
  };
}

export async function getStaticProps({ params }) {
  const topicRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&category=${params.id}&country=jp&apiKey=あなたのNewsAPIのAPIKey`
  );
  const topicJson = await topicRes.json();
  const topicArticles = await topicJson.articles;

  const title = params.id;

  return {
    props: { topicArticles, title },
    revalidate: 60 * 10,
  };
}

export default Topic;
```

これでページ遷移が可能になりました。

![スクリーンショット 2021-02-10 12.09.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/52a2b483-8227-e718-7766-e2f8a3d7faf7.png)

# Weatherコンポーネント

OpenWeatherMapで取得した天気情報を表示するコンポーネントを作成していきます。

```src/components/weather-news/index.tsx
import Image from "next/image";
import styles from "../weather-news/index.module.scss";
import Link from "next/link";
import Props from "../types";

const week = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

const WeatherNews: React.FC<Props> = ({ weatherNews }) => {
  const currentWeatherMain = weatherNews.current.weather[0].main
  const currentWeatherTemp = weatherNews.current.temp
  const currentWeatherIcon = weatherNews.current.weather[0].icon.slice(0, 2) + "d";
  return (
    <section className={styles.weather}>
      <h1>Tokyo</h1>
      <div className={styles.weather__main}>
        <div className={styles.weather__top}>
          <div className={styles.weather__heading}>
            <a>{currentWeatherMain}</a>
            <p>
              {currentWeatherTemp.toString().slice(0, 1)}
              <span>˚c</span>
            </p>
          </div>
          <Image
            className={styles.weather__icon}
            src={`/img/weatherIcons/${currentWeatherIcon}.png`}
            alt="Tokyo's weather icon"
            loading="eager"
            width={52}
            height={52}
            priority
          />
        </div>
        <div className={styles.weather__weekly}>
          <ul className={styles.weather__weekly__list}>
            {weatherNews.daily.map((date, index) => {
              const time = new Date(date.dt * 1000);
              let day = week[time.getDay()];
              const nowDay = week[(new Date()).getDay()]
              if (day ==  nowDay) {
                day = "Today"
              }
              if (index > 4) {
                return;
              }
              return (
                <li key={index}>
                  <p>{day}</p>
                  <span>
                    <Image
                      src={`/img/weatherIcons/${date.weather[0].icon}.png`}
                      className={styles.weatehr__icon}
                      alt={`${day}'s weather icon`}
                      loading="eager"
                      width={41}
                      height={41}
                      priority
                    />
                  </span>
                  <div className={styles.weather__temp}>
                    <p className={styles.weather__temp__high}>
                      {parseInt(date.temp.max.toLocaleString(), 10)}˚c
                    </p>
                    <p className={styles.weather__temp__low}>
                      {parseInt(date.temp.min.toLocaleString(), 10)}˚c
                    </p>
                  </div>
                </li>
              );
            })}
          </ul>
        </div>
        <div className={styles.weather__bottom}>
          <Link href="https://weathernews.jp/onebox/">
            <a target="_blank" rel="noopener">
              ウェザーニュース
            </a>
          </Link>
        </div>
      </div>
    </section>
  );
};

export default WeatherNews;

```

```src/components/weather-news/index.module.scss
.weather {
  border: 1.2px solid rgba(135, 135, 135, 0.5);
  border-radius: 15px;
  padding: 0 20px 0 20px;
  margin: 30px 20px 0 20px;
  h1 {
    font-weight: 1000;
    font-size: 16px;
    color: rgb(233, 163, 163);
    padding: 12px 0;
    border-bottom: 1px solid rgba(150, 113, 113, 0.5);
  }
}

.weather__top {
  display: flex;
  justify-content: space-between;
  padding-top: 20px;
  padding-bottom: 30px;
}

.weather__heading {
  a {
    font-weight: 20;
  }
  p {
    font-weight: 400;
    font-size: 30px;
    span {
      font-weight: 100;
    }
  }
}

.weather__icon {
  line-height: 79px;
  padding-right: 10px;
}

.weather__weekly {
  .weather__weekly__list {
    display: flex;
    list-style-type: none;
    display: flex;
    justify-content: space-around;
    li {
      p {
        text-align: center;
      }
      .weather__temp {
        margin-bottom: 10px;
        .weather__temp__high {
          font-weight: 50;
        }
        .weather__temp__low {
          font-weight: 100;
        }
      }
    }
  }
}

.weather__weekly__list__icon {
  font-size: 70px;
}

.weather__bottom {
  padding: 10px 0;
  border-top: 1px solid rgba(150, 113, 113, 0.5);
  a {
    color: rgba(233, 163, 163, 0.8);
    font-size: 12px;
    text-align: right;
  }
}

```


# PickupArticle

## PickupArticleコンポーネントの作成

ピックアップの記事を表示するコンポーネントを作成していきます。
キーワードで検索することで好きな記事をピックアップできます。

```src/components/pickup-article/index.tsx
import styles from "./index.module.scss";
import moment from "moment";
import Props from "../types";

const PickupArticle: React.FC<Props> = ({ articles }) => {
  return (
    <section className={styles.pickup}>
      <h1 className={styles.article__heading}>PickUp</h1>
      {articles.map((article, index) => {
        const time = moment(article.publishedAt || moment.now())
            .fromNow()
            .slice(0, 1) == "a"
            ? 1
            : moment(article.publishedAt || moment.now())
                .fromNow()
                .slice(0, 1);
        return (
          <a href={article.url} key={index} target="_blank" rel="noopener">
            <article className={styles.article__main}>
              <div className={styles.article__title}>
                <p>{article.title}</p>
                <p className={styles.article__time}>
                  {time}時間前
                </p>
              </div>
              {article.urlToImage && (
                <img
                  key={index}
                  src={article.urlToImage}
                  className={styles.article__img}
                />
              )}
            </article>
          </a>
        );
      })}
    </section>
  );
};

export default PickupArticle;
```

```src/components/pickup-article/index.module.scss
.pickup {
  border: 1.2px solid rgba(135, 135, 135, 0.5);
  border-radius: 15px;
  padding: 0 20px 0 20px;
  margin: 40px 20px 0 20px;
  h1 {
    font-weight: 1000;
    font-size: 16px;
    color: rgb(233, 163, 163);
    padding: 12px 0;
    border-bottom: 1px solid rgba(150, 113, 113, 0.5);
  }
}
.article__main {
  display: flex;
  margin-top: 20px;
  // padding: 15px;
  border-radius: 10px;
}
.article__title {
  flex: 4;
  margin-right: 5px;
  p {
    font-size: 13px;
  }
}

.article__time {
  opacity: 0.5;
}

.article__img {
  width: 80px;
  height: 80px;
  margin: auto;
  object-fit: cover;
}
```

## PickupArticleコンポーネントの表示

```src/pages/index.tsx
import Head from "next/head";
import MainLayout from "../layouts";
import styles from "../styles/Home.module.scss";
import Article from "../components/article";
import Nav from "../components/nav";
import WeatherNews from "../components/weather-news";
import PickupArticle from "../components/pickup-article";

export default function Home(props) {
  return (
    <MainLayout>
      <Head>
        <title>Simple News</title>
      </Head>
      <div className={styles.contents}>
        <div className={styles.nav}>
          <nav>
            <Nav />
          </nav>
        </div>
        <div className={styles.blank} />
        <div className={styles.main}>
          <Article title="headline" articles={props.topArticles} />
        </div>
        <div className={styles.aside}>
          <WeatherNews weatherNews={props.weatherNews} />
          <PickupArticle articles={props.pickupArticles} />
        </div>
      </div>
    </MainLayout>
  );
}

export const getStaticProps = async () => {
  // NewsAPIのトップ記事の情報を取得
  const pageSize = 10  //取得する記事の数
  const topRes = await fetch(
    `https://newsapi.org/v2/top-headlines?country=jp&pageSize=${pageSize}&apiKey=あなたのNewsAPIのAPIKey`
  );
  const topJson = await topRes.json();
  const topArticles = topJson?.articles;

  // OpenWeatherMapの天気の情報を取得
  const lat = 35.4122    // 取得したい地域の緯度と経度(今回は東京)
  const lon = 139.4130
  const exclude = "hourly,minutely"   // 取得しない情報(1時間ごとの天気情報と1分間ごとの天気情報)
  const weatherRes = await fetch(
    `https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&units=metric&exclude=${exclude}&appid=あなたのOpenWeatherMapのAPIKey`
  );
  const weatherJson = await weatherRes.json();
  const weatherNews = weatherJson;

  // NewsAPIのピックアップ記事の情報を取得
  const keyword = "software"   // キーワードで検索(ソフトウェア)
  const sortBy = "popularity"  // 表示順位(人気順)
  const pickupPageSize = 5     // ページサイズ(5)
  const pickupRes = await fetch(
    `https://newsapi.org/v2/everything?q=${keyword}&language=jp&sortBy=${sortBy}&pageSize=${pickupPageSize}&apiKey=あなたのNewsAPIのAPIKey`
  );
  const pickupJson = await pickupRes.json();
  const pickupArticles = pickupJson?.articles;

  return {
    props: {
      topArticles,
      weatherNews,
      pickupArticles
    },
    revalidate: 60,
  };
};
```

![スクリーンショット 2021-02-10 12.21.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/2262026f-59e1-ba30-d810-44536a351a9d.png)

これで完成です。

# Vercelでデプロイ

Next.jsを作った[Vercel][Vercel]でデプロイします。


今回はGithubアカウントで[ログイン][Vercel]します。
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/2d5950a7-2817-e084-6338-baccd7f13f84.png" />
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/36eeafe8-e9c5-0f06-0ced-a666de3d8937.png" />
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/a0024da3-375a-a8bb-2e83-5b0bffa30dac.png" />
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/06d6169e-17fe-ca13-99fc-a64567a09cab.png" />
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/ee62e9c8-7298-4320-e8e7-7c2c80d02d09.png" />
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/861901/2cd5e2e7-84bc-a04d-79fc-1fa3b37cf914.png" />

これでデプロイができました。


# まとめ

今回初めてQiitaで記事を書いて、誤字脱字や理解の浅い部分もあると思います。
初めてNext.jsを使って何かを作りたい人たちなどの役に立てれば幸いです。
最後までこの記事を読んで頂いて本当にありがとうございます。🙇‍♀️🙇‍♀️🙇‍♀️🙇‍♀️

ソースコードは、[こちら][Simple News]

`使用したものや参考にしたサイト`
- [NewsAPI][NewsAPI]
- [OpenWeatherMap][OpenWeatherMap]
- [flaticon][flaticon]
- [Google News][Google News]

___

駆け出しの学生エンジニアです。
[Twitter][Twitter]のフォローよろしくお願いします。