CleanMyMac

在 Eleventy(11ty)文章中做出像 WordPress 的 Feature Image 特色圖片

任何技術都需要踩坑的

從 WordPress 搬家到 11ty 之後,有個想保留的一個使用體驗,就是:

每篇文章可以在文章開頭放一張「feature image」,並且順手帶出文章摘要,或一些有趣文字。

WordPress 裡這件事很自然;到了 11ty,就要自己定規格、自己做模板與樣式。

這篇文章分享我現在採用的做法:

在 frontmatter 先定義一些資料格式~

  • 放 feature_image(圖片路徑)
  • 放 feature_meta_text(文章摘要/或一段簡單文字)
  • 放 use_feature_split(要不要啟用 Hover 特效)

你可以把它當成「像 WordPress 一樣的特色圖片機制」,只是更自由、更可控。


目標效果

  1. 一般模式(預設)

    • 文章開頭顯示靜態 feature image
  2. 特效模式(可選)

    • hover 時 feature image 裂成四塊飛散
    • 同時顯示文章資訊文字(feature_meta_text)

步驟 1:在每篇文章 frontmatter 加上 feature image 設定

參考如下 frontmatter 範例:

---
# 文章標題
title: 從 WordPress 搬家到 Eleventy(11ty):你一定會遇到的幾個注意事項整理

# 文章描述(常用在 SEO、OpenGraph、列表摘要)
description: 從 WordPress 搬家到 Eleventy 靜態網站生成器的完整指南,整理了遷移過程中常見的注意事項、坑洞與解決方案。包含內容匯出、圖片處理、前端框架整合等實戰經驗分享。

# Feature image:文章開頭的主圖(你用相對路徑)
feature_image: "../img/feature-images/11ty-notes.png"

# 是否啟用「裂開 hover 特效」
# false = 用一般 <img> 呈現
# true  = 用特效版本(四塊碎片 + 文字)
use_feature_split: false

# 特效模式時要顯示的文章資訊文字(簡短、像副標)
feature_meta_text: WordPress 到 Eleventy 的遷移之旅

# 發佈時間(用 ISO 格式很標準)
date: 2025-12-22T10:00:00.000Z

# metadata 結構(分類、文章型態、canonical url)
metadata:
  categories:
    - 架設網站
    - 靜態網站生成器
  type: wordpress-migration
  url: https://kamadiam.com/wordpress-to-eleventy-migration/

# tags(文章標籤)
tags:
  - wordpress
  - eleventy
  - 11ty
  - 靜態網站
  - 網站搬家
  - 遷移指南
  - 前端開發
  - 網站優化

# 指定 Markdown 的模板引擎, 可以避免不同程式語法衝突
templateEngineOverride: md
---

frontmatter 可以很簡單的自定義資料格式,可以控制文章的顯示或行為。

  • feature_meta_text 建議保持「一句話」即可,越短越像 WordPress 的「精選摘要」
  • use_feature_split 預設 false,要用特效的文章再開即可,這個是額外加入的小趣味。

步驟 2:在 post.njk 加入條件邏輯

修改的主要邏輯是:

  • 如果 feature_image 存在 → 即顯示 feature image
  • 如果 use_feature_splitfeature_image 存在 → feature image 要加上特效

請參考範例,(PS:nunjucks 註解格式為 {# ... #}) :


{# 
============================================================
  Feature image 渲染入口:
  - use_feature_split = true  → 顯示裂開特效版本
  - use_feature_split = false → 顯示一般 <img>
============================================================ 
#}

{# 1) 這裏是特效模式:需要 use_feature_split=true 且有 feature_image #}
{%- if use_feature_split and feature_image %}

  {# 你自訂的 filter:把 frontmatter 的相對路徑,解析成可用的 URL #}
  {%- set resolvedImagePath = feature_image | resolveImagePath %}

  {# 確保解析成功才渲染,避免 broken image #}
  {%- if resolvedImagePath %}

    <div class="feature-wrap">
      {# figure:使用 CSS 變數 --img 把圖片 URL 傳給 CSS #}
      <figure
        class="feature-split"
        aria-label="Article feature image"
        style="--img: url('{{ resolvedImagePath }}')"
      >
        {# 視覺底座:提供圓角/背景框(因為不使用 overflow hidden) #}
        <div class="feature-frame" aria-hidden="true"></div>

        {# 四塊碎片:四個 span 用同一張圖切成 2x2 #}
        <div class="tiles" aria-hidden="true">
          <span class="tile t1"></span>
          <span class="tile t2"></span>
          <span class="tile t3"></span>
          <span class="tile t4"></span>
        </div>

        {# 底部文字資訊:如果有 feature_meta_text 才顯示 #}
        {%- if feature_meta_text %}
          <div class="feature-text">{{ feature_meta_text }}</div>
        {%- endif %}

      </figure>
    </div>

  {%- endif %}

{# 2) 這裏是一般模式:有 feature_image 就用普通 img #}
{%- elif feature_image %}

  {# 這裡直接用 feature_image(相對路徑)顯示圖片 #}
  <img
    src="{{ feature_image }}"
    alt="Feature image for {{ title }}"
    class="post-feature-image"
  >

{%- endif %}

步驟 3:套用 CSS,做出「裂成四塊」的核心效果

效果就是自由發揮,如果不想只是顯示一張圖,讓文章有趣一點,或是埋藏彩蛋,可以自由修改 html 搭配 css 展現特別效果。

/* ============================================================
  feature-wrap:用 padding + 負 margin 擴大 hover 觸發區域
  目的:讓滑鼠不要一定得壓在圖片上,稍微靠近也能觸發
============================================================ */
.feature-wrap {
  position: relative;

  /* 擴大 hover 區域(包含圖片周圍) */
  padding: var(--space-2xl);

  /* 用負 margin 把版面位置「抵回來」,避免整體被撐大 */
  margin: calc(-1 * var(--space-2xl));
}


/* ============================================================
  feature-split:核心容器(不裁切,碎片可以飛出去)
  --img:圖片 URL(由 inline style 傳進來)
  --radius:圓角
  --spread:裂開距離
  --lift:hover 上浮高度
============================================================ */
.feature-split {
  --img: url("");
  --radius: var(--space-3xs);
  --spread: var(--space-l);
  --lift: var(--space-s);

  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;

  /* ✅ 不裁切:碎片可以飛出原本框 */
  overflow: visible;

  border-radius: var(--radius);

  /* 初始位置明確指定 translateY(0),避免 hover 回彈視覺跳一下 */
  transform: translateY(0) translateZ(0);

  /* 只讓 transform 動畫(效能最好) */
  transition: transform 620ms cubic-bezier(.2, .85, .2, 1);

  margin: var(--space-m) 0;
}


/* ============================================================
  feature-frame:視覺底座
  因為 overflow 沒有 hidden,不能靠裁切做圓角
  所以用這層提供背景/邊框/圓角,讓「合起來」時像一張完整圖
============================================================ */
.feature-frame {
  position: absolute;
  inset: 0;
  background: var(--neutral-color);
  border-radius: var(--radius);
  pointer-events: none;
}


/* ============================================================
  tiles:四塊碎片的容器
============================================================ */
.tiles {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}


/* ============================================================
  tile:單一碎片
  技術重點:
  - 每塊 50% x 50%
  - 背景圖放大成 200% 200%(因為只顯示四分之一)
  - 用 background-position 決定顯示哪一個象限
============================================================ */
.tile {
  position: absolute;
  width: 50%;
  height: 50%;

  /* ✅ 同一張圖,放大兩倍再切成四塊 */
  background: var(--img) center / 200% 200% no-repeat;

  border-radius: calc(var(--radius) - var(--space-3xs));

  /* 合起來時不顯示邊框,裂開時才顯示(避免抖動就用 transition) */
  border: 1px solid transparent;

  box-shadow: 0 var(--space-s) var(--space-l) rgba(0, 0, 0, .22);

  /* ✅ 動畫只做 transform/filter/opacity/border-color(效能好) */
  transition:
    transform 620ms cubic-bezier(.2, .85, .2, 1),
    filter 620ms cubic-bezier(.2, .85, .2, 1),
    opacity 620ms cubic-bezier(.2, .85, .2, 1),
    border-color 620ms cubic-bezier(.2, .85, .2, 1);

  will-change: transform;
  backface-visibility: hidden;
}


/* ============================================================
  四塊的位置與對應的背景象限
============================================================ */
.t1 { left: 0;  top: 0;  background-position: 0% 0%; }
.t2 { left: 50%; top: 0; background-position: 100% 0%; }
.t3 { left: 0;  top: 50%; background-position: 0% 100%; }
.t4 { left: 50%; top: 50%; background-position: 100% 100%; }


/* ============================================================
  feature-text:hover 時顯示的文字資訊
============================================================ */
.feature-text {
  position: absolute;
  left: 50%;
  top: 50%;

  /* 初始略縮小 + 隱藏 */
  transform: translate(-50%, -50%) scale(0.9);
  opacity: 0;

  transition: transform 520ms cubic-bezier(.2, .85, .2, 1), opacity 520ms ease;
  pointer-events: none;

  color: var(--text-color);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: .2px;
  line-height: 1.6;
  text-align: center;

  white-space: nowrap;

  background: rgba(255, 255, 255, .92);
  backdrop-filter: blur(12px);
  border-radius: var(--space-l);
  border: 1px solid var(--color-gray-20);
  box-shadow: 0 var(--space-l) var(--space-2xl) rgba(0, 0, 0, .15);
  padding: var(--space-2xs) var(--space-2xs);
  max-width: 80%;
}


/* ============================================================
  Hover 動畫(只在支援 hover 的裝置上)
============================================================ */
@media (hover: hover) and (pointer: fine) {

  /* 容器上浮(你同時支援 feature-wrap:hover 與 figure:hover) */
  .feature-wrap:hover .feature-split,
  .feature-split:hover {
    transform: translateY(calc(-1 * var(--lift))) translateZ(0);
  }

  /* 四塊向四個方向飛散 + 旋轉 */
  .feature-wrap:hover .feature-split .t1,
  .feature-split:hover .t1 {
    transform: translate(calc(-1 * var(--spread)), calc(-1 * var(--spread))) rotate(-4deg);
  }

  .feature-wrap:hover .feature-split .t2,
  .feature-split:hover .t2 {
    transform: translate(var(--spread), calc(-1 * var(--spread))) rotate(4deg);
  }

  .feature-wrap:hover .feature-split .t3,
  .feature-split:hover .t3 {
    transform: translate(calc(-1 * var(--spread)), var(--spread)) rotate(3deg);
  }

  .feature-wrap:hover .feature-split .t4,
  .feature-split:hover .t4 {
    transform: translate(var(--spread), var(--spread)) rotate(-3deg);
  }

  /* 碎片顏色更有精神 + 裂開時顯示邊框 */
  .feature-wrap:hover .feature-split .tile,
  .feature-split:hover .tile {
    filter: brightness(.92) saturate(1.12) contrast(1.03);
    border-color: var(--color-gray-20);
  }

  /* 裂縫光出現 */
  .feature-wrap:hover .feature-split .crack,
  .feature-split:hover .crack {
    opacity: 1;
  }

  /* 文字卡片縮放淡入 */
  .feature-wrap:hover .feature-split .feature-text,
  .feature-split:hover .feature-text {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
}

CSS 技術實作原理

一張圖切四塊,不需要四張圖片

每個 .tile 都是同一張背景圖:

  • tile 本身只有 50% × 50%
  • 但背景圖用 200% 200% 放大
  • 然後用 background-position 指定顯示哪個象限

所以你只要準備 一張 feature image,就能切出四塊。

為什麼動畫只用 transform?

transform(位移/旋轉/縮放)通常可以走 GPU 合成層,效能最好。

如果你用 top/left 去動,瀏覽器比較容易重新排版,會卡。


最後:整合到 11ty 的使用方式

你的使用方式會長這樣:

  • 一般文章(不需要特效):use_feature_split: false
  • 特別文章(想要效果):use_feature_split: true

在 11ty 只要維持一致的 frontmatter key,就能像 WordPress 一樣「每篇都有封面」,而且還能加進你的個人風格(裂開特效、資訊卡片、系列標籤都能玩)。

另外 手機沒有 hover,可以改成「進入視窗」才顯示文字,利用 IntersectionObserver 加 style class。

以上範例也可以獨立拆成獨立檔案例如 feature_image.njk,讓 post.njk 只要 {% include "feature_image.njk" %} 一行,管理 layout 更省心力。