🤖

Next.js v13のMiddlewareを使ってvercelステージング環境を構築しよう

2023/03/04に公開

この記事について

この記事では、Next.js v12 から導入された Middleware を使って、プロジェクトで求められることが多い以下の要件を満たすステージング環境を構築します 💪

  • 検索エンジンにインデックスさせないように HTTP ヘッダーを設定
  • パスワードによるアクセス制御(ベーシック認証)

具体的な手順

vercel のドメイン設定

  1. Settings → Domains から利用したいドメインを追加する

赤枠の部分に利用したいドメインを入力し、Addボタンを押すことで追加できます。
👇

  1. 追加したドメインに割り当てる Branch を変更する

先程追加したドメインの右側にあるEditボタンを押すと、ドメイン設定を変更できます。
👇

Git Branch にステージング環境として設定したい Branch 名を入力して、保存します。
developブランチをステージング環境として設定したい場合は、画像のようにdevelopと入力します。
👇

本番環境(Production)、ステージング環境(Preview)の環境変数設定

環境変数を使っているアプリケーションの場合は、それらを vercel に設定する必要があります。
Settings → Environment Variables で画像の赤枠で示している部分に入力することで設定できます。
👇

Environment のチェックボックスで、環境変数をどの環境(Production Preview Development)に設定するかを指定できます。これによって本番環境(Production)とステージング環境(Preview)で設定する環境変数を変えることができます。

以上で、vercel のドメイン設定は完了です 👏

検索エンジンにインデックスされないように HTTP ヘッダーを設定する

結論から言うと、検索エンジンにインデックスされないようにするにはnext.config.jsに以下を追加してください。
👇

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
    ...
  async headers() {
    const headers = []
    if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
      headers.push({
        headers: [
          {
            key: 'X-Robots-Tag',
            value: 'noindex',
          },
        ],
        source: '/:path*',
      })
    }
    return headers
  },
}

module.exports = nextConfig

👀 もっと詳しく
vercel の Preview 環境はデフォルトで HTTP ヘッダーであるX-Robots-Tagnoindexに設定されており、検索エンジンにインデックスされないようになっています。しかし、Preview 環境に独自ドメインを使用している場合は、X-Robots-Tag: noindexは設定されません。
試しに、curl コマンドを実行して Preview 環境に設定したドメインの HTTP ヘッダーを確認してみるとX-Robots-Tagが設定されていないことが分かります。
👇

$> curl -I https://[設定したドメイン]
HTTP/2 200
accept-ranges: bytes
access-control-allow-origin: *
age: 2285775
cache-control: public, max-age=0, must-revalidate
content-disposition: inline
content-type: text/html; charset=utf-8
date: Fri, 03 Mar 2023 12:45:43 GMT
etag: "7b052e82f8dbd50afd0759998bad2456"
server: Vercel
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-matched-path: /
x-vercel-cache: HIT
x-vercel-id: kix1:kix1::7tc9m-1677847543308-cb280fab7193
content-length: 11570

これは、vercel 公式ガイドに記載されていたので気になる方はそちらもご参照ください。

そのため、先程の内容をnext.config.jsに追加することで、独自ドメインを設定した Preview 環境にもX-Robots-Tag: noindexを設定でき、検索エンジンにインデックスされないようにできます。
追加後に、curl コマンドをもう一度実行してみると、X-Robots-Tag: noindexが設定されていることが確認できました。
👇

curl -I https://[設定したドメイン]
HTTP/2 200
accept-ranges: bytes
access-control-allow-origin: *
age: 0
cache-control: public, max-age=0, must-revalidate
content-disposition: inline
content-type: text/html; charset=utf-8
date: Fri, 03 Mar 2023 13:26:00 GMT
etag: "053a80bfd6f243d476e5c1d695e1f7ab"
server: Vercel
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-matched-path: /
x-robots-tag: noindex 👈😆
x-vercel-cache: HIT
x-vercel-id: kix1:kix1::hsz75-1677849959681-b9a7e441fa89
content-length: 11570

Next.js の Middleware を使ってベーシック認証を実装する

結論から言うと、以下の流れで行えば実装できます。

  1. pages フォルダと同じ階層に、以下のmiddleware.tsを追加する
src/middleware.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export const config = {
  matcher: "/:path*",
};

export function middleware(req: NextRequest) {
  if (process.env.NEXT_PUBLIC_VERCEL_ENV === "preview") {
    const basicAuth = req.headers.get("authorization");
    const url = req.nextUrl;
    if (basicAuth) {
      const authValue = basicAuth.split(" ")[1];
      const [user, pwd] = atob(authValue).split(":");

      if (
        user === process.env.BASIC_USERNAME &&
        pwd === process.env.BASIC_PASSWORD
      ) {
        return NextResponse.next();
      }
    }
    url.pathname = "/api/auth";
    return NextResponse.rewrite(url);
  }
  return NextResponse.next();
}
  1. pages/apiフォルダ内にauth.tsを追加する
src/pages/api/auth.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.setHeader("WWW-authenticate", 'Basic realm="Secure Area"');
  res.statusCode = 401;
  res.end(`Auth Required.`);
}
  1. 環境変数を追加する
.env.local
BASIC_USERNAME='任意のユーザー名'
BASIC_PASSWORD='任意のパスワード'

👀 もっと詳しく
公式がベーシック認証の実装例(Basic Auth Password Protection)を公開しているので、これを参考に Preview 環境のみにベーシック認証を行うように実装しました。
まず、実装例を見ると以下の 2 つのファイルを使ってベーシック認証を実装しています。

middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export const config = {
  matcher: ['/', '/index'],
}

export function middleware(req: NextRequest) {
  const basicAuth = req.headers.get('authorization')
  const url = req.nextUrl

  if (basicAuth) {
    const authValue = basicAuth.split(' ')[1]
    const [user, pwd] = atob(authValue).split(':')

    if (user === '4dmin' && pwd === 'testpwd123') {
      return NextResponse.next()
    }
  }
  url.pathname = '/api/auth'

  return NextResponse.rewrite(url)
}
pages/api/auth.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.setHeader('WWW-authenticate', 'Basic realm="Secure Area"')
  res.statusCode = 401
  res.end(`Auth Required.`)
}

auth.tsは変更せずにそのまま使っており、middleware.tsは以下のように変更を加えました。
👇

src/middleware.ts
- import { NextRequest, NextResponse } from 'next/server'
+ import type {NextRequest} from 'next/server'
+ import {NextResponse} from 'next/server'

export const config = {
-  matcher: ['/', '/index'],
+  matcher: "/:path*",
}

export function middleware(req: NextRequest) {
+  if (process.env.NEXT_PUBLIC_VERCEL_ENV === "preview") {
    const basicAuth = req.headers.get("authorization");
    const url = req.nextUrl;
    if (basicAuth) {
      const authValue = basicAuth.split(" ")[1];
      const [user, pwd] = atob(authValue).split(":");

-      if (user === '4dmin' && pwd === 'testpwd123') {
+      if (
+        user === process.env.BASIC_USERNAME &&
+        pwd === process.env.BASIC_PASSWORD
+      ) {
        return NextResponse.next();
      }
    }
    url.pathname = "/api/auth";
    return NextResponse.rewrite(url);
  }
+  return NextResponse.next();
}

変更を加えた内容は、以下の 3 点になります。

  1. matcherで全てのパスを対象にするように指定
  2. ベーシック認証を vercel の Preview 環境のみ行う
  3. userpasswordは環境変数として外部から見えないようにする

vercel への環境変数の追加も忘れないように!!!

ベーシック認証のために環境変数を追加したので、vercel にも設定する必要があります。
👇

Preview 環境のみベーシック認証を行うようにしているため、Environmentには Preview のみにチェックを入れます。

実装完了

以上で プロジェクトで求められることが多い SEO やセキュリティの問題を考慮したステージング環境構築が完了です 🎉

お疲れさまでした!!

GitHubで編集を提案

Discussion