各種設定

各種設定の案内です。 サイトの見た目や振る舞いを変更する際に参考にしてください。

環境変数

環境変数は.envファイルにより設定しています。

環境変数は次の 2 つです。

  • NEXT_PUBLIC_ROOT_URL:サイトのルート URL
  • NEXT_PUBLIC_BASE_PATHnext.config.mjsbasePathに対応するパス

本番環境で運用する際は、適宜変更を加えてください。

また、デプロイ先を GitHub Pages にする場合は こちら を参照してください。

config/app

ROOT_URL

NEXT_PUBLIC_ROOT_URLそのものです。 使用箇所はlib/url.tsに限られます。

SITE_NAME

サイト名です。ヘッダー・フッターの表記と タイトル・メタデータ の設定に利用しています。

SITE_DESCRIPTION

メタデータ のデフォルトのdescriptionです。

タイトル・メタデータ(Seo)

components/Seo.tsxでタイトルやメタデータの設定を行っています。

Front Matter

data/blog/posts/example.mdx
---
title: 'Example Blog Post'
description: '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.'
image: '/blog/posts/nextjs.png'
date: '2023-1-1'
author: John Doe
tags:
  - 'site'
  - 'blog'
---

ページ固有の変数を定義したいときに使用してください。

ここでお伝えしたいことは 1 点、 image: '/blog/posts/nextjs.png'に関してです。 後述するgetImageUrlを使用して publicフォルダの画像にアクセスするため、public/assets/... に続く/blog/posts/nextjs.pngというパスを指定している ことに注意してください。

静的ファイルの格納

public/assetsフォルダ以下に格納しています。 格納するフォルダを変更する際は、 getImageUrl の振る舞いを変更してください。

dataフォルダ

MDX ファイルの格納先です。データを格納しているdata配下のフォルダと 対応するデータを表示するpages配下のフォルダ構成は一致しています。 このプロジェクトでは

  • data/blog/posts/xxx.mdxpages/blog/posts/[...slug].tsx
  • data/docs/xxx/xxx.mdxpages/docs/[...slug].tsx

が対応しています。管理と実装が容易であるため、このフォルダ構成を採用しています。 実装はこの前提の下に成り立っているので、注意してください。詳しくは mdxをご覧ください。

libフォルダ

ほとんどのロジックはlibフォルダに格納されています。

routes

globalRoutesdocsRoutesが格納されています。それぞれ

  • globalRoutes:サイト全体の案内
  • docsRoutes:ドキュメント内のリンク

を表しています。

url

lib/url.ts
import { ROOT_URL } from '@/config/app';

export const resolveUrl = (...paths: string[]) => {
  const rootUrl = ROOT_URL.endsWith('/') ? ROOT_URL : ROOT_URL + '/';
  return (
    rootUrl +
    paths
      .flatMap((path) => path.split('/'))
      .filter(Boolean)
      .join('/')
  );
};

export const getImageUrl = (image: string | undefined) =>
  resolveUrl('assets', image ?? 'default.png');

resolveUrl

クライアント側で動作するpath.resolveに類する機能が欲しかったので追加しました。 使用範囲が限られているので、実装は簡易的なものであることに注意してください。

getImageUrl

public/assets配下に格納された画像ファイルへのリンクを 生成するために使用しています。 画像ファイルの格納先を変更する場合は、適宜 resolveUrlに渡す第 1 変数を変更してください。

path

lib/path.ts
import path from 'path';

export const resolvePath = (...paths: string[]) =>
  path.resolve(...paths).replaceAll('\\', '/');

const ROOT_PATH = resolvePath(process.cwd());
export const DATA_PATH = resolvePath(ROOT_PATH, 'data');

DATA_PATHdataフォルダの絶対パスです。

resolvePathpath.resolveのラッパー関数です。 OS が Windows である場合、path.resolve\で パスを結合してしまうので、\/に変換しています。 glob\で結合されたパスを受け付けないので、このような変換を加えています。

mdx

lib/mdx.ts
import { MdxSource } from '@/types/mdx';
import fs from 'fs';
import glob from 'glob';
import matter from 'gray-matter';
import { serialize } from 'next-mdx-remote/serialize';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { Frontmatter, FrontmatterWithPath } from '../types/fromtmatter';
import { DATA_PATH, resolvePath } from './path';

const getFilePath = (basePath: string, slug: string[]) =>
  resolvePath(DATA_PATH, basePath, ...slug) + '.mdx';

export const getMdxBySlug = async (basePath: string, slug: string[]) => {
  const filePath = getFilePath(basePath, slug);
  const source = fs.readFileSync(filePath, 'utf8');
  const { data, content } = matter(source);
  const mdxSource = await serialize(content, {
    mdxOptions: {
      remarkPlugins: [remarkGfm],
      rehypePlugins: [rehypeSlug],
    },
    scope: data,
  });
  return mdxSource as MdxSource;
};

export const getAllFrontmatters = (basePath: string) => {
  const PATH = resolvePath(DATA_PATH, basePath);
  const paths = glob.sync(`${PATH}/**/*.mdx`);

  return paths
    .map((filePath) => {
      const source = fs.readFileSync(resolvePath(filePath), 'utf8');
      const { data } = matter(source);

      return {
        ...(data as Frontmatter),
        path: filePath.replace(`${DATA_PATH}`, '').replace('.mdx', ''),
      } as FrontmatterWithPath;
    })
    .sort((a, b) => Number(new Date(b.date)) - Number(new Date(a.date)));
};

export const getAllPaths = (basePath: string) => {
  const frontmatters = getAllFrontmatters(basePath);

  return frontmatters.map((frontmatter) => ({
    params: {
      slug: frontmatter.path.replace(`/${basePath}/`, '').split('/'),
    },
  }));
};

実装の詳細には立ち入りません。

各関数の振る舞い

  • getMdxBySlug:指定された MDX ファイルをMdxSourceに変換する
  • getAllFrontmatters:対象フォルダ内のデータ全てを検索し、その Front Matter + pathをリストにして返す
  • getAllPaths:対象フォルダ内のデータに対応するパスをgetStaticPathsの形式に沿って生成する

MdxSourcenext-mdx-remoteMDXRemoteに渡すデータです。

またpathは、データに対応するページの絶対パスを格納した変数になります。 data/blog/posts/xxx.mdxROOT_URL/blog/posts/xxx.tsxに表示されますが、 このとき、pathには/blog/posts/xxx.tsxが格納されています。 pathは本来pages以下のフォルダ構造(=ルート構造)をもとに生成されるべきですが、 このプロジェクトではpages以下のフォルダ構造に対応したdataフォルダから生成しています。 getAllFrontmattersの使用する際のpathの生成が容易であるため、こうした実装を採用しましたが、 dataフォルダの構造がpagesフォルダの構造と一致していないと正しいpathが生成できません。 ページ内リンクが正しく張れないといったバグを生むことになるので注意してください。

実装パターン 1

pages/blog/posts/[...slug].tsx
...
type Props = {
  mdxSource: MdxSource;
};
...
export default function Page({ mdxSource }: Props) {
  ...
  return (
    ...
        <MDXRemote {...mdxSource} components={components} />
    ...
  );
}

const BASE_PATH = 'blog/posts';

type Params = NextParsedUrlQuery & {
  slug: string[];
};

export const getStaticPaths: GetStaticPaths<Params> = async () => {
  return {
    paths: getAllPaths(BASE_PATH),
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps<Props, Params> = async ({
  params,
}) => {
  const slug = params!.slug;
  const mdxSource = await getMdxBySlug(BASE_PATH, slug);

  return {
    props: {
      mdxSource,
    },
  };
};

実装パターン 2

pages/blog/tags/index.tsx
...
type Props = {
  tags: string[];
};

export default function Page({ tags }: Props) {
  return (
    <DefaultPage>
      {tags.map((tag) => (
        <PostTag key={tag} href={`/blog/tags/${tag}`} label={tag} />
      ))}
    </DefaultPage>
  );
}

const BASE_PATH = 'blog/posts';

export const getStaticProps: GetStaticProps<Props> = async () => {
  const frontmatters = getAllFrontmatters(BASE_PATH);
  const tags = distinct(
    frontmatters.flatMap((frontmatter) => frontmatter.tags),
  );

  return {
    props: {
      tags,
    },
  };
};

MDX で使用するコンポーネント

MDX で使用するコンポーネントは components/mdx/index.tsx から一括でエクスポートしています。

コンポーネントの登録の仕方は Using MDX with Next.jsnext-mdx-remote のドキュメント を参照してください。

MDX を直接使用する

Using MDX with Next.js の設定を行っているので、直接 MDX ファイルをpages配下に置くことも可能です。