Remove Country Code Prefix in Next.js Starter Storefront
In this guide, you'll learn how to remove the country code prefix from the URLs in the Next.js Starter Storefront.
Overview#
By default, the Next.js Starter Storefront includes a country code prefix in URLs that indicates the customer's selected country. For example, if a customer selects the United States, the URL includes the prefix /us.
You may want to remove the country code prefix for a cleaner URL structure or to handle country selection differently.
Summary#
To remove the country code prefix from URLs, you need an alternative way to store and access the country code. The country code is necessary to determine the associated region, which is used for carts, prices, and available payment methods.
This guide shows you how to:
- Store and manage the country code using cookies.
- Update the middleware to handle country code retrieval from cookies.
- Restructure the routes to remove the country code prefix.
- Update components and pages to use the country code from cookies.
1. Add Country Code Cookie Utilities#
You'll start by adding utility functions to manage the country code cookie.
In src/lib/data/cookies.ts, add the following at the end of the file:
1export const COUNTRY_CODE_COOKIE_NAME = "_medusa_country_code"2 3/**4 * Gets the current country code from cookies5 */6export const getCountryCode = async (): Promise<string | null> => {7 try {8 const cookies = await nextCookies()9 return cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value ?? null10 } catch {11 return null12 }13}14 15/**16 * Sets the country code cookie17 */18export const setCountryCode = async (countryCode: string) => {19 const cookies = await nextCookies()20 cookies.set(COUNTRY_CODE_COOKIE_NAME, countryCode, {21 maxAge: 60 * 60 * 24 * 365, // 1 year22 httpOnly: false, // Allow client-side access23 sameSite: "strict",24 secure: process.env.NODE_ENV === "production",25 })26}
You add two utility functions:
getCountryCode: Retrieves the country code from cookies.setCountryCode: Sets the country code cookie with appropriate options.
The country code is stored in the _medusa_country_code cookie, following the Next.js Starter Storefront's naming conventions.
2. Update Middleware to Remove Country Code from URL#
The Next.js Starter Storefront uses middleware at src/middleware.ts to handle country code prefixes in URLs, among other functionalities.
You'll update the middleware to remove the country code prefix from URLs and store it in a cookie instead. This ensures that all requests are handled correctly without requiring a country code in the URL.
In src/middleware.ts, add the following import at the top of the file:
Then, update the getCountryCode function in the middleware to the following:
1/**2 * Determines the country code from cookie or headers.3 * @param request4 * @param regionMap5 */6async function getCountryCode(7 request: NextRequest,8 regionMap: Map<string, HttpTypes.StoreRegion | number>9) {10 try {11 // First, check if country code is already in cookie12 const cookieCountryCode = request.cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value?.toLowerCase()13 if (cookieCountryCode && regionMap.has(cookieCountryCode)) {14 return cookieCountryCode15 }16 17 // Check Vercel IP country header18 const vercelCountryCode = request.headers19 .get("x-vercel-ip-country")20 ?.toLowerCase()21 if (vercelCountryCode && regionMap.has(vercelCountryCode)) {22 return vercelCountryCode23 }24 25 // Fall back to default region26 if (regionMap.has(DEFAULT_REGION)) {27 return DEFAULT_REGION28 }29 30 // Last resort: use first available region31 if (regionMap.keys().next().value) {32 return regionMap.keys().next().value33 }34 35 return null36 } catch (error) {37 if (process.env.NODE_ENV === "development") {38 console.error(39 "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."40 )41 }42 return null43 }44}
In this updated function, you:
- Check if the country code is already stored in the cookie.
- Fall back to the Vercel IP country header if not found in the cookie.
- Use the default region or the first available region if necessary.
Finally, update the middleware function to the following:
1/**2 * Middleware to handle region selection and country code cookie management.3 */4export async function middleware(request: NextRequest) {5 // Check if the url is a static asset6 if (request.nextUrl.pathname.includes(".")) {7 return NextResponse.next()8 }9 10 const cacheIdCookie = request.cookies.get("_medusa_cache_id")11 const cacheId = cacheIdCookie?.value || crypto.randomUUID()12 13 const regionMap = await getRegionMap(cacheId)14 15 if (!regionMap) {16 return new NextResponse(17 "No valid regions configured. Please set up regions with countries in your Medusa Admin.",18 { status: 500 }19 )20 }21 22 const countryCode = await getCountryCode(request, regionMap)23 24 if (!countryCode) {25 return new NextResponse(26 "No valid regions configured. Please set up regions with countries in your Medusa Admin.",27 { status: 500 }28 )29 }30 31 // Create response32 const response = NextResponse.next()33 34 // Set cache ID cookie if not set35 if (!cacheIdCookie) {36 response.cookies.set("_medusa_cache_id", cacheId, {37 maxAge: 60 * 60 * 24,38 })39 }40 41 // Set country code cookie if not set or different42 const cookieCountryCode = request.cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value43 if (!cookieCountryCode || cookieCountryCode !== countryCode) {44 response.cookies.set(COUNTRY_CODE_COOKIE_NAME, countryCode, {45 maxAge: 60 * 60 * 24 * 365, // 1 year46 httpOnly: false, // Allow client-side access47 sameSite: "strict",48 secure: process.env.NODE_ENV === "production",49 })50 }51 52 return response53}
In this updated middleware function, you make the following key changes regarding the country code:
- Retrieve the country code using the updated
getCountryCodefunction. - Set the country code cookie if it's not already set or if it differs from the current value.
With these changes, the middleware no longer requires the country code prefix in URLs and manages the country code using cookies instead.
3. Remove Routes from Country Code Prefix#
Next, you need to restructure the routes in the Next.js Starter Storefront to remove the country code prefix.
Currently, there are two directories under src/app/[countryCode]: (checkout) and (main). These directories contain all routes that include the country code prefix.
To remove the country code prefix, move the (checkout) and (main) directories directly under src/app, then delete the [countryCode] directory. Make sure to update any imports or references to and from the routes in these directories accordingly.

4. Update Country Selector to Use Cookie#
Next, you'll update the CountrySelect component that allows customers to select their country from the side menu. You'll modify it to set the country code cookie when a customer selects a different country, rather than changing the URL.
In src/modules/layout/components/country-select/index.tsx, add the following helper function before the CountrySelect component:
1// Helper function to get cookie value on client side2function getCookie(name: string): string | null {3 if (typeof document === "undefined") {return null}4 const value = `; ${document.cookie}`5 const parts = value.split(`; ${name}=`)6 if (parts.length === 2) {return parts.pop()?.split(";").shift() || null}7 return null8}
This function retrieves cookie values on the client side, allowing you to update the selected country in the dropdown based on the cookie value without refreshing the page.
Then, inside the CountrySelect component, add a new variable and update the currentPath variable declaration:
You define a new state variable countryCode to store the currently selected country code from the cookie. You also update the currentPath variable declaration to remove the country code retrieval from the URL.
Next, add the following functions in the CountrySelect component to handle country selection and changes:
1const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {2 // ...3 4 // Function to update country code from cookie5 const updateCountryCodeFromCookie = () => {6 const cookieCountryCode = getCookie("_medusa_country_code")7 setCountryCode(cookieCountryCode)8 }9 10 useEffect(() => {11 // Get country code from cookie on client side12 updateCountryCodeFromCookie()13 14 // Listen for focus events to refresh country code when user returns to the page15 const handleFocus = () => {16 updateCountryCodeFromCookie()17 }18 19 window.addEventListener("focus", handleFocus)20 return () => window.removeEventListener("focus", handleFocus)21 }, [])22 23 const handleChange = async (option: CountryOption) => {24 // Optimistically update the UI immediately25 const newCountryCode = option.country.toLowerCase()26 setCountryCode(newCountryCode)27 const selectedOption = options?.find(28 (o) => o?.country?.toLowerCase() === newCountryCode29 )30 if (selectedOption && selectedOption.country) {31 setCurrent({32 country: selectedOption.country,33 region: selectedOption.region,34 label: selectedOption.label,35 })36 }37 close()38 39 try {40 // Update the region (this will set the cookie and redirect)41 await updateRegion(option.country, currentPath)42 } catch (error) {43 // If update fails, revert to previous country code44 updateCountryCodeFromCookie()45 console.error("Failed to update region:", error)46 }47 }48 49 // ...50}
You add two functions:
updateCountryCodeFromCookie: Retrieves the country code from the cookie and updates the state.handleChange: Handles country selection changes, optimistically updates the UI, sets the country code cookie, and reverts the UI if the update fails.
Then, update the useEffect usage in the component to the following:
1const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {2 // ...3 4 useEffect(() => {5 if (countryCode) {6 const option = options?.find(7 (o) => o?.country === countryCode.toLowerCase()8 )9 setCurrent(option)10 }11 }, [options, countryCode])12 13 // ...14}
You make a small change to transform the country code to lowercase when finding the corresponding option.
Finally, in the return statement of the CountrySelect component, update the defaultValue prop of the Listbox component to the following:
1const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {2 // ...3 return (4 <div>5 <Listbox6 as="span"7 onChange={handleChange}8 defaultValue={9 countryCode10 ? (options?.find(11 (o) => o?.country?.toLowerCase() === countryCode.toLowerCase()12 ) as CountryOption | undefined)13 : undefined14 }15 >16 {/* ... */}17 </Listbox>18 </div>19 )20}
You update the defaultValue prop to find the option based on the country code stored in the cookie.
With these changes, the CountrySelect component now uses cookies to set and retrieve the country code instead of the URL.
5. Update Country Code Retrieval in Pages#
Next, you'll update how the country code is retrieved across various pages of the Next.js Starter Storefront.
[countryCode] directory, as described in Step 3.a. Update Home Page#
In src/app/(main)/page.tsx, add the following import at the top of the file:
Then, change the Home component to remove the countryCode parameter and retrieve the country code using the getCountryCode utility:
b. Update Store Page#
In src/app/(main)/store/page.tsx, add the following import at the top of the file:
Then, remove the params prop from the Params type. It should only have a searchParams prop:
Finally, update the StorePage component to retrieve the country code using the getCountryCode utility:
1export default async function StorePage(props: Params) {2 const searchParams = await props.searchParams3 const { sortBy, page } = searchParams4 const countryCode = await getCountryCode()5 6 if (!countryCode) {7 return null8 }9 10 return (11 <StoreTemplate12 sortBy={sortBy}13 page={page}14 countryCode={countryCode}15 />16 )17}
c. Update Product Page#
In src/app/(main)/products/[handle]/page.tsx, add the following import at the top of the file:
Then, update the Props type to remove countryCode from the params prop:
Next, update the generateMetadata function to retrieve the country code using the getCountryCode utility:
1export async function generateMetadata(props: Props): Promise<Metadata> {2 const countryCode = await getCountryCode()3 4 if (!countryCode) {5 notFound()6 }7 8 const region = await getRegion(countryCode)9 10 if (!region) {11 notFound()12 }13 14 const product = await listProducts({15 countryCode,16 queryParams: { handle },17 }).then(({ response }) => response.products[0])18 19 // ...20}
This retrieves the country code using the getCountryCode utility, then passes it to the getRegion and listProducts functions.
Finally, update the ProductPage component to retrieve the country code using the getCountryCode utility and pass it to the used functions and components:
1export default async function ProductPage(props: Props) {2 const params = await props.params3 const countryCode = await getCountryCode()4 const searchParams = await props.searchParams5 6 if (!countryCode) {7 notFound()8 }9 10 const region = await getRegion(countryCode)11 12 if (!region) {13 notFound()14 }15 16 const selectedVariantId = searchParams.v_id17 18 const pricedProduct = await listProducts({19 countryCode,20 queryParams: { handle: params.handle },21 }).then(({ response }) => response.products[0])22 23 const images = getImagesForVariant(pricedProduct, selectedVariantId)24 25 if (!pricedProduct) {26 notFound()27 }28 29 return (30 <ProductTemplate31 product={pricedProduct}32 region={region}33 countryCode={countryCode}34 images={images}35 />36 )37}
d. Update Collection Page#
In src/app/(main)/collections/[handle]/page.tsx, add the following import at the top of the file:
Then, update the Props type to remove countryCode from the params prop:
Finally, update the CollectionPage component to retrieve the country code using the getCountryCode utility and pass it to the used component:
1export default async function CollectionPage(props: Props) {2 const searchParams = await props.searchParams3 const params = await props.params4 const { sortBy, page } = searchParams5 const countryCode = await getCountryCode()6 7 if (!countryCode) {8 notFound()9 }10 11 const collection = await getCollectionByHandle(params.handle).then(12 (collection: StoreCollection) => collection13 )14 15 if (!collection) {16 notFound()17 }18 19 return (20 <CollectionTemplate21 collection={collection}22 page={page}23 sortBy={sortBy}24 countryCode={countryCode}25 />26 )27}
This retrieves the country code using the getCountryCode utility and passes it to the CollectionTemplate component.
e. Update Categories Page#
In src/app/(main)/categories/[...category]/page.tsx, add the following import at the top of the file:
Then, update the Props type to remove countryCode from the params prop:
Finally, update the CategoriesPage component to retrieve the country code using the getCountryCode utility and pass it to the used component:
1export default async function CategoryPage(props: Props) {2 const searchParams = await props.searchParams3 const params = await props.params4 const { sortBy, page } = searchParams5 const countryCode = await getCountryCode()6 7 if (!countryCode) {8 notFound()9 }10 11 const productCategory = await getCategoryByHandle(params.category)12 13 if (!productCategory) {14 notFound()15 }16 17 return (18 <CategoryTemplate19 category={productCategory}20 sortBy={sortBy}21 page={page}22 countryCode={countryCode}23 />24 )25}
This retrieves the country code using the getCountryCode utility and passes it to the CategoryTemplate component.
f. Update Addresses Page#
In src/app/(main)/account/@dashboard/addresses/page.tsx, add the following import at the top of the file:
Then, update the Addresses component to remove the countryCode parameter and retrieve the country code using the getCountryCode utility:
6. Update Country Usage in Components#
In this section, you'll update various components in the Next.js Starter Storefront to use the country code from cookies instead of the URL.
a. Update AccountNav Component#
In src/modules/account/components/account-nav/index.tsx, the country code is used to prefix URLs with the country code. You'll update it to remove the country code from the URLs.
In the AccountNav component, remove the country code retrieval using the useParams hook. You should have only the following lines before the return statement:
Then, in the return statement of the AccountNav component, change the condition checking the route variable's value to the following:
1const AccountNav = ({2 customer,3}: {4 customer: HttpTypes.StoreCustomer | null5}) => {6 // ...7 return (8 <div>9 <div className="small:hidden" data-testid="mobile-account-nav">10 {route !== `/account` ? (11 {/* ... */}12 ) : (13 {/* ... */}14 )}15 </div>16 {/* ... */}17 </div>18 )19}
You remove the country code prefix from the URL checks in the AccountNav component.
Finally, in the AccountNavLink component defined in the same file, change the active variable declaration to the following:
You remove the country code prefix from the URL check in the AccountNavLink component.
b. Update ProductActions Component#
The ProductActions component uses the country code when adding products to the cart. You'll remove the need to pass the country code to the addToCart function. You'll update the addToCart function later to retrieve the country code from the cookie.
In src/modules/products/components/product-actions/index.tsx, remove the countryCode variable from the ProductActions component:
Then, in the handleAddToCart function inside the ProductActions component, remove the countryCode argument when calling the addToCart function:
1export default function ProductActions({2 product,3 disabled,4}: ProductActionsProps) {5 // ...6 7 const handleAddToCart = async () => {8 if (!selectedVariant?.id) {return null}9 10 setIsAdding(true)11 12 await addToCart({13 variantId: selectedVariant.id,14 quantity: 1,15 // REMOVE THIS LINE16 // countryCode,17 })18 19 setIsAdding(false)20 }21 22 // ...23}
Ignore any type errors from the removed countryCode argument. You'll update the addToCart function later to remove the countryCode parameter.
c. Update LocalizedClientLink Component#
The LocalizedClientLink component creates links that include the country code prefix in URLs. Update it to remove the country code from the URLs.
In src/modules/common/components/localized-client-link/index.tsx, replace the content with the following:
1"use client"2 3import Link from "next/link"4import React from "react"5 6/**7 * Use this component to create a Next.js `<Link />` that works without country code in the URL.8 * Country code is now stored in a cookie instead.9 */10const LocalizedClientLink = ({11 children,12 href,13 ...props14}: {15 children?: React.ReactNode16 href: string17 className?: string18 onClick?: () => void19 passHref?: true20 [x: string]: any21}) => {22 return (23 <Link href={href} {...props}>24 {children}25 </Link>26 )27}28 29export default LocalizedClientLink
You make the following key changes:
- Remove the
useParamshook import and usage. - Remove the logic that adds the country code prefix to the
hrefprop.
With these changes, the LocalizedClientLink component now creates links without the country code prefix in URLs.
7. Update Checkout Components#
The checkout flow components also need to be updated to handle URLs without country codes. This includes forms and buttons that redirect to other checkout steps.
Update Checkout Navigation#
In checkout components that handle navigation between steps (such as shipping, payment, and review), ensure that any redirect URLs or form actions don't include the country code prefix.
For example, if you have checkout step components that redirect to delivery or payment URLs, update them to use the new URL structure:
Update Form Actions in Checkout#
If your checkout forms have action URLs that include country codes, update them to the new structure. This is particularly important for:
- Shipping address forms
- Payment method selection
- Order review and confirmation
Look for any hardcoded URLs in checkout components and remove the country code prefix.
8. Update Server Functions to Use Country Code from Cookie#
Finally, you'll update the server functions in the Next.js Starter Storefront to retrieve the country code from the cookie instead of receiving it as a parameter.
a. Update Customer Functions#
In src/lib/data/customer.ts, find the signout function and update it to the following:
1export async function signout() {2 await sdk.auth.logout()3 4 await removeAuthToken()5 6 const customerCacheTag = await getCacheTag("customers")7 revalidateTag(customerCacheTag)8 9 await removeCartId()10 11 const cartCacheTag = await getCacheTag("carts")12 revalidateTag(cartCacheTag)13 14 redirect(`/account`)15}
You remove the countryCode parameter and the country code usage in the redirect function.
b. Update Cart Functions#
In src/lib/data/cart.ts, add the following import at the top of the file:
Then, find the addToCart function and update it to the following:
1export async function addToCart({2 variantId,3 quantity,4}: {5 variantId: string6 quantity: number7}) {8 if (!variantId) {9 throw new Error("Missing variant ID when adding to cart")10 }11 12 const countryCode = await getCountryCode()13 14 if (!countryCode) {15 throw new Error("Country code not found. Please select a country.")16 }17 18 const cart = await getOrSetCart(countryCode)19 20 if (!cart) {21 throw new Error("Error retrieving or creating cart")22 }23 24 const headers = {25 ...(await getAuthHeaders()),26 }27 28 await sdk.store.cart29 .createLineItem(30 cart.id,31 {32 variant_id: variantId,33 quantity,34 },35 {},36 headers37 )38 .then(async () => {39 const cartCacheTag = await getCacheTag("carts")40 revalidateTag(cartCacheTag)41 42 const fulfillmentCacheTag = await getCacheTag("fulfillment")43 revalidateTag(fulfillmentCacheTag)44 })45 .catch(medusaError)46}
You remove the countryCode parameter and retrieve the country code using the getCountryCode utility inside the function.
Next, find the placeOrder function in the same file and update it to the following:
1export async function placeOrder(cartId?: string) {2 const id = cartId || (await getCartId())3 4 if (!id) {5 throw new Error("No existing cart found when placing an order")6 }7 8 const headers = {9 ...(await getAuthHeaders()),10 }11 12 const cartRes = await sdk.store.cart13 .complete(id, {}, headers)14 .then(async (cartRes) => {15 const cartCacheTag = await getCacheTag("carts")16 revalidateTag(cartCacheTag)17 return cartRes18 })19 .catch(medusaError)20 21 if (cartRes?.type === "order") {22 const orderCacheTag = await getCacheTag("orders")23 revalidateTag(orderCacheTag)24 25 removeCartId()26 redirect(`/order/${cartRes?.order.id}/confirmed`)27 }28 29 return cartRes.cart30}
You change the redirect logic to remove the country code from the URL.
Finally, find the updateRegion function in the same file and update it to the following:
1export async function updateRegion(countryCode: string, currentPath: string) {2 const cartId = await getCartId()3 const region = await getRegion(countryCode)4 5 if (!region) {6 throw new Error(`Region not found for country code: ${countryCode}`)7 }8 9 // Set country code cookie10 await setCountryCode(countryCode)11 12 if (cartId) {13 await updateCart({ region_id: region.id })14 const cartCacheTag = await getCacheTag("carts")15 revalidateTag(cartCacheTag)16 }17 18 const regionCacheTag = await getCacheTag("regions")19 revalidateTag(regionCacheTag)20 21 const productsCacheTag = await getCacheTag("products")22 revalidateTag(productsCacheTag)23 24 redirect(currentPath || "/")25}
You update the function to set the country code cookie using the setCountryCode utility instead of relying on the URL, and you remove the country code from the redirect URL.
Test Your Changes#
After completing these steps, you can use the Next.js Starter Storefront without country code prefixes in URLs. The country code is now managed using cookies.
To test it, run the following command in the directory of the Medusa application that the storefront connects to:
Then, in a separate terminal, run the following command in the Next.js Starter Storefront directory to start the development server:
You should be able to navigate the storefront, select different countries using the country selector, go through checkout, and place orders without country code prefixes in URLs. The country code is stored and retrieved using cookies as intended.