0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【意訳】Next.jsのチュートリアルをTypeScriptでする

Posted at

Next.jsのチュートリアルをTypeScriptでなぞってみます。
今回の記事ではデプロイは対象外としてます。
公式はこちらです。
https://nextjs.org/learn/basics/create-nextjs-app

Create a Next.js App

プロジェクトを作成します。
すべてデフォルトを選択しました。

npx create-next-app nextjs-blog
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*

プロジェクトを作成できたら以下のコマンドで開発サーバーを立てることができます。

npm run dev

Navigate Between Pages

Pages in Next.js

Next.jsにおけるpagesディレクトリの説明です。
pages/index.js/に、
pages/posts/first-post.js/posts/first-postに該当します。

Create a New Page

pagesディレクトリ直下にpostsディレクトリを作成して、その直下にfirst-posts.tsxファイルを作成します。

pages/posts/first-post.tsx
export default function FirstPost() {
    return (
        <h1>First Post</h1>
    )
}

posts/first-postにアクセスすると画像のような表示になります。

Link Component

ページ間の画面線に着目します。
クライアントサイドのナビゲーションを可能にします。

トップページからFirstPostへ遷移できるようにしましょう。

pages/index.tsx
import Link from "next/link"

export default function Home() {
    return (
        <h1 className="title">
            Read <Link href="/posts/first-post">this page!</Link>
        </h1>
    )
}

FirstPostからトップページへ戻れるようにします。

pages/posts/first-post.tsx
import Link from 'next/link';

export default function FirstPost() {
    return (
        <>
            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}

Assets, Metadata, and CSS

Adding Head to first-post.js

FirstPostページのタイトル属性を変更してみましょう。
Headコンポーネントを使うことで実現できます。

pages/posts/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';

export default function FirstPost() {
    return (
        <>
            <Head>
                <title>First Post</title>
            </Head>

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}

Third-Party JavaScript

プロジェクトのサードパーティ製のJavaScriptを追加します。
Next.jsにはScriptというコンポーネントをが用意されています。
first-post.tsxに使ってみましょう。

pages/posts/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';
import Script from 'next/script';

export default function FirstPost() {
    return (
        <>
            <Head>
                <title>First Post</title>
            </Head>
            <Script
                src='https://connect.facebook.net/en_US/sdk.js'
                strategy='lazyOnload'
                onLoad={() => {
                    console.log('loaded');
                }}
            />

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}

Scriptにはさまざまなオプションが用意されています。
strategylazyonloadを指定するとブラウザがアイドル状態の時にJavaScriptを読み込んでくれます。
onLoadに処理を記載することで、読み込んだ後に実行したい処理を指定できます。

サードパーティJavaScriptの導入はブログ作成とは関係ないので、確認したら削除してください。

Layout Component

複数の共通のパーツを使いまわしたい時があると思います。
layout.tsxというファイルを作成してみましょう。

components/layout.tsx
import { ReactNode } from 'react'

interface Props {
    children: ReactNode
}

export default function Layout({ children }: Props) {
    return (
        <div>
            {children}
        </div>
    )
}

作成したファイルをfirst-post.tsxで使います。

pages/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';
import Layout from '@/components/layout';

export default function FirstPost() {
    return (
        <Layout>
            <Head>
                <title>First Post</title>
            </Head>

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </Layout>
    );
}

webページのヘッダーやフッターは同じパーツを使うことで統一感が出てわかりやすいと思います。

Adding CSS

先ほど作成したlayout.tsxにスタイルを適用しましょう。
layout.module.cssを作成します。

components/layout.module.css
.container {
    max-width: 36rem;
    padding: 0 1rem;
    margin: 3rem auto 6rem;
}

cssをlayout.tsxに適用します。

components/layout.tsx
import { ReactNode } from 'react'
import styles from './layout.module.css'

interface Props {
    children: ReactNode
}

export default function Layout({ children }: Props) {
    return (
        <div className={styles.container}>
            {children}
        </div>
    )
}

Global Styles

global.cssを編集することでサイト全体にスタイルを適用できます。
リセットCSSなどを読み込むのが一般的かと思います。
global.cssを以下のように編集してみましょう。

styles/global.css
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  line-height: 1.6;
  font-size: 18px;
}

* {
  box-sizing: border-box;
}

a {
  color: #0070f3;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  max-width: 100%;
  display: block;
}

Polishing Layout

CSSモジュールを使ってレイアウトを更新していきます。

components/layout.module.css
.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

.header {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.backToHome {
  margin: 3rem 0 0;
}
styles/utils.module.css
.heading2Xl {
  font-size: 2.5rem;
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingXl {
  font-size: 2rem;
  line-height: 1.3;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingLg {
  font-size: 1.5rem;
  line-height: 1.4;
  margin: 1rem 0;
}

.headingMd {
  font-size: 1.2rem;
  line-height: 1.5;
}

.borderCircle {
  border-radius: 9999px;
}

.colorInherit {
  color: inherit;
}

.padding1px {
  padding-top: 1px;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.listItem {
  margin: 0 0 1.25rem;
}

.lightText {
  color: #666;
}
component/layout.tsximport Head from 'next/head';
import Image from 'next/image';
import styles from './layout.module.css';
import utilStyles from '../styles/utils.module.css';
import Link from 'next/link';
import { ReactNode } from 'react';

const name = 'Your Name';
export const siteTitle = 'Next.js Sample Website';

interface Props {
    children: ReactNode
    home: boolean
}

export default function Layout({ children, home }: Props) {
    return (
        <div className={styles.container}>
            <Head>
                <link rel="icon" href="/favicon.ico" />
                <meta
                    name="description"
                    content="Learn how to build a personal website using Next.js"
                />
                <meta
                    property="og:image"
                    content={`https://og-image.vercel.app/${encodeURI(
                        siteTitle,
                    )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
                />
                <meta name="og:title" content={siteTitle} />
                <meta name="twitter:card" content="summary_large_image" />
            </Head>
            <header className={styles.header}>
                {home ? (
                    <>
                        <Image
                            priority
                            src="/images/profile.jpg"
                            className={utilStyles.borderCircle}
                            height={144}
                            width={144}
                            alt=""
                        />
                        <h1 className={utilStyles.heading2Xl}>{name}</h1>
                    </>
                ) : (
                    <>
                        <Link href="/">
                            <Image
                                priority
                                src="/images/profile.jpg"
                                className={utilStyles.borderCircle}
                                height={108}
                                width={108}
                                alt=""
                            />
                        </Link>
                        <h2 className={utilStyles.headingLg}>
                            <Link href="/" className={utilStyles.colorInherit}>
                                {name}
                            </Link>
                        </h2>
                    </>
                )}
            </header>
            <main>{children}</main>
            {!home && (
                <div className={styles.backToHome}>
                    <Link href="/">← Back to home</Link>
                </div>
            )}
        </div>
    );
}
pages/index.tsx
import Head from 'next/head';
import Layout, { siteTitle } from '../components/layout';
import utilStyles from '../styles/utils.module.css';

export default function Home() {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>
        </Layout>
    );
}

見た目が整えられてきました。

Pre-rendering and Data Fetching

今まではファイル分割の仕方やスタイルの当て方などUIに関わること主にみてきました。
ここからはロジックの箇所を詳しく学習します。

Pre-rendering

Next.jsの特徴の一つがプリレンダリングです。
プリレンダリングはサーバー側でHTMLを作成することで、ファーストビューでUIが見えるというメリットがあります。

プリレンダリングにはSSGとSSRという二つの種類があります。
SSGはビルド時のみHTMLを作成します。
SSRはリクエストのたびにHTMLを作成します。

SSGの方が高速なため、主にSSGを使用して要件に合わせてSSRを使うのが良さそうです。

Creating the markdown files

今回作成するブログではプロジェクトにマークダウンファイルを作成して、そのファイルをブログの記事とします。
またSSGを使ってブログシステムを構築します。
まずマークダウンファイルを追加しましょう。

posts/pre-rendering.md
---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.

- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.

Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
posts/ssg-ssr.md
---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---

We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation

You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.

Creating the utility function to read the file system

マークダウンファイルを追加できたら、それらを表現するモデルを作成します。

types/post.ts
export class Post {
    id: string;
    date: string;
    title: string;
    content: string;

    constructor(id: string, date: string, title: string, content: string) {
        this.id = id;
        this.date = date;
        this.title = title;
        this.content = content;
    }
}

ファイルシステムから読み込んでPostの配列を返す関数を作成します。

lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { Post } from '@/types/post';

const postsDirectory = path.join(process.cwd(), 'posts');

export function getSortedPostsData(): Post[] {
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.replace(/\.md$/, '');

    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const matterResult = matter(fileContents);

    return new Post(
      id,
      matterResult.data.date,
      matterResult.data.title,
      matterResult.content,
    )
  });

  const sorted = allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  })

  return JSON.parse(JSON.stringify(sorted));
}

Using Static Generation (getStaticProps())

getStaticProps()を使います。
HTMLのビルドに必要な外部データを取得することができます。
index.tsxで使ってみましょう。

pages/index.tsx
import Head from 'next/head';
import Layout, { siteTitle } from '../components/layout';
import utilStyles from '../styles/utils.module.css';
import { getSortedPostsData } from '@/lib/posts';
import { Post } from '@/types/post';

interface Props {
    allPostsData: Post[]
}

export async function getStaticProps(): Promise<{ props: Props }> {
    const allPostsData = getSortedPostsData();
    return {
        props: {
            allPostsData
        }
    }
}

export default function Home({ allPostsData }: Props) {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>

            <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
                <h2 className={utilStyles.headingLg}>Blog</h2>
                <ul className={utilStyles.list}>
                    {allPostsData.map(({ id, date, title }) => (
                        <li className={utilStyles.listItem} key={id}>
                            {title}
                            <br />
                            {id}
                            <br />
                            {date}
                        </li>
                    ))}
                </ul>
            </section>
        </Layout>
    );
}

取得したデータはpropsとしてHomeコンポーネントに渡すことができます。

Dynamic Routes

投稿の詳細画面へ遷移できるようにします。
詳細画面のパスは/posts/ブログ名です。
pages/posts/[id].tsxを作成することで該当の画面へ遷移できます。
このような動的ルーティングを実現するためにはgetStaticPathsgetStaticPropsを実装します。

Implement getStaticPaths

ここではファイル名の一覧を返してあげます。
まずlib/posts.tsにファイルの一覧を返す関数を追加しましょう。

lib/posts.ts
export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\.md$/, ''),
      },
    }
  })
}

ここで実装した関数を[id].tsxgetStaticPathsで呼び出します。

pages/posts/[id].tsx
export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}

Implement getStaticProps

次に詳細画面で表示したいデータを取得します。
ファイル名をキーにブログの詳細を取得します。

lib/posts.tsにメソッドを追加しましょう。

lib/posts.ts
export function getPostData(id: string): Post {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const matterResult = matter(fileContents);

  const post = new Post(
    id,
    matterResult.data.date,
    matterResult.data.title,
    matterResult.content,
  )
  return JSON.parse(JSON.stringify(post))
}

getPostData[id].tsxgetStaticPropsで実行します。

pages/posts/[id].tsx
import Layout from "@/components/layout";
import { getAllPostIds, getPostData } from "@/lib/posts";
import { Post } from "@/types/post";

export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({ params }: { params: { id: string } }) {
    const postData = await getPostData(params.id);
    return {
        props: {
            postData
        }
    }
}

interface Props {
    postData: Post
}

export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
        </Layout>
    );
}

取得した内容を画面に表示すると画像のようになります。

Render Markdown

ブログの本文を画面に表示しましょう。
マークダウンをHTMLへ変換するライブラリとしてremarkremark-htmlをインストールします。

npm install remark remark-html

Postの詳細情報を取得するときにHTMLの文字列を取得するように修正します。
HTML文字列をそのまま画面に表示します。

まずPost詳細情報取得の箇所を修正します。

lib/posts.ts
export async function getPostData(id: string): Promise<Post> {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const matterResult = matter(fileContents);

  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)

  const contentHtml = processedContent.toString()

  const post = new Post(
    id,
    matterResult.data.date,
    matterResult.data.title,
    contentHtml,
  )

  return JSON.parse(JSON.stringify(post))
}

取得してきた文字列を詳細画面で表示します。
dangerouslySetInnerHTMLに文字列を指定することで実現できます。

pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
            <br />
            <div dangerouslySetInnerHTML={{ __html: postData.content }} />
        </Layout>
    );
}

Adding title to the Post Page

詳細画面のタイトルを設定しましょう。
pages/posts/[id].tsxにもHeadコンポーネントを追加します。

pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            <Head>
                <title>{postData.title}</title>
            </Head>
            <article>
                <h1 className={utilStyles.headingXl}>{postData.title}</h1>
                <div className={utilStyles.lightText}>
                    <Date dateString={postData.date} />
                </div>
                <div dangerouslySetInnerHTML={{ __html: postData.content }} />
            </article>
        </Layout>
    );
}

Formatting the Date

日付の表示を整えます。
January 2, 2020という表示になるようにします。
date-fnsというライブラリをインストールします。

npm install date-fns

date-fnsを使って日付を表示するコンポーネントを実装します。

components/date.tsx
import { format, parseISO } from "date-fns"

interface Props {
    dateString: string
}

export default function Date({ dateString }: Props) {
    const date = parseISO(dateString)
    return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}

Dateコンポーネントをpages/posts/[id].tsxで使います。

pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            <Head>
                <title>{postData.title}</title>
            </Head>
            <article>
                <h1 className={utilStyles.headingXl}>{postData.title}</h1>
                <div className={utilStyles.lightText}>
                    <Date dateString={postData.date} />
                </div>
                <div dangerouslySetInnerHTML={{ __html: postData.content }} />
            </article>
        </Layout>
    );
}

表示は画像のような感じです。

Polishing the Index Page

ホームページから各投稿の詳細へ遷移できるようにします。
既存のli要素をにLinkコンポーネントを使って対応します。

pages/index.tsx
export default function Home({ allPostsData }: Props) {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>

            <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
                <h2 className={utilStyles.headingLg}>Blog</h2>
                <ul className={utilStyles.list}>
                    {allPostsData.map(({ id, date, title }) => (
                        <li className={utilStyles.listItem} key={id}>
                            <Link href={`/posts/${id}`}>{title}</Link>
                            <br />
                            <small className={utilStyles.lightText}>
                                <Date dateString={date} />
                            </small>
                        </li>
                    ))}
                </ul>
            </section>
        </Layout>
    );
}

以上でブログアプリは完成です。
ソースコードはこちらです。
https://github.com/h-taro/nextjs-blog

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?