作ったもの
タイトルはSimple Newsです。
サイトURLは、 **こちら**です。
ソースコードは **こちら**です。
Google News を参考にしました。
どんな機能があるの?
-
NewsAPIからニュースサイトの記事のデータを取得し、タイトルと画像を表示
- NewsAPIは、世界中のニュースサイトから検索をかけて、情報を取得できるAPI ( Application Programming Interface )
-
OpenWeatherMapから取得した、現在の天気情報と5日間の天気予報を表示
- OpenWeatherMapは、現在の気象やある期間の気候の予測データを取得できるAPI
-
ページ遷移が高速
記事をクリックすると、記事が掲載されているサイトへ飛びます。
こんな感じでとてもシンプルで高速に動くニュースサイトです。
こんな人に読んでほしい
- Next.jsを使ってみたい初心者
- 外部APIを叩きたい人
- React,TypeScriptの基本は理解できている人
何が学べる?
- Next.jsの基本的な扱い方 (getStaticPropsを使った外部データの取得と表示の仕方やLinkコンポーネントを使ったページ遷移の仕方)
- Vercelを使ったデプロイの仕方
- APIの叩き方 (NewsAPI, OpenWeatherMap)
#プロジェクトを作成する前に
必要なAPIKeyを取得します。
※ NewsAPIとOpenWeatherMapのAPIKeyの取得の仕方が分かる方は、読み飛ばして構いません。
NewsAPIのAPIKeyを取得する
NewsAPIにログインします。
これでNewsAPIのAPI Keyを取得することができたので、メモを取っておいて下さい。
##OpenWeatherMapのAPIKeyを取得する
OpenWeatherMapにログインします。
アカウントのメールアドレスとパスワードを設定します。
ユーザーネームをクリックし、My API Keysを選択します。
Keyの下に、API Keyが表示されているはずなので、メモを取っておいて下さい。
環境構築
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
ファイルをルートディレクトリに作成します。
{
"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"
]
}
/// <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 を開く)
多分こんな感じになります。
必要なファイルとフォルダを作成
ルートディレクトリにsrc
フォルダを作成し、下のフォルダを入れます。
pages,layouts,styles,components
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フォルダはルートディレクトリ直下のままで大丈夫です。
中身はナビのアイコンや天気のアイコンです。必要でしたらこちらからダウンロードして下さい。
iconはflaticonというフリーアイコンサイトからダウンロードしました。
レイアウト
全体のレイアウトを整えるためにレイアウトコンポーネントを作成します。
layoutsフォルダにindex.tsx
とindex.module.scss
を作成します。
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;
// Hederコンポーネントの分上にmarginをとります。
.main {
margin-top: 60px;
}
これでメインのレイアウトが決まりました。
#Headerコンポーネント
次にヘッダーを作るためにHeaderコンポーネントを作成します。
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;
.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 を立ち上げます。
$ npm run dev
そうするとこのような画面になると思います。
まだヘッダーだけなので寂しいですよね(笑)
ここからどんどんコンポーネントを書いていきます。
Articleコンポーネント
NewsAPIで取得した記事たちを表示するコンポーネント
Next.jsのデータフェッチについて
Next.jsではgetStaticProps
を使うことで、ビルド時に外部からのデータなどを事前にフェッチすることができます。これがSSG (Static Site Generations)
です。正直自分にとって理解するのに難しいと思いました。
revalidate
とは、設定した時間(ここでは60×10秒)が経ち、リクエストががあって初めて再生成される。これがいわゆるISR (Incremental Static Regeneration)
です。これでSSGのページを更新することができます。
参考文献:Next.js公式チュートリアルより
NewsAPIで記事を取得
まずはgetStaticPropsで記事を取得し、propsに返り値としていきます。
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個のオブジェクトが取得できていると思います。
※できなかった場合は、もう一度コンソールで npm run dev
を実行してみたら、取得できるかもしれません
コンポーネントを作成する前に
typeを作成します。
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コンポーネントの作成
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;
.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コンポーネントを加えます。
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,
};
};
これで
取得した記事を一覧として表示できたかと思います。
Navコンポーネント
トピックごとのページに飛ぶためのナビバーを作成していきます。
<Link>
について
<Link>
はNext.jsでルーティング用のコンポーネントです。クライアントサイドでルーティングを可能にします。
Navコンポーネントの作成
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;
.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コンポーネントの表示
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,
};
};
.contents {
display: flex;
margin: 0 auto;
}
.main {
flex: 6;
}
.nav {
flex: 2;
position: fixed;
}
.blank {
flex: 2;
}
結果、こんな感じになると思います。
まだBusinessやTechnologyを押しても、ページは表示されません。
動的ルート (トピックス)
Next.jsでは[id].jsで、動的ルートを作成します。( [ ]鍵カッコの中身は任意です。)
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;
これでページ遷移が可能になりました。
Weatherコンポーネント
OpenWeatherMapで取得した天気情報を表示するコンポーネントを作成していきます。
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;
.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コンポーネントの作成
ピックアップの記事を表示するコンポーネントを作成していきます。
キーワードで検索することで好きな記事をピックアップできます。
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;
.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コンポーネントの表示
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,
};
};
これで完成です。
Vercelでデプロイ
Next.jsを作ったVercelでデプロイします。
今回はGithubアカウントでログインします。
これでデプロイができました。
まとめ
今回初めてQiitaで記事を書いて、誤字脱字や理解の浅い部分もあると思います。
初めてNext.jsを使って何かを作りたい人たちなどの役に立てれば幸いです。
最後までこの記事を読んで頂いて本当にありがとうございます。🙇♀️🙇♀️🙇♀️🙇♀️
ソースコードは、こちら
使用したものや参考にしたサイト
駆け出しの学生エンジニアです。
Twitterのフォローよろしくお願いします。