/ src / middleware.ts
middleware.ts
  1  import { HttpTypes } from "@medusajs/types"
  2  import { NextRequest, NextResponse } from "next/server"
  3  
  4  const BACKEND_URL = process.env.MEDUSA_BACKEND_URL
  5  const PUBLISHABLE_API_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
  6  const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "us"
  7  
  8  const regionMapCache = {
  9    regionMap: new Map<string, HttpTypes.StoreRegion>(),
 10    regionMapUpdated: Date.now(),
 11  }
 12  
 13  async function getRegionMap(cacheId: string) {
 14    const { regionMap, regionMapUpdated } = regionMapCache
 15  
 16    if (!BACKEND_URL) {
 17      throw new Error(
 18        "Middleware.ts: Error fetching regions. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
 19      )
 20    }
 21  
 22    if (
 23      !regionMap.keys().next().value ||
 24      regionMapUpdated < Date.now() - 3600 * 1000
 25    ) {
 26      // Fetch regions from Medusa. We can't use the JS client here because middleware is running on Edge and the client needs a Node environment.
 27      const { regions } = await fetch(`${BACKEND_URL}/store/regions`, {
 28        headers: {
 29          "x-publishable-api-key": PUBLISHABLE_API_KEY!,
 30        },
 31        next: {
 32          revalidate: 3600,
 33          tags: [`regions-${cacheId}`],
 34        },
 35        cache: "force-cache",
 36      }).then(async (response) => {
 37        const json = await response.json()
 38  
 39        if (!response.ok) {
 40          throw new Error(json.message)
 41        }
 42  
 43        return json
 44      })
 45  
 46      if (!regions?.length) {
 47        throw new Error(
 48          "No regions found. Please set up regions in your Medusa Admin."
 49        )
 50      }
 51  
 52      // Create a map of country codes to regions.
 53      regions.forEach((region: HttpTypes.StoreRegion) => {
 54        region.countries?.forEach((c) => {
 55          regionMapCache.regionMap.set(c.iso_2 ?? "", region)
 56        })
 57      })
 58  
 59      regionMapCache.regionMapUpdated = Date.now()
 60    }
 61  
 62    return regionMapCache.regionMap
 63  }
 64  
 65  /**
 66   * Fetches regions from Medusa and sets the region cookie.
 67   * @param request
 68   * @param response
 69   */
 70  async function getCountryCode(
 71    request: NextRequest,
 72    regionMap: Map<string, HttpTypes.StoreRegion | number>
 73  ) {
 74    try {
 75      let countryCode
 76  
 77      const vercelCountryCode = request.headers
 78        .get("x-vercel-ip-country")
 79        ?.toLowerCase()
 80  
 81      const urlCountryCode = request.nextUrl.pathname.split("/")[1]?.toLowerCase()
 82  
 83      if (urlCountryCode && regionMap.has(urlCountryCode)) {
 84        countryCode = urlCountryCode
 85      } else if (vercelCountryCode && regionMap.has(vercelCountryCode)) {
 86        countryCode = vercelCountryCode
 87      } else if (regionMap.has(DEFAULT_REGION)) {
 88        countryCode = DEFAULT_REGION
 89      } else if (regionMap.keys().next().value) {
 90        countryCode = regionMap.keys().next().value
 91      }
 92  
 93      return countryCode
 94    } catch (error) {
 95      if (process.env.NODE_ENV === "development") {
 96        console.error(
 97          "Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
 98        )
 99      }
100    }
101  }
102  
103  /**
104   * Middleware to handle region selection and onboarding status.
105   */
106  export async function middleware(request: NextRequest) {
107    let redirectUrl = request.nextUrl.href
108  
109    let response = NextResponse.redirect(redirectUrl, 307)
110  
111    let cacheIdCookie = request.cookies.get("_medusa_cache_id")
112  
113    let cacheId = cacheIdCookie?.value || crypto.randomUUID()
114  
115    const regionMap = await getRegionMap(cacheId)
116  
117    const countryCode = regionMap && (await getCountryCode(request, regionMap))
118  
119    const urlHasCountryCode =
120      countryCode && request.nextUrl.pathname.split("/")[1].includes(countryCode)
121  
122    // if one of the country codes is in the url and the cache id is set, return next
123    if (urlHasCountryCode && cacheIdCookie) {
124      return NextResponse.next()
125    }
126  
127    // if one of the country codes is in the url and the cache id is not set, set the cache id and redirect
128    if (urlHasCountryCode && !cacheIdCookie) {
129      response.cookies.set("_medusa_cache_id", cacheId, {
130        maxAge: 60 * 60 * 24,
131      })
132  
133      return response
134    }
135  
136    // check if the url is a static asset
137    if (request.nextUrl.pathname.includes(".")) {
138      return NextResponse.next()
139    }
140  
141    const redirectPath =
142      request.nextUrl.pathname === "/" ? "" : request.nextUrl.pathname
143  
144    const queryString = request.nextUrl.search ? request.nextUrl.search : ""
145  
146    // If no country code is set, we redirect to the relevant region.
147    if (!urlHasCountryCode && countryCode) {
148      redirectUrl = `${request.nextUrl.origin}/${countryCode}${redirectPath}${queryString}`
149      response = NextResponse.redirect(`${redirectUrl}`, 307)
150    }
151  
152    return response
153  }
154  
155  export const config = {
156    matcher: [
157      "/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp).*)",
158    ],
159  }