- 完成品
- この記事でやること
- やらないこと
- 使うライブラリ
- 参考にさせていただいた動画
- Nextプロジェクトの作成
- HelloWorldしてみる
- 下準備
- mdxファイルの情報を取得するプログラムの作成
- mdxのfrontmatterから情報を抽出する
- ブログリストコンポーネント
- BlogItemCardコンポーネント
- 日付の表示
- date-fnsを使った日付フォーマット
- タグの表示
- 説明の表示
- ルーティング
- Meta情報やtitleの設定
- バナー画像の表示
- 記事詳細ページにタグ、日付の表示
- MDXを表示
- コードハイライト
- 記事詳細ページへのリンク
- タグ絞り込み機能を作る
- アルファベット順にソート
- タグボタンの追加
- 修正
- ページネーションの追加
- ローカル画像をMDXから読み込めるようにする
完成品
ソースコード:
GitHub - Tebaeleven/next-mdx-b...
Next + MDX Blog site. Contribute to...
この記事でやること
- Next13を使った簡易的なブログサイトの構築
- MDX形式のマークダウン
- frontmatter
- ソースコードハイライト
- タグ機能
- 絞り込み
- all
- ページネーション
- MDX形式のマークダウン
やらないこと
- 目次
- こちらの記事が参考になりそうです
- CSSスタイリング
- MDXの読み込みと表示機能がメインなので、CSSは最低限のみしかやっていません
- もちろん自由にTailwindCSSやMUI、Chakra UIなど使えます
使うライブラリ
- MDX
- next-mdx-remote
- MDX形式のファイルを読み込んでHTMLに変換してサイトに表示
- gray-matter
- frontmatterを解析して扱いやすい形式に
- next-mdx-remote
- ソースコードハイライト
- highlight.js
- クラスをもとにハイライト
- rehype-highlight
- ソースコードにクラスを適用
- highlight.js
- css
- clsx
- Reactでクラス名を動的に変更
- clsx
- 日付
- date-fns
- 日付のフォーマット
- 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
Docs | Next.js
Welcome to the Next.js Documentatio...
公式ドキュメントを参考にプロジェクトを作成します。
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'
}
]
ブログリストコンポーネント
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の無料プランでどこまでやれるか?料金プラ...
コメント