LoginSignup
67
43

More than 1 year has passed since last update.

【初心者向け】Next.js + Rails APIで認証付きCRUDアプリ作って学んだこと

Last updated at Posted at 2022-12-17

はじめに

Next.js + Rails API で個人 PF 作ろうと思っているので、React や TypeScript、Next.js を勉強しています。
この間、勉強の成果検証として、簡単な認証付き CRUD アプリを作りました。
機能はすごく簡単だけど、(特にTypeScriptで)色々躓いたので、メモとしてまとめたいと思います。
※ この記事では主に Next.js 側についてまとめています。

この記事はこんな人におすすめ

  • React の基礎知識があり、Next.js についても興味ある人。
  • Next.js を使いたいけど、レンダリング形式の違いなど具体的なイメージがまだ詳しくない人。
  • Axios や next-seo, React Hook Form と React-toastify などのライブラリを使いたい人。
  • Firebase の認証機能を使いたいけど、イメージがまだわからない人。

この記事で触れること

  • Next.js を使う理由(React と比べてのメリット)
  • Next.js で CRUD アプリを作る流れ
    • バックエンドからデータ取得 (getStaticProps vs getServerSideProps)
    • Routing (file-base routing, next/router)
    • React Hook Form を使って、投稿作成・編集フォームを作る
    • React-toastify を使って、通知メッセージ表示機能作る
    • Metadata (next-seo, next/head)
    • 画像表示(svg 画像処理, next/image)
    • デバッグ
  • Next.js で Typescript を取り組むイメージ
  • Firebase 認証機能の使い方(少し触れる)
  • Vercel へのデプロイ

この記事で触れないこと

  • Rails API の構築関連
  • React の基本知識
  • TypeScript の基本知識
  • Tailwind CSS の使い方

利用技術

フロントエンド

  • Next.js 13.0.4
  • React 18.2.0
  • TypeScript 4.9.3
  • Tailwind CSS 3.2.4

バックエンド

  • Ruby 3.1.2
  • Rails 7.0.4  (API モード)

認証部分

  • Firebase Authentication(V9) - Google ログインのみ

基本機能

  • ユーザーログイン、ログアウト機能
  • 記事閲覧、作成、編集、削除機能(未ログインユーザーは閲覧のみ)

Demo サイトとソースコード

ソースコード: Backend: Rails APIFrontend: Next.js

Demo: next-firebase-auth-sample-app

なぜ Next.js を使いたいのか?

最初は React のみを使う予定だったけど、Next.js のことを知って、少し調べたら、途中で Next.js に決めました。

理由は主に二つ。

  • ルーティング設定は React より比較的に簡単
  • SEO 効果が良い

Routing 機能は比較的に簡単

Next.js では、基本的に file-system based routing という機能を使って、ルートを生成しています。
pagesフォルダにファイルを入れることで、pagesからそのファイルへの path が自動的にルートになる。

pages/index.tsxで、/root path が生成される。
pages/user/profile.tsxで、ユーザープロフィールページの path が/user/profileになる。

動的なルートなら、pages/blog/[id].tsxで、ブログ詳細ページの path が/blog/:id/になる。

また、ページ間の遷移は、主に<Link />コンポーネントを使います。
基本的な使い方は<a>タグと似てるけど、rendering 方式は react の SPA 風に近いです。

import Link from "next/link";

<Link href="/">Home</Link>
<Link href="/about">About Us</Link>
<Link href="/login">Login</Link>

Next.js はデフォルトで、ページに埋め込んでいる<Link />のリンク先のページを事前に HTML ページを生成しています。

例えば、投稿一覧ページ内に各投稿詳細ページへのリンクがある。

そして、ユーザーが投稿リストを閲覧している間に、Next.js はユーザーはこれからリンクをクリックするかもと仮定して、事前に関連の投稿詳細ページを生成しておきます。データ取得必要なら、事前のデータ取得も含む。

それで、ユーザーが記事リンクをクリックしたら、事前用意済みのページをすぐに表示することができます。

Pre-rendering による SEO 効果

React でのページ表示は client-side rendering のみで、全てのページ内容がクライアント側(つまりブラウザ)で Javascript により生成するので、実際の HTML 中身は空の状態のため、検索エンジンのクロールはそのページの内容を取得できないので、そのページをインデックスに収録しない。

Next.js では、client-side rendering も対応するが、基本的な方針は pre-rendering

つまり、ユーザーがそのページを見る前に、Next.js が事前に静的な HTML ページを生成している。

React で生成するページは Chrome でソースを確認すると、実は何もない空のまま( <div id="root"></div>になっている)だけど、Next.js だと、そのページの HTML 内容がちゃんと見れる。

さらに、Chrome で JS を無効にすると、React アプリの内容が全部空白になる。
Next.js は事前に HTML ファイルを生成しているので、普通に表示できる。

なので、SEO 効果を重視するなら、絶対 Next.js の方が良いかなと思います。

pre-rendering を実現する 2 種類の rendering 形式、 static generation (SSG) と server-side rendering (SSR)は後で触れます。

Pre-rendering の紹介について、Next.js の公式チュートリアル(Pre-rendering and Data Fetching)があるので、ぜひ読んでみてください。

CRUD アプリ構築

それでは、実際にアプリを作ってみましょう。
細かい内容は機能を作りながら説明していきたいと思います。

Next.js の基本設定

まずはnpx create-next-app@latest sample-app --tsでアプリを生成する。 今回は TypeScript を使うので、 --tsで指定する。

ESlint

アプリ生成する前にESLintもインストールするかと聞かれるので、そこでyを出す。
✔ Would you like to use ESLint with this project? … No / Yes

Next.js は必要だと考える ESlint ルールを全部組み込み済みなので、基本的に追加設定は不要かと思います。
詳しい説明は公式ドキュメントESLint

TypeScript Path 設定

他のファイルから import するとき、ルートが深いと相対 path が読みづらいので、よく使うディレクトリの paths を特別指定する。

tsconfig.json


{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "components/*": ["components/*"],
      "context/*": ["context/*"],
      "hooks/*": ["hooks/*"],
      "types/*": ["types/*"]
    }
  }
}

srcディレクトリを使う予定なら、baseUrl"src"に指定した方が良い。

これで import するとき、../../../のような相対 path ではなく、直接components/layout/headerのように使える。

詳しくは公式説明Paths - paths

Tailwind CSS インストール

公式説明参照通りにする -> Install Tailwind CSS with Next.js

Axios - baseURL 設定とエラー処理

npm install axiosでインストール

pages/_app.tsx内で baseUrl を設定する。

import axios from "axios";

axios.defaults.baseURL =
  process.env.NEXT_PUBLIC_BASE_URL || "http://127.0.0.1:3001/api/v1";

これで、本番環境で使う url と開発環境で使うローカルホスト両方使えるようになるので、とても便利だと思います。

デプロイ先に本番の url をNEXT_PUBLIC_BASE_URL環境変数に登録したら、本番環境でそれを使う。
開発環境では環境変数を登録していないので、ローカルホストの url を使うことになる。

ついでに、axios を使う時のエラー処理についても紹介したいと思います。
(前に Typescript でエラーの型について躓いた...)

How to type axios error in Typescript? #3612を色々参照して、エラー処理は下記にしています。

Axios にはAxiosErrorという型を定義されているクラスがあるので、isAxiosError関数を使って、AxiosErrorかどうかを判断できる。

もしAxiosErrorなら、エラーメッセージを出力する。
それ以外のエラーなら、エラーを String 化して出力する。

try {
  const response = await axios.post("/login");
} catch (err) {
  let message;
  if (axios.isAxiosError(err) && err.response) {
    console.error(err.response.data.message);
  } else {
    message = String(err);
    console.error(message);
  }
}

React Hook Form インストール

npm install react-hook-formでインストール

React-toastify - 基本設定

npm install --save react-toastifyでインストール

pages/_app.tsx内に<ToastContainer />を追加することで、全ページで使えるようにする。

<ToastContainer />にプロパーティを追加することで、デフォルトの設定を指定できる。

// pages/_app.tsx
import { ToastContainer } from "react-toastify";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <AuthContextProvider>
      <Component {...pageProps} />
      <ToastContainer
        position="top-center" // 表示位置は画面上中央に指定
        autoClose={5000} // 5秒後自動消える
        hideProgressBar={false} // Progress barは表示する
        newestOnTop // 最新の通知をTOPにする
        closeOnClick // クリックで閉じる
        rtl={false} // 右から左へのlayoutを使えない
        pauseOnFocusLoss={false} // ウィンドウがフォーカスを失ったときにトーストを一時停止にしない
        draggable={false} // トーストをドラッグするのをできないようにする
        pauseOnHover // ホバーする時はトーストのprogressカウントダウンを一時停止する
        theme="colored" // coloeredテーマ仕様を使用
      />
    </AuthContextProvider>
  );
}

デザイン仕様をカスタマイズしたいなら、styles/global.cssで指定すれば、デフォルトの設定をオーバーライドすることできる。
個人的に、今の文字がちょっと浅いと感じるので、font weightを指定しました。

/* styles/global.css */

.Toastify__toast--warning {
  font-weight: 600 !important;
}
.Toastify__toast--error {
  font-weight: 600 !important;
}
.Toastify__toast--success {
  font-weight: 600 !important;
}
.Toastify__toast--info {
  font-weight: 600 !important;
}

他に基本色とかを変更したいなら、色の値などを追加して変更できます。

.Toastify__toast--error {
  font-weight: 600 !important;
  color: #e74c3c !important;
  background-color: #34a853 !important;
  border-radius: 8px !important;
}

公式説明:How to style

ユーザーログイン、ログアウト機能

Firebase 認証機能の導入について、まとめ記事は先日別途書いたので、
ここで詳しい話は省略します。

詳細はこちらを参照ください。

Rails 編 - Rails + Next.js + Firebase V9 Authentication で認証付きの CRUD アプリを作る

Next.js 編 - Rails + Next.js + Firebase V9 Authentication で認証付きの CRUD アプリを作る

作成した context の中身は下記です。

import { useAuthContext } from "context/AuthContext";

const { currentUser, loading, loginWithGoogle, logout } = useAuthContext();

特に currentUser 情報取得や認可判定に使います。

例えばログイン後のユーザーのGoogleアカウントのプロフィール画像Urlを取得する

const { currentUser } = useAuthContext();
const userPhotoUrl = currentUser?.photoURL;

投稿詳細ページで、もしcurrentUserが投稿作者だったら、編集&削除ボタンを表示させる。

  const { currentUser } = useAuthContext();
  const [isAuthor, setIsAuthor] = useState(false);

  useEffect(() => {
    if (currentUser && currentUser.uid === post.user_uid) {
      setIsAuthor(true);
    }
  }, [currentUser]);

  {isAuthor && <Link href={`/posts/edit/${post.id}`} type="button"> Edit</Link>}
  {isAuthor && <button onClick={deletePost}>Delete</button>}

記事作成機能 - React Hook Form の使い方

pages/posts/new.tsxで記事作成ページを作リます。

// pages/posts/new.tsx

import { useForm, SubmitHandler } from "react-hook-form";

interface PostInputs {
  title: string;
  body: string;
}

export default function NewPostPage() {
  const {register, handleSubmit, formState: { errors }} = useForm<PostInputs>();

  const createPost: SubmitHandler<PostInputs> = (postData) => {
    console.log(postData);
  };

  return (
    <form onSubmit={handleSubmit(createPost)}>
      <input {...register("title", { required: true, maxLength: 60 })} />
      {errors.title &&
        "Title is required and should be less than 60 characters."}
      <textarea
        {...register("body", { required: true, maxLength: 500 })}
      ></textarea>
      {errors.body && "Body is required and should be less than 500 words."}
      <button type="submit">Create Post</button>
    </form>
  );
}

React Hook Form を使って入力データの一括取得や、バリデーション、エラー処理がすごく簡単にできます。

まずは入力データを取得するため、フィールドをregisterすることが必要。

フィールドに対してのバリデーションも追加可能。

<input {...register("title", { required: true, maxLength: 60 })} />

これで、記事タイトルフィールドに対して、記入必須で最大文字数 60 までの制限をかける。

{
  errors.title && "Title is required and should be less than 60 characters.";
}

下に ↑ を追加することで、エラーメッセの表示設定もできる。

<form onSubmit={handleSubmit(createPost)}></form>

ユーザーが投稿ボタンを押すと、ここのcreatePost関数が呼び出される

// ここでデータをオブジェクト形式で出力される
// {title: "title", body: "body"}
const createPost: SubmitHandler<PostInputs> = (postData) => {
  console.log(postData);
};

そして Axios 使って、post データを Rails 側に送る。

投稿成功したら、toast.success("Post was successfully created!")で成功メッセージを表示して、
router.push("/")でホームページへ遷移させる。

エラーが起こったら、toast.error("Failure: something wrong happened!")でエラーメッセージを表示。

const createPost: SubmitHandler<PostInputs> = (data) => {

  try {
    const response = axios.post(
      "/posts",
      { post: postInputData },
      config
    );
    if (response.status === 200) {
      toast.success("Post was successfully created!");
      router.push("/");
    }
  } catch (err) {
    toast.error("Failure: something wrong happened!");
    ...
  }
}

記事一覧、詳細ページ - 動的ルーティングとデータ取得

そして記事一覧ページと詳細ページを作ります。

今回は記事一覧はホームページで表示するので、pages/index.tsxになります。

記事詳細ページはpages/posts/[id]/index.tsxで作成します。

そしてデータ取得に関して、まずNext.jsで使う三つのレンダリング形式について触れたいと思います。

Client-side Rendering, Static-site Generation と Server-side Rendering

前述したように、Next.js では React のような Client-side Rendering より、Static-site Generation と Server-side Rendering による Pre-rendering を推奨しています。

とりわけ Static-site Generation の使用を推奨しています。

この三つのレンダリング形式、それぞれの違いを簡単にまとめてみると、下記になります。

Client-side Rendering

  • メリット:
    • データがいつも最新状態で表示される。
      • データの更新が検知されたら、画面がすぐ再レンダリングすることができる。
    • コンポーネントレベルでも実行できる。他の 2 種類はページ内でしか使えない。
  • デメリット:
    • SEO 効果に対応できない
    • ページのロード速度は遅くなる可能性がある。
      • データが事前取得ではなく、コンポーネントやページがマウントされる時に行われるため
      • データがキャッシュされないため
  • 想定利用シーン:
    • ページが SEO インデックスを必要としない場合
    • データを事前にレンダリングする必要がない場合
    • ページのコンテンツを頻繁に更新する必要がある場合
    • 例えば管理画面など。

Static-site Generation (Incremental Static Regeneration)

  • メリット:
    • ページが非常に高速的に表示される。
      • ページの生成(データの取得を含む)がアプリのビルド時に行われるため
      • 静的なページなので、CDN に置くことが可能。
    • SEO 対応できる。
  • デメリット:
    • 最新のデータを届けるのは遅くなる。
      • ただこの点は Incremental Static Regeneration によって改善できる
        • 最短 1 秒ごとにページを再ビルドすることができる
        • その場合は、サーバーへの負荷も高くなる?
  • 想定利用シーン:
    • データを頻繁に更新したりしない場合。
      • LP や、コーポーレートサイト、ブログ、EC サイトなど

Server-side Rendering

  • メリット:
    • ページの表示が速い。
    • ページが常に最新状態で届ける。
    • SEO 対応できる。
  • デメリット:
    • ページの表示は SSG より遅くなる
      • ページの生成は事前生成でなく、リクエストがある時に行うため
      • ページが CDN にキャッシュされない.(キャッシュする場合は cache-control ヘッダの設定が必要)
    • サーバー側の負荷が高い
  • 想定利用シーン:
    • ページは事前生成より、リクエスト時に生成することが必要な場合
    • 例えばコンテンツ更新が頻繁な SNS サイトなど

現状では、データの更新頻度が 1 秒よりも速い場合を除く、
Next.js はできるだけ SSG を使用することを推奨しています。

参照:
Static Generation
Server-side Rendering
Client-side data fetching
結局 React と Next.js のどちらで開発を進めればいいの?

Static-site Generation でのデータ取得

まずは Static-site Generation を使ってみましょう。

getStaticPathsでpathを生成する

動的ルートに対応するため、先にgetStaticPaths()で path を生成することが必要。

interface PostData extends PostInputs {
  id: number;
  user_uid: string;
  created_at: string;
}

export async function getStaticPaths() {
  const response = await axios.get("/posts");
  const posts: PostData[] = response.data;

  return {
    fallback: "blocking",
    paths: posts.map((post) => ({
      params: {
        id: post.id.toString(),
      },
    })),
  };
}

まず全ての投稿 id を取得するため、投稿データを全部取得する。そして、取得した id を params に渡す。
paramsはページファイル名にマッチする。

つまりpages/posts/[id].tsxの場合は、params の値は id になる。

もし新しい投稿がきて、paths の更新が遅れた場合、ページの表示はどうなる?
その場合は、fallbackを使う。

fallback: "blocking"にすることで、もし paths に含まれない path にアクセスが来たら、Next.js は SSR でそのページをレンダリングするので、404 ページにはならない。

fallback: "false"にしたら、ここの paths に含まれない path にアクセスしたら、直接 404 ページが表示される。

blocking と true の違いについて。

fallback: "true"はもし paths に含まれない path にアクセスが来たら、まずデータを含まないページを表示する(fallback page)。そしてバックグランドでデータ取得してページ内容を入れ替える。

ただ、もしLinkなどサイト内部の遷移だったら、fallback page を先に表示するのでなく、blockingのように、直接 SSR でページを生成してレンダリングする。

基本的にblockingと似ているけど、fallback: "true"は特にページ数が非常に多い大型 EC サイトなどに向いている。
なぜなら、事前生成するページの数が非常に多くなると、ビルド時間も長くなるので、先に一部のページのみをビルドして、残りはリクエストが来るたびに対応すれば良いという形にすると、ユーザー体験を損なうことなく、ビルド時間も短縮できる。

参照:getStaticPaths

getStaticProps で 記事詳細データを取得する

そしてgetStaticPropsを使って、バックエンドから投稿データを取得する。

※TypeScriptを使う場合、 contextの型GetStaticPropsContextをimportする必要。
{id}の型も別途指定する必要。

import { GetStaticPropsContext } from "next";

interface Params extends ParsedUrlQuery {
  id: string;
}

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const { id } = context.params as Params;

  try {
    const response = await axios.get(`/posts/${id}`);
    const post: Post = response.data;
    return {
      props: {
        post: post,
      },
      revalidate: 10,
    };
  } catch (err) {
    ...
  }
};

ここでまずcontextというオブジェクトから、今の route パラメーター、つまりidを取得する。
そして、Axios でリクエストを送って、該当記事の詳細データ取得する。
取得したデータをpropsオブジェクトに渡して、戻り値として返す。
このpropsfunction PostDetailPage({ post }: Props) {}が受け取る。

また、revalidate: 10にすることで、Next.js は 10 秒ごとにデータの状態を確認する。

参照:getStaticProps

Server-side Rendering でのデータ取得

Server-side Rendering でのデータ取得も見てみましょう。

import { GetServerSideProps} from "next";
import fetch from "node-fetch";

export const getServerSideProps: GetServerSideProps<Props> = async (
  context
) => {
  const { id } = context.query;

  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/posts/${id}`);

  const post: PostData = (await res.json()) as any;

  res.headers.set(
    "Cache-Control",
    "public, s-maxage=10, stale-while-revalidate=59"
  );

  return { props: { post } };
};

GetServerSidePropsでは、context.queryidを取得している。

そしてキャッシュの設定もしたいため、公式説明の通りに、ここでfetch を使ってデータ取得する。
※ 通常のFetch APIはクライアントサイドで実行するので、サーバーで使うなら、node-fetch使う必要。

データをキャッシュするため、Cache-Controlを手動で設定する。

参照:Caching

記事編集機能

まずpages/posts/[id]/edit.tsxで編集ページを作成する。

編集ページでは、投稿データの表示が必要なので、先にgetServerSidePropsでデータを取得する。

ここでGetStaticPropsを使わないのは、ユーザーが投稿の作者なのか、認可を行う必要なので、ページ内容を先に表示してしまうことを避けるため。

コードは記事詳細ページの時と同じで、キャッシュを設定していないだけ。

export const getServerSideProps: GetServerSideProps<{
  post: PostData,
}> = async (context) => {
  const { id } = context.query;

  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/posts/${id}`);

  const post: PostData = await res.json();

  return { props: { post } };
};

新規作成、編集 Form 共通化

新規作成か、編集するか、を判断するプロセスを追加すれば、新規作成ページと編集ページで使うフォームが共通化できる。

ここで判断に使う要素はpropsの中に投稿データが存在するかどうか。
もし、投稿データが渡されたら、編集モードで、投稿データがないときは、新規作成モードになる。

export default function PostForm(props: Props) {
  const post = props?.postData;
  const isAddMode = !post;
}

そして、submit 後の処理も使い分けの判断を追加する

const onSubmit: SubmitHandler<PostInputs> = (postInputData) => {
  return isAddMode
    ? createPost(postInputData)
    : updatePost(post.id, postInputData);
};

createPostupdatePost関数をそれぞれ作成

async function createPost(postInputData: PostInputs) {
  const response = await axios.post("/posts", { post: postInputData }, config);
}

async function updatePost(id: number, postInputData: PostInputs) {
  const response = await axios.patch(
    `/posts/${id}`,
    { post: postInputData },
    config
  );
}

これで新規作成ページには、<PostForm />コンポーネントだけを入れることで済む。
編集ページでは、<PostForm postData={post} />で取得したデータをフォームコンポーネントに渡す。

Vercel へデプロイ

Vercel へのデプロイはすごく簡単。
Import Git Repositoryから Github のレポジトリを import することで、自動でデプロイしてくれる。
ESlint チェックも自動的に行われる。

何より、特に Github Actions で設定することなく、任意のブランチに push するたびに、
新しいバージョンを本番と異なるドメインに自動デプロイしてくれる。

これは毎回新しい画面修正とかできたら、main ブランチにマージしなくても、すぐ本番環境で確認できる。

サイトパフォーマンスも自動分析してくれる。非常に良いサービスだと思う。

その他 - デバッグ、Metadata、画像表示

最後に、Next.jsでのデバッグやSEO対策のMetadata設定、画像表示(特にSVG画像処理)について触れたいと思います。

Chrome DevToolsを使ってのデバッグ

クライアントサイドでのデバッグはdebugger宣言を使って行う。ここはRailsとも似ている感じ。

止まってほしいところに、debuggerを入れる。
そして、Chrome検証を開いて、 Sourcesタブをクリック。
それで、画面操作して、debugger入れるところまでに行ったら、プロフラムが止まる。

サーバーサイドコードのデバッグなら、next devコマンドの前にNODE_OPTIONS='--inspect'を指定する必要。
npm run dev使うなら、package.jsondevスクリプトを変更する必要。

"dev": "NODE_OPTIONS='--inspect' next dev"

そしてサーバー立ち上げたら、下記のような提示が出る。

Debugger listening on ws://127.0.0.1:9229/0cf90313-350d-4466-a748-cd60f4e47c95
For help, see: https://nodejs.org/en/docs/inspector
ready - started server on 0.0.0.0:3000, url: http://localhost:3000

参照:Debugging with Chrome DevTools

Metadata設定

Next.jsでのMetadata設定は、組み込みのnext/headを使うか、外部ライブラリのnext-seoを使う方法がある。

next/headを使う

import Head from "next/head";

<Head>
  <title>{post.title}</title>
  <meta name="description" content={post.body} />
</Head>

Open Graph Protocolも対応するなら、追加すれば良い。

{/* Twitter */}
<meta name="twitter:card" content="summary" key="twcard" />
<meta name="twitter:creator" content={twitterHandle} key="twhandle" />

{/* Open Graph */}
<meta property="og:url" content={currentURL} key="ogurl" />
<meta property="og:image" content={previewImage} key="ogimage" />
<meta property="og:site_name" content={siteName} key="ogsitename" />
<meta property="og:title" content={pageTitle} key="ogtitle" />
<meta property="og:description" content={description} key="ogdesc" />

ここでkeyプロパーティを指定することで、重複したタグが一つだけ使われると制限する。
下記の場合は、下の方がだけが使われる。

<meta property="og:url" content={currentURL} key="ogurl" />
<meta property="og:url" content={newURL} key="ogurl" />

参照:next/head

next-seoを使う

Open Graph対応はnext-seoを使った方がより簡単かも。

import { NextSeo } from 'next-seo';

const Page = () => (
  <>
    <NextSeo
      title="Using More of Config"
      description="This example uses more of the available config options."
      canonical="https://www.canonical.ie/"
      openGraph={{
        url: 'https://www.url.ie/a',
        title: 'Open Graph Title',
        description: 'Open Graph Description',
        images: [
          {
            url: 'https://www.example.ie/og-image-01.jpg',
            width: 800,
            height: 600,
            alt: 'Og Image Alt',
            type: 'image/jpeg',
          },
        ],
        siteName: 'SiteName',
      }}
      twitter={{
        handle: '@handle',
        site: '@site',
        cardType: 'summary_large_image',
      }}
    />
  </>
);

export default Page;

画像表示処理

今回のアプリで、三つの場所で画像を使っていて、それぞれの処理は若干異なる。

  • 投稿詳細ページでのダミー画像(ローカルpngファイル)
  • ログインボタンでのGoogleロゴ(svgコンポーネント形式)
  • ヘッダーで表示するユーザープロフィール画像(外部リンク)

ローカル画像

Next.jsでは普通に<img>タグを使うことができるけど、画像表示のパフォーマンス面から、Next.js 公式は<img>を拡張した を使うのが推奨。

ローカル画像ファイルを使う一番簡単の方法は、直接静的画像を名前付けてimportする。
それでnext/imageは自動的に画像のサイズを識別する。

import Image from 'next/image'
import postImage from "components/posts/postImage.png";

<Image alt="Post image" src={postImage}/>

ここでのsrcの型はsrc: string | StaticImportで、StaticImportにはStaticImageDataが含まれる。
ここでimportした画像の型はStaticImageDataとなる。
中身を見ると, srcと、heightwidthがついている。

export interface StaticImageData {
    src: string;
    height: number;
    width: number;
    blurDataURL?: string;
    blurWidth?: number;
    blurHeight?: number;
}

もし画像pathをstringとして直接使うと、別途heightwidthを明示にする必要がある。

<Image 
  alt="Post image" 
  src={postImage}
  width={500}
  height={500}
/>

SVG画像をコンポーネントとして使う

通常のpngやjpg画像と違って、svg画像はhtmlタグのようなものになる。

ダウンロードGoogle ロゴの svg 画像を VSCode で開くと、中身は全てコードになっていることがわかるはず。

今回使っている Google ロゴの中身はこんな感じ

googleLogo.svg
<svg
  width="24px"
  height="24px"
  viewBox="0 0 24 24"
  xmlns="http://www.w3.org/2000/svg"
>
  <path
    fill="#EA4335 "
    d="M5.26620003,9.76452941 C6.19878754,6.93863203 8.85444915,4.90909091 12,4.90909091 C13.6909091,4.90909091 15.2181818,5.50909091 16.4181818,6.49090909 L19.9090909,3 C17.7818182,1.14545455 15.0545455,0 12,0 C7.27006974,0 3.1977497,2.69829785 1.23999023,6.65002441 L5.26620003,9.76452941 Z"
  />
  <path
    fill="#34A853"
    d="M16.0407269,18.0125889 C14.9509167,18.7163016 13.5660892,19.0909091 12,19.0909091 C8.86648613,19.0909091 6.21911939,17.076871 5.27698177,14.2678769 L1.23746264,17.3349879 C3.19279051,21.2936293 7.26500293,24 12,24 C14.9328362,24 17.7353462,22.9573905 19.834192,20.9995801 L16.0407269,18.0125889 Z"
  />
  <path
    fill="#4A90E2"
    d="M19.834192,20.9995801 C22.0291676,18.9520994 23.4545455,15.903663 23.4545455,12 C23.4545455,11.2909091 23.3454545,10.5272727 23.1818182,9.81818182 L12,9.81818182 L12,14.4545455 L18.4363636,14.4545455 C18.1187732,16.013626 17.2662994,17.2212117 16.0407269,18.0125889 L19.834192,20.9995801 Z"
  />
  <path
    fill="#FBBC05"
    d="M5.27698177,14.2678769 C5.03832634,13.556323 4.90909091,12.7937589 4.90909091,12 C4.90909091,11.2182781 5.03443647,10.4668121 5.26620003,9.76452941 L1.23999023,6.65002441 C0.43658717,8.26043162 0,10.0753848 0,12 C0,13.9195484 0.444780743,15.7301709 1.23746264,17.3349879 L5.27698177,14.2678769 Z"
  />
</svg>

next.jsでsvg画像を使うなら、直接<svg>タグを使ってそのまま貼り付けて良いし、
上のように、`でも問題ない。

だた画像ファイルとしてimportするには、エラーが起こる

Module parse failed: Unexpected token (1:2)
 You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

ここでwebpackかbabelのpluginを使う必要。
一回やってちょっとややこしいと思ったので、一旦この方法を諦めた。

もしsvg画像をもっと操作したいなら、コンポーネントにするのが良いかも。

export const GoogleLogo = ({ width = 48, height = 48, fill = "#EA5335" }) => {
  return (
    <svg
      width={width || 24}
      height={height || 24}
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fill={fill || "#EA4335"}
        d="M5.26620003,9.76452941 C6.19878754,6.93863203 8.85444915,4.90909091 12,4.90909091 C13.6909091,4.90909091 15.2181818,5.50909091 16.4181818,6.49090909 L19.9090909,3 C17.7818182,1.14545455 15.0545455,0 12,0 C7.27006974,0 3.1977497,2.69829785 1.23999023,6.65002441 L5.26620003,9.76452941 Z"
      />
    ...
    </svg>
  );
};

これで場所によって、サイズや色を指定することが可能になる。

import GoogleLogo from "./GoogleLogo.tsx"

export default function LoginPage(){

  return (
    <GoogleLogo width={50} heigth={50} />
    )
}

もう一つメリットとして、特にsvgアイコンを使う場合、数多くのアイコンを一つのファイルに集中することができる。


const loginIcon = () => {
    <svg...>
}

const likeIcon = () => {
    <svg...>
}

const editIcon = () => {
    <svg...>
}

参照:Importing SVGs to Next.js

外部画像を使う

今回のユーザープロフィール画像はGoogleから直接取得下のもので、urlはこんな感じhttps://lh3.googleusercontent.com/a/FALm5wu32vHBl...

これをのsrcに直接入れると、エラーが起こる。

Invalid src prop ({userPhotoURL}) on 'next/image', hostname is not configured under images in your 'next.config.js'

Next.jsは安全考慮のため、外部画像を使う場合は、事前のドメイン許可指定が必要。

今回の場合は下記となる。

//next.config.js

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "lh3.googleusercontent.com",
        pathname: "/**",
      },
    ],
  },
}

もし全てのサイトを許可するなら、これでいける。

const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**",
      },
    ],
  },
};

参照:next/image

おわりに

本当にすごく簡単なアプリだったけど、想定外に色々細かいところで詰まってました。
今回は、もし誰かに事前教えてくれたら良いなと思うところをまとめてみました。

もし何か間違いや不足があったら、コメントにてご指摘いただけると嬉しいです!

67
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
43