使用 Next.js 生成 Blog 時如何解決圖片長寬值問題

使用 Next.js 來生成 Blog 的一個主要好處是可以使用 next/image component 來做圖片最佳化。

在官網的範例看起來,似乎是一個非常容易使用的 component

import Image from 'next/image'
import profilePic from '../public/me.png'

// in your component...
return <Image src={profilePic} alt="Picture of the author" />

然而這裡如此簡單的使用是因為 profilePic 是被 static import 的。這個情況下 Next.js 可以自動幫你判斷圖片大小。

但當我們的 Blog 資料來源是 markdown 檔案時,圖片的位置是被寫在 markdown 中的,自然無法像範例這樣 static import。

當非 static import 的時候,Next.js 會要求我們提供圖片的 widthheight 值。 如果沒有提供的話,就會看到像這樣的錯誤。

Error: Image with src "/images/xxx.png" must use "width" and "height"
properties or "layout='fill'" property.

告訴我們必須使用 layout="fill" 或是提供圖片的長寬值。 由於 layout="fill" 並不符合我們的需求,所以需要想辦法提供圖片長寬值。

我的作法是在 getStaticProps 時,同時去找出所使用的圖片的大小,建表給 <Image> component 使用。

部分程式碼片段:

import { promises as fs, existsSync, createReadStream } from 'fs'
import { join } from 'path'
import probe from 'probe-image-size';

export async function getImageSizeMap(slug: string) {
  const postImagesDir = join(IMAGES_DIR, slug)
  const imageFilenames = await fs.readdir(postImagesDir)

  const dict = {}
  const promises = imageFilenames.map(async (filename) => {
    const path = join(postImagesDir, filename)
    let {width, height} = await probe(createReadStream(path));
    dict[`${slug}/${filename}`] = {width, height}
  })

  await Promise.all(promises)

  return dict
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const { slug } = params
  const [post, sizes] = await Promise.all([
    getPostBySlug(slug, [
      'title',
      'date',
      'slug',
      'content',
    ]),
    getImageSizeMap(slug)
  ])

  return {
    props: { post, sizes },
  }
}

如此拿到各個圖片大小之後,就可以將長寬塞給 <Image> 而解決上述的問題。另外有需要的話也可以自己按比例調整圖片大小的值。

若使用 react-markdown 可以參考如下做法:

export default function Post({ post, sizes }: Props) {
  function CustomImage({ alt, src }) {
    const { width, height } = sizes[src]

    return (
      <Image
        src={src} alt={alt}
        width={width} height={height}
      />
    )
  }

  return (
    <AppLayout>
      // ...
      <ReactMarkdown components={{ img: CustomImage }}>
        {post.content}
      </ReactMarkdown>
    </AppLayout>
  )
}