iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Modern Web

用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)系列 第 18

圖片只用原生 img 夠嗎?Astro Image/Picture 處理了什麼?

  • 分享至 

  • xImage
  •  

原生 <img> 會照你提供的 URL 顯示圖片。Astro 的 <Image> 則把本地圖變成可檢查的 build
input:讀取原始尺寸、產生最佳化 URL,並要求每張圖都處理 alt。需要同一張圖提供 AVIF、WebP 與 fallback 時,再交給
<Picture>

瀏覽器在不同螢幕該拿哪個尺寸,還需要 srcsetsizesgetImage(),那一層留到 Day
19。這篇先看檔案能不能被驗證、輸出是什麼、版面會不會先跳一下。

src/assetspublic 走的是兩條路

圖片放在哪裡,決定 Astro 會不會處理它:

圖片位置 寫法 Astro 會做什麼
src/assets/ 靜態 import、Markdown 相對路徑、collection image() 讀取 metadata、最佳化、產生帶 hash 的 URL
public/ 從網站根路徑引用,例如 /bench-demo.svg 原樣複製,不做圖片最佳化
遠端 URL 完整網址 可顯示;只有設定允許的來源才會最佳化

public 適合 favicon、固定公開網址,或不交給 pipeline 處理的檔案。一般文章截圖放
src/assets;若路徑搬錯或檔案被刪,content sync/build 會直接報錯,不必等到上線後才看見破圖。

這個 repo 已有兩種圖片引用方式:BenchArticle.astro 用原生 <img src="/bench-demo.svg">
public;7 篇 Markdown 文章則從 src/assets 引用 13 張 PNG。Day 18 的首頁 cards 使用這批既有圖片。

全部走 public 的代價,可以從一個真實專案量出來。一個上線中的多語系品牌官網把所有圖片都放在 public/,頁面一律用原生
<img>

<img src="/images/pc/home/hero.png" alt="首頁主視覺" />

頁面沒有使用 astro:assets<Image>,也沒有 widthheight 屬性。瀏覽器在圖片載入前無法預留空間,因而產生 Layout
Shift;檔案又放在 public/,所以路徑打錯或檔案被刪都不會讓 build 失敗,只會在線上顯示破圖。

圖示走另一條自建路線:用 import.meta.glob 把 SVG 當原始字串讀進來,在瀏覽器用 DOMParser 解析、組成 <symbol>
再注入頁面。實際跑一次 build 產物,輸出共有 57 支 JS,其中 40 多支是單一圖示各自成一個 chunk,最小的只有 268
bytes。同一個專案另外還裝了一個處理 SVG 的 Vite plugin,也用過 ?url 的引用方式,四種處理同一件事的方法並存。

在沒有內建圖片 pipeline 的專案裡,public 加原生 <img> 是最直接的做法,圖示自建 sprite 也曾經是主流。現在把圖片放進
src/assets,檔案是否存在、原始尺寸多少、輸出什麼格式,都能在 build 時檢查。

<Image> 接的不是字串,是圖片 metadata

Day 17:網站骨架已經有文章 cards,但 collection
schema 沒有可顯示的 card 圖片。這次新增的 cover 把圖片與 alt 綁在一起:

const blog = defineCollection({
  loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/blog" }),
  schema: ({ image }) =>
    z.object({
      // 其他文章欄位
      cover: z
        .object({
          src: image(),
          alt: z.string().min(1),
        })
        .optional(),
    }),
});

frontmatter 的路徑相對於文章檔案:

cover:
  src: ../../assets/day-17/day17-home-desktop.png
  alt: Day 17 首頁桌面版由 hero、三張能力卡與最近文章區組成

經過 image() 驗證,cover.src 會成為含原始寬高與格式的
ImageMetadata,不再是一般路徑字串。Day 10:Content Collections建立的內容模型也把圖片納入 build 前驗證;驗證留在 schema,沒有移到 UI 元件,相關拆解見 Day
12。

ArticleCard 接收 metadata 即可:

---
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';

interface Props {
  cover?: {
    src: ImageMetadata;
    alt: string;
  };
}
---

{cover && <Image src={cover.src} width={720} alt={cover.alt} />}

資料流沿用
Day 5:Astro 元件與 props:page 把 card 真正需要的欄位傳進去,元件不用知道整包 collection
entry。

本地 static import 讓 Astro 推斷等比例高度。輸出的 <img> 會帶 widthheightloading="lazy"
decoding="async";尺寸先寫進 HTML,瀏覽器在圖片下載前就能保留空間,降低版面位移。alt
不能省略;裝飾圖則明確使用空字串,仍要保留屬性。

heroImage 保留為 OG、Twitter Card 與 JSON-LD 使用的公開 URL;cover 則是網站上實際顯示、交給 image()
處理的圖片。兩者用途不同,不能因為名稱都有 image 就合成同一欄。

<Picture> 多做的是格式選擇

首頁三張能力卡分別對應內容系統、互動 islands、server/data,引用 Day 10、Day
8、Day 21:Actions 與 Endpoints的真實實作截圖:

---
import type { ImageMetadata } from 'astro';
import { Picture } from 'astro:assets';

interface Props {
  image: ImageMetadata;
  imageAlt: string;
}
---

<Picture
  src={image}
  formats={['avif', 'webp']}
  fallbackFormat="png"
  width={720}
  alt={imageAlt}
/>

build 後,一張圖會變成一個 <picture>、兩個 <source>,以及一個 PNG fallback:

<picture>
  <source srcset="/_image?...&w=720&h=325&f=avif" type="image/avif" />
  <source srcset="/_image?...&w=720&h=325&f=webp" type="image/webp" />
  <img
    src="/_image?...&w=720&h=325&f=png"
    width="720"
    height="325"
    loading="lazy"
    decoding="async"
    alt="文章列表由 Content Collection 產生,畫面列出 Day 1、Day 8 與 Day 10"
  />
</picture>

瀏覽器會從上往下挑第一個支援的格式,不支援 AVIF 時可退到 WebP,再不行還有 PNG。這段設定仍然只有一個 720px 寬版本;<Picture>
不會自動完成所有 responsive images。

Day 18 首頁能力區呈現三張真實實作截圖,分別對應 Content system、Interactive islands 與 Server data cards

Markdown 圖片也走同一條 pipeline

.md 不能直接放 <Image> 元件,但本地圖片仍可用標準 Markdown:

![Day 17 首頁桌面版,hero 下方依序呈現三張能力卡與最近文章](../../assets/day-17/day17-home-desktop.png)

Astro 7 會處理這種指向 src/ 的相對路徑。Day 18 改版前先 build,13 張 Markdown 圖都已經自動帶出原始寬高、lazy
loading、async decoding 與 WebP endpoint。相同語法若改指 /public 的根路徑,則只會原樣顯示。

這條流程也能提早暴露錯誤。Day 17 曾先讓 Markdown 引用尚未產生的截圖,content sync 直接拋出 ImageNotFound。補檔後若 dev
server 仍保留舊的 content cache,重啟即可恢復;排查時先確認檔案路徑與實體存在,再決定是否清除快取重啟。

不只看副檔名,直接量輸出

本次以 Node 24.16.0、Astro 7.1.1 與 Cloudflare adapter 14.1.3,在本機 astro preview 量 Day 10 的 collection 截圖:

版本 尺寸 bytes
原始 PNG 1280×577 61,809
720px PNG fallback 720×325 43,396
720px WebP 720×325 11,740
720px AVIF 720×325 8,411

這組數字同時包含縮圖與格式轉換,不能把差距全部算成「AVIF 比 PNG 省多少」。

Cloudflare adapter 的 production HTML 指向 /_image,轉檔發生在 runtime;dist/client/_astro
仍保存帶 hash 的來源 PNG。若只把 dist/client 當純靜態檔案丟出去,/_image
沒有 server 接手就不完整。

自動最佳化有邊界

完成 Day 18 後,首頁輸出 3 個 <picture>、6 個 <source> 與 4 個
<img>;文章分頁也能從 collection 讀到 7 張可選 cover。首頁仍是 0 個
<astro-island>。圖片元件在 build/server 端產生 HTML 和 URL,不需要為了顯示圖片送一份 Vue runtime;這跟
Day 14:搜尋 island需要瀏覽器狀態的情況不同。

Day 18 首頁在 390px 寬度維持單欄,三張能力卡圖片與文章 cover 都沒有造成水平溢出

這次只固定產生 720px 圖,尚未處理 390px 手機和高密度桌面螢幕是否該下載同一份檔案。圖片裁切仍由
Day 6:scoped 與 global CSS控制,遠端來源也仍要設定允許規則。Astro 自動處理檔案,不會替內容作者決定構圖、alt 或效能預算。

Astro 7 的現行規則可查 Images 官方指南
astro:assets API

Day 19 接著加入 layoutsrcsetsizesgetImage(),處理不同 viewport 該下載哪個尺寸。

本日程式碼:step-18|只看這天的改動:step-17...step-18


上一篇
把前面學的拼成網站,Astro 專案骨架該先放什麼?
下一篇
不同螢幕要不同圖,Astro 響應式圖片怎麼設定?
系列文
用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言