Closed41

gatsby-image

HishoHisho

最終目標

  1. ✅Gatsby Imageを使った画像最適化
  2. ✅全画像から特定の画像を返す汎用componentを作成する
  3. ✅アートディレクションにも自動で対応する
  4. ✅SSGした時の初回ロードでアートディレクションのアスペクト比がバグっている(v2)部分を確認対応
  5. ✅画像がない場合はエラーを投げる
  6. ✅TS対応
  7. ❌AVIF対応(AVIFは画質が悪いため断念)
HishoHisho

フロー

  1. ✅document読みながら適当に作る
  2. ✅とりあえず、動くまで作る
  3. ✅リファクタリングする
  4. ドキュメント書く
HishoHisho

比較

StaticImage

StaticImage
import React, {FCP} from 'react';
import {Layout, SEO} from '@src/layouts';
import {StaticImage} from "gatsby-plugin-image";

const IndexPage: FCP = () => {
  return (
    <Layout>
      <SEO/>
      <h1 className="text-4xl">index.page</h1>
      <StaticImage src="../images/screenshot.png" alt="" formats={["auto", "webp", "avif"]}/>
    </Layout>
  );
};

export default IndexPage;

GatsbyImage

GatsbyImage
import React, {FCP} from 'react';
import {Layout, SEO} from '@src/layouts';
import {GatsbyImage} from "gatsby-plugin-image";
import {graphql, useStaticQuery} from 'gatsby';

const IndexPage: FCP = () => {
    const images = useStaticQuery(graphql`
      query MyQuery {
        file(relativePath: {eq: "screenshot.png"}) {
          childImageSharp {
            gatsbyImageData(layout: CONSTRAINED, formats: [AUTO, WEBP, AVIF])
          }
        }
      }
    `);

  return (
    <Layout>
      <SEO/>
      <h1 className="text-4xl">index.page</h1>
      <GatsbyImage image={images.file.childImageSharp.gatsbyImageData} alt="" />
    </Layout>
  );
};

export default IndexPage;

HishoHisho

というかpadding-bottomでアスペクト比取ってたdiv消えたんですね
まぁwidthとheightが指定してあるから必要ないっちゃ無いけど

HishoHisho

StaticImage楽でいいけどpath違うと急に画像でなくなる🤔
あとpathは../imagesで固定っぽい?
low/low/lowとかSSGするとpath変わりそう
それなら相対パス割り出さないといけないし手動で書くのめんどくさいなぁ

HishoHisho

現状StaticImageまとめ

HishoHisho

ぶっちゃけアートディレクションそんなに使わ無いけど、エラー出せたりwrapper作れるならGatsbyImageでもいい感じあるな🤔

HishoHisho

とりあえずGatsbyImageの方の汎用componentも作っていこう

HishoHisho

仕様

  • 画像ファイルをすべて予め取得しておいて、relativePathを受け取り表示したい画像を返す
  • 画像がない場合はエラーを吐く
  • prefixsp_から始まるファイルはアートディレクションとして処理する
  • その他
HishoHisho
src/components/Picture.tsx
import React, {FCX} from 'react';
import {graphql, useStaticQuery} from 'gatsby';
import {GatsbyImage, IGatsbyImageData} from 'gatsby-plugin-image';

type PicturePropsType = {
  relativePath: string;
};

export const Picture: FCX<PicturePropsType> = ({relativePath}) => {
  const images = useStaticQuery<{
    allFile: {
      nodes: {
        relativePath: string;
        childImageSharp: {
          gatsbyImageData: IGatsbyImageData;
        };
      }[];
    };
  }>(graphql`
    query AllImages {
      allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: CONSTRAINED
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentImages = images.allFile.nodes.find(
    (n) => n.relativePath === relativePath
  );
  

  if (!currentImages) {
    return (
      <div>{relativePath}は存在しないよ</div>
    )
  } else {
    return (
      <div>
        <GatsbyImage
          image={currentImages.childImageSharp.gatsbyImageData}
          alt=""
        />
      </div>
    )
  }
};
  • sourceInstanceNameにはgatsby-config.jsgatsby-transformer-sharp.options.name値を指定してあげる
  • extension(拡張子)の絞り込みを正規表現で/(png|jpe?g)/とする事でpng,jpg,jpegのみを対象として画像を取ってくる
  • images.allFile.nodes配列なのでfindで最初に見つけた一つを切り出して返す
  • currentImagesが無い時はエラーを返す
HishoHisho

今日はもう時間切れなので、アートディレクションの処理はまた明日以降

HishoHisho

PC版とSP版の画像切り替えをするためにクエリを分ける

src/components/picture.tsx
import React, {FCX} from 'react';
import {graphql, useStaticQuery} from 'gatsby';
import {GatsbyImage, IGatsbyImageData} from 'gatsby-plugin-image';

type PicturePropsType = {
  relativePath: string;
};

type PictureQueryType = {
  nodes: {
    relativePath: string;
    childImageSharp: {
      gatsbyImageData: IGatsbyImageData;
    };
  }[];
};

export const Picture: FCX<PicturePropsType> = ({relativePath}) => {
  const {desktopImages, mobileImages} = useStaticQuery<{
    desktopImages: PictureQueryType;
    mobileImages: PictureQueryType
  }>(graphql`
    query AllImages {
      desktopImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^(?!sp_)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: CONSTRAINED
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      },
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: CONSTRAINED
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentDesktopImage = desktopImages.nodes.find(
    (n) => n.relativePath === relativePath
  );
  const currentMobileImage = currentDesktopImage ? mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  }) : undefined;

  if (!currentDesktopImage) {
    return <div>{relativePath}は存在しないよ</div>;
  } else {
    return (
      <div>
        <GatsbyImage
          image={currentDesktopImage.childImageSharp.gatsbyImageData}
          alt=""
        />
      </div>
    );
  }
};

HishoHisho

とりあえずアートディレクションが動くまで

src/components/picture.tsx
import React, {FCX} from 'react';
import {graphql, useStaticQuery} from 'gatsby';
import {GatsbyImage, IGatsbyImageData, withArtDirection} from 'gatsby-plugin-image';

type PicturePropsType = {
  relativePath: string;
};

type PictureQueryType = {
  nodes: {
    relativePath: string;
    childImageSharp: {
      gatsbyImageData: IGatsbyImageData;
    };
  }[];
};

export const Picture: FCX<PicturePropsType> = ({relativePath}) => {
  const {desktopImages, mobileImages} = useStaticQuery<{
    desktopImages: PictureQueryType;
    mobileImages: PictureQueryType
  }>(graphql`
    query AllImages {
      desktopImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^(?!sp_)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: CONSTRAINED
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      },
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: CONSTRAINED
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentDesktopImage = desktopImages.nodes.find(
    (n) => n.relativePath === relativePath
  );
  
  if (!currentDesktopImage) {
    return <div>{relativePath}は存在しないよ</div>;
  }

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });


  function test({desktopImage, mobileImage}: {
    desktopImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    },
    mobileImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    } | undefined,
  }) {
    if (desktopImage && mobileImage) {
      return withArtDirection(desktopImage.childImageSharp.gatsbyImageData, [
        {
          media: "(max-width: 1024px)",
          image: mobileImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  }

  const images = test({desktopImage: currentDesktopImage, mobileImage: currentMobileImage});

  return (
    <div>
      <GatsbyImage
        image={images}
        alt=""
      />
    </div>
  );
};

HishoHisho

padding-bottomがなくなったのでアスペクト比が違う画像の場合はPC版のアスペクト比のまま変わらないみたい

HishoHisho

graphql見る限りgatsbyImageDataにwidthとheightが含まれるっぽいのでそこからアスペクト比を割り出した、divなどを作りcssでdisplay切り替えすれば良さそう
無論JSでも行けそうだけSSG考えるとバグるかも?

HishoHisho

ちなみにv2ではwidthとheightは取れなかった記憶がある

HishoHisho

presentationWidthpresentationHeightを別に取ってこないといけなかったのがwidthとheightで取れるのは楽でいいね😸

HishoHisho
src/components/AspectRatio.tsx
import React, {FCX} from "react";

type AspectRatioPropsType = {
  width: number
  height: number
}

export const AspectRatio: FCX<AspectRatioPropsType> = (
  {
    className="",
    children,
    width,
    height
  }) => {
  return (
    <div className={className} style={{paddingTop: `${height/width*100}%`}}>
      {children}
    </div>
  )
}
src/components/Picture.tsx
import React, {FCX} from 'react';
import {graphql, useStaticQuery} from 'gatsby';
import {GatsbyImage, IGatsbyImageData, withArtDirection} from 'gatsby-plugin-image';
import {AspectRatio} from "@src/components";

type PicturePropsType = {
  relativePath: string;
};

type PictureQueryType = {
  nodes: {
    relativePath: string;
    childImageSharp: {
      gatsbyImageData: IGatsbyImageData;
    };
  }[];
};

export const Picture: FCX<PicturePropsType> = ({relativePath}) => {
  const {desktopImages, mobileImages} = useStaticQuery<{
    desktopImages: PictureQueryType;
    mobileImages: PictureQueryType
  }>(graphql`
    query AllImages {
      desktopImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^(?!sp_)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      },
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentDesktopImage = desktopImages.nodes.find(
    (n) => n.relativePath === relativePath
  );

  if (!currentDesktopImage) {
    return <div>{relativePath}は存在しないよ</div>;
  }

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });


  function test({desktopImage, mobileImage}: {
    desktopImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    },
    mobileImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    } | undefined,
  }) {
    if (desktopImage && mobileImage) {
      return withArtDirection(mobileImage.childImageSharp.gatsbyImageData, [
        {
          media: "(min-width: 768px)",
          image: desktopImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  }

  const images = test({desktopImage: currentDesktopImage, mobileImage: currentMobileImage});

  return (
    <div className="relative">
      {currentMobileImage && <AspectRatio className="block md:hidden" width={currentMobileImage.childImageSharp.gatsbyImageData.width} height={currentMobileImage.childImageSharp.gatsbyImageData.height}/>}
      <AspectRatio className="hidden md:block" width={currentDesktopImage.childImageSharp.gatsbyImageData.width} height={currentDesktopImage.childImageSharp.gatsbyImageData.height}/>
      <GatsbyImage
        className="absolute inset-0"
        image={images}
        alt=""
      />
    </div>
  );
};

HishoHisho

ということでこの邪魔なpadding-bottomを消す

HishoHisho

ちなみにv2ではSSGした時にこのpadding-bottomのレンダリングがうまくいかないみたいで、画像はちゃんと表示されるけど、アスペクト比はあってないみたいなバグ(?)があった

HishoHisho

SSGするとtailwindがinlineになるから.gatsby-image-wrapperに負けてrelativeになるのね
まぁtailwindは!important安定かな

HishoHisho
src/components/picture/picture.tsx
import React, {FCX} from 'react';
import {graphql, useStaticQuery} from 'gatsby';
import {GatsbyImage, IGatsbyImageData, withArtDirection} from 'gatsby-plugin-image';
import {AspectRatio} from "@src/components";
import * as styles from "@src/components/Picture/picture.module.css";

type PicturePropsType = {
  relativePath: string;
};

type PictureQueryType = {
  nodes: {
    relativePath: string;
    childImageSharp: {
      gatsbyImageData: IGatsbyImageData;
    };
  }[];
};

export const Picture: FCX<PicturePropsType> = ({relativePath}) => {
  const {desktopImages, mobileImages} = useStaticQuery<{
    desktopImages: PictureQueryType;
    mobileImages: PictureQueryType
  }>(graphql`
    query AllImages {
      desktopImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^(?!sp_)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      },
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentDesktopImage = desktopImages.nodes.find(
    (n) => n.relativePath === relativePath
  );

  if (!currentDesktopImage) {
    return <div>{relativePath}は存在しないよ</div>;
  }

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });


  function test({desktopImage, mobileImage}: {
    desktopImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    },
    mobileImage: {
      relativePath: string;
      childImageSharp: {
        gatsbyImageData: IGatsbyImageData;
      }
    } | undefined,
  }) {
    if (desktopImage && mobileImage) {
      return withArtDirection(mobileImage.childImageSharp.gatsbyImageData, [
        {
          media: "(min-width: 768px)",
          image: desktopImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  }

  const images = test({desktopImage: currentDesktopImage, mobileImage: currentMobileImage});

  return (
    <div className="relative">
      {currentMobileImage && <AspectRatio className="block md:hidden" width={currentMobileImage.childImageSharp.gatsbyImageData.width} height={currentMobileImage.childImageSharp.gatsbyImageData.height}/>}
      <AspectRatio className="hidden md:block" width={currentDesktopImage.childImageSharp.gatsbyImageData.width} height={currentDesktopImage.childImageSharp.gatsbyImageData.height}/>
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt=""
      />
    </div>
  );
};

src/components/picture/picture.module.css
.gatsby-image-wrapper:not(#id) {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
}

.gatsby-image-wrapper:not(#id) [aria-hidden="true"][style*="padding-top"] {
  display: none;
}

SSGした時に.gatsby-image-wrapperに詳細度で負けるので:not(#id)で無理矢理上げて勝つ

HishoHisho

現状比較

StaticImage

  • アートディレクションが不要
  • srcの部分は手動(pathが違うと表示されない)
  • ぶっちゃけ上記の2点を除けば、GatsbyImageと同等
  • 基本的にwrapperが作れない

GatsbyImage

  • アートディレクションが必要
  • srcの部分は手動(pathが違うとエラーを投げることが可能)
  • graphql書くのがだるいけど、エラー投げれたりする
  • wrapperが作れる

まとめ

StaticImageはお手軽に使えるけど、graphqlを書いていればGatsbyImageで良い感ある
雰囲気でtypescriptやってるからできるかわからないけど、graphqlからrelativePathの型を作れそう
というかぶっちゃけアスペクト比取ってるpadding-topを消して今回みたいに自分でアスペクト比取れば、layoutはFULL_WIDTHで良いんじゃない?と思う。
例えば、アスペクト比固定したい時は手動でアスペクト比のやつを入れるようにすればいいし、高さ固定の場合はアスペクト比を入れなければいいし、横幅はmax-widthで制御するはずなので、もとの画像より大きい方が都合が良さそう

HishoHisho

というかStaticImageのsrcはどうかけば良いのか

Source image, processed at build time. Can be a path relative to the source file, or an absolute URL.

なぜかpathはミスってても表示されるっぽい

zsh
warn Could not find image "../../images/screenshot.png". Looked for

相対パスミスって上記のwarnが出ても表示される
ちなみにルート相対パス/src/imagesは表示されない

HishoHisho

いけるpath
/low/../../../../../../../../../
/low/low/../../../../../../../../../
/low/low/low/../../../../../../../../../
ほんと謎😂

とりあえずgatsby developした時に見えたらSSGしても見えるっぽい

HishoHisho

とりあえず、外部切り出し

src/hooks/useAnyImage.ts
import { graphql, useStaticQuery } from 'gatsby';
import { IGatsbyImageData } from 'gatsby-plugin-image';

export type anyImageQueryType = {
  relativePath: string;
  childImageSharp: {
    gatsbyImageData: IGatsbyImageData;
  };
};

export type PictureQueryType = {
  nodes: anyImageQueryType[];
};

type useAnyImageType = (
  relativePath: string
) => {
  anyImage?: anyImageQueryType;
  mobileImage?: anyImageQueryType;
};

export const useAnyImage: useAnyImageType = (relativePath) => {
  const { anyImages, mobileImages } = useStaticQuery<{
    anyImages: PictureQueryType;
    mobileImages: PictureQueryType;
  }>(graphql`
    query AllImageas {
      anyImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentAnyImage = anyImages.nodes.find(
    (n) => n.relativePath === relativePath
  );

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });

  return {
    anyImage: currentAnyImage,
    mobileImage: currentMobileImage,
  };
};
src/components/picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage, withArtDirection } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage, anyImageQueryType } from '@src/hooks';

type PicturePropsType = {
  relativePath: string;
};

export const Picture: FCX<PicturePropsType> = ({ relativePath }) => {
  const { anyImage, mobileImage } = useAnyImage(relativePath);

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }

  function test({
    desktopImage,
    mobileImage,
  }: {
    desktopImage: anyImageQueryType;
    mobileImage?: anyImageQueryType;
  }) {
    if (desktopImage && mobileImage) {
      return withArtDirection(mobileImage.childImageSharp.gatsbyImageData, [
        {
          media: '(min-width: 768px)',
          image: desktopImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  }

  const images = test({
    desktopImage: anyImage,
    mobileImage,
  });

  return (
    <div className="relative">
      {mobileImage && (
        <AspectRatio
          className="block md:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <AspectRatio
        className="hidden md:block"
        width={anyImage.childImageSharp.gatsbyImageData.width}
        height={anyImage.childImageSharp.gatsbyImageData.height}
      />
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt=""
      />
    </div>
  );
};

HishoHisho

リファクタリングその2

アートディレクションの処理をuseAnyImageに移動

src/hooks/useAnyImage.ts
import { graphql, useStaticQuery } from 'gatsby';
import { IGatsbyImageData, withArtDirection } from 'gatsby-plugin-image';

export type anyImageQueryType = {
  relativePath: string;
  childImageSharp: {
    gatsbyImageData: IGatsbyImageData;
  };
};

export type PictureQueryType = {
  nodes: anyImageQueryType[];
};

type createArtDirectionType = (
  images: {
    desktopImage: anyImageQueryType;
    mobileImage?: anyImageQueryType;
  },
  media?: string
) => IGatsbyImageData;

type useAnyImageType = (
  relativePath: string
) => {
  anyImage?: anyImageQueryType;
  mobileImage?: anyImageQueryType;
  createArtDirection: createArtDirectionType;
};

export const useAnyImage: useAnyImageType = (relativePath) => {
  const { anyImages, mobileImages } = useStaticQuery<{
    anyImages: PictureQueryType;
    mobileImages: PictureQueryType;
  }>(graphql`
    query AllImageas {
      anyImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentAnyImage = anyImages.nodes.find(
    (n) => n.relativePath === relativePath
  );

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });

  const createArtDirection: createArtDirectionType = (
    { desktopImage, mobileImage },
    media = `min-width: ${768 / 16}em`
  ) => {
    if (desktopImage && mobileImage) {
      return withArtDirection(mobileImage.childImageSharp.gatsbyImageData, [
        {
          media: `(${media})`,
          image: desktopImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  };

  return {
    anyImage: currentAnyImage,
    mobileImage: currentMobileImage,
    createArtDirection,
  };
};

src/components/picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage } from '@src/hooks';

type PicturePropsType = {
  relativePath: string;
};

export const Picture: FCX<PicturePropsType> = ({ relativePath }) => {
  const { anyImage, mobileImage, createArtDirection } = useAnyImage(
    relativePath
  );

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }
  const images = createArtDirection({ desktopImage: anyImage, mobileImage });

  return (
    <div className="relative">
      {mobileImage && (
        <AspectRatio
          className="block md:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <AspectRatio
        className="hidden md:block"
        width={anyImage.childImageSharp.gatsbyImageData.width}
        height={anyImage.childImageSharp.gatsbyImageData.height}
      />
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt=""
      />
    </div>
  );
};
HishoHisho

機能追加その1

アスペクト比の有無を自由に決めれるように

src/components/picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage } from '@src/hooks';

type PicturePropsType = {
  aspect?: boolean;
  relativePath: string;
};

export const Picture: FCX<PicturePropsType> = ({
  aspect = true,
  relativePath,
}) => {
  const { anyImage, mobileImage, createArtDirection } = useAnyImage(
    relativePath
  );

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }
  const images = createArtDirection({ desktopImage: anyImage, mobileImage });

  return (
    <div className="relative w-full h-full">
      {aspect && mobileImage && (
        <AspectRatio
          className="block sm:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      {aspect && (
        <AspectRatio
          className="hidden sm:block"
          width={anyImage.childImageSharp.gatsbyImageData.width}
          height={anyImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt=""
      />
    </div>
  );
};

HishoHisho

機能追加その2

PicctureGatsbyImagePropsを渡せるようにする

src/components/Picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage, GatsbyImageProps } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage } from '@src/hooks';

type PicturePropsType = {
  aspect?: boolean;
  relativePath: string;
  alt?: string;
  GatsbyImageProps?: Omit<GatsbyImageProps, 'alt' | 'image' | 'className'>;
};

export const Picture: FCX<PicturePropsType> = ({
  aspect = true,
  relativePath,
  alt = '',
  GatsbyImageProps = {},
}) => {
  const { anyImage, mobileImage, createArtDirection } = useAnyImage(
    relativePath
  );

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }
  const images = createArtDirection({ desktopImage: anyImage, mobileImage });

  return (
    <div className="relative w-full h-full">
      {aspect && mobileImage && (
        <AspectRatio
          className="block sm:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      {aspect && (
        <AspectRatio
          className="hidden sm:block"
          width={anyImage.childImageSharp.gatsbyImageData.width}
          height={anyImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt={alt}
        {...GatsbyImageProps}
      />
    </div>
  );
};

HishoHisho

機能追加 その3
sp版の画像がない場合はpc,spで同じアスペクト比を維持する

src/components/picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage, GatsbyImageProps } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage } from '@src/hooks';

type PicturePropsType = {
  aspect?: boolean;
  relativePath: string;
  alt?: string;
  GatsbyImageProps?: Omit<GatsbyImageProps, 'alt' | 'image' | 'className'>;
};

export const Picture: FCX<PicturePropsType> = ({
  aspect = true,
  relativePath,
  alt = '',
  GatsbyImageProps = {},
}) => {
  const { anyImage, mobileImage, createArtDirection } = useAnyImage(
    relativePath
  );

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }
  const images = createArtDirection({ desktopImage: anyImage, mobileImage });

  return (
    <div className="relative w-full h-full">
      {aspect && mobileImage && (
        <AspectRatio
          className="block sm:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      {aspect && (
        <AspectRatio
          className={mobileImage && `hidden sm:block`}
          width={anyImage.childImageSharp.gatsbyImageData.width}
          height={anyImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt={alt}
        {...GatsbyImageProps}
      />
    </div>
  );
};

HishoHisho

とりあえず現状まとめ

src/hooks/anyImage.ts
import { graphql, useStaticQuery } from 'gatsby';
import { IGatsbyImageData, withArtDirection } from 'gatsby-plugin-image';

export type anyImageQueryType = {
  relativePath: string;
  childImageSharp: {
    gatsbyImageData: IGatsbyImageData;
  };
};

export type PictureQueryType = {
  nodes: anyImageQueryType[];
};

type createArtDirectionType = (
  images: {
    desktopImage: anyImageQueryType;
    mobileImage?: anyImageQueryType;
  },
  media?: string
) => IGatsbyImageData;

type useAnyImageType = (
  relativePath: string
) => {
  anyImage?: anyImageQueryType;
  mobileImage?: anyImageQueryType;
  createArtDirection: createArtDirectionType;
};

export const useAnyImage: useAnyImageType = (relativePath) => {
  const { anyImages, mobileImages } = useStaticQuery<{
    anyImages: PictureQueryType;
    mobileImages: PictureQueryType;
  }>(graphql`
    query AllImageas {
      anyImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
      mobileImages: allFile(
        filter: {
          sourceInstanceName: { eq: "images" }
          extension: { regex: "/(png|jpe?g)/" }
          name: { regex: "/^sp_/" }
        }
      ) {
        nodes {
          relativePath
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
  `);

  const currentAnyImage = anyImages.nodes.find(
    (n) => n.relativePath === relativePath
  );

  const currentMobileImage = mobileImages.nodes.find((n) => {
    return n.relativePath.replace(/sp_/, '') === relativePath;
  });

  const createArtDirection: createArtDirectionType = (
    { desktopImage, mobileImage },
    media = `min-width: ${768 / 16}em`
  ) => {
    if (desktopImage && mobileImage) {
      return withArtDirection(mobileImage.childImageSharp.gatsbyImageData, [
        {
          media: `(${media})`,
          image: desktopImage.childImageSharp.gatsbyImageData,
        },
      ]);
    } else {
      return desktopImage.childImageSharp.gatsbyImageData;
    }
  };

  return {
    anyImage: currentAnyImage,
    mobileImage: currentMobileImage,
    createArtDirection,
  };
};
src/component/picture.module.css
.gatsby-image-wrapper:not(#id) {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
}

.gatsby-image-wrapper:not(#id) [aria-hidden="true"][style*="padding-top"] {
  display: none;
}

src/components/picture.tsx
import React, { FCX } from 'react';
import { GatsbyImage, GatsbyImageProps } from 'gatsby-plugin-image';
import { AspectRatio } from '@src/components';
import * as styles from '@src/components/Picture/picture.module.css';
import { useAnyImage } from '@src/hooks';

type PicturePropsType = {
  aspect?: boolean;
  relativePath: string;
  alt?: string;
  GatsbyImageProps?: Omit<GatsbyImageProps, 'alt' | 'image' | 'className'>;
};

export const Picture: FCX<PicturePropsType> = ({
  aspect = true,
  relativePath,
  alt = '',
  GatsbyImageProps = {},
}) => {
  const { anyImage, mobileImage, createArtDirection } = useAnyImage(
    relativePath
  );

  if (!anyImage) {
    return <div>{`${relativePath}は存在しません!`}</div>;
  }
  const images = createArtDirection({ desktopImage: anyImage, mobileImage });

  return (
    <div className="relative w-full h-full">
      {aspect && mobileImage && (
        <AspectRatio
          className="block sm:hidden"
          width={mobileImage.childImageSharp.gatsbyImageData.width}
          height={mobileImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      {aspect && (
        <AspectRatio
          className={mobileImage && `hidden sm:block`}
          width={anyImage.childImageSharp.gatsbyImageData.width}
          height={anyImage.childImageSharp.gatsbyImageData.height}
        />
      )}
      <GatsbyImage
        className={styles.gatsbyImageWrapper}
        image={images}
        alt={alt}
        {...GatsbyImageProps}
      />
    </div>
  );
};

呼び出し

src/pages/index.tsx
import React, { FCP } from 'react';
import { Layout, SEO } from '@src/layouts';
import { Picture } from '@src/components';

const IndexPage: FCP = () => {
  return (
    <Layout>
      <SEO />
      <Picture relativePath="screenshot.png" /> {/* ps,pcでアスペクト比が違う画像 */}
      <Picture relativePath="icon.png" />{/* ps,pcで何も変わらない画像 */}
      <div className="w-20 h-40">
        <Picture aspect={false} relativePath="screenshot.png" />{/* アスペクト比を無視したい画像 */}
      </div>
    </Layout>
  );
};

export default IndexPage;

HishoHisho

コメントアウト書かないの良くないなぁ😂

このスクラップは2021/04/26にクローズされました