Skip to content

Instantly share code, notes, and snippets.

@manabuyasuda
Last active July 7, 2025 02:05
Show Gist options
  • Save manabuyasuda/36c190ba32968f5b1c37c05e16fcd26a to your computer and use it in GitHub Desktop.
Save manabuyasuda/36c190ba32968f5b1c37c05e16fcd26a to your computer and use it in GitHub Desktop.
microCMSの画像APIを利用して最適化した画像コンポーネント
import { SupportedImageFormat } from './types';
/**
* w記述子で生成する画像サイズパターン
* https://nextjs.org/docs/pages/api-reference/components/image#advanced
*/
export const SRCSET_SIZES = [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1010];
/**
* 画像の初期設定値
*/
export const DEFAULT_IMAGE_QUALITY = 90;
export const DEFAULT_IMAGE_FORMAT: SupportedImageFormat = 'webp';
/**
* デバイスピクセル比の設定値
* 標準解像度(1x)と高解像度ディスプレイ(2x)用
*/
export const DEVICE_PIXEL_RATIOS = [1, 2];
import {
ImageGenerationBaseParams,
ImageGenerationWSrcSetParams,
ImageUrlGenerationParams,
} from './types';
import { SRCSET_SIZES, DEVICE_PIXEL_RATIOS } from './constants';
/**
* 画像URLにパラメータを追加する関数
* @param params - 画像生成パラメータ(src, width, height, quality, format, dpr, isForW)
* @returns パラメータが追加された画像URL
*/
const generateImageUrl = (params: ImageUrlGenerationParams): string => {
// URLが存在しない場合は空文字を返す
if (!params.src) return '';
// URLSearchParamsを使用してクエリパラメータを構築する
const urlParams = new URLSearchParams();
// 必要なオプションを取得する(完全形)
const { src, width, height, quality, dpr, format, isForW } = params;
// 幅と高さのパラメータを設定する(存在する場合のみ)
// microCMS APIは省略形を要求するため、省略形で設定
if (width) urlParams.append('w', width.toString());
if (height) urlParams.append('h', height.toString());
// 画質とフォーマットのパラメータを設定する
if (quality) urlParams.append('q', quality.toString());
if (format) urlParams.append('fm', format);
// dprの設定(w記述子の場合は設定しない)
if (dpr && !isForW) urlParams.append('dpr', dpr.toString());
// 最終的なURLを生成して返す
return `${src}?${urlParams.toString()}`;
};
/**
* 画像のsrc属性用URLを生成する関数
* @param params - 画像生成パラメータ
* @returns 生成されたsrc URL
*/
export const generateSrc = (params: ImageGenerationBaseParams): string => {
const { src, width, height, quality, format } = params;
return generateImageUrl({
src,
width: typeof width === 'number' ? width : undefined,
height: typeof height === 'number' ? height : undefined,
quality,
format,
isForW: false,
});
};
/**
* w記述子のsrcSetを生成する関数
* 複数の解像度に対応した画像URLのセットを作成します
* @param params - 画像生成パラメータ
* @returns 生成されたsrcSet文字列
*/
export const generateWSrcSet = (params: ImageGenerationWSrcSetParams): string => {
const { src, width, height, quality, format } = params;
// widthを数値型に変換(文字列の場合はパースする)
const numWidth = typeof width === 'string' ? parseInt(width, 10) : width;
// デバイスピクセル比の最大値を取得
const maxDpr = Math.max(...DEVICE_PIXEL_RATIOS);
return (
SRCSET_SIZES
// 指定された幅にデバイスピクセル比の最大値を掛けたサイズまでをフィルタ
.filter(size => size <= numWidth * maxDpr)
// 各サイズごとに画像URLを生成
.map(size => {
// 高さの計算(アスペクト比を維持)
let calculatedHeight: number | undefined;
if (height && typeof height === 'number' && typeof width === 'number') {
// 元の幅と高さから比率を計算し、新しいサイズに適用
calculatedHeight = Math.round((height * size) / width);
}
// 各サイズに対応した画像URLを生成
const url = generateImageUrl({
src,
width: size, // 新しい幅
height: calculatedHeight, // 計算された高さ(アスペクト比維持)
quality, // 画質
format, // フォーマット
isForW: true, // w記述子用フラグをtrueに設定
});
// w記述子形式で返す(例: "image.jpg?w=300 300w")
return `${url} ${size}w`;
})
// カンマ区切りで結合
.join(', ')
);
};
/**
* x記述子のsrcSetを生成する関数
* デバイスピクセル比に基づいた画像URLのセットを作成します
* @param params - 画像生成パラメータ
* @returns 生成されたsrcSet文字列
*/
export const generateXSrcSet = (params: ImageGenerationBaseParams): string => {
const { src, width, height, quality, format } = params;
// 定義されたデバイスピクセル比の種類を使用して画像を生成
return (
DEVICE_PIXEL_RATIOS.map(scale => {
// 各デバイスピクセル比に対応した画像URLを生成
const url = generateImageUrl({
src,
width, // 元の幅をそのまま使用
height, // 元の高さをそのまま使用
quality, // 画質
dpr: scale, // デバイスピクセル比を設定
format, // フォーマット
isForW: false, // w記述子用ではないのでfalse
});
// x記述子形式で返す(例: "image.jpg?dpr=2 2x")
return `${url} ${scale}x`;
})
// カンマ区切りで結合
.join(', ')
);
};
import { ImageProps } from './types';
import { generateSrc, generateWSrcSet, generateXSrcSet } from './helpers';
import { DEFAULT_IMAGE_QUALITY, DEFAULT_IMAGE_FORMAT } from './constants';
/**
* microCMSの画像APIを利用して最適化した画像コンポーネントです。
*
* @example
* // 基本的な使用方法です。
* // デフォルトではw記述子で出力されます。
* // レイアウトシフトを避けるにwidthとheightの両方を指定してください。
* // sizesで画像が表示される幅を適切に指定してください。
* <Image
* src="https://images.microcms-assets.io/assets/example.jpg"
* alt=""
* width={1000}
* height={670}
* sizes="(max-width: 599px) 100vw, 1010px"
* />
*
* // x記述子を使用した方法です。
* // PCとSPで画像サイズに差が小さい場合に使用します。
* // widthとheightに指定した値が基準になるので、ブラウザ上で表示したいサイズをwidthに指定してください。
* <Image
* src="https://images.microcms-assets.io/assets/example.jpg"
* alt=""
* width={100}
* srcSetType="x"
* />
*
* // imageApiOptionsで画像の出力品質とフォーマットを上書きできます。
* // https://document.microcms.io/image-api/quality
* // https://document.microcms.io/image-api/format
* <Image
* src="https://images.microcms-assets.io/assets/example.jpg"
* alt=""
* width={1000}
* height={670}
* imageApiOptions={{ q: 100, fm: 'jpg' }}
* />
*
* // ファーストビューやCSRなどで遅延読み込みを避けたい場合はloadingをeagerにしてください。
* <Image
* src="https://images.microcms-assets.io/assets/example.jpg"
* alt=""
* width={1000}
* height={670}
* loading="eager"
* />
*/
export default function Image(props: ImageProps) {
/** srcSetTypeが指定されていなければデフォルト値として`w`を設定する */
const srcSetType = 'srcSetType' in props ? props.srcSetType || 'w' : 'w';
/** srcSetTypeがwの場合はsizesが必須なので受け取り、xの場合はfalsyな値を設定する */
const sizes = 'sizes' in props ? props.sizes : undefined;
/** imageApiOptionsはDOMに出力しないので分離しておく */
const { imageApiOptions, ...allProps } = props;
/**
* DOMに出力しないプロパティを除去する。
* sizesはフォールバック値を設定するために後で処理する。
*/
const restProps = Object.fromEntries(
Object.entries(allProps).filter(([key]) => !['srcSetType', 'sizes'].includes(key)),
);
/**
* sizes属性の最終的な値を決める。
* - w記述子でsizesが未指定の場合は'100vw'をデフォルト設定
* - w記述子でsizesが指定されている場合はそのまま使用
* - x記述子の場合はfalsyな値が入っているので出力されない
*/
const resolvedSizes = srcSetType === 'w' && !sizes ? '100vw' : sizes;
/** 画質の上書きがあればそれを優先し、なければデフォルト値を使用する */
const quality = imageApiOptions?.quality ?? DEFAULT_IMAGE_QUALITY;
/** フォーマットの上書きがあればそれを優先し、なければデフォルト値を使用する */
const format = imageApiOptions?.format ?? DEFAULT_IMAGE_FORMAT;
/** src属性を生成する */
const imgSrc = generateSrc({
src: props.src,
width: props.width,
height: props.height,
quality,
format,
});
/**
* srcSet属性を生成する。
* widthが指定されていない場合は適切な値を生成できないため、DOMに出力できないように空文字を返す。
*/
const srcSet = !props.width
? ''
: (() => {
const commonParams = {
src: props.src,
width: props.width,
height: props.height,
quality,
format,
};
/** srcSetTypeに応じて適切な画像生成関数を選択する */
return srcSetType === 'w' ? generateWSrcSet(commonParams) : generateXSrcSet(commonParams);
})();
return (
<img {...restProps} src={imgSrc} alt={props.alt ?? ''} sizes={resolvedSizes} srcSet={srcSet} />
);
}
import { ComponentPropsWithoutRef } from 'react';
/**
* microCMS画像APIがサポートする画像フォーマット
*/
export type SupportedImageFormat = 'webp' | 'jpg' | 'png';
/**
* 画像の基本パラメータ(共通部分)
*/
export type ImageBaseParams = {
/** 画像幅 */
width?: number | string;
/** 画像高さ */
height?: number | string;
/** 画質 (1-100) */
quality?: number;
/** 画像フォーマット */
format?: SupportedImageFormat;
};
/**
* microCMS画像APIのオプション
* ImageBaseParamsのサブセット
*/
export type MicroCMSImageApiOptions = Pick<ImageBaseParams, 'quality' | 'format'>;
/**
* 画像生成関数の基本パラメータ
* ImageBaseParamsを拡張してsrcを追加
*/
export type ImageGenerationBaseParams = ImageBaseParams & {
/** 画像URL */
src: string;
};
/**
* 画像URL生成の内部パラメータ
* ImageGenerationBaseParamsを拡張してdpr, isForWを追加
*/
export type ImageUrlGenerationParams = ImageGenerationBaseParams & {
/** デバイスピクセル比 */
dpr?: number;
/** w記述子用かどうかのフラグ */
isForW?: boolean;
};
/**
* w記述子用の画像生成関数パラメータ(widthが必須)
*/
export type ImageGenerationWSrcSetParams = Omit<ImageGenerationBaseParams, 'width'> & {
/** 画像幅(必須) */
width: number | string;
};
type BaseImageProps = Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'sizes' | 'srcSet'> & {
/** 標準のsrcはBlobを許容するため、string型に限定する */
src: string;
/** 画像APIのオプションを上書きする場合に使用する */
imageApiOptions?: MicroCMSImageApiOptions;
};
/**
* 画像コンポーネントのプロパティ型
* srcSetTypeによってsizesプロパティの扱いが変わる:
* - 'w'または未指定:sizesプロパティが必須
* - 'x':sizesプロパティは不要
*/
export type ImageProps = BaseImageProps &
(
| {
/** x記述子 */
srcSetType: 'x';
}
| {
/** w記述子 */
srcSetType?: 'w';
/** 画像の表示領域のサイズを指定する */
sizes: string;
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment