CloudFront + S3向けに Nuxt.js の generate + subFolders を使おうとして大ハマリした件のメモ

# はじめに

現在作成した Nuxt.js のプロジェクトを Amazon CloudFront + S3 で公開したいと考えている。
というか、これができるフレームワークとして `generate` によるSSR対応の静的ファイル生成がある Nuxt.js を選んだ、というのが技術選定の理由。
そのため、この `generate` 周りを調べてみていてかなり時間を使ったので、書き記しておく。

利用しているnuxt.jsのバージョンは `v2.12.2` となる


# subFolders の有効化に至るまで

ここでは、以下の `pages` を対象とする。
vueファイルは `create-nuxt-app` で自動生成される `index.vue` をコピペしただけ。


```bash
$ tree pages/
pages/
├── README.md
├── index.vue
└── index2.vue
```

これをデフォルトの状態で `generate` すると、以下のようなhtmlファイル郡が `dist` 以下に出力される。

```bash:関連するhtmlファイルのみ表示
$ tree dist/
dist/
├── index.html
└── index2
    └── index.html
```

これはモチベーションであるAmazon CloudFront + S3を使う場合、使えなくはないが厄介な形式である。


## なぜコレが問題か

以下のインフラ仕様によるもの。 
ここでは index.html をルートオブジェクトとして指定したくない、というモチベーションがあるということだけ分かれば読み飛ばしOK。

Amazon CloudFront + S3をオリジンとして使う場合、少々厄介な仕様がある。

- ウェブディストリビューションのルート URL が指定された場合 (`GET /` の場合) は返すオブジェクトを指定できる
    - デフォルトルートオブジェクトの設定: https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html
- サブディレクトリについてはこれを指定できない ( `GET /index2/` は `/index2/index.html` を意味しない)
    - 解決策としては２つある
        - S3 を Static Web Hosting として公開してウェブサーバーとして、ウェブサーバーにリクエストを渡すようにする方法
            - 渡されたリクエストをS3がウェブサーバーとして解釈してくれるため index.html （などの設定したデフォルトを）返すようにできる
            - しかしこの方法はS3を公開することになるため、プライベートなデータ（CloudFrontの署名付きクッキー経由でアクセスするものなど) を持っている場合は許容できない
        - Lambda@Edge を使ってパスを書き換えて対応する方法
            - このパターンはLambdaの実行料金が別途かかる


# generate#subFolders

で、この方式は何とかできないかと探していたら、そもそも目的のキーワードにたどり着くまでにすごく苦労しました。
昨日の `extractCSS` の件といい、最初にコンフィグを全部読むのではなく道具の使い方のシナリオベースで探すからか、なかなか見つからない。

結局、これを実現するためには `nuxt.config.js` 内の `generate` で `subFolders: false` を指定する、ということでした。

https://nuxtjs.org/api/configuration-generate/#subfolders

これを指定した結果、

```bash
$ tree dist/
dist/
├── index.html
└── index2.html
```

となり、希望どおりのHTMLファイルが出力されました。


# nginxで見てみよう

これらの HTML ファイルをローカルで開くとページの中身は見れました。
ただ、JavaScriptのパスが `/_nuxt` となっており、ローカルから開いた場合はJavaScriptがうまく読み込めていない状態でした。
そこで、ローカルに nginx を立てて、generateで出力した `dist` ディレクトリをドキュメントルートとして表示してみました。

## Access to `http://localhost`

正常。

![SnapCrab_NoName_2020-4-26_3-2-17_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/7c820718-1218-5d43-3a17-a97116cd93f4.png)


## Access to `http://localhost/index.html` 

ファイルに `index.html` はあるので、アクセスできて当然……だと思ったら、

![SnapCrab_NoName_2020-4-26_3-2-41_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/bf8a108b-ca96-b275-3d8f-1eac065e86df.png)

アクセスはできている。 できているが…… **This page could not be found** ... ページが表示されているにも関わらず、見つからないとな？

## Access to `http://localhost/index2.html` 

同様。 index.html はまだしも、普通に作ったページすら表示できていないのですかそうですか……

![SnapCrab_NoName_2020-4-26_3-2-59_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/3de06dad-d9a1-8858-396e-fa871c19e71e.png)


# path, route, fileの理解

同じような問題を書いてくれている人の記事を発見。

> index.vueからindex.htmlが作られる、というのはまあ良いのですが、しかしそのindex.htmlのrouteはあくまでも/であって、/index.htmlではない。ので、/にアクセスしてDirectoryIndexとしてindex.htmlが表示されるならば何の問題もないですが、/index.htmlにアクセスしなければ表示できないとなると、routeが一致しないためきちんと動きません。
> 
> [nuxt generateして出来たindex.htmlにアクセスすると正しく動かない件 - https://qiita.com/aql/items/11b0f9743dff982c2526](https://qiita.com/aql/items/11b0f9743dff982c2526)

Vue.jsを生で触ったことがないので、このあたりの仕組みはそもそも頭になかったです。 これを読んで概念は理解できましたが、`subFolders` の説明にせめて注意書きぐらいは欲しかった。。。

まあ、これが分かったので、`npm run dev` (開発モード) と `npm run generate` (本番生成) で出力した両方についでに同じパスでアクセスできるようにしていきましょう。


# 開発モードのルーティングの修正

`subFolders` はあくまで `generate` のプロパティなので、開発モードとは関係ない。
しかし、開発モードでも generate で作成されるURLと同じものでアクセスを行えるようなルーティング設定を行う。

設定は `nuxt.config.js` 内の [router#extendRoutes プロパティ](https://ja.nuxtjs.org/api/configuration-router/#extendroutes)で行う。 この引数 `routes` にはNuxtが自動生成したルーティング情報の配列が渡されるので、これをこのタイミングで書き換える・追加・削除することでルーティングを変更できる。
具体的には以下のような自動生成されたルーティング情報が入ってくる。

```javascript:routesの要素の一部
routes[0] = {
  name: 'index',
  path: '/',
  component: '[nuxtプロジェクトのルート]/pages/index.vue',
  chunkName: 'pages/index'
}
routes[1] = {
  name: 'index2',
  path: '/index2',
  component: '[nuxtプロジェクトのルート]/pages/index2.vue',
  chunkName: 'pages/index2'
}
```

この `path` が nuxt 上で実際にアクセスしたときに有効となるパスである。
上記参考記事では `index.vue` に対応する path は `/` のみであり、`/index.html` が存在しないため、`index.html`へのアクセスが失敗するようになっている。

今回は以下のルールでルーティングを変更する。

- トップレベルの `/` のみ `/index.html` を参照する形で有効にする（エイリアスを利用して別名をつける）
- それ以外のパスは subFolders: false として出力される generate の出力結果 html ファイルパスと同じになるようにする。

```javascript:nuxt.config.js
export default {
  mode: 'universal',
  // 中略
  router: {
    extendRoutes(routes, resolve) {
      routes.forEach(r => {
        if (r.path.endsWith('/')) {
          // "/" で終わっている場合は index.html を指定
          r.path += 'index.html';
        } else {
          // それ以外はファイル名 + .html の形式になるため
          // パス名を .html 付きに修正
          r.path += '.html';
        }
      });
      // CloudFront の仕様対応
      // ドキュメントルートのみルートオブジェクトを追加
      routes.push({
        path: '/index.html',
        alias: '/',
        component: resolve(__dirname, 'pages/index.vue'),
      });
    }
  },
  // 後略
}
```

なお、ここでは設定ファイルの直接的な修正を行っているが、map/filterなどを使ったイミュータブルな設定で書き直しても良い。

この設定を行った後 `npm run dev` として開発サーバーを `http://localhost:3000` で起動した場合、以下のアドレスではアクセスができる。

- http://localhost:3000
- http://localhost:3000/index.html
- http://localhost:3000/index2.html

一方、この変更を行う前までは有効であった

- http://localhost:3000/index2

についてはアクセスできなくなっている (This page could not be found)。 

# generate してみると…

問題は解決したので `npm run generate` をしてみる。
その結果がこちら。

```bash
$ tree dist/
dist/
├── index.html.html
├── index2.html.html
└── sub.html.html
```

ええ……、何か余計な `.html` がついてらっしゃる…。
なお、このファイルを直接URLを入力して叩いてみてもNuxt.jsで設定したpathと設定が異なるのでnot foundのエラーメッセージが出る。

どうも[ソースコードのここら](https://github.com/nuxt/nuxt.js/blob/dev/packages/generator/src/generator.js#L247)で単純に追加している模様。

ただ、このページ名についても変更したいと思う需要は（今回みたいに）あると思うので何かないかを調べてみると[Generator のフックである generate:page](https://ja.nuxtjs.org/api/internals-generator/#%E3%83%95%E3%83%83%E3%82%AF) で対応可能との事。

pageの引数はドキュメントに書かれている通り、`route, path, html` を属性に持つオブジェクトである。 今回はここの `path` を書き換えれば出力されるファイル名も変わる。 
今回は実施しないが、`html` が出力されたHTMLなので、ここを無理やり書き換えるということすら可能である。


生成後のHTMLページ名を修正するフックを以下のように記載する。

```javascript:nuxt.config.js
  // 前略
  generate: {
    subFolders: false,
  },
  hooks: {
    generate: {
      async page(p) {
        if (p.path.endsWith('.html.html')) {
          p.path = p.path.substr(0, p.path.length - 5);
        }
      }
    }
  }
  // 後略
```

`.html.html` というファイル名で作られてしまう場合、そこから末尾５文字を取り除いて `.html` という拡張子にしている。


# 再び nginx でのアクセス

以下の３つのアクセスで全てアクセスが可能になっている（エラーメッセージが出力されない）。

- http://localhost/
- http://localhost/index.html
- http://localhost/index2.html

![SnapCrab_NoName_2020-4-26_3-2-17_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/7c820718-1218-5d43-3a17-a97116cd93f4.png)


nuxt.js入門２日目。 まだファイルが増えていくと問題が起こるかもしれませんが、開発を続けられる設定にすることができました。


# 参考

- https://qiita.com/teriyakisan/items/bdd7079cf4ab3f285a64
- https://qiita.com/resistance_gowy/items/70d2e07125a0c4857db5