📝
Next.jsによる自己紹介サイトの制作
Next.jsを始めたきっかけ
今まで、PHPフレームワークのLaravelやPythonフレームワークのDjangoを用いてWebサイトのサンプルを実装していました。
最近、億千よろずさん(YouTube、Twitter)がYouTubeの配信で制作なさっていたNext.jsフレームワーク製の「相互フォローさんを見るWebサービス」に刺激を受け、私もNext.jsを触って簡単な自己紹介サイトを作ってみることにしました。
制作したサイト
制作したサイトのスクリーンショット
ホーム画面
(良い画像が見つかったら、カードに埋め込みます...)
自己紹介画面
ブログ画面
ゲーム制作画面、TODOまとめ画面は工事中です
使ったフレームワーク・技術
- Next.js (v12.3.1)
- Vercel (デプロイ先として)
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@mui/material": "^5.10.6",
"next": "12.3.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"rss-parser": "^3.12.0"
},
"devDependencies": {
"eslint": "8.23.1",
"eslint-config-next": "12.3.1"
}
↑ package.jsonより抜粋
実装のポイント
note、zennの記事情報の取得と表示
表示ページを作成しやすくするために、返すJSONの形式を合わせておく
noteでの記事情報の取得
こちらのnoteのAPI一覧記事を参考に実装
lib/getMyNotePost.js
const userName = "aielement";
export async function GetMyNotePost() {
const noteResponse = await fetch(`https://note.com/api/v2/creators/${userName}/contents?kind=note&page=1`)
const noteJson = await noteResponse.json()
return noteJson['data']['contents'].map((content) => {
return {
title: content['name'],
url: content['noteUrl'],
publishAt: content['publishAt'],
site: "note"
}
});
}
一旦、1ページまでを取得(今後全ページ取得に対応予定)
zennでの記事情報の取得
こちらの記事を参考に実装
lib/getMyZennFeed.js
import Parser from "rss-parser";
const userName = "aielement";
export async function GetMyZennFeed() {
const parser = new Parser();
const feed = await parser.parseURL(`https://zenn.dev/${userName}/feed`);
return feed.items.map( ({ title, link, pubDate }) => {
return {
title: {title}.title,
url: {link}.link,
publishAt: {pubDate}.pubDate,
site: "zenn"
}
})
}
ブログ画面での表示
pages/blog/index.js
import Layout from "../../components/Layout";
import generalStyle from "../../styles/GeneralContent.module.css"
import ActionAreaCard from "../../components/ActionAreaCard";
import { GetMyNotePost } from "../../lib/getMyNotePost"
import { GetMyZennFeed } from "../../lib/getMyZennFeed";
// SSG(Static Site Generation)のためのデータを渡す
export async function getStaticProps() {
const allNotePostData = await GetMyNotePost()
const allZennFeedData = await GetMyZennFeed()
// noteとzennの記事情報のJSONを結合する
const concatPostData = allNotePostData.concat(allZennFeedData)
// 公開日時の新しい順に並べる
const allPostData = concatPostData.sort(
function (a, b) {
const aDate = new Date(a.publishAt).getTime()
const bDate = new Date(b.publishAt).getTime()
return bDate - aDate
}
);
return {
props: {
allPostData
},
};
}
export default function Blog( {allPostData} ){
return (
<Layout>
<main className={generalStyle.main}>
{
allPostData.map( ({ title, url, publishAt, site }) => {
const date = new Date(publishAt);
const publishedYear = date.getFullYear();
// getMonthは0〜11の整数値が返ってくるので、+1する
const publishedMonth = date.getMonth() + 1;
const publishedDate = date.getDate();
const content = `${publishedYear}年${publishedMonth}月${publishedDate}日`;
// サイトによって、カードに表示する画像を変える
if (site === "note") {
return (
<ActionAreaCard key={title} route={url} title={title} content={content} image="/notelogo.svg"/>
)
}
else if (site === "zenn") {
return (
<ActionAreaCard key={title} route={url} title={title} content={content} image="/zennlogo.svg"/>
)
}
})
}
</main>
</Layout>
)
}
Layoutコンポーネントの実装
components/Layout.js
import Head from "next/head";
import {siteTitle} from "../pages";
import styles from "../styles/Home.module.css";
import Link from "next/link";
export default function Layout({children}){
return (
<div className={styles.container}>
<Head>
<title>{siteTitle}</title>
<meta name="description" content="近況や使用技術を中心に書いていきます" />
<link rel="icon" href="/profile.jpeg" />
</Head>
<div className="main_element">{children}</div>
<footer className={styles.footer}>
<Link href="/"><a>えれめんの小部屋</a></Link>
</footer>
</div>
);
}
ActionAreaCardコンポーネントの実装
こちらのサイトを参考に実装
components/ActionAreaCard.js
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { CardActionArea } from '@mui/material';
import Link from "next/link";
export default function ActionAreaCard({route, image, title, content}) {
return (
<Link href={route}>
<Card sx={{ maxWidth: 345 }}>
<CardActionArea>
<CardMedia
component="img"
width="345"
height="140"
image={image}
alt=""
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{title}
</Typography>
<Typography variant="body2" color="text.secondary">
{content}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Link>
);
}
つまづいたポイント
- table表示
table表示で<thead>、<tbody>を使用しないと以下のエラーが出る
- SSGの際にコンポーネントのキーを指定していない場合にエラーが出る
pages/blog/index.js
<ActionAreaCard key={title} route={url} title={title} content={content} image="/notelogo.svg"/>
の部分を
pages/blog/index.js
<ActionAreaCard route={url} title={title} content={content} image="/notelogo.svg"/>
としていると、ESLintでエラーになる
今後の課題
- TypeScriptで実装してみる
- SSR(Server Side Rendering)も活用してみる
- 定期的にビルドするようにして、SSGを利用している部分が定期的に最新の内容に更新されるようにする
参考
記事情報取得関連
コンポーネント作成
つまづきポイント解決の参考にしたサイト
Next.jsの習得に使った講座
サクッと数時間でSSGの実装まで理解し、実装までもっていけました。
最後に
Next.jsで実装していて楽しかったので、YouTubeの検索結果ビューワーの試作についてもNext.jsで本格的に実装してみてもおもしろいと思いました。
時間を見つけて手をつけていきたいと思います。また、配信で技術周りを実装している方を見ると、その技術に関する興味が出てきて、今回も自分の実装意欲にもつながリました。
今回に関しては、かなり億千よろずさんの影響が大きかったので、改めてここで感謝申し上げます。
ありがとうございます。今後も応援していきます。
この記事をご覧になった方も是非、億千よろずさんの配信を見に行ってみてください。
億千よろずさんのYouTube
YouTubeのvideoIDが不正です
億千よろずさんのTwitter
Discussion