MonkeyBinBin

被程式設計的猴子

Nuxt.js

Nuxt 系列 - #4 自訂 HTML 模板、Header、佈局與 Pages


了解如何調整畫面顯示相關的設定來滿足需求,除了基本的 Pages 與 Layout vue 組件的開發之外,也可以調整 HTML 模板還有 Head 可達到不同頁面使用不同的 meta 設定,優化 SEO。

Nuxt 視圖架構

了解 Nuxt 的構成為何,理解各層可使用的方法與設定,幫助在開發時能夠製作出更符合需求的設計,並且能夠適當分配排版佈局與組件切分。

nuxt views schema

App Template

Nuxt 預設的模板如下

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>
如何自訂 HTML 模板?

於專案的根目錄資料夾新增一個 app.html 的檔案。並調整 html 文件的內容,記得與上述預設模版一樣,將 HTML_ATTRS、HEAD_ATTRS 與 BODY_ATTRS 分別置入 html、head 還有 body 的屬性。

以下為官方提供的範例,添加 IE 的條件表達式

<!DOCTYPE html>
<!--[if IE 9]><html lang="en-US" class="lt-ie9 ie9" {{ HTML_ATTRS }}><![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--><html {{ HTML_ATTRS }}><!--<![endif]-->
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

HTML Head

利用 vue-meta 套件,更新 document head 與 meta, vue-meta 套件還有非常多強大的功能,可以花點時間研究。

Nuxt.js 預設 vue-meta 的參數配置如下

{
  keyName: 'head', // 設定 meta 資訊在 vue 組件內存在的物件名稱,vue-meta 會使用這個 key 取得設定值
  attribute: 'n-head', // vue-meta 在監聽標簽時所添加的屬性名稱
  ssrAttribute: 'n-head-ssr', // 讓 vue-meta 知道 meta 已完成伺服器端渲染的屬性名稱
  tagIDKeyName: 'hid' // vue-meta 用來決定是否覆蓋或增加 tag 的屬性名稱
}

可在 nuxt.config.js 定義所有 head 預設的標籤(title、meta、link、script),這樣所有的頁面都會套用。
由於文章內容頁需要自行定義 keywords、description 等設定,所以有一部份的 meta 有設定 hid 屬性,讓子組件可以進行覆蓋。

Layouts

功能相當於一般 vue 專案中我們會建立的 App.vue 檔案,用來定義最底層的 html 排版。預設檔案為 layouts/default.vue。

預設的程式碼如下

<template>
  <nuxt/>
</template>

Nuxt 另外提供了自定義 Layouts 的方式

  1. 於 layouts 資料夾內建立 .vue 檔案。
  2. 建立 pages 時,在 script 中增加 layout 屬性指定為上一步驟建立的 vue 檔案名稱。

以下為官方範例
建立一個 layouts/blog.vue

<template>
  <div>
    <div>My Blog</div>
    <nuxt/>
  </div>
</template>

於頁面 pages/posts/index.vue 使用自定義的 layout

<template>
<!-- Your template -->
</template>
<script>
export default {
  layout: 'blog'
  // page component definitions
}
</script>

錯誤頁面也可自訂,在 layouts 資料夾內建立 error.vue 檔案來客製錯誤頁面。
error.vue 雖然此檔案放置於 layouts 資料夾,但 nuxt 把它視為 page,所以 template 中不需包含 <nuxt/>,並且會在指定的 layout 中 <nuxt/> 區塊呈現(未指定則為預設)。
當應用程式發生 404、500…等錯誤時,就會顯示此錯誤頁面,並且透過 props 將當次錯誤資訊傳遞至 error.vue 頁面內使用。

Nuxt 預設錯誤頁面程式碼:nuxt-error.vue

以下為官方範例
透過 props 的 error 傳遞當次錯誤資訊,提供頁面使用
error 為 Object,預設內含 message、path 與 statusCode,屬性也可透過 context.error 自訂

<template>
  <div class="container">
    <h1 v-if="error.statusCode === 404">Page not found</h1>
    <h1 v-else>An error occurred</h1>
    <nuxt-link to="/">Home page</nuxt-link>
  </div>
</template>

<script>
export default {
  props: ['error']
}
</script>

Pages

每個 page 組件實際上就是 vue 組件,只是增加了一些 Nuxt 特殊的配置項目,讓開發者能夠快速開發通用於 SSR 與 CSR 的應用程式。

<template>
  <h1>Hello {{ name }}!</h1>
</template>

<script>
export default {
  asyncData (context) {
    // called every time before loading the component
    // as the name said, it can be async
    // Also, the returned object will be merged with your data object
    return { name: 'World' }
  },
  fetch () {
    // The `fetch` method is used to fill the store before rendering the page
  },
  head () {
    // Set Meta Tags for this Page
  },
  // and more functionality to discover
  ...
}
</script>

Nuxt 為 page 提供的特殊配置項目

  • asyncData - 當需要獲取資料並呈現,在初始化組件之前處理非同步資料操作,並且會與組件的 data 合併,此方法第一個參數為當前頁面組件的 context。
  • fetch - 在頁面 render 之前將資料寫入 Vuex store,與 asyncData 方法類似,不一樣的是 fetch 不會設置組件的 data,此外 nuxt 2.12 以後的版本(包含2.12)的使用方法與先前有些不同,使用上要特別注意。
  • head - 配置目前頁面的 head 標籤。
  • layout - 指定在 layouts 目錄中定義的 vue 檔案。
  • loading - 定義目前頁面的 loading 效果,也可關閉之後透過 this.$nuxt.$loading.finish() 與 this.$nuxt.$loading.start() 手動控制此效果。
  • transition - 指定頁面切換時的轉場特效的名稱,需搭配樣式設定。
  • scrollToTop - 設定目前頁面在渲染頁面前是否要將畫面滾動至頂部。
  • validate - 驗証動態路由參數的方法,防止非預期的參數值傳入。
  • middleware - 在頁面渲染前調用的方法。

實作案例

Blog 網站地圖

blog sitemap

以下為針對 HTML Head 的配置說明
目前 blog 的預設配置在 nuxt.config.js,有可能被覆寫 meta 設定加上 hid 屬性

// nuxt.config.js
export default {
  head: {
    title: '被程式設計的猴子',
    meta: [
      { 'http-equiv': 'Content-Type', content: 'text/html; charset=UTF-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' },
      { name: 'author', content: 'MonkeyBinBin' },
      { property: 'fb:app_id', content: config.fbId },
      { hid: 'keywords', name: 'keywords', content: '被程式設計的猴子, Nuxt.js, Bootstrap 4, MonkeyBinBin, blog' },
      { hid: 'description', name: 'description', content: '使用 Nuxt.js、 Bootstrap 4 建立的blog。分享與紀錄一些程式開發的東西。' },
      { hid: 'og:title', property: 'og:title', content: '被程式設計的猴子' },
      { hid: 'og:type', property: 'og:type', content: 'article' },
      { hid: 'og:url', property: 'og:url', content: path.join(config.domain, baseUrl) },
      { hid: 'og:image', property: 'og:image', content: path.join(config.domain, baseUrl, '/img/fb.jpg') },
      { hid: 'og:image:width', property: 'og:image:width', content: '474' },
      { hid: 'og:image:height', property: 'og:image:height', content: '474' },
      { hid: 'og:description', property: 'og:description', content: '使用 Nuxt.js、 Bootstrap 4 建立的blog。分享與紀錄一些程式開發的東西。' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: `${baseUrl}favicon.ico` }
    ]
  }
}

若其他頁面有需要更改預設的 meta 標籤,就在子組件使用"hid"屬性來設定覆蓋。
this.post 為 data 內的資料,從 api 取得資料後會存放於此。
增加 script 設定是為了可以在文章內崁入 codepen。

以下為文章詳細內容頁(pages/article/_id.vue) head 部份程式碼

// /pages/article/_id.vue
export default {
  name: 'Article',
  head () {
    const _head = {
      title: this.errorMsg || this.post.title,
      meta: [
        { hid: 'og:url', property: 'og:url', content: `${constant.domain}${constant.baseUrl}article/${this.id}/` }
      ],
      script: [
        { src: '//assets.codepen.io/assets/embed/ei.js' }
      ]
    }
    if (this.post && this.post.tags) {
      _head.meta.push({ hid: 'keywords', name: 'keywords', content: this.post.tags.join() })
    }
    if (this.post && this.post.slug) {
      _head.meta.push({ hid: 'description', property: 'description', content: this.post.slug })
      _head.meta.push({ hid: 'og:description', property: 'og:description', content: this.post.slug })
    }
    if (this.post && this.post.title) {
      _head.meta.push({ hid: 'og:title', property: 'og:title', content: this.post.title })
    }
    return _head
  }
}

以下為2篇不同頁面的結果
比較2張圖片右側開發者工具呈現的 meta 資訊就可以看出差異
主要是為了 seo 與 fb 分享可以針對文章的內容取得更精準的資訊

meta sample 01

meta sample 02

使用首頁來解析一下 Layout 切分方式

  1. 所有頁面都存在 Header 與 Footer,所以將這2個組件都放置於 Layout。
  2. 置入 <nuxt/> 讓切換路由時在此區塊渲染對應 pages 畫面。

blog layout

layouts/default.vue 部份程式碼,可看到 template 中 HTML 排列方式

<template>
  <div>
    <page-header/>
    <div class="container">
      <nuxt/>
    </div>
    <page-footer/>
  </div>
</template>

<script>
import PageHeader from '~/components/PageHeader'
import PageFooter from '~/components/PageFooter'
export default {
  name: 'Pages',
  components: {
    PageHeader,
    PageFooter
  },
  // ...略
}
</script>

客製錯誤頁面 layouts/error.vue

blog error

layouts/error.vue 部份程式碼
透過 props 傳遞 error 參數,取得相關的錯誤資訊提供畫面顯示使用

<template>
  <div class="row">
    <div class="col-12 text-center">
      <h1 class="not-found-title">{{error.statusCode}}</h1>
      <p class="text-black-50" v-if="error.statusCode === 404">Page not found</p>
      <p class="text-black-50" v-else>Oops, the page you're looking for doesn't exist.</p>
      <nuxt-link
        to="/"
        class="btn btn-dark"
      >回首頁</nuxt-link>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Error',
  props: ['error'],
  // ...略
}
</script>

參考資料

Nuxt.js - Views
Vue Meta