原生 <img> 會照你提供的 URL 顯示圖片。Astro 的 <Image> 則把本地圖變成可檢查的 build
input:讀取原始尺寸、產生最佳化 URL,並要求每張圖都處理 alt。需要同一張圖提供 AVIF、WebP 與 fallback 時,再交給<Picture>。
瀏覽器在不同螢幕該拿哪個尺寸,還需要 srcset、sizes 或 getImage(),那一層留到 Day
19。這篇先看檔案能不能被驗證、輸出是什麼、版面會不會先跳一下。
src/assets 和 public 走的是兩條路圖片放在哪裡,決定 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>,也沒有 width、height 屬性。瀏覽器在圖片載入前無法預留空間,因而產生 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> 接的不是字串,是圖片 metadataDay 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> 會帶 width、height、loading="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。

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

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需要瀏覽器狀態的情況不同。

這次只固定產生 720px 圖,尚未處理 390px 手機和高密度桌面螢幕是否該下載同一份檔案。圖片裁切仍由
Day 6:scoped 與 global CSS控制,遠端來源也仍要設定允許規則。Astro 自動處理檔案,不會替內容作者決定構圖、alt 或效能預算。
Astro 7 的現行規則可查 Images 官方指南與astro:assets API。
Day 19 接著加入 layout、srcset、sizes 與 getImage(),處理不同 viewport 該下載哪個尺寸。
本日程式碼:step-18|只看這天的改動:step-17...step-18