---
# System prepended metadata

title: Nuxt
tags: [Vue]

---

# Nuxt





## References

+ 📑 [**Doc - Nuxt 3**](https://nuxt.com/)
+ 📘 [**Vue 開發者升級指南 - Nuxt 3 入門：打造 SSR 專案**](https://books.google.com.tw/books/about/Vue%E9%96%8B%E7%99%BC%E8%80%85%E5%8D%87%E7%B4%9A%E6%8C%87%E5%8D%97_Nuxt3_%E5%85%A5%E9%96%80.html?id=I6Y4EQAAQBAJ)
+ 🎬 [**Nuxt 3 - Course for Beginners**](https://youtu.be/fTPCKnZZ2dk)
+ 🎬 [**企鵝先生 Mr.Penguin - Nuxt3 怎麼優化網頁的 SEO？**](https://youtu.be/mTfCZ43rG9Y)
+ 🎬 [**企鵝先生 Mr.Penguin - Nuxt3 怎麼做到 API 生成 HTML 做 SSR？**](https://youtu.be/42DKs_tvuhs)
+ 🔗 [**iThome - Nuxt 3 實戰筆記**](https://ithelp.ithome.com.tw/users/20152617/ironman/6959)
+ 🔗 [**Claire's Note - Nuxt 3**](https://clairechang.tw/categories/Nuxt3/)





## Installation

### create project
  ```
  pnpm create nuxt@latest
  ```

### eslint
  詳見 [ESlint (Nuxt)](https://hackmd.io/@RogelioKG/eslint#Nuxt)
  若要直接使用大神 Antfu 的設置，詳見 [ESlint (Antfu)](https://hackmd.io/@RogelioKG/eslint#antfueslint-config)

### tailwind
  詳見 [Tailwind-Installation (Nuxt)](https://hackmd.io/@RogelioKG/tailwind#Installation)





## Rendering Modes

### [client-side rendering](https://nuxt.com/docs/guide/concepts/rendering#client-side-rendering)

#### about

![image](https://hackmd.io/_uploads/SkVB5Dkd1l.png)


#### example

詳見：[enable-CSR](#enable-CSR) (只需一行配置解決)


### [universal rendering](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering)

#### about

![](https://hackmd.io/_uploads/SJBga4wu1x.png)

![](https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png)

#### exapmle

```html
<script setup lang="ts">
const counter = ref(0) // 在 server 和 client 端都會執行

const handleClick = () => {
  counter.value++ // 只在 client 端執行
}
</script>

<template>
  <div>
    <p>Count: {{ counter }}</p>
    <button @click="handleClick">Increment</button>
  </div>
</template>
```


### [hybrid rendering](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering)

#### about

詳細指定不同路由頁面的渲染策略

#### example

詳見：[hybrid rendering example](#hybrid-rendering-example)





## Feature

### auto import

🏔️ <mark><u>Vue API, Nuxt API, components 的自動 import 與 tree-shaking</u></mark>

|🚨 <span class="caution">CAUTION</span>|
|:---|
|在 `.nuxt` 目錄會自動創建一個<br />  + `imports.d.ts` 去聲明使用到的 Vue API, Nuxt API<br />  + `components.d.ts` 去聲明使用到的 components<br />以便提供良好的 IDE 提示 |
| 要 `nuxi dev` 背景運行，才能自動更新這些檔案 |

如此一來，就不用像 Vue 中
  + 使用 Vue API 時，需 `import { ref } from 'vue'`
  + 使用自己寫的 component 時，需 `import Card from '@/components/Card.vue'`


### `pages/`

🏔️ <mark><u>使用檔案系統的巢狀特性，管理巢狀路由</u></mark>

+ ❌ `router/index.ts`：Vue 管理路由的方式，我們不需要寫這個了
+ ✅ `pages/*.vue`：每個在 `pages` 目錄底下的 Vue 檔案，會自動辨識為一個頁面
    + `pages/index.vue`：index.vue 為根頁面
    + `pages/[id].vue`：動態路由，例如 `/123`、`/abc`。可用 `useRoute().params.id` 獲取值
    + `pages/blog/[...slug].vue`：表示捕獲剩餘路由，`/blog` 將導向 index.vue 頁面，假如 blog 目錄底下只有 index.vue，那麼剩餘的路由 `/blog/` 將導向 [...slug].vue。可用 `useRoute().params.slug` 獲取值


### `components/`

🏔️ <mark><u>以 camel-case 命名，管理元件</u></mark>

|📗 <span class="tip">TIP</span>|
|:---|
|若目錄過深導致元件名稱落落長，可手動 import。<br />如 `import Input from '~/components/Form/Input/Group/index.vue'`。|

```
components/
├── Card/
│   ├── index.vue
│   └── Item.vue
└── Todo/
    ├── index.vue
    └── Item.vue
```

Nuxt 會將如上目錄結構視為
+ `Card` 元件：`components/Card/index.vue`
+ `CardItem` 元件：`components/Card/Item.vue`
+ `Todo` 元件：`components/Todo/index.vue`
+ `TodoItem` 元件：`components/Todo/Item.vue`

### `public/` & `assets/`

🏔️ <mark><u>靜態資源</u></mark>

#### `public/`
- 資源不會經過 Vite 或 Webpack 的後處理
- 資源可透過 URL 直接訪問
  - `public/logo.png` → `https://example.com/logo.png` 
  - `public/favicons/favicon.ico` → `https://example.com/favicons/favicon.ico`

#### `assets/`
- 資源會經過 Vite 或 Webpack 的後處理 (壓縮與 hash)
- 資源無法透過 URL 直接訪問
- 會被 import 進元件的資源，通常歸類於此
    ```html
    <template>
      <img :src="logo" alt="Logo" />
    </template>
    
    <script setup lang="ts">
    import logo from '@/assets/logo.png'
    </script>
    ```

### `plugins/`
🏔️ <mark><u>[註冊插件](https://clairechang.tw/2023/07/10/nuxt3/nuxt-v3-plugins/)、注入全域變數</u></mark>

+ 注入全域變數
  ```ts
  // plugins/hello.ts
  export default defineNuxtPlugin(nuxtApp => {
    // 方法一
    nuxtApp.provide('hello', (msg: string) => `Hello ${msg} !`);

    // 方法二 (✅有型別提示，較好)
    return {
      provide: {
        hello: (msg: string) => `Hello ${msg} !`
      }
    };
  })
  ```

  ```html
  <!-- pages/index.vue -->
  <script setup lang="ts">
  const { $hello } = useNuxtApp() // 以 $hello 調用
  </script>
  ```

+ 註冊插件

  ```ts
  import Notifications, { useNotification } from '@kyvg/vue3-notification'
  const { notify } = useNotification()

  export default defineNuxtPlugin(nuxtApp => {
    // 全域註冊插件，讓所有的元件都能使用插件
    nuxtApp.vueApp.use(Notifications)
    // 注入全域變數 $notify
    return {
      provide: {
        notify,
      },
    }
  })
  ```

  ```html
  <script setup lang="ts">
  const { $notify } = useNuxtApp()
  onMounted(() => {
    $notify({
      type: 'success',
      title: 'Notification Title',
      text: 'Notification Text',
    })
  })
  </script>
  ```

+ 插件僅在應用啟動時載入

+ 僅限於 client 或 server 載入

  檔名加上 `.client` 或是 `.server` 後綴即可。

  |🚨 <span class="caution">CAUTION</span>|
  |:---|
  |  當我們使用的第三方套件定義了 window、document 等瀏覽器全域變數，<br />直接定義 plugin，執行時可能會拋錯誤 window is not defined，<br />因為 server 端預渲染時找不到變數，<br />這時候就可以加上 `.client` 後綴來限制載入時機。|

+ [object syntax](https://youtu.be/2aXZyXB1QGQ)
  這種寫法可以細項調整插件載入的更多細節，尤其是插件彼此有依賴關係時。

  |🚨 <span class="caution">CAUTION</span>|
  |:---|
  |插件預設的載入順序是按照 alphabetical，而且是同步載入。|

  ```ts
  export default defineNuxtPlugin({
    name: 'depends-on-my-plugin', // 插件名稱
    dependsOn: ['my-plugin'], // my-plugin 插件載入完後，才會載入這個插件 (依賴關係)
    parallel: true, // 當插件載入到一半進入 await 時，先跳出並載入其他插件
    async setup (nuxtApp) {
      // 插件載入
    }
  })
  ```

### `middleware/`

🏔️ <mark><u>攔截 navigation 並做一些處理的中介層</u></mark>

+ [more info....](https://clairechang.tw/2023/09/05/nuxt3/nuxt-v3-middleware/)

### `layouts/`

🏔️ <mark><u>版型模板 (如果只有一種版型，那直接寫在 App.vue 內就好)</u></mark>

+ [more info....](https://clairechang.tw/2023/09/01/nuxt3/nuxt-v3-layouts/)


## Config

`nuxt.config.ts`

### env var


在模板中可直接使用 `$config` 調用，
在 TS 中使用 `useRuntimeConfig()` 調用。

```ini
# .env
NUXT_API_SECRET=your-secure-api-key
NUXT_API_BASE=/new-api
```

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 非 public：只能在 server 讀取
    apiSecret: process.env.NUXT_API_SECRET,
    // public：可在 client 或 server 讀取
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
    },
  },
})
```
```html
<!-- app.vue -->
<template>
  <div id="app">
    <p>{{ config.public.apiBase }}</p>
    <p>{{ $config.public.apiBase }}</p>
  </div>
</template>

<script setup lang="ts">
const config = useRuntimeConfig()
</script>

<style scoped lang="css">
</style>
```

在執行時，都可以在命令行給定環境變數

```
NUXT_API_SECRET=123 pnpm dev
```

### &lt;head&gt; info

由於 Nuxt 沒有 index.html，因此 head 內容都在 config 裡設定<br />(比如 `title`、`meta`、`script` 等)

font awesome
```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://kit.fontawesome.com/024f8fd2b4.js',
          crossorigin: 'anonymous',
          defer: true,
        },
      ],
    },
  },
})
```

bootstrap

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      link: [
        {
          rel: "stylesheet",
          href: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
          integrity: "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH",
          crossorigin: "anonymous",
        },
      ],
      script: [
        {
          src: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js",
          integrity: "sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r",
          crossorigin: "anonymous",
          defer: true,
        },
        {
          src: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js",
          integrity: "sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy",
          crossorigin: "anonymous",
          defer: true,
        },
      ],
    },
  },
})
```

### diff config for diff env

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  // 全域設定
  ...
  // 開發環境
  $development: {
    app: {
      ...
    },
  },
  // 部署環境
  $production: {
    app: {
      ...
    },
  },
  // 測試環境
  $test: {
    app: {
      ...
    },
  },
})
```

### enable CSR

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: false
})
```

### hybrid rendering example

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },
    // Products page generated on demand, revalidates in background, cached until API response changes
    '/products': { swr: true },
    // Product pages generated on demand, revalidates in background, cached for 1 hour (3600 seconds)
    '/products/**': { swr: 3600 },
    // Blog posts page generated on demand, revalidates in background, cached on CDN for 1 hour (3600 seconds)
    '/blog': { isr: 3600 },
    // Blog post page generated on demand once until next deployment, cached on CDN
    '/blog/**': { isr: true },
    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },
    // Add cors headers on API routes
    '/api/**': { cors: true },
    // Redirects legacy urls
    '/old-page': { redirect: '/new-page' }
  }
})
```





## CLI

`nuxi` 是 Nuxt 的 CLI 工具

### references
+ 📑 [**Doc - nuxi**](https://nuxt.com/docs/api/commands)
+ 🔗 [**iThome - nuxi**](https://ithelp.ithome.com.tw/articles/10319420)

### `nuxi add`
  可以快速新增樣板檔案。\
  比如 `pnpm exec nuxi add component TheHeader`\
  能快速產生 `components/TheHeader.vue` 檔案。





## Built-in Components

### `NuxtLink`
+ `external` : 表示導向外部連結，提示 Nuxt 不要做 client nav





## Built-in API

### references

+ 📑 [**Doc - API**](https://nuxt.com/docs/api)
+ 🔗 [**Nuxt 3 發送 API 請求資料 - &#36;fetch / useAsyncData / useFetch**](https://ithelp.ithome.com.tw/articles/10326675)
+ 🎬 [**Alexander Lichter - useAsyncData vs. useFetch**](https://youtu.be/0X-aOpSGabA)

### components

#### `resolveComponent`

若要在 Nuxt 中使用 dynamic component，使用 import #components 或 resolveComponent 引入元件

```html
<script setup lang="ts">
import { SomeComponent } from '#components'

const MyButton = resolveComponent('MyButton')
</script>

<template>
  <component :is="clickable ? MyButton : 'div'" />
  <component :is="SomeComponent" />
</template>
```

### data fetching

#### `$fetch`

```ts
// GET /api/result?page=2&otherParam=1
const data = await $fetch('/api/result', {
  query: {
    page: page.value,
    otherParam: otherParam.value,
  },
})
```

|📗 <span class="tip">TIP</span> : param|
|:---|
|`options` : 剩餘選項<br />+ `method` : HTTP Method<br />+ `query` : 指定 URL query (<mark>無法給定 ref</mark>)|

|📘 <span class="note">NOTE</span>|
|:---|
|`$fetch` 使用官方自肥的 fetch API - [ofetch](https://github.com/unjs/ofetch) 實現。|
|`$fetch` 基本和原生 fetch API 無異，但有針對 SSR 優化，若在 SSR 階段時使用 `$fetch`，會直接以函數呼叫代替 HTTP request 以節省時間。|

|🚨 <span class="caution">CAUTION</span>|
|:---|
|若在 component 最外層中直接使用 `$fetch`，會造成兩次的 API call，一次是 SSR 階段，一次是 Hydration 階段，造成效能浪費。建議使用 `useFetch` 或 `useAsyncData + $fetch` 的組合。|

#### [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data)

```ts
// GET /api/result?page=2&otherParam=1
const { data } = await useAsyncData('result', async () => $fetch('/api/result', {
  query: {
    page: page.value,
    otherParam: otherParam.value,
  },
}), {
  watch: [page, otherParam],
})
```

|📗 <span class="tip">TIP</span> : param|
|:---|
|`key` : 唯一 key|
|`handler` : 返回資料 Promise 的函數 (通常是 fetch API)|
|`options` : 剩餘選項<br />+ `lazy` : 若為 true，等到對應的 DOM 元素存在時，才會抓取資料 (<mark>會影響 SEO</mark>)<br />+ `immediate` : 若為 false，先不抓取資料，等到需要時手動執行 `execute()` 抓取資料 (<mark>未抓資料時的狀態為 `idle`</mark>) (<mark>會影響 SEO</mark>)<br />+ `server` : 若為 false，表示此 request 僅限在 client 端執行，不讓它在  SSR 階段先執行 (<mark>會影響 SEO</mark>)<br />+ `watch` : 追蹤 URL query 用到的響應式參數，一旦響應式參數有變化，再重新 request 一次|

|📗 <span class="tip">TIP</span> : return|
|:---|
| `data` : 抓取成功的資料 (響應式) |
| `error` : 抓取失敗的回應 (響應式) |
| `status` : fetch 狀態 (響應式)，有 <mark>`'idle'` \| `'pending'` \| `'success'` \| `'error'`</mark> 四種<br />(這很好用，可以利用它來做資料加載動畫) |
| `execute` : 手動發起 request 抓取資料的功能函數 |

|🔮 <span class="important">IMPORTANT</span> : 快取機制|
|:---|
| `useAsyncData()` 會將抓到的資料儲存至快取中<br />(快取即 [Nuxt 環境] `useNuxtApp()` 中的 `payload.data`，也可用 `window.__NUXT__.data` 查看)<br />(更準確地說，`useAsyncData()` 是在 SSR 階段將資料儲存至 `payload.data`，並渲染至 HTML。<mark>在 Hydration 階段時，就不會再重新 request 一次了</mark>，但資料會再藉由 `payload.data` 重新填入 DOM 元素，這是為了讓資料綁定響應式，假如你有指定 `options` 的 `watch` 選項 -- 即監控響應式的參數，自動 refresh，就是重新 request 辣 -- 時，畫面才會顯示新的資料。) |
| 給定唯一 key 做為快取物件的 property，抓到的資料做為快取物件的 value|
| 若不給定 key，將依據檔名與行號自動產生<br />(建議手動提供 key，以確保快取的穩定性)|
| 若一個頁面中多個地方使用相同的 key，`useAsyncData()` 會直接存取快取<br />(避免了重複 request)|

|📘 <span class="note">NOTE</span>|
|:---|
|若某 3rd party lib 附帶自己的 fetch API，使用 `useAsyncData` 是更好的選擇|

#### `useFetch`

```ts
// GET /api/result?page=2&otherParam=1
const { data } = await useFetch('/api/result', {
  query: {
    page,
    otherParam,
  },
})
```

|📘 <span class="note">NOTE</span>|
|:---|
|`useFetch(url)` 大致等價於 `useAsyncData(url, () => $fetch(url))`|

### composables

#### `useHead`

設定 `<head>` (可動態配置)

```html
<script setup lang="ts">
useHead({
  title: 'My App',
  meta: [
    { name: 'description', content: 'My amazing site.' }
  ],
  bodyAttrs: {
    class: 'test'
  },
  script: [ { innerHTML: 'console.log(\'Hello world\')' } ]
})
</script>
```

#### `useSeoMeta`

設定 SEO 標籤 (可動態配置)

```html
<script setup lang="ts">
useSeoMeta({
  title: 'My Amazing Site',
  ogTitle: 'My Amazing Site',
  description: 'This is my amazing site, let me tell you all about it.',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: 'https://example.com/image.png',
  twitterCard: 'summary_large_image',
})
</script>
```

#### `useRoute`

獲取 `route`

```html
<!-- pages/[id].vue -->

<script setup lang="ts">
const route = useRoute()
</script>

<template>
  <div>
    <h1>id: {{ route.params.id }}</h1>
  </div>
</template>
```

#### `useState`

全域狀態，類似 Pinia。
詳見 [Alexander Lichter - Why you should use useState()](https://youtu.be/mv0WcBABcIk)

```ts
// composables/useUser.ts
export default function useUser() {
  const userName = useState('username', () => '')
  const setUserName = (name: string) => {
    userName.value = name
  }

  return {
    userName: readonly(userName),
    setUserName
  }
}
```

除此之外它還有一個特點。
在 SSR 情況下，當 server 渲染完畢時，\
資料會留下來並藉由 `<script type="application/json"></script>` 傳到 client。
也就是說，<mark>資料必須是 JSON serializable</mark>。
<mark>在 Hydration 階段，client 會直接拿這筆資料直接渲染，並綁定響應式關係</mark>。
而不會像一般情況重新執行一次。
這可以用在資料產生方式是隨機的局面，
避免了 server rendering 與 client hydration 結果不符 (隨機性)，
從而意外產生 [Hydration Error](#Hydration-Error)。

```html
<template>
  <div id="index">
    <ul>
      <li v-for="link in links" :key="link.name">{{ link.name }}: {{ link.url }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
type Link = { name: string, url: string }
// 只要使用相同 key，就可以在其他元件到用到這個全域狀態
const links = useState<Link[]>('links', () => generateRandomLinks(10))

function generateRandomLinks (n: number): Link[] {
  const links: Link[] = []
  for (let i = 0; i < n; i++) {
    links.push(generateRandomLink(i))
  }
  return links
}

function generateRandomLink (serial: number): Link {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  const length = 10
  let randomChars = ''
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * chars.length)
    randomChars += chars[randomIndex]
  }
  return {
    name: `data ${serial}`,
    url: `https://example.com/${randomChars}`,
  }
}
</script>
```


## Module

+ `@nuxtjs/i18n` 多國語系
  + [Doc - @nuxtjs/i18n](https://v8.i18n.nuxtjs.org/)
  + [Claire's Note - Nuxt.js 3.x 導入 I18n 實作多國語系](https://clairechang.tw/2023/08/29/nuxt3/nuxt-v3-i18n/)
  + 全域翻譯檔要放在 `i18n/locales/` 目錄
+ `@nuxt/content` Markdown Blog
  + [Doc - @nuxt/content](https://content.nuxt.com/)
  + [用 Nuxt Content 重寫我的部落格](https://blog.twjoin.com/%E7%94%A8-nuxt-content-%E9%87%8D%E5%AF%AB%E6%88%91%E7%9A%84%E9%83%A8%E8%90%BD%E6%A0%BC-278ce9e8580c)

+ `@nuxt/ui` UI
  + [Doc - @nuxt/ui](https://ui.nuxt.com/)

[Nuxt 環境]: https://nuxt.dev.org.tw/docs/guide/going-further/nuxt-app#the-nuxt-context





## Console Error

### Hydration Error
+ `Hydration completed but contains mismatches.`