Back to blog
|8 min read

ポートフォリオサイトを作りました

Next.js 16 + shadcn/ui + MDX でポートフォリオサイトを構築した話。MDXを選んだ理由や今後やりたいことなど。

Next.js
TypeScript
MDX

はじめまして

ioriです🙇。普段はフルスタックエンジニア、PM、テックリードをしています。

普段は新規事業の 0→1 開発をメインにしていて、要件定義から開発まで一気通貫で携わっています。新規事業の立ち上げが好きです。また一方で法人を立ち上げ、CRM 系 SaaS を作ったりしています。

せっかくなので自分のことをまとめられるサイトが欲しいなと思って、このポートフォリオを作りました。

使っている技術

  • Next.js 16 (App Router)
  • TypeScript
  • shadcn/ui + Tailwind CSS v4
  • MDX でブログ記事を管理
  • Framer Motion でちょっとしたアニメーション

わりと王道な構成だと思います。

なぜブログを MDX にしたのか

ブログを作るとき、DB、CMSを使う構成にするか MDX にするかで少し悩みました。結論から言うと MDX 一択 でした。

DB を使わなかった理由

正直、DB を持つほどのブログじゃないんですよね。

  • 最初はコストをかけたくない — DB があるとホスティング費用が発生する。
  • そんな複雑なことしない — タグで絞り込んで日付でソートするくらい。ファイル読むだけで十分
  • まず出すのが大事 — 最小構成でサクッと公開して、必要になったら足せばいい

MDX のいいところ

  • Markdown のノリで気軽に書ける
  • でも React コンポーネントも使える(ここが最高)
  • gray-matter でメタデータ管理もシンプル
  • エディタでプレビューしやすい

要は「Markdown の手軽さ」と「React の表現力」のいいとこ取りです。

gray-matter って何?

gray-matter について少し補足します。

MDX ファイルの先頭にある --- で囲まれた部分、これを フロントマター と呼びます。

---
title: "ポートフォリオサイトを作りました"
date: "2026-03-09"
tags: ["Next.js", "TypeScript"]
---

gray-matter は、このフロントマターを YAML としてパースして JavaScript のオブジェクトに変換してくれるライブラリです。使い方はこんな感じ。

import matter from 'gray-matter'

const file = `---
title: "記事タイトル"
date: "2026-03-09"
tags: ["Next.js", "MDX"]
---

ここから本文が始まる。
`

const { data, content } = matter(file)

// data = { title: "記事タイトル", date: "2026-03-09", tags: ["Next.js", "MDX"] }
// content = "\nここから本文が始まる。\n"

data にメタデータ、content に本文がきれいに分かれて返ってきます。これのおかげで、タイトルや日付、タグといった情報を記事ファイルの中に一緒に書けるんですよね。DB にメタデータを持つ必要がないのはこいつのおかげです。

ちなみに YAML 以外にも JSON や TOML のフロントマターにも対応しています。

MDX の使い方(Next.js での例)

MDX では普通の Markdown に加えて、React コンポーネントをそのまま書けます。

---
title: "記事タイトル"
date: "2026-03-09"
---

## 普通に Markdown が書ける

テキストも **太字** もいつも通り。

{/* JSX のコメントも書ける */}

<Callout type="info">
  これは React コンポーネント。インポートなしで使える。
</Callout>

<Counter initialCount={0} />

実際にこの記事の中で動かしてみます。

これが Callout コンポーネント。MDX に直接書くだけで、こういう注釈ブロックが出せます。

そしてこれが Counter。ボタン押してみてください、ちゃんと動きます。

Counter Demo
0

こういう 記事の中で動く UI が作れるのが、MDX の一番の魅力だと思っています。

Next.js App Router での読み込み

このサイトでは next-mdx-remote/rsc を使っています。サーバーコンポーネントから MDX をレンダリングする仕組みです。

import { MDXRemote } from 'next-mdx-remote/rsc'
import { readFile } from 'fs/promises'
import matter from 'gray-matter'

// カスタムコンポーネントを定義
const components = {
  Callout: ({ type, children }) => (
    <div className={`callout callout-${type}`}>
      {children}
    </div>
  ),
  Counter: ({ initialCount }) => {
    return <InteractiveCounter initial={initialCount} />
  },
}

export default async function BlogPost({ params }) {
  const file = await readFile(`content/blog/${params.slug}.mdx`, 'utf-8')
  const { content, data } = matter(file)

  return (
    <article>
      <h1>{data.title}</h1>
      <MDXRemote source={content} components={components} />
    </article>
  )
}

ポイントは components を渡すところ。これで MDX 内で使えるコンポーネントを好きなだけ増やせます。

記事の取得もシンプル

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

export function getAllPosts() {
  const dir = path.join(process.cwd(), 'src/content/blog')
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.mdx'))

  return files.map((filename) => {
    const raw = fs.readFileSync(path.join(dir, filename), 'utf-8')
    const { data } = matter(raw)
    return {
      slug: filename.replace('.mdx', ''),
      title: data.title,
      date: data.date,
      tags: data.tags,
    }
  })
}

DB も API もいらない。ファイルを置くだけで記事が増えます。

もし将来、記事が大量になって検索が複雑になったら Headless CMS や DB に移行するかもしれません。でも当面はこれで十分です。

これからやりたいこと

このサイトはまだ v1 なので、やりたいことがたくさんあります。

認証(Firebase Auth)

管理者ログインを Firebase Auth で作る予定。下書き管理とか、限定公開記事のアクセス制御に使いたい。

コメント機能

記事にコメントを残せるようにしたい。Firebase Firestore をバックエンドにして、ログインしたユーザーがコメントできる形を考えています。

いいね機能

「この記事よかった」がわかると嬉しいので。どの記事が参考になったか見える化したい。

その他いろいろ

  • OGP 画像の自動生成
  • 記事の目次

全部できるかはわからないけど、少しずつ育てていきます。

おわりに

「まず出す。足りなかったら足す」で作りました。個人開発のポートフォリオなら、MDX ブログはかなりいい選択肢だと思います。

このブログも含めて、アップデートがあればどんどん書いていくので、よかったらまた見にきてください。