🎨

エンジニアなら自分でブログを作れ!⑤タグ機能実装編

2022/10/02に公開

初めに

この記事は「エンジニアなら自分でブログを作れ!」の第五弾です。
過去の記事の実装が前提になっている可能性もあります。特に第一弾では無料の自作ブログを作成するために必要な導入について解説しているので良ければ一緒にご覧ください。

第一弾↓

https://zenn.dev/miketako3/articles/9b2b1a9ec13901

第二弾↓

https://zenn.dev/miketako3/articles/bfdc1b09ba8ed3

第三段↓

https://zenn.dev/miketako3/articles/66e1cc17193168

第四段↓

https://zenn.dev/miketako3/articles/2afb29824e578d

この記事はNext.jsの
blog-starter
(公式のデモページ)
について、投稿にタグを追加し、同じタグがついている投稿をまとめるページを作成します。

今回は細かい変更が多くなってしまうので、Githubのリンクを張る形にしています。

また、不要なコードが残っていたりする場合がありますが、変更箇所を少なくするためにあえて残しています。

タグを入稿して記事ページに表示する

https://github.com/miketako3/blog-example/commit/5189445901747401cd6d73132f28c2d567c26ca4

まずはMarkdownのファイルにタグを追加してみましょう。

_posts/dynamic-routing.md
---
title: 'Dynamic Routing and Static Generation'
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.'
coverImage: '/assets/blog/dynamic-routing/cover.jpg'
date: '2020-03-16T05:35:07.322Z'
author:
  name: JJ Kasper
  picture: '/assets/blog/authors/jj.jpeg'
ogImage:
  url: '/assets/blog/dynamic-routing/cover.jpg'
+ tags: 
+   - 'tag1'
+   - 'tag2'
---

他の記事についても追加しておきましょう (全ての記事に追加しておかないと後でエラーになります)

そして、 interface/posts.tspages/posts/[slug].tsx でタグの情報を渡すようにしましょう。

interface/posts.ts
type PostType = {
  slug: string
  title: string
  date: string
  coverImage: string
  author: Author
  excerpt: string
  ogImage: {
    url: string
  }
-   content: string
+   content: string,
+   tags: string[]
}
pages/posts/[slug].tsx
export default function Post({ post, morePosts, preview }: Props) {
  const router = useRouter()
  if (!router.isFallback && !post?.slug) {
    return <ErrorPage statusCode={404} />
  }
  useEffect(() => {
    import('zenn-embed-elements');
  }, []);
  return (
    <Layout preview={preview}>
      <script
          dangerouslySetInnerHTML={{
            __html: initTwitterScriptInner
          }}
      />
      <Container>
        <Header />
        {router.isFallback ? (
          <PostTitle>Loading…</PostTitle>
        ) : (
          <>
            <article className="pb-32">
              <Head>
                <title>
                  {post.title} | Next.js Blog Example with {CMS_NAME}
                </title>
                <meta property="og:image" content={post.ogImage.url} />
              </Head>
              <PostHeader
                title={post.title}
                coverImage={post.coverImage}
                date={post.date}
                author={post.author}
+                 tags={post.tags}
              />
              <PostBody content={post.content} />
            </article>
          </>
        )}
      </Container>
    </Layout>
  )
}
type Params = {
  params: {
    slug: string
  }
}
export async function getStaticProps({ params }: Params) {
  const post = getPostBySlug(params.slug, [
    'title',
    'date',
    'slug',
    'author',
    'content',
    'ogImage',
    'coverImage',
+     'tags',
  ])
  const content = await markdownToHtml(post.content || '')

  return {
    props: {
      post: {
        ...post,
        content,
      },
    },
  }
}

最後に、 components/post-header.tsx で受け取ったタグを表示しましょう。

components/post-header.tsx
type Props = {
  title: string
  coverImage: string
  date: string
-   author: Author
+   author: Author,
+   tags: string[]
}

- const PostHeader = ({ title, coverImage, date, author }: Props) => {
+ const PostHeader = ({ title, coverImage, date, author, tags }: Props) => {
  return (
    <>
      <PostTitle>{title}</PostTitle>
      <div className="hidden md:block md:mb-12">
        <Avatar name={author.name} picture={author.picture} />
      </div>
+       <ul className="flex gap-x-2">
+         {
+           tags.map((tag) => <li className="font-bold mb-12">{tag}</li>)
+         }
+       </ul>
      <div className="mb-8 md:mb-16 sm:mx-0">
        <CoverImage title={title} src={coverImage} />
      </div>
      <div className="max-w-2xl mx-auto">
        <div className="block md:hidden mb-6">
          <Avatar name={author.name} picture={author.picture} />
        </div>
        <div className="mb-6 text-lg">
          <DateFormatter dateString={date} />
        </div>
      </div>
    </>
  )
}

これで記事ページのタイトルの下あたりにタグが表示されるはずです。

タグごとの記事一覧ページを作成する

https://github.com/miketako3/blog-example/commit/06a5fbf69fabcabb617651caa32b2dc90ca8209c

まずは必要なデータを取得できるように lib/api.ts に機能を追加しましょう。

追加するのは次の2つです。

  • タグを指定して該当する記事の一覧を取得する
  • 全てのタグを取得する
lib/api.ts
export function getPostsByTag(tag: string, fields: string[] = []) {
  const slugs = getPostSlugs()
  return slugs
  .map((slug) =>  getPostBySlug(slug, fields))
  .filter((post) => post.tags.includes(tag))
  .sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
}

export function getAllTags() {
  const allPostTags = getAllPosts(['tags'])
  .flatMap((post) => post.tags)
  .sort()

  return Array.from(new Set(allPostTags))
}

次に、タグごとの記事一覧ページを作りましょう。 pages/tags/[tag].tsx というファイルを以下の内容で作成します。

pages/tags/[tag].tsx
import {getAllTags, getPostsByTag} from "../../lib/api";
import Post from "../../interfaces/post";
import Head from "next/head";
import Layout from "../../components/layout";
import Container from "../../components/container";
import MoreStories from "../../components/more-stories";

type Props = {
  posts: Post[],
  tag: string
}

export default function Index({ posts, tag }: Props) {
  return (
      <>
        <Layout>
          <Head>
            <title>Tag: {tag}</title>
          </Head>
          <Container>
            <MoreStories posts={posts} />
          </Container>
        </Layout>
      </>
  )
}

type Params = {
  params: {
    tag: string
  }
}


export const getStaticProps = ({ params }: Params) => {
  const posts = getPostsByTag(params.tag, [
    'title',
    'date',
    'slug',
    'author',
    'coverImage',
    'excerpt',
    'tags'
  ])

  return {
    props: {
      posts: posts,
      tag: params.tag
    },
  }
}

export function getStaticPaths() {
  const tags = getAllTags();

  return {
    paths: tags.map((tag) => {
      return {
        params: {
          tag: tag,
        },
      }
    }),
    fallback: false,
  }
}

内容としては記事一覧ページとほとんど変わりませんね。変わっているのは以下の3点です。

  • タグの情報を各所に追加
  • ルーティングのための getStaticPaths() 関数を追加
  • 先ほど lib/api.ts に追加した関数を使用するように変更

最後に、記事ページのタグ部分をクリックできるようにしましょう。

components/post-header.tsx
<ul className="flex gap-x-2">
    {
-       tags.map((tag) => <li className="font-bold mb-12">{tag}</li>)
+       tags.map((tag) => <li className="font-bold mb-12"><a href={`/tags/${tag}`}>{tag}</a></li>)
    }
</ul>

これで完成です! Markdown側のタグを色々変えたりして、ちゃんと絞り込めているか確認してみてください。

追加のカスタマイズ方針

これで最低限のタグ機能はできていますが、特にデザイン的にはまだまだかもしれません。例えば以下のような部分を洗練させるとカッコよくなるでしょう。

  • 記事一覧にもタグを表示する。
  • タグの表示をZennのように縁で囲ったりする
  • ページのタイトルを適切なものに変更する
  • タグ一覧のページを作成する

この記事はここで終わりになりますが、自作ブログは自由です。是非色々試してみてください!

まとめ

この記事ではNext.jsで書かれたブログにタグ機能を追加する実装を紹介しました。

みなさんの独自ブログの参考になれば幸いです。

最後に、私の投稿はZenn以外の投稿もまとめて以下ブログで取得できますので、良ければ見てみてください。

https://blog.miketako.xyz

Discussion