Counts the Clouds

Next.jsでブログを構築する (2)

Next.jsでブログを構築する〜アーカイブ画面の追加
2021.11.18

Next.js

florian-klauer--K6JMRMj4x4-unsplash.jpg

タグクラウド

タグアーカイブへのナビゲーションとして、タグクラウドを追加する。

既存のgetAllPostsを利用して、タグを全取得するAPIを追加。

// lig/api.js
export function getAllTags() {
  return getAllPosts(['tags'])
    .map(post => post.tags)
    .flat()
    .filter((e, i, a) => a.indexOf(e) === i)
}

フッターにタグクラウドのUIを追加。APIから受け取ったデータをまずはバケツリレーでレイアウトからフッターにわたす。

// pages/index.js
// ...
import { getAllPosts, getAllTags } from '~/lib/api'
// ...
export default function Home({ allPosts, allTags }) {
  // ...
}

export async function getStaticProps() {
  // ...
  return {
    props: {
      // ...
      allTags: getAllTags()
    },
  }
}
// components/AppFooter.js
import React from 'react'
import Link from 'next/link'

function AppFooter({ tags }) {
  return (
      {/* ... */}
        <ul>
          {tags && tags.map((tag) => (
            <li key={tag} className="tag-label tag-lv-5">
              <Link href={`/tags/${tag.toLowerCase()}`}>
                <a>{tag}</a>
              </Link>
              {/* TODO: Tag Count */}
              {/* <var className="count">20</var> */}
            </li>
          ))}
        </ul>
      {/* ... */}
  )
}

export default AppFooter

タグアーカイブ

既存のgetAllPostsを利用して、指定したタグの記事を返すAPIを追加。

// lig/api.js
export function getPostsByTag(tag, fields = []) {
  const posts = getAllPosts(fields).filter((post) => {
    return post.tags.map(t => t.toLowerCase()).indexOf(tag.toLowerCase()) > -1
  })
  return posts
}

pagesにtags/[tag].jsを追加。

// tags/[tag].js
import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { getPostsByTag, getAllTags } from '~/lib/api'
import Layout from '~/components/Layout'

export default function TagPosts({ posts, allTags }) {
  const router = useRouter()
  const tagName = allTags.find(tag => tag.toLowerCase() === router.query.tag)
  return (
    <Layout tags={allTags}>
      <Head>
        <title>{tagName} | 110chang.com</title>
      </Head>
      <h1>{tagName}</h1>
      {posts.length > 0 && posts.map((post) => (
        <div key={post.date}>
          <h1><Link href={`/posts/${post.category}/${post.slug}`}>{post.title}</Link></h1>
        </div>
      ))}
    </Layout>
  )
}

export async function getStaticProps({ params }) {
  const posts = getPostsByTag(params.tag, [
    'title',
    'date',
    'slug',
    'tags',
    'content',
    'categories',
  ])

  return {
    props: {
      posts: posts.map(post => ({
        ...post,
        category: post.categories ? post.categories[0] : 'diary',
      })),
      allTags: getAllTags(),
    },
  }
}

export async function getStaticPaths() {
  const tags = getAllTags()
  return {
    paths: tags.map((tag) => {
      return {
        params: {
          tag: tag.toLowerCase(),
        },
      }
    }),
    fallback: false,
  }
}

タグカウント

指定したタグを付けられている記事の数。getAllTagsを改善して、タグに関する補足情報も返すようにした。

// lig/api.js
// ...
export function getAllTags() {
  const allTags = getAllPosts(['tags'])
  return allTags
    .map(post => post.tags)
    .flat()
    .filter((e, i, a) => a.indexOf(e) === i)
    .map((tagText) => {
      const tagId = tagText.toLowerCase()
      return {
        id: tagId,
        text: tagText,
        count: allTags.filter(post => post.tags.indexOf(tagText) > -1).length
      }
    })
}

取得側も変更。

  export async function getStaticPaths() {
    const tags = getAllTags()
    return {
      paths: tags.map((tag) => {
        return {
          params: {
-           tag: tag.toLowerCase(),
+           tag: tag.id,
          },
        }
      }),
      fallback: false,
    }
  }

カウント表示部分。

// components/AppFooter.js
import React from 'react'
import Link from 'next/link'

function AppFooter({ tags }) {
  return (
      {/* ... */}
        <ul>
          {tags.length > 0 && tags.map((tag) => (
            <li key={tag.id} className="tag-label tag-lv-5">
              <Link href={`/tags/${tag.id}`}>
                <a>{tag.text}</a>
              </Link>
              <var className="count">{tag.count}</var>
            </li>
          ))}
        </ul>
      {/* ... */}
  )
}

export default AppFooter

年別アーカイブ・月別アーカイブ

pages直下にダイナミックルートを置きたい。

これを見ると、できそうな感じ。

存在する記事からユニークなすべての年数を取ってくるAPIを追加。該当する記事のカウントも行う。このAPIを使ってパスの定義とUIの生成を行う。

// lib/api.js
// ...
export function getAllYears() {
  const allDates = getAllPosts(['date'])
  return allDates
    .map(post => post.date.split('-')[0])
    .filter((e, i, a) => a.indexOf(e) === i) // [2005, 2006, 2007, ... 2016]
    .map((year) => {
      return {
        id: year,
        text: year,
        count: allDates.filter(post => post.date.match(new RegExp(`^${year}`))).length
      }
    })
}

[year].jsを追加。

import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { getAllPosts, getAllTags, getAllYears } from '~/lib/api'
import Layout from '~/components/Layout'

export default function YearlyPosts({ allPosts, allTags, allYears }) {
  const router = useRouter()
  return (
    <Layout tags={allTags} years={allYears}>
      <Head>
        <title>{router.query.year} | 110chang.com</title>
      </Head>
      <h1>{router.query.year}</h1>
      {allPosts.length > 0 && allPosts.map((post) => (
        <div key={post.date}>
          <h1><Link href={`/posts/${post.category}/${post.slug}`}>{post.title}</Link></h1>
        </div>
      ))}
    </Layout>
  )
}

export async function getStaticProps() {
  const allPosts = getAllPosts([
    'title',
    'date',
    'slug',
    'content',
    'categories',
  ])

  return {
    props: {
      allPosts: allPosts.map(post => ({
        ...post,
        category: post.categories ? post.categories[0] : 'diary',
      })),
      allTags: getAllTags(),
      allYears: getAllYears(),
    },
  }
}


export async function getStaticPaths() {
  const years = getAllYears()
  return {
    paths: years.map(year => {
      return {
        params: {
          year: year.id,
        },
      }
    }),
    fallback: false,
  }
}

フッターに年別アーカイブへのリンクを追加。デザインが月まで表示する前提なので、再現するには「月別アーカイブ」も必要。

import React from 'react'
import Link from 'next/link'

function AppFooter({ tags = [], years = [] }) {
  return (
    <footer id="colophon">
        {/* ... */}
        <ul className="by-year">
          {years.length > 0 && years.map((year) => (
            <li key={year.id}>
              <span className="year-label">
                <Link href={`/${year.id}`}>
                  <a>{year.text}</a>
                </Link>
              </span>
            </li>
          ))}
        </ul>
        {/* ... */}
    </footer>
  )
}

export default AppFooter

月別アーカイブ

存在する記事中のすべての月(年-月)を取得するAPIと、ある月(年-月)の記事を取得するAPIを追加。

// lib/api.js
// ...
export function getAllMonths() {
  const allDates = getAllPosts(['date'])
  return allDates
    .map(post => ({
      year: post.date.match(/^\d{4}/)[0],
      month: post.date.match(/^\d{4}\-(\d{1,2})/)[1],
    }))
    .filter((e, i, a) => a.indexOf(e) === i)
    .map((post) => {
      const id = `${post.year}-${post.month}`
      return {
        ...post,
        id,
        text: `${post.year}.${post.month}`,
        count: allDates.filter(post => post.date.match(new RegExp(`^${id}`))).length
      }
    })
}

export function getPostsByMonth(year, month, fields = []) {
  const posts = getAllPosts(fields).filter((post) => {
    return (new RegExp(`^${year}\-${month}`)).test(post.date)
  })
  return posts
}

パスの定義。

// pages/[year]/[month].js
// ...
export async function getStaticPaths() {
  const months = getAllMonths()
  return {
    paths: months.map(month => {
      return {
        params: month,
      }
    }),
    fallback: false,
  }
}

このAPIにgetStaticPropsからアクセスして、月別の記事の情報を得る。

// ...
export async function getStaticProps({ params }) {
  const posts = getPostsByMonth(params.year, params.month, [
    'title',
    'date',
    'slug',
    'content',
    'categories',
  ])

  return {
    props: {
      posts: posts.map(post => ({
        ...post,
        category: post.categories ? post.categories[0] : 'diary',
      })),
      allTags: getAllTags(),
      allYears: getAllYears(),
      allMonths: getAllMonths(),
    },
  }
}

月別の記事の情報をUIにわたす。

import React from 'react'
import Link from 'next/link'

function AppFooter({ tags = [], years = [], months = [] }) {
  // ...
  const getMonth = (year, month) => {
    return months.find(item => item.year === `${year}` && Number(item.month) === month)
  }
  // ...
  return (
    <footer id="colophon">
      {/* ... */}
        <ul className="by-year">
          {years.length > 0 && years.map((year) => (
            <li key={year.id}>
              {/* ... */}
              <ul className="by-month">
                {(new Array(12)).fill(1).map((e, i) => i + 1).map((month) => {
                  const data = getMonth(year.id, month)
                  if (!data) {
                    return (
                      <li key={`${year}-${month}`}>
                        <span className="month-label count-zero">
                          <b>{month}</b>
                        </span>
                      </li>
                    )
                  }
                  return (
                    <li key={`${year}-${month}`}>
                      <span className="month-label">
                        <a href={`/${data.year}/${data.month}`}>{data.month}</a>
                        <var className="count">{data.count}</var>
                      </span>
                    </li>
                  )
                })}
              </ul>
            </li>
          ))}
        </ul>
      {/* ... */}
    </footer>
  )
}

export default AppFooter

_app.jsでgetStaticPropsは使えるか

_app.jsでgetStaticPropsが使えれば、ルートにContextを流してバケツリレーを回避することができる。

Adding a custom getInitialProps in your App will disable Automatic Static Optimization in pages without Static Generation.

getInitialPropsを使うと、Static GenerationなしのAutomatic Static Optimizationが無効になるとのこと。わかるようなわからないような。。

When you add getInitialProps in your custom app, you must import App from “next/app”, call App.getInitialProps(appContext) inside getInitialProps and merge the returned object into the return value.

getInitialPropsを使う場合は、import App from "next/app"して、getInitialPropsの中でApp.getInitialProps(appContext)する必要があるとのこと。

App currently does not support Next.js Data Fetching methods like getStaticProps or getServerSideProps.

getStaticPropsgetServerSidePropsといったData Fetching methodsは未対応。

単独のContextを用意して、各ページでuseContextで呼び出すようにした。ネストが浅いので現時点では手間の割にあまり意味がない。趣味の範囲。