Integration Guide: Builder.io + MedusaJS Storefront

MedusaJS is a powerful open-source e-commerce backend that provides flexibility for modern storefronts, while Builder.io is a visual CMS that enables no-code page creation and A/B testing. By integrating these two tools, you can build a dynamic, customizable storefront with the power of Medusa’s backend and Builder.io’s visual editing capabilities.

In this guide, I’ll walk you through how to integrate Builder.io with MedusaJS to create custom e-commerce pages seamlessly.


:hammer_and_wrench: Prerequisites

Before getting started, ensure you have the following installed:

:white_check_mark: Node.js v20+
:white_check_mark: Git CLI
:white_check_mark: PostgreSQL (Required for Medusa’s database)
:white_check_mark: Builder.io React SDK


:rocket: Step 1: Set Up MedusaJS

If you don’t already have a Medusa application installed, run the following command to install both the Medusa backend and the Next.js storefront starter:

npx create-medusa-app@latest --with-nextjs-starter

During setup, you’ll be prompted to enter a project name. Once the installation completes:

  • The Medusa backend will be in the {project-name} directory.
  • The Next.js storefront will be in {project-name}-storefront.

After installation, you can access:

:small_blue_diamond: Medusa Admin Dashboard: http://localhost:9000/app
:small_blue_diamond: Next.js Storefront: http://localhost:8000

For additional setup details, refer to the Medusa installation guide.


:building_construction: Step 2: Install Builder.io in Your Next.js Storefront

Assuming you are using Next.js for the storefront, install the required Builder.io packages:

npm install @builder.io/react @builder.io/sdk

For additional details, check out Builder.io’s Next.js integration guide.


:jigsaw: Step 3: Add a Builder Component

Create a new file at:
:file_folder: /src/modules/common/components/builder/index.tsx (Create the directory if it doesn’t exist).

Paste the following code inside:

"use client";
import { ComponentProps } from "react";
import { BuilderComponent, useIsPreviewing } from "@builder.io/react"; 
import { builder } from "@builder.io/sdk";
import DefaultErrorPage from "next/error";

type BuilderPageProps = ComponentProps<typeof BuilderComponent>;

// Replace with your Builder.io Public API Key
builder.init("Your-Builder-Public-API-Key");

export function RenderBuilderContent(props: BuilderPageProps) { 
  const isPreviewing = useIsPreviewing(); 

  return props.content || isPreviewing ? (
    <BuilderComponent {...props} />
  ) : (
    <DefaultErrorPage statusCode={404} />
  );
}

:pushpin: Step 4: Enable Dynamic Pages with Builder.io

Add Builder Page Support

Create a file at:
:file_folder: /src/app/[countryCode]/(main)/[...page]/page.tsx

Paste the following code inside:

import { Metadata } from "next";
import { builder } from "@builder.io/sdk";
import { RenderBuilderContent } from "@modules/common/components/builder";

// Initialize Builder.io
builder.init("Your-Builder-Public-API-Key");

export const metadata: Metadata = {
  title: "Builder.io + Medusa Next.js Starter Template",
  description: "A performant e-commerce starter template with Next.js 14 and Medusa.",
};

interface BuilderPageProps {
  params: {
    page?: string[];
    countryCode: string;
  };
}

export default async function BuilderPage({ params }: BuilderPageProps) {
  const { countryCode, page = [] } = params;
  const urlPath = `/${countryCode}/${page.join("/")}`.replace(/\/$/, "");

  const content = await builder
    .get("page", {
      userAttributes: { urlPath },
      options: { countryCode },
      locale: countryCode,
      prerender: false,
    })
    .toPromise();

  return <RenderBuilderContent content={content} model="page" locale={countryCode} />;
}

Set Up Builder for the Home Page

If you want your home page (e.g., http://localhost:8000/dk) to be editable via Builder, modify:
:file_folder: /src/app/[countryCode]/(main)/page.tsx

Paste the following:

import { Metadata } from "next";
import { builder } from "@builder.io/sdk";
import { RenderBuilderContent } from "@modules/common/components/builder";

import FeaturedProducts from "@modules/home/components/featured-products";
import { listCollections } from "@lib/data/collections";
import { getRegion } from "@lib/data/regions";

// Initialize Builder.io
builder.init("Your-Builder-Public-API-Key");

export const metadata: Metadata = {
  title: "Builder.io + Medusa Next.js Starter Template",
  description: "A performant e-commerce starter template with Next.js 14 and Medusa.",
};

interface HomeProps {
  params: {
    countryCode: string;
  };
}

export default async function Home({ params }: HomeProps) {
  const { countryCode } = params;
  const region = await getRegion(countryCode);
  const { collections } = await listCollections({ fields: "id, handle, title" });

  if (!collections || !region) {
    return null;
  }

  const content = await builder
    .get("page", {
      userAttributes: { urlPath: "/" },
      options: { countryCode },
      locale: countryCode,
      prerender: false,
    })
    .toPromise();

  return (
    <>
      <RenderBuilderContent content={content} model="page" locale={countryCode} />
      <div className="py-12">
        <ul className="flex flex-col gap-x-6">
          <FeaturedProducts collections={collections} region={region} />
        </ul>
      </div>
    </>
  );
}

:link: Step 5: Configure Builder.io Preview

To enable Builder.io’s Visual Editor, follow these steps:

  1. Go to Builder.io Models and select the Page model.
  2. Set the Preview URL to http://localhost:<your-port> (replace <your-port> with your app’s port).
  3. Click Save.

This allows Builder.io to preview and edit your Next.js pages in real-time.


:tada: Conclusion

By integrating MedusaJS with Builder.io, you now have a flexible e-commerce storefront with powerful backend capabilities and no-code page editing. You can now:

:white_check_mark: Manage products and orders via Medusa.
:white_check_mark: Customize page layouts with Builder.io’s drag-and-drop editor.
:white_check_mark: Implement dynamic content updates without touching code.

If you have any questions or run into issues, drop them in the comments! :rocket:

1 Like

If you’re experiencing preview issues within the Builder.io app, it’s likely due to Medusa Storefront’s middleware.ts, which includes certain redirect conditions. Here’s a breakdown of how the middleware works and how to fix the issue.

Why is the Preview Not Working?

The middleware in Medusa Storefront performs redirects based on specific conditions:

  • It redirects users if a valid country code is missing or if they need to be directed to a country-specific region.
  • If the cacheId cookie is missing but the country code is present, it will redirect and set the cookie.
  • If no country code is specified in the URL, the middleware will append the country code and redirect the user.

:white_check_mark: How to Fix It

To ensure Builder.io’s Visual Editor preview works correctly, you need to modify the middleware to skip redirects when previewing inside Builder.io.

:small_blue_diamond: Solution: Modify middleware.ts

Use builder.preview and __builder_editing__ to detect when the page is in preview mode and prevent unnecessary redirects. Below is a working example:

// Check if the URL has 'builder.preview' or '__builder_editing__'
const isPreviewing =
  request.nextUrl.searchParams.has("builder.preview") ||
  request.nextUrl.searchParams.has("__builder_editing__");

// If the page is in preview mode, skip redirect logic
if (isPreviewing) {
  return NextResponse.next();
}

:pushpin: Full working example:
Here’s the complete middleware.ts with the necessary fix applied: :point_right: Click to View Full Middleware Code


:link: Additional Resources

For a hands-on reference, you can check out this Builder.io + MedusaJS Starter Template on GitHub:

This should resolve your preview issues inside Builder.io while keeping Medusa’s region-based redirects intact. :rocket:

Hope this helps! Let me know if you have any questions. :blush:

1 Like

Hello @manish-sharma ,

I followed all the steps and seem to be getting an error about a deprecated feature. I’m unable to even open this in the visual editor.
I have made sure to replace "Your-Builder-Public-API-Key" with my API key in the code examples.

Attached is the dev console photo

Thanks,

Hello @Nando_C,

Could you please share the integration code from /src/app/[countryCode]/(main)/page.tsx ?

Additionally, if possible, please share a screen recording of your setup. This would help us better understand the issue.

Hello @manish-sharma ,

For the code inside /src/app/[countryCode]/(main)/page.tsx I just replaced it with the code from the corresponding step in the original post above, as well as supplying my public API key.

Attached is a video of following the steps.

I am using node.js v22.13.1 and macOS Sequoia 15.3

Thanks,

Hello @Nando_C,

We are unable to reproduce the issue you’re experiencing. Could you please share your app repository?

Hello @manish-sharma ,

Here is the uploaded repository of what I tested:

Thanks,

Hello @Nando_C,

Thanks for sharing the GitHub repo. I was able to recreate the issue you were experiencing, and it was caused by the way the URL was set in Builder.

You were adding /dk as part of the page URL, which is incorrect. For landing pages, the URL should be /, and for other pages, it should follow the format /xyz. The /dk in the URL represents the country code, and you don’t need to add it manually. Instead, you should use the dynamic preview URL to handle country codes automatically.

return `http://localhost:8000/dk${targeting?.urlPath}`;

To help clarify, I’ve recorded a Loom video demonstrating the issue and the fix. You can check it out here:

Hope this helps! Let me know if you have any questions.

Thanks,

1 Like

@Nando_C,

Also, since you are not using builder locale feature, you may need to update your code /src/app/[countryCode]/(main)/[...page]/page.tsx as shown below and remove locale

export default async function BuilderPage({ params }: BuilderPageProps) {
    const { countryCode, page = [] } = params;
    const urlPath = `/${page.join("/")}`.replace(/\/$/, "");

    const content = await builder
        .get("page", {
            userAttributes: { urlPath },
            prerender: false,
        })
        .toPromise();

    return <RenderBuilderContent content={content} model="page" />;
}

Forgot to follow up on this. It now all works as intended following all the steps above.

Thanks again,