📝

Next.jsによる自己紹介サイトの制作

2022/09/25に公開

Next.jsを始めたきっかけ

今まで、PHPフレームワークのLaravelやPythonフレームワークのDjangoを用いてWebサイトのサンプルを実装していました。
最近、億千よろずさん(YouTubeTwitter)がYouTubeの配信で制作なさっていたNext.jsフレームワーク製の「相互フォローさんを見るWebサービス」に刺激を受け、私もNext.jsを触って簡単な自己紹介サイトを作ってみることにしました。

https://www.youtube.com/watch?v=gJ22XnC0rbs

制作したサイト

https://aielement-github-io.vercel.app/

制作したサイトのスクリーンショット


ホーム画面
(良い画像が見つかったら、カードに埋め込みます...)

自己紹介画面

ブログ画面

ゲーム制作画面、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を利用している部分が定期的に最新の内容に更新されるようにする

参考

記事情報取得関連

https://note.com/hagure_melon/n/n964ff6f7ad0e
https://b.0218.jp/202104131234.html

コンポーネント作成

https://mui.com/material-ui/react-card/

つまづきポイント解決の参考にしたサイト

https://www.utakata.work/entry/nextjs/table-thead-tbody

Next.jsの習得に使った講座

https://www.udemy.com/course/nextjs-microblog-for-beginner/learn/lecture/33057926
ちょうどセールをやっていたので、ありがたかったです。
サクッと数時間でSSGの実装まで理解し、実装までもっていけました。

最後に

Next.jsで実装していて楽しかったので、YouTubeの検索結果ビューワーの試作についてもNext.jsで本格的に実装してみてもおもしろいと思いました。
https://note.com/aielement/n/nca44e148152e
時間を見つけて手をつけていきたいと思います。

また、配信で技術周りを実装している方を見ると、その技術に関する興味が出てきて、今回も自分の実装意欲にもつながリました。
今回に関しては、かなり億千よろずさんの影響が大きかったので、改めてここで感謝申し上げます。
ありがとうございます。今後も応援していきます。

この記事をご覧になった方も是非、億千よろずさんの配信を見に行ってみてください。

億千よろずさんのYouTube

YouTubeのvideoIDが不正ですhttps://www.youtube.com/c/okuchiyorozu/

億千よろずさんのTwitter

https://twitter.com/okuchi_yorozu

Discussion