スポンサーリンク

Next.js + MDXでブログサイトを作ってみよう!

当サイトではアフィリエイト広告を利用して商品を紹介しています。

Next.js

完成品

ソースコード:

GitHub - Tebaeleven/next-mdx-b...
Next + MDX Blog site. Contribute to...

この記事でやること

  • Next13を使った簡易的なブログサイトの構築
    • MDX形式のマークダウン
      • frontmatter
      • ソースコードハイライト
    • タグ機能
      • 絞り込み
      • all
    • ページネーション

やらないこと

  • 目次
  • CSSスタイリング
    • MDXの読み込みと表示機能がメインなので、CSSは最低限のみしかやっていません
    • もちろん自由にTailwindCSSやMUI、Chakra UIなど使えます

使うライブラリ

  • MDX
    • next-mdx-remote
      • MDX形式のファイルを読み込んでHTMLに変換してサイトに表示
    • gray-matter
      • frontmatterを解析して扱いやすい形式に
  • ソースコードハイライト
    • highlight.js
      • クラスをもとにハイライト
    • rehype-highlight
      • ソースコードにクラスを適用
  • css
    • clsx
      • Reactでクラス名を動的に変更
  • 日付
    • date-fns
      • 日付のフォーマット

参考にさせていただいた動画

今回の解説記事を作成するにあたり、こちらの動画を参考にさせていただきました。

Nextプロジェクトの作成

mkdir nextjs-mdx-blog
npx create-next-app@latest

Need to install the following packages:
  create-next-app@13.3.0
Ok to proceed? (y) y
✔ What is your project named? … .
✔ Would you like to use TypeScript with this project? … No
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? … No 
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … 
Creating a new Next.js app in /Users/nano/nextjs-mdx-blog.

Using npm.

Initializing project with template: default


Installing dependencies:
- react
- react-dom
- next
- eslint
- eslint-config-next
Getting Started | Next.js
Get started with Next.js in the off...

公式ドキュメントを参考にプロジェクトを作成します。

rm -r pages/api

apiフォルダはいらないので削除

npm run dev

サーバーを起動

HelloWorldしてみる

styles/Home.module.cssの削除

export default function Home() {
  return (
    <h1>Hello World</h1>
  )
}

下準備

mkdir components
mkdir posts
---
title: 1つ目の記事
author: chicken
date: 2023-4-16
tags: [first,blog,mdx]
description: 初めての投稿です
bannerUrl: https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640
---

## Hello World

test

### three

```js
console.log("Hello World")
```
export default function Home({ name }) {
  return (
    <>
      <h1>Hello World</h1>
      <h2>{name}</h2>
    </>
  )
}
//サーバーサイドで実行されるNextjsが提供している関数
export async function getStaticProps() {
  return {
    props: {
      name: "chickensblog"
    }
  }
}

mdxファイルの情報を取得するプログラムの作成

mkdir utils
touch utils/mdxUtils.js
//nodejsに標準搭載されているpathとfsをimport
import path from "path"
import fs from "fs"
//mdxが入っているフォルダのpath
export const postsPath = path.join(process.cwd(), "posts")
//postsPathmdx形式のファイルを全て取得する
export const postsFileNames = fs
    .readdirSync(postsPath)
    .filter((fileName)=>/\.mdx$/.test(fileName))

mdxのfrontmatterから情報を抽出する

npm install gray-matter
gray-matter
Parse front-matter from a string or...

markdownのfrontmatterを読み出すライブラリです。

import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
export default function Home({ posts }) {
  console.log(posts)
  return (
    <>
      <h1>Hello World</h1>
    </>
  )
}

export async function getStaticProps() {
  //postsFileNamesには全てのファイル名が入っているので、mapする
  const posts=postsFileNames.map((slug) => {
    //readFileSyncでpostsPathと各ファイル名を繋げたものを読み込む
    const content = fs.readFileSync(path.join(postsPath, slug))
    //gray-matterライブラリを使ってcontentからfrontmatter情報を読み込む
    const { data } = matter(content)
    return { 
      frontmatter: data,
      slug
    }
  })
  return {
    props: {
      posts
    }
  }
}

出力結果⬇️

[
  {
    frontmatter: {
      title: '1つ目の記事',
      author: 'chicken',
      date: '2023-4-16',
      tags: [Array],
      description: '初めての投稿です',
      bannerUrl: 'https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640'
    },
    slug: 'first.mdx'
  },
  {
    frontmatter: {
      title: '2つ目の記事',
      author: 'chicken',
      date: '2023-4-17',
      tags: [Array],
      description: '2投稿です',
      bannerUrl: 'https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640'
    },
    slug: 'second.mdx'
  }
]

エラーが出た場合

動画ではJSONにしないとエラーが出ていたので、設定やバージョンなどでエラーが出るかもしれない

  return {
    props: {
      posts:JSON.parse(JSON.stringify(posts))
    }
  }

ブログリストコンポーネント

mkdir components/blogs
touch components/blogs/BlogList.js
import React from 'react'

function BlogList({posts}) {
  return (
      <>
          {posts.map((post) => (
              <h1>{post.frontmatter.title}</h1>
          ))}
      </>
  )
}

export default BlogList
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
export default function Home({ posts }) {
  console.log(posts)
  return (
    <>
      <h1>Hello World</h1>
      <BlogList posts={posts}></BlogList> //追加
    </>
  )
}

BlogItemCardコンポーネント

touch components/blogs/BlogItemCard.js
import React from 'react'
import BlogItemCard from "./BlogItemCard"
function BlogList({ posts }) {
    return (
        <>
            {posts.map((post) => (
                <BlogItemCard post={post} key={post.slug}></BlogItemCard>
            ))}
        </>
    )
}

export default BlogList
import React from 'react'
import Image from 'next/image'
function BlogItemCard({ post }) {
    return (
        <>
            <div>{post.frontmatter.title}</div>
            {
                post.frontmatter.bannerUrl && (
                    <div>
                        <Image
                            src={post.frontmatter.bannerUrl}
                            alt={post.frontmatter.title}
                            width={50}
                            height={50}
                        >
                        </Image>
                    </div>
                )
            }
        </>
    )
}

export default BlogItemCard

Imageを使ってfrontmatterに記載されたURL画像を表示します。

以下のようなエラーが出ました。

Error: Invalid src prop (https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640) on `next/image`, hostname "images.unsplash.com" is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host

URLのアクセスを追加する

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  //追加
  images: {
    domains: ["images.unsplash.com"],
  }
}

module.exports = nextConfig

アクセスを許可するリンクを追加します。

日付の表示

import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
function BlogItemCard({ post }) {
    return (
        <>
            <div>{post.frontmatter.title}</div>
            {
                post.frontmatter.bannerUrl && (
                    <div>
                        <Image
                            src={post.frontmatter.bannerUrl}
                            alt={post.frontmatter.title}
                            width={50}
                            height={50}
                        >
                        </Image>
                    </div>
                )
            }
            <Link href={`blogs/${post.slug}`}>
                {post.frontmatter.title}
            </Link>
            {
                post.frontmatter.date && (
                    <p>{post.frontmatter.date }</p>
                )
            }
        </>
    )
}

export default BlogItemCard

date-fnsを使った日付フォーマット

npm install date-fns
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import format from 'date-fns/format'
import ja from "date-fns/locale/ja";

function BlogItemCard({ post }) {
    return (
        <>
            <div>{post.frontmatter.title}</div>
            {
                post.frontmatter.bannerUrl && (
                    <div>
                        <Image
                            src={post.frontmatter.bannerUrl}
                            alt={post.frontmatter.title}
                            width={50}
                            height={50}
                        >
                        </Image>
                    </div>
                )
            }
            <Link href={`blog/${post.slug}`}>
                {post.frontmatter.title}
            </Link>
            {
                post.frontmatter.date && (
                    <p>{format(new Date(post.frontmatter.date), "PPP", { locale: ja }) }</p>
                )
            }
        </>
    )
}

export default BlogItemCard

日本の表示形式に変換します。

タグの表示

            {
                post.frontmatter.tags && (
                    <p>タグ:
                        {post.frontmatter.tags.map((tag, index, tags) => (
                            <span key={tag}>
                                {tag}
                                {index < tags.length - 1 ? ", " : ""}
                            </span>
                        ))}
                    </p>
                )
            }
        </>
    )
}

export default BlogItemCard

説明の表示

            {
                post.frontmatter.tags && (
                    <p>タグ:
                        {post.frontmatter.tags.map((tag, index, tags) => (
                            <span key={tag}>
                                {tag}
                                {index < tags.length - 1 ? ", " : ""}
                            </span>
                        ))}
                    </p>
                )
            }
            //追加
            {
                post.frontmatter.description && (
                    <p>
                        説明:{post.frontmatter.description}
                    </p>
                )
            }
        </>
    )
}

export default BlogItemCard

ルーティング

基本的なルーティングを試してみる(静的)

mkdir pages/blogs
touch pages/blogs/first-blog.js
import React from 'react'

function Page() {
    return (
        <div>first-blog</div>
    )
}

export default Page
http://localhost:3001/blogs/fi...

動的なルーティング

first-blog.jsは削除

touch pages/blogs/[slug].js
import React from 'react'
import { postsFileNames } from 'utils/mdxUtils'

export default function SingleBlogPage() {
    return (
        <div>[slug]</div>
    )
}

export async function getStaticProps({ params }) {
    console.log(params)
    return { 
        props: {
            params
        }
    }
}

export async function getStaticPaths(){
    const postsPaths = postsFileNames.map((slug) => ({
        params: {
            slug: slug.replace(/\.mdx?$/, ""),
        }
    }))

    return {
        paths: postsPaths,
        fallback:false
    }
}
http://localhost:3001/blogs/se...

アクセスするとそのページのslugがconsole.logされる

MDXを読み込む

touch components/blogs/SingleBlog.js
touch components/blogs/BlogHeader.js
GitHub - hashicorp/next-mdx-re...
Load mdx content from anywhere thro...

mdxを表示するためのライブラリです。

import { postsPath } from '/utils/mdxUtils'
import React from 'react'
import { postsFileNames } from 'utils/mdxUtils'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import path from 'path'
import fs from 'fs'
import SingleBlog from "/components/blogs/SingleBlog"
export default function SingleBlogPage({mdxSource,frontmatter}) {
    return ( 
        <SingleBlog mdxSource={mdxSource} frontmatter={frontmatter}></SingleBlog>
    )
}

export async function getStaticProps({ params }) {
    const { slug } = params
    const filePath = path.join(postsPath, `${slug}.mdx`)
    const fileContent = fs.readFileSync(filePath, 'utf-8')
    const { data: frontmatter, content } = matter(fileContent)
    const mdxSource=await serialize(content)
    return { 
        props: {
            mdxSource,
            frontmatter,
            slug,
        }
    }
}
import Link from 'next/link'
import React from 'react'
import BlogHeader from "components/blogs/BlogHeader"

function SingleBlog({mdxSource,frontmatter}) {
    return (
        <>
            <Link href="/">
                top
            </Link>
            <BlogHeader frontmatter={frontmatter}></BlogHeader>
        </>

    )
}

export default SingleBlog

Meta情報やtitleの設定

import  Head  from 'next/head'
import React from 'react'

function BlogHeader({frontmatter}) {
    return (
        <Head>
            <title>{frontmatter.title}</title>
            <meta name='description' content={frontmatter.description } /> 
        </Head>
    )
}

export default BlogHeader

meta情報を追加します。

バナー画像の表示

import Head from 'next/head'
import React from 'react'

function BlogHeader({ frontmatter }) {
    return (
        <>
            <Head>
                <title>{frontmatter.title}</title>
                <meta name='description' content={frontmatter.description} />
            </Head>
            <div>
                {frontmatter.bannerUrl && (
                    <Image
                        src={frontmatter.bannerUrl}
                        width={300}
                        height={200}
                    ></Image>
                )}
            </div>
            <h1>
                {frontmatter.title}
            </h1>
            {frontmatter.date && (
                <p>
                    {format(new Date(frontmatter.date), "PPP", { locale: ja })}
                </p>
            )}
            {
                frontmatter.tags && (
                    <p>タグ:
                        {frontmatter.tags.map((tag, index, tags) => (
                            <span key={tag}>
                                {tag}
                                {index < tags.length - 1 ? ", " : ""}
                            </span>
                        ))}
                    </p>
                )
            }
            {
                frontmatter.description && (
                    <p>
                        説明:{frontmatter.description}
                    </p>
                )
            }
        </>
    )
}

export default BlogHeader

記事詳細ページにタグ、日付の表示

import Link from 'next/link'
import React from 'react'
import BlogHeader from "components/blogs/BlogHeader"
import Image from 'next/image'
import ja from "date-fns/locale/ja";
import format from 'date-fns/format';

function SingleBlog({mdxSource,frontmatter}) {
    return (
        <>
            <Link href="/">
                top
            </Link>
            <BlogHeader frontmatter={frontmatter}></BlogHeader>
            <div>
                {frontmatter.bannerUrl && (
                    <Image
                        src={frontmatter.bannerUrl}
                        width={300}
                        height={200}
                    ></Image>
                )}
            </div>
            <h1>
                {frontmatter.title}
            </h1>
            {frontmatter.date && (
                <p>
                    {format(new Date(frontmatter.date), "PPP", { locale: ja })}
                </p>
            )}
        </>

    )
}

export default SingleBlog

MDXを表示

touch components/blogs/BlogContent.js
import Link from 'next/link'
import React from 'react'
import BlogHeader from "./BlogHeader"
import Image from 'next/image'
import ja from "date-fns/locale/ja";
import format from 'date-fns/format';
import BlogContent from './BlogContent';
function SingleBlog({mdxSource,frontmatter}) {
    return (
        <>
            <Link href="/">
                top
            </Link>
            <BlogHeader frontmatter={frontmatter}></BlogHeader>
            <BlogContent mdxSource={mdxSource}></BlogContent>
        </>

    )
}

export default SingleBlog
import { MDXRemote } from 'next-mdx-remote'
import React from 'react'

function BlogContent({ mdxSource }) {
    return (
        <div>
            <MDXRemote {...mdxSource}></MDXRemote>
        </div>
    )
}

export default BlogContent
---
title: 1つ目の記事
author: chicken
date: 2023-4-16
tags: [first,blog,mdx]
description: 初めての投稿です
bannerUrl: https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640
---

## Hello World

test

### three

```js
console.log("Hello World")
```
<div style={{ padding: '20px', backgroundColor: 'red' }}>
  <h3>JSXの埋め込み!</h3>
</div>

MDXに独自スタイルの適用

touch /components/blogs/TextHeading.js
touch /components/blogs/TextHeading.module.css
import { MDXRemote } from 'next-mdx-remote'
import React from 'react'
import TextHeading from './TextHeading'
const components = {
    h1: (props) => <TextHeading level={1} {...props}></TextHeading>,
    h2: (props) => <TextHeading level={2} {...props}></TextHeading>,
    h3: (props) => <TextHeading level={3} {...props}></TextHeading>,
}
function BlogContent({ mdxSource }) {
    return (
        <div>
            <MDXRemote {...mdxSource} components={components}></MDXRemote>
        </div>
    )
}

export default BlogContent
import React from 'react'
import styles from "./TextHeading.module.css"

function TextHeading({level,children}) {
    if (level === 1) {
        return (
            <h1 className={styles.heading}>{children}</h1>
        )
    }
    if (level === 2) {
        return (
            <h2 className={styles.h2Heading}>{children}</h2>
        )
    }
    if (level === 3) {
        return (
            <h3 className={styles.h3Heading}>{children}</h3>
        )
    }
}

export default TextHeading
.heading {
    font-size: 3rem;
    font-weight: 700;
    line-height: 1.5;
    margin-bottom: 1rem;
}

.h2Heading {
    font-size: 2rem;
}

.h3Heading {
    font-size: 1.5rem;
}

コードハイライト

GitHub - rehypejs/rehype-highl...
plugin to highlight code blocks. Co...
GitHub - highlightjs/highlight...
JavaScript syntax highlighter with ...
npm install rehype-highlight
npm install highlightjs
import { postsPath } from '/utils/mdxUtils'
import React from 'react'
import { postsFileNames } from 'utils/mdxUtils'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import path from 'path'
import fs from 'fs'
import SingleBlog from "/components/blogs/SingleBlog"
import rehypeHighlight from 'rehype-highlight/lib'
export default function SingleBlogPage({mdxSource,frontmatter}) {
    return ( 
        <SingleBlog mdxSource={mdxSource} frontmatter={frontmatter}></SingleBlog>
    )
}

export async function getStaticProps({ params }) {
    const { slug } = params
    const filePath = path.join(postsPath, `${slug}.mdx`)
    const fileContent = fs.readFileSync(filePath, 'utf-8')
    const { data: frontmatter, content } = matter(fileContent)
    const mdxSource = await serialize(content, {
        mdxOptions: {
            rehypePlugins:[rehypeHighlight]
        }
    })
import '@/styles/globals.css'
import "highlight.js/styles/night-owl.css"
export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}

記事詳細ページへのリンク

.mdxが入っていると404になるので、削除する

export async function getStaticProps() {
  const posts=postsFileNames.map((slug) => {
    const content = fs.readFileSync(path.join(postsPath, slug))
    const { data } = matter(content)
    return { 
      frontmatter: data,
      //追加
      slug: slug.replace(/\.mdx?$/, ""),
    }
  })

タグ絞り込み機能を作る

import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
import TextHeading from '/components/blogs/TextHeading'
import TagFilter from "/components/blogs/TagFilter"
import { useState } from "react"
export default function Home({ posts }) {
  const [seletctedTag, setSelectedTag] = useState("all")
  
  const allTagSet = posts.reduce((acc, posts) => {
    posts.frontmatter.tags.map((tag) => acc.add(tag))
    return acc
  }, new Set([]))
  
  console.log(allTagSet)
  
  return (
    <>
      <h1>Hello World</h1>
      <TagFilter
        seletctedTag={seletctedTag}
        setSelectedTag={setSelectedTag}
      ></TagFilter>
      <BlogList posts={posts}></BlogList>
    </>
  )
}
import React from 'react'

function TagFilter({selectedTag,setSelectedTag}) {
    return (
        <div>TagFilter</div>
    )
}

export default TagFilter
event - compiled client and server successfully in 110 ms (274 modules)
Set(3) { 'first', 'blog', 'mdx' }
---
title: 2つ目の記事
author: chicken
date: 2023-4-17
tags: [mdx,二つ目,プログラミング,自作,Next13]
description: 2投稿です
bannerUrl: https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640
---

## Hello World

test

### three

```js
console.log("Hello World")
```

二つ目のmdxのタグを変えてみる。

wait  - compiling...
event - compiled client and server successfully in 108 ms (274 modules)
Set(7) { 'first', 'blog', 'mdx', '二つ目', 'プログラミング', '自作', 'Next13' }

タグの重複なく、タグが取得されているのが分かる。

アルファベット順にソート

import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
import TextHeading from '/components/blogs/TextHeading'
import TagFilter from "/components/blogs/TagFilter"
import { useState } from "react"
export default function Home({ posts }) {
  const [seletctedTag, setSelectedTag] = useState("all")
  
  const allTagSet = posts.reduce((acc, posts) => {
    posts.frontmatter.tags.map((tag) => acc.add(tag))
    return acc
  }, new Set([]))
  
  //アルファベット順にソート
  const allTagsArr = [...allTagSet].sort((a, b) => a.localeCompare(b))
  //allを先頭に追加
  allTagsArr.unshift("all")
  console.log(allTagsArr)
  return (
    <>
      <h1>Hello World</h1>
      <TagFilter
        seletctedTag={seletctedTag}
        setSelectedTag={setSelectedTag}
        tags={allTagsArr}
      ></TagFilter>
      <BlogList posts={posts}></BlogList>
    </>
  )

タグボタンの追加

import React from 'react'

function TagFilter({selectedTag,setSelectedTag,tags}) {
    return (
        <>
            {tags.map(tag => (
                <button key={tag}>
                    {tag}
                </button>
            ))}
        </>
    )
}

export default TagFilter
npm install clsx
import clsx from 'clsx';
import React from 'react'
import classes from "./TagFilter.module.css"
function TagFilter({selectedTag,setSelectedTag,tags}) {
    return (
        <>
            {tags.map(tag => (
                <button
                    key={tag}
                    className={clsx(
                        classes.tagButton,
                        selectedTag === tag && classes.selected
                    )}
                    onClick={() => {
                        console.log("押された")
                        setSelectedTag(tag);
                    }
                }>
                    {tag}
                </button>
            ))}
        </>
    )
}

export default TagFilter

修正

import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
import TextHeading from '/components/blogs/TextHeading'
import TagFilter from "/components/blogs/TagFilter"
import { useState } from "react"
export default function Home({ posts }) {
  const [selectedTag, setSelectedTag] = useState("all")
  
  const allTagSet = posts.reduce((acc, posts) => {
    posts.frontmatter.tags.map((tag) => acc.add(tag))
    return acc
  }, new Set([]))
  
  //アルファベット順にソート
  const allTagsArr = [...allTagSet].sort((a, b) => a.localeCompare(b))
  //allを先頭に追加
  allTagsArr.unshift("all")
  console.log(allTagsArr)

  return (
    <>
      <h1>Hello World</h1>
      <TagFilter
        selectedTag={selectedTag}
        setSelectedTag={setSelectedTag}
        tags={allTagsArr}
      ></TagFilter>
      <BlogList posts={posts}></BlogList>
    </>
  )
}

selectedtagのスペルが間違っていたので修正

touch components/blogs/TagFilter.module.css
.selected{
    background-color: blue;
}
.tagButton{
    padding:1rem;
}
import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
import TextHeading from '/components/blogs/TextHeading'
import TagFilter from "/components/blogs/TagFilter"
import { useEffect, useState } from "react"
export default function Home({ posts }) {
  const [selectedTag, setSelectedTag] = useState("all")
  const [filterPosts, setFilterPosts] = useState(posts)
  
  const allTagSet = posts.reduce((acc, posts) => {
    posts.frontmatter.tags.map((tag) => acc.add(tag))
    return acc
  }, new Set([]))
  
  //アルファベット順にソート
  const allTagsArr = [...allTagSet].sort((a, b) => a.localeCompare(b))
  //allを先頭に追加
  allTagsArr.unshift("all")

  useEffect(() => {
    let tempPosts = [...posts]
    if (selectedTag && selectedTag !== 'all') {
      tempPosts =posts.filter(post =>
        post.frontmatter.tags.includes(selectedTag)
      )
    }
    setFilterPosts(tempPosts)
  },[selectedTag,posts])
  return (
    <>
      <h1>Hello World</h1>
      <TagFilter
        selectedTag={selectedTag}
        setSelectedTag={setSelectedTag}
        tags={allTagsArr}
      ></TagFilter>
      <BlogList posts={filterPosts}></BlogList>
    </>
  )
}

ページネーションの追加

import { postsFileNames, postsPath } from "utils/mdxUtils"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import BlogList from "/components/blogs/BlogList"
import TextHeading from '/components/blogs/TextHeading'
import TagFilter from "/components/blogs/TagFilter"
import { useEffect, useState } from "react"
import { useRouter } from "next/router"
export default function Home({ posts }) {
  const postPerPage = 3;
  const [currentPage, setCurrentPage] = useState(null)
  const [selectedTag, setSelectedTag] = useState("all")
  const [filterPosts, setFilterPosts] = useState(posts)
  const router = useRouter()
  const allTagSet = posts.reduce((acc, posts) => {
    posts.frontmatter.tags.map((tag) => acc.add(tag))
    return acc
  }, new Set([]))

  //アルファベット順にソート
  const allTagsArr = [...allTagSet].sort((a, b) => a.localeCompare(b))
  //allを先頭に追加
  allTagsArr.unshift("all")

  useEffect(() => {
    let tempPosts = [...posts]
    if (selectedTag && selectedTag !== 'all') {
      tempPosts = posts.filter(post =>
        post.frontmatter.tags.includes(selectedTag)
      )
    }
    const page = parseInt(router.query.page, 10) || 1
    setCurrentPage(page)
    const start = (page - 1) * postPerPage
    const end =
      start + postPerPage > posts.length
      ? posts.length 
      : start + postPerPage
    const paginatedPosts = tempPosts.slice(start, end)
    setFilterPosts(paginatedPosts)
  }, [selectedTag, posts,router])

  const totalPages =
    selectedTag === 'all'
      ? Math.ceil(posts.length / postPerPage) 
      : Math.ceil(filterPosts.length / postPerPage)

  return (
    <>
      <h1>Hello World</h1>
      <TagFilter
        selectedTag={selectedTag}
        setSelectedTag={setSelectedTag}
        tags={allTagsArr}
      ></TagFilter>
      <BlogList posts={filterPosts}></BlogList>
    </>
  )
}

ページネーションボタンの作成

touch components/Pagination.js
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'

function Pagination({ currentPage, totalPages }) {
    const router = useRouter()
    return (
        <>
            <p>
                ページ:{currentPage} / {totalPages}
            </p>
            {currentPage > 1 && (
                <Link href={`/?page=${currentPage - 1}`} >
                    戻る
                </Link>
            )}
            {currentPage < totalPages && (
                <Link href={`/?page=${currentPage + 1}`}>
                    進む
                </Link>
            )}
        </>
    )
}

export default Pagination

postPerPage=1

ページが少ない部分へ移動した時の対処

ページ2から1しかないタグへ移動する

こうなってしまうので、修正する

import clsx from 'clsx';
import React from 'react'
import classes from "./TagFilter.module.css"
import { useRouter } from 'next/router';
function TagFilter({ selectedTag, setSelectedTag, tags }) {
    const router = useRouter()
    return (
        <>
            {tags.map(tag => (
                <button
                    key={tag}
                    className={clsx(
                        classes.tagButton,
                        selectedTag === tag && classes.selected
                    )}
                    onClick={() => {
                        setSelectedTag(tag);
                        //これを追加するだけ
                        router.push('/')
                    }
                }>
                    {tag}
                </button>
            ))}
        </>
    )
}

export default TagFilter

ローカル画像をMDXから読み込めるようにする

---
title: 4つ目の記事
author: chicken
date: 2023-4-17
tags: [mdx,4つ目,プログラミング,自作,Next13]
description: 4投稿です
bannerUrl: https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640
---


![test-img](https://images.unsplash.com/photo-1485395578879-6ba080c9cdba?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=360&ixid=MnwxfDB8MXxyYW5kb218MHwxNzUwODN8fHx8fHx8MTY4MTY1MTA0Mg&ixlib=rb-4.0.3&q=80&w=640)
## Hello World

test

### three

```js
console.log("Hello World")
```

test-imgを追加

サイズ調整

import { MDXRemote } from 'next-mdx-remote'
import React from 'react'
import TextHeading from './TextHeading'
import Image from 'next/image'
const components = {
    h1: (props) => <TextHeading level={1} {...props}></TextHeading>,
    h2: (props) => <TextHeading level={2} {...props}></TextHeading>,
    h3: (props) => <TextHeading level={3} {...props}></TextHeading>,
    //追加
    img: (props) => (
        <Image
            {...props}
            alt={props.alt}
            style={{ objectFit: 'contain' }}
            width={500}
            height={500}
        ></Image>
    )

}
function BlogContent({ mdxSource }) {
    return (
        <div>
            <MDXRemote {...mdxSource} components={components}></MDXRemote>
        </div>
    )
}

export default BlogContent

next13になってimageのサイズ指定が変更されたので、注意しましょう。クラスとかを追加してうまくcssを適用してください。

Next.js 13 next/imageでサイズ指定せずに...
こんにちは。キューでWebエンジニアをしている永井です。今回はこちらの...

publicフォルダに画像追加

public/images/test.png
![test](/images/test.png)

クラウド(Cloudinaryなど)で画像をホストするのも良いと動画では紹介されていました。

Cloudinaryの無料プランでどこまでやれるか?料金プラ...

コメント

タイトルとURLをコピーしました