Loading 4%

Nuxt Content 進階設定與踩坑紀錄【2】

這一篇是 Nuxt Content 系列的最後一章,主要分享進階設定與實際開發時踩過的坑。如果你有客製內容樣式或正在評估是否使用 Docus,希望這些經驗能對你有所幫助。

前言

上篇完成了內容顯示與互動功能後,這一篇則聚焦在後期優化與實務設定,包含樣式客製、資料結構設計,最後也會分享我對nuxt-content/Docus的一些使用心得,作為 Nuxt Content 系列的收尾。

標籤元件的客製調整

文章內容除了使用 TailwindCSS 做樣式調整,在某些標籤想要客製功能與新增區塊,官方有提供標籤元件調整,能夠覆蓋掉原本標籤,我針對圖片與多行程式碼標籤做客製調整,接下來我分別說明:

圖片標籤

圖片呈現後顯示原生 <img> 標籤,在觀看上無法放大,也沒有圖片說明,這也是我想動此元件原因,在開始之前造著官方建立在 components/content 裡面放 ProseImg.vue 元件。

元件裡面先放上官方 github 元件原始碼在開始客製。

  1. 新增圖片與說明:清空原始碼,新增 <img><span> 並且 props 參數傳遞上去
ProseImg.vue
       <template>
  <div class="my-5">
    <img :src :alt :width :height class="pb-2 mb-0 mt-0" />
    <span v-if="alt" class="font-bold pl-2">{{ alt }}</span>
  </div>
 </template>

    
  1. 顯示圖片 Lightbox:新增點擊開啟事件,Lightbox 傳遞到 body 同層,Lightbox 顯示放大圖片與說明
ProseImg.vue
      <template>
  <div @click="isOpenModal = true" class="my-5">
    <!-- ...略 (圖片與説明) -->
  </div>
  <Teleport to="body">
    <div v-if="isOpenModal" @click="isOpenModal = false"
      class="fixed inset-0 z-200 bg-black/60 flex items-center justify-center p-4">
      <div @click.stop class="relative max-w-9/10 lg:max-w-200 max-h-full">
        <!-- ...略 (Lightbox 內容) -->
      </div>
    </div>
  </Teleport>
</template>
<script setup lang="ts">
    const isOpenModal = ref(false);
</script>

    

多行程式碼標籤

呈現顯示高光的多行 code,這部分可能在撰寫技術文上,會不知道這段撰寫語言與檔案,並且複製程式碼需要自己選擇範圍,所以針對這部分我新增程式碼標題與一鍵複製,在 components 新增 ProsePre.vue 元件。

  1. 程式碼標題 : 檔名顯示 props 的 filename 傳遞上去 ; 撰寫語言標示以 props 的 language 比對自訂列表顯示 Icon。
ProsePre.vue
      <template>
  <div class="my-6 relative group">
    <div v-if="language !== 'plaintext'"
      class="p-2 h-10 bg-white rounded-t border border-gray-300 dark:bg-neutral-900 flex items-center ">
      <Icon v-if="language && LanguageIconList[language]" :name="LanguageIconList[language]" class="align-middle" />
      <span class="pl-2">{{ filename }}</span>
    </div>
    <!-- ...略 (code) -->
  </div>
</template>
<script setup lang="ts">
type LanguageIconKey = keyof typeof LanguageIconList;
// 圖示 https://icon-sets.iconify.design/
const LanguageIconList = {
  plaintext: '',
  javascript: 'logos:javascript',
  typescript: 'logos:typescript-icon',
  ...
} as const;
</script>

    
  1. 一鍵複製 : <template> 設定按鈕點擊觸發事件,事件上使用原生 Clipboard API 提供 writeText() 方法結果會回傳 Promise,此外設定時間內防止二次點擊機制。
ProsePre.vue
      <script setup lang="ts">
const isCopied = ref(false);
const copyToClipboard = async () => {
  try {
    await navigator.clipboard.writeText(props.code);
    isCopied.value = true;

    // 2秒後重置按鈕狀態
    setTimeout(() => {
      isCopied.value = false;
    }, 2000);
  } catch (err) {
    console.error('無法複製內容: ', err);
  }
};
</script>

    

JSON 資料集合設計與取用

Nuxt content 除了 Markdown 還有JSON、YAML、CSV 可以內容管理,在我的個人網站有使用到 JSON 管理我的作品,主要在列表與內頁上不需要像文章需要撰寫段落與標題,所以選擇我熟悉 JSON 檔新增作品欄位,在 content/works 裡面放置每個作品檔案,方便後續管理。

與上篇一樣先依照官方文件完成集合設定 。要注意 type 的值為 data

作品列表

取資料並進行最新篩選,與上篇一樣使用官方 queryCollection.all() 取所有資料,再篩選欄位與排序

useWorks.ts
       const { data: works, pending } = await useAsyncData(
    "works-data",
    async () => {
      const data = await queryCollection("works").all();
      //...略 (篩選新增 sortIndex 與 slug)
      return processedWorks.sort((a, b) => Number(b.sortIndex) - Number(a.sortIndex));
    },
  );

    

作品內頁

取單一特定資料時,透過 queryCollection 搭配 .where().first() 取得符合條件的第一筆資料。 由於 stem 會以檔案設計 works/1.xxx 帶排序前綴,而我在內頁路徑篩選掉順序,因此在查詢時改用模糊比對尾端的 slug取得文章。

useWorkDetail.vue
      export const useWorkDetail = (path: string, slug: string) => {
  return useAsyncData(path, () => {
    return queryCollection("works").where("stem", "LIKE", `%${slug}`).first();
  });
};

    

與上面內容一樣放在 composables ,解決 middleware 與頁面初始化。

使用 Docus 強化內容呈現

Docus 是 Nuxt Content 團隊開發的套件,用來美化 Markdown 內頁,並結合 Nuxt UI 快速套版,支援客製化調整。

我在個人網站沒有使用 Docus,原因是希望元件與內容呈現保留彈性。如果日後改用資料庫,現有元件仍能復用;不過相對地使用 Content 會花比較多時間在設計元件與切版上。

如果想要快速生成一個技術文件、教學網站,很適合使用。

Docus 範例 (目前只做到基本專案初始化,Docus 本身有提供不少初始元件與版型,可以再依需求調整)

總結

這兩篇文章在分享我在個人網站使用 Nuxt Content 的核心做法與實作心得。 如果你對 Nuxt Content 某些區塊或寫法有疑問,歡迎透過留言板與我交流~

下一篇我會帶大家了解網站留言板,我為什麼選擇 Disqus、怎麼載入專案,以及在實作過程遇到的問題。有興趣的話可以繼續往下看!(待我上稿 XD)

留言板