Next.jsをGitHub Pagesにデプロイしたらリンクが壊れた

*この記事の Next.js はバージョン 9.3 です。以下を参照したほうが良いかもしれません。*
[Next.js 9.5のアップデートにより、GitHub Pagesで妥協しない動的ルーティングが可能になった](https://qiita.com/syakoo/items/2eea9dd1b2519a89cc12)

# 背景

[新しくなった Next.js のチュートリアル](https://qiita.com/thesugar/items/01896c1faa8241e6b1bc) を完走し、せっかくだからと GitHub Pages にデプロイしてみたところ、ハイパーリンクや画像が正しく参照されなかった。

一般ユーザーの GitHub Pages の URL は `https://USER_NAME.github.io/PROJECT_NAME/` の形式だが、 Next.js は原則としてルートからの絶対パスでリンクするため、例えばチュートリアルで作成するブログサイトにおける 1 つ目の記事は `https://USER_NAME.github.io/posts/ssg-ssr` にリンクされてしまう。

# 注記
本記事は [Next.js のチュートリアルのコード](https://github.com/zeit/next-learn-starter/tree/master/demo)を前提としているが、筆者の趣味で **TypeScript に置き換えられている**ので注意。JavaScript で書いても大差ないはず。
なお筆者は TypeScript も React もチュートリアルしか触ったことがない初学者なので、より良い方法や間違った使い方があれば教えてください！

# 結論

公式な方法が見当たらないので、`/PROJECT_NAME` を環境変数に持たせて無理やり参照に追加した。

必要な環境変数を設定する。今回はビルド時に `production` を指定したときだけ `/PROJECT_NAME` がルートとなるようにした。

```js:next.config.js
module.exports = {
  assetPrefix: process.env.NODE_ENV === "production" ? "/PROJECT_NAME" : "",

  publicRuntimeConfig: {
    basePath: process.env.NODE_ENV === "production" ? "/PROJECT_NAME" : "",
  },
};
```
`assetPrefix` は生成される静的サイトのうち `_next` ディレクトリが置かれるパスを指定するもの[^1]。
[^1]: 本来は静的リソースを CDN に置くために使う
`publicRuntimeConfig` には[任意の定数を設定できる](https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration)。

次に、文字列に `/PROJECT_NAME` を付与するモジュールを作成する。

```tsx:/lib/basepath.ts
import { format } from 'url';
import getConfig from 'next/config';

const { publicRuntimeConfig = {} } = getConfig() || {};
const { basePath } = publicRuntimeConfig;

export default (path: string): string =>
  format((basePath || "") + (path || ""));
```

先ほどのモジュールを任意の名前[^2]でインポートして、サイト内のパスを指定する**すべての**文字列を囲う。
[^2]: [HtmlSpecialChars](https://www.flatflag.nir87.com/htmlspecialchars-555#htmlspecialchars-2) の慣習に習って 1 文字にすれば許せる程度の手間かなと

ここでは `layout.tsx` の一部を例に示す。

```tsx:/components/layout.tsx
import Head from 'next/head'
import styles from './layout.module.css'
import utilStyles from '../styles/utils.module.css'
import Link from 'next/link'
import B from '../lib/basepath';
...
          <>
            <Link href={B("/")}>
              <a>
                <img
                  src={B("/images/profile.jpg")}
                  className={`${styles.headerImage} ${utilStyles.borderCircle}`}
                  alt={name}
                />
              </a>
            </Link>
            <h2 className={utilStyles.headingLg}>
              <Link href={B("/")}>
                <a className={utilStyles.colorInherit}>{name}</a>
              </Link>
            </h2>
          </>
...
```
`index.tsx` にある動的ルーティングは `href` ではなく `as` の方を囲う。

```tsx:/pages/index.tsx
...
              <Link href="/posts/[id]" as={B(`/posts/${id}`)}>
                <a>{title}</a>
              </Link>
...
```

次に、 npm scripts を設定する。

```json:package.json
...
  "scripts": {
    "dev": "next dev",
    "build": "env NODE_ENV='production' next build",
    "start": "next start",
    "export": "next export"
  },
...
```

`build` に環境変数 `production` を設定し、静的サイトを生成する `export` を追加。

最後に以下を参照して `.github/workflows/gh-pages.yml` を追加し、GitHub のパブリックリポジトリに PUSH すれば良い。

- [React Next アプリを GitHub Actions で GitHub Pages にデプロイ - Qiita](https://qiita.com/peaceiris/items/9c569125b25fc090c515)


# 調べたこと

## 相対パスで参照する

チュートリアルの例では `layout.tsx` などのコンポーネントが参照をもつ場合、相対位置を予め決めることができず破綻する。

## Issue

類似の Issue がいくつか上がっているものの、現時点 (2020/5/14) で未解決。

- [Use relative URLs instead of absolute · Issue #2581 · zeit/next.js](https://github.com/zeit/next.js/issues/2581)

どうやら結構難しいらしい。

## Multi Zones

同じホスト上に別のパスで複数のアプリケーションをデプロイするための機能。

- [Advanced Features: Multi Zones | Next.js](https://nextjs.org/docs/advanced-features/multi-zones)

今回とは別な環境向けのような気がした。

## 先人たち

いずれも `next/link` のラッパーコンポーネントを作ってパスを修正する方法。

- [Next.js app on Github Pages - ITNEXT](https://itnext.io/next-js-app-on-github-pages-768020f2b65e)
- [Deploy your NextJS Application on a different base path (i.e. not root)](https://levelup.gitconnected.com/deploy-your-nextjs-application-on-a-different-base-path-i-e-not-root-1c4d210cce8a)

通常のリンクで使う `href` と動的ルーティングで使う `as` の振り分けを実装する力がなかったのと、`img` や `link` などの参照を含むあらゆるタグのラッパーコンポーネントを用意するのが辛そうだったのでやめた。そのほか、外部リンクと内部リンクの振り分け、ないしタグの使い分けも必要になりそう。