viteSSGとvue3で静的ページをhostingで公開

vitePressでもいいのですが、基本を勉強しようとVue3とViteSSGを使って静的ページの生成をやります。

## 目的
* WebpackやViteでMarkdownからHTMLの書き出しをしてたのでVueでもやりたい
* hostingサービスで使えるページを生成
* LPやコーポレートサイトにVueの便利さを追加したい
* コンポーネントを使いまわしたい
* ページのロード遅延を少なくしたい
* 画像やCSSの圧縮したい

## 参考

https://ja.vuejs.org/

https://ja.vitejs.dev/

https://github.com/antfu-collective/vite-ssg

https://firebase.google.com/docs/hosting?hl=ja

## Node
node -v 22.2.0

## クイックスタートでvue3を開始

```
npm create vue@latest
```
ターミナルの指示に従って適切な環境を作成する。
個人的には以下。

```
✔ Project name: … vue-project
✔ Add TypeScript? …  Yes
✔ Add JSX Support? … No 
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No
```

* TSは使いたい
* vue構文で記述するのでJSXは不要
* 下層ページを作るのでvue-routerは必要
* データ保存が必要な場合は、SSGしないのでPiniaは不要
* 簡単なページなので、テストは不要。複雑な場合は必要
* ESlintは後で独自にいれるので不要
* DevToolsはどっちでもいいが、Pinia使ってないしSSGだけなら不要

## ESLintとPrettierとVSCode
VScodeのESLintが保存時に遅延して保存できないissueがあるので、無効化した。

https://github.com/microsoft/vscode-eslint/issues/1902

代わりに`Run on Save`で保存時にpackage.jsonの`npm run lint`を実行する。

https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave

## packageのアップロード(注意)

`npm-check-updates`で全部のpackageをアップロードする。ESLint8から9への更新は注意する

https://github.com/raineorshine/npm-check-updates

```
ncu
```
以下の更新がある
```
 @rushstack/eslint-patch    ^1.8.0  →  ^1.10.4
 @types/node              ^20.14.5  →  ^22.5.4
 @vitejs/plugin-vue         ^5.0.5  →   ^5.1.3
 eslint                    ^8.57.0  →  ^9.10.0
 eslint-plugin-vue         ^9.23.0  →  ^9.28.0
 npm-run-all2               ^6.2.0  →   ^6.2.2
 prettier                   ^3.2.5  →   ^3.3.3
 typescript                 ~5.4.0  →   ~5.6.2
 vite                       ^5.3.1  →   ^5.4.4
 vue                       ^3.4.29  →   ^3.5.4
 vue-router                 ^4.3.3  →   ^4.4.4
 vue-tsc                   ^2.0.21  →   ^2.1.6

Run ncu -u to upgrade package.json
```

### ESLint9への更新
ESLint8からESLint9は破壊的な更新なので、注意。ESLint10にflat構文が基準になるので、新しくサービスを作成するならESLint9を勉強しておく

### typescriptは5.4で止めとく
`@typescript-eslint/typescript-estree.`がWARNINGを出す

```
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=4.7.4 <5.6.0
```

typescript5.6にupgradeした場合、npmでは削除してから入れ直す
```
npm rm typescript
npm i -D typescript@5.4  
```

## ESLint9のセットアップ

```
npm init @eslint/config@latest
```
ターミナルの指示に従って適切な環境を作成する。
個人的には以下。

```
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · typescript
✔ Where does your code run? · browser, node
The config that you've selected requires the following dependencies:

eslint, globals, @eslint/js, typescript-eslint, eslint-plugin-vue
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · npm
```
`eslint.config.js`が作成される。`eslint.config.ts`と変更してts化。
vueのセットアップで作成された、`.eslintrc.cjs`があったら不要なので削除。`eslint-plugin-vue`や他のpackageがセットアップ時にはいるので`@vue/eslint-config-prettier`や`@vue/eslint-config-typescript`も不要になる。あるとESLint9をサポートしてないので、Errorになる

## run lintの追加
```
"lint": "eslint --fix 'src/**/*.{js,ts,vue}'"
```
src配下のファイルだけlintをかける
WARNINGが出た場合には、上記のtsのダウングレードする

## firebase の設定(任意)
hostingだけなので、パッケージのインストールはない

```
firebase init
```
ViteSSGなのでFrameworkは使用しない
hostuingを選択して、projectを選択
チームメンバーがいるならgithubワークフローも追加
viteのbuildはdistフォルダに追加されるので`firebase.json`を書き換える
```
{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

```

## パッケージインストールしてvueの起動を確認

```
npm install
npm run dev
```

## viteSSGと便利なPlugin追加

```
npm i -D vite-ssg
npm i -D vite-plugin-vue-layouts vite-plugin-pages unplugin-vue-router unplugin-vue-markdown critters @unhead/vue vue-gtag vite-ssg-sitemap autoprefixer vite-plugin-compression vue-cookie-accept-decline @macropygia/vite-plugin-imagemin-cache
```
vite-plugin-vue-layouts // unheadのmetaでlayout設定
vite-plugin-pages // pagesフォルダ内のファイルをpageComponentとして設定
unplugin-vue-router // viteSSGのvue-routerのErrorを解消
unplugin-vue-markdown  // Markdownを使えるように
critters // inline CSS化
@unhead/vue // metaタグをpageコンポーネントから設定
vue-gtag // Analyticsの追加
vite-ssg-sitemap // サイトマップの自動生成
autoprefixer // 言わずもがな
vite-plugin-compression // gzip圧縮
@macropygia/vite-plugin-imagemin-cache // 画像圧縮
vue-cookie-accept-decline // GDPR対応

## viteSSGとPluginの設定

READMEを見ながら始める
https://github.com/antfu-collective/vite-ssg

packageのbuildをvitSSGに変更
```
"build-only": "vite-ssg build",
```

main.tsのcreateAppをViteSSGに変更。typesは適切に追加

```ts:main.ts
import './assets/main.css'

import { ViteSSG } from 'vite-ssg'
import App from './App.vue'

import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
import VueGtag from 'vue-gtag'
import { createHead } from '@unhead/vue'
import VueCookieAcceptDecline from 'vue-cookie-accept-decline'

const ga = 'G-XXXXXXXXXXXXXXX'

export const createApp = ViteSSG(
  App,
  { routes: setupLayouts(routes) },
  ({ app, router, routes, isClient, initialState }) => {
    app.use(VueGtag, {
      config: { id: ga, },
    }, router),
    createHead(),
    app.component('vue-cookie-accept-decline',
      VueCookieAcceptDecline)
  },
)
```

```ts:vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import ViteSSGOptions from "vite-ssg";
import VueRouter from 'unplugin-vue-router/vite'
import Layouts from 'vite-plugin-vue-layouts';
import Pages from 'vite-plugin-pages'
import generateSitemap from 'vite-ssg-sitemap'
import imageminPlugin from '@macropygia/vite-plugin-imagemin-cache'
import viteCompression from 'vite-plugin-compression';
import Markdown from 'unplugin-vue-markdown/vite'
import autoprefixer from 'autoprefixer'

const hostname = "https://example.com";
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    VueRouter(),
    vue(),
    Layouts(),
    Pages(),
    Markdown({}),
    imageminPlugin(),
    viteCompression(),
  ],
  css: {
    devSourcemap: true,
    postcss: {
      plugins: [
        autoprefixer({}),
      ]
    }
  },
  ssgOptions: {
    dirStyle: "nested",
    script: "async",
    formatting: "minify",
    format: "esm",
    onFinished: () => {
      generateSitemap({
        hostname: hostname,
        exclude: []
      })
    },
    crittersOptions: {
      preload: 'media',
    },
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
```

```vue:App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>

<template>
  <router-view v-slot="{ Component, route }">
    <component :is="Component" :key="route" />
  </router-view>
</template>

```

ここまで設定したら、buildしてみる。hostingで動くかも確認

```
npm run build
```

以上で基本的な設定は終了です。

ViteSSGは`about/index.vue`を`about.html`変換するので、`dirStyle: "nested",`を追加すると、`about.vue`や`about/index.vue`を`about/index.vue`に書き出してくれる。