Nuxt3のuseHeadを試してみた

# はじめに

Google Tag Managerのように `<script>` タグを埋め込むタイプの3rd PartyツールをNuxt3に適用するための手法を検討した。

https://developers.google.com/tag-manager/devguide?hl=ja

### 実行環境

- MacOS 13.4.1 (Apple M1)
- Node 18.13.0
- Nuxt 3.6.2

# script setupの実行場所

https://zenn.dev/upopon/articles/2282b2c73b68a3

とのこと。実際にやってみる。

```vue:pages/index.vue
<script setup>
  console.log(process.server)
  console.log(process.client)
</script>
```

↓ `http://localhost:3000/` 表示

```:サーバログ
true
false
```

```:クライアントログ
false
true
```

上記記事の通り、サーバとクライアント両方で実行されており、`process.server`, `process.client` で判定できることが確認できた。

# useHead呼び出し
## titleを上書き

```vue:pages/index.vue
<script setup>
const env = process.server ? 'Server' : 'Client'
useHead({
  title: `TOP titled | ${env}`
})
</script>
```

- リロードすると一瞬は `TOP titled | Server` と表示される
- すぐに `TOP titled | Client` に切り替わる

→サーバとクライアント両方で実行されている。

### useServerHeadを使ってみる

`useServerHead` なる関数を発見したので試してみる。

```vue:pages/index.vue
<script setup>
const env = process.server ? 'Server' : 'Client'
useServerHead({
  title: `TOP titled | ${env}`
})
</script>
```

- `TOP titled | Server` と表示されたままである

→サーバのみで実行されるよう関数内で分岐している模様。

## `<script>` を挿入

`<script>` タグを埋め込む場合は `useHead` の引数の `script` が利用できる。


`tagPriority`の基準は以下の通り（数字が小さいほど優先度高）

- -2: `<meta charset ...>`
- -1: `<base>`
- 0: `<meta http-equiv="content-security-policy" ...>`
- 10: 上記以外のタグのデフォルト値

https://github.com/unjs/unhead/blob/b58c9501e2ef119918261938942e2fe05a9eda53/packages/schema/src/tags.ts#L67

```vue: pages/script-insertion.vue
<script setup>
  const config = useRuntimeConfig()
  const env = process.server ? 'Server' : 'Client'
  useHead({
    title: "useHeadデモ",
    script: [
      {
        type: "text/javascript",
        innerHTML: `console.log("${env} script loaded(5)")`,
        tagPriority: 5
      },
      {
        type: "text/javascript",
        innerHTML: `console.log("${env} script loaded(4)")`,
        tagPriority: 4
      }
    ]
  })
</script>
```

↓ `http://localhost:3000/script-insertion` 表示

```: コンソールログ
script-insertion:5 Server script loaded(4)
script-insertion:6 Server script loaded(5)
chunk-VWWL2I6E.js:2710 <Suspense> is an experimental feature and its API will likely change.
VM1678:1 Client script loaded(4)
VM1679:1 Client script loaded(5)
```

```html: 生成されたHTML(DevToolsで確認)
<head>
  <meta charset="utf-8">
  <title>useHeadデモ</title>
  <script type="text/javascript">console.log("Server script loaded(4)")</script>
  <script type="text/javascript">console.log("Server script loaded(5)")</script>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link ...>
  <script>...</script>
  <style>...</style>
  <script type="text/javascript">console.log("Client script loaded(4)")</script>
  <script type="text/javascript">console.log("Client script loaded(5)")</script>
  <!-- 以下略 -->
</head>
```

- Serverで追加した `<script>` は設定した優先度通り　`<title>` と `<meta>` の間に挿入
- Clientで追加した `<script>` は初期表示時の `<style>` より後に表示
  - Clientで追加したスクリプト同士の優先度は有効

### NuxtConfigで挿入するscriptを定義

全ページ共通で挿入するscriptはNuxtConfigで定義することもできる。

```ts:nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          type: "text/javascript",
          innerHTML: `console.log("Config script loaded")
window.onload = () => {console.log("page is fully loaded")}
window.addEventListener('DOMContentLoaded', () => {console.log('DOM fully loaded and parsed')})`,
          tagPriority: 9
        }
      ]
    }
  },
  runtimeConfig: {
    public: {
      priorityOfBeforeCommon: PRIORITY_OF_BEFORE_COMMON
    }
  }
})
```

↓ `http://localhost:3000/script-insertion` 表示

```: コンソールログ
script-insertion:5 Server script loaded(4)
script-insertion:6 Server script loaded(5)
script-insertion:7 Config script loaded
script-insertion:9 DOM fully loaded and parsed
script-insertion:8 page is fully loaded
chunk-VWWL2I6E.js:2710 <Suspense> is an experimental feature and its API will likely change.
VM2198:1 Client script loaded(4)
VM2199:1 Client script loaded(5)
```


```html: 生成されたHTML(DevToolsで確認)
<head>
  <meta charset="utf-8">
  <title>useHeadデモ</title>
  <script type="text/javascript">console.log("Server script loaded(4)")</script>
  <script type="text/javascript">console.log("Server script loaded(5)")</script>
  <script type="text/javascript">
    console.log("Config script loaded")
    window.onload = () => {console.log("page is fully loaded")}
    window.addEventListener('DOMContentLoaded', () => {console.log('DOM fully loaded and parsed')})
  </script>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link ...>
  <script>...</script>
  <style>...</style>
  <script type="text/javascript">console.log("Client script loaded(4)")</script>
  <script type="text/javascript">console.log("Client script loaded(5)")</script>
  <!-- 以下略 -->
</head>
```

- Configのscriptの優先度はServerのscriptと同じレベルで判断される
- Config, ServerのscriptはDOM読み込み時にすでに存在する
  - ↑`DOMContentLoaded`より前に読み込まれることから

# まとめ

サーバサイドでuseHeadするのがよさそう

# 補足

GTMであれば `@gtm-support/vue-gtm` でもよさそう。

https://www.npmjs.com/package/@gtm-support/vue-gtm