How to get content from Dynamic URL?

Context: I’m building a Next.js App Router project and I’m trying to build a product details page by using dynamic routes. I followed the instructions on Dynamic Preview URLs - Builder.io.
On the page model, I have a page field called slug and I also have the logic on Dynamic URL Preview, as the documentation suggested.

I have the code on app/products/[handle]/page.tsx like this:


import { RenderBuilderContent } from "../../../components/builder"
import { builder } from "@builder.io/sdk";

import { ProductDisplay } from '@/components/ProductDisplay/';

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

export default async function ProductPage({
  params,
}: {
  params: Promise<{ handle: string }>
}) {
  const builderModelName = "product-details";

  const { handle } = await params;

  const content = await builder
    .get(builderModelName, {
      query: {
        "data.slug": handle,
      },
    
    })
    .toPromise();

  console.log(handle)
  console.log(content);

  return (
    <div>
      <ProductDisplay handle={handle} />
      <RenderBuilderContent
        model={builderModelName}
        content={content || undefined}
      />
    </div>
  );
}

Problem: My problem is that the content is undefined. The console.log(handle) is fine, and the ProductDisplay component renders ok on http://localhost:3000/product-details/product1 (or whatever slug I put, the component is working well). But what is below the ProductDisplay shows as “404” on localhost and on Visual Editor, the blocks of components I put below the ProductDisplay appear correctly but in the console (f12), is printing undefined as well.

What I am trying to accomplish: I want to have custom product pages, where the first block is the same for all product pages, but what’s below that can be customize by clients. So some products may have Sliders, others may have only Texts and images, but the first block will always be the ProductDisplay component. But this content below is coming as undefined on console and 404.

What I tried
I tried to get the content like:

const content = await builder
    .get(builderModelName, {
      userAttributes: {
        urlPath: `/${handle}`,
      },
    })
    .toPromise();

and with prerender: false, but the content is always undefined.

Please, if anyone knows the correct way to get the builder content I’ll be gratefull.
Thank you

Hello @kevinroy,

Welcome to the builder.io forum post.

To assist you better, could you please provide the Builder Content Entry link where you’re experiencing an issue? This will help me further my investigation.

Here is how to find it:

Builder Content Entry: Builder.io: Visual Development Platform

Hello @kevinroy,

We attempted to reproduce the issue on our end and found that you’re using the page model type for the product-details page. To resolve the issue, you’ll need to pass the userAttributes in your API call.

Updating the builder.get call as shown below should return the content as expected:

  const builderModelName = "product-details";

  const { handle } = await params;

  const content = await builder
    .get(builderModelName, {
      userAttributes: {
        urlPath: '/product-details'
      }, 
      query: {
        "data.slug": handle,
      },
    })
    .toPromise();
  1. Integrate Pages - Builder.io
  2. Content API - Builder.io

I hope this helps! Feel free to let us know if you need any further assistance.

Thanks,

Hi Manish,

It partially worked! The content is showing only for one slug at time. For example, for http://localhost:3000/product-details/product-1 it worked well, but when I access http://localhost:3000/product-details/product-2, the first block render ok but the content is again 404 below the first block as before, and the console.log for content is again undefined. I change the slug to product-2 in the visual editor, published and it worked on http://localhost:3000/product-details/product-2, but it got undefined and 404 below ProductDisplay component on http://localhost:3000/product-details/product-1 again.
Do you know if there is a way to keep it dynamic and load the content below the ProductDisplay for all products slugs? As I’m trying to accomplish custom product pages, where the first block is the same for all product pages, but what’s below that can be customize by clients, it will be necessary to create one page for each product page details?
Thank you!

Hello @kevinroy,

Could you please confirm how you plan to pass the product data with different slugs to Builder?

Yes, this project have integration with shopify. So I’m filling the slug field with some product handle that copying manually from shopify and pasting in the URL http://localhost:3000/product-details/[slug]. The integration is fine, the product information is dynamically changing on ProductDisplay component, when I changed it manually on URL.

The problem is what is below the block, that is showing 404 on localhost (but in visual editor works fine). After the code you sent, is working only when I publish with the slug I’m trying to access, so it is showing the blocks below for only one slug at time. Is there a way to load the content without having to publish each slug?

@manish-sharma

Just for curiosity: I tried this getAll, but it is returning an empty array on both cases, with the slug published and on the url with some not published slug. This is my actual code:

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

export default async function ProductPage({
  params,
}: {
  params: Promise<{ handle: string }>
}) {
  const builderModelName = "product-details";

  const products = await builder.getAll("product-details", {
    fields: 'data.slug', 
    options: { enrich: true }
  });

  console.log(products)

  const { handle } = await params;

  const content = await builder
    .get(builderModelName, {
      userAttributes: {
        urlPath: '/product-details'
      }, 
      query: {
        "data.slug": handle,
      },
    
    })
    .toPromise();

  console.log(handle)
  console.log(content);

  return (
    <div>
      <ProductDisplay handle={handle} />
      <RenderBuilderContent
        model={builderModelName}
        content={content || undefined}
      />
    </div>
  );
}

The console.logs when I change the slug for test-3:

But when I set the slug as test-3 and publish updates, it works:

i

But with the others slugs, it stays as the first print.

Hello @kevinroy,

Could you please try using the noTargeting property?

  const products = await builder.getAll("product-details", {
    fields: 'data.slug',
    options: { enrich: true, noTargeting: true }
  });

Let me know how that works.

Thanks,

For your implementation, I assume that the slug will be dynamically passed to the builder based on the Shopify product handle. In that case, I recommend passing the product data to the <BuilderComponent> like so:

<BuilderComponent data={{ product }} />

This will ensure that the relevant product data is available within the Builder Component, allowing it to render dynamically based on the product handle. For more details on how to pass data and functions to the builder, you can refer to the below link

Hi, thank you for your suggestion! I followed your advice to pass the product data to the <BuilderComponent> (in my case, the RenderBuilderContent component), but I’m still encountering an issue that I’d like to explain for further guidance.

Current Scenario

  1. Builder.io Setup:
  • I’m using Builder.io to manage dynamic blocks on the product detail page.
  • The “product-details” model has a slug field, which I use to map each product through the Shopify handle.
  • In the Visual Editor, I can see the blocks correctly for all products, regardless of the selected slug, but the console.log(content) is undefined except for the last slug that was published.
  1. Behavior in Code:
  • My product detail page (app/products/[handle]/page.tsx) fetches content from Builder.io using the product handle with the query:
    query: { "data.slug": handle }.
  • In the console, the content from Builder only returns data for the last slug that was published in Builder.io. For other slugs, it returns undefined.
  • The ProductDisplay component, which fetches product data directly from Shopify, works perfectly for all products, the problem is whats bellow this block.
  1. Difference Between Visual Editor and Browser:
  • In the Visual Editor, blocks appear correctly for all slugs.
  • In the browser, only the last published slug returns valid content from Builder. For all other slugs, the content is undefined, resulting in a 404.

Current Implementation

import { RenderBuilderContent } from "../../../components/builder";
import { builder } from "@builder.io/sdk";
import { ProductDisplay } from "@/components/ProductDisplay";

import { getProductByHandle, ShopifyProduct } from "../../../lib/shopify";

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!);

export default async function ProductPage({
  params,
}: {
  params: Promise<{ handle: string }>
}) {
  const builderModelName = "product-details";
  const { handle } = await params
  const product: ShopifyProduct | null = await getProductByHandle(handle);

  if (!product) {
    console.error(`Product not found for handle: ${handle}`);
  }

  const content = await builder
    .get(builderModelName, {
      userAttributes: {
        urlPath: "/product-details",
      },
      query: {
        "data.slug": handle,
      },
    })
    .toPromise();

  console.log("Handle:", handle);
  console.log("Builder Content:", content);

  return (
    <div>
      <ProductDisplay product={product} />
      <RenderBuilderContent
        model={builderModelName}
        content={content || undefined}
        data={{ product }}
      />
    </div>
  );
}

And the BuilderComponent:

"use client"

import "../builder-registry"

import { useIsPreviewing } from "@builder.io/react"
import { builder } from "@builder.io/sdk"
import dynamic from "next/dynamic"
import DefaultErrorPage from "next/error"
import type { ComponentProps } from "react"

const BuilderComponent = dynamic(
  () => import("@builder.io/react").then((mod) => mod.BuilderComponent),
  { ssr: false }
)

type BuilderPageProps = ComponentProps<typeof BuilderComponent>

// Builder Public API Key set in .env file
builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

export function RenderBuilderContent({ content, model, data }: BuilderPageProps) {
  // Call the useIsPreviewing hook to determine if
  // the page is being previewed in Builder
  const isPreviewing = useIsPreviewing()
  // If "content" has a value or the page is being previewed in Builder,
  // render the BuilderComponent with the specified content and model props.
  if (content || isPreviewing) {
    return <BuilderComponent content={content} model={model} data={data}/>
  }
  // If the "content" is falsy and the page is
  // not being previewed in Builder, render the
  // DefaultErrorPage with a 404.
  return <DefaultErrorPage statusCode={404} />
}

@manish-sharma Given that the Visual Editor displays the blocks correctly for all slugs, but the browser only works for the last published slug, could this issue be related to how Builder.io caches or indexes the content? Is there a specific setting or approach I need to adjust to ensure that content for all slugs is accessible in the browser?

Hi @kevinroy,

This behavior is expected because you currently have a single page where content is dynamically published based on the most recent slug. As a result, our content API only returns content that matches the slug of the last published update.

If it helps here is a loom video with similar implementations that could help you achieve what you are trying

Let us know if you need any further assistance.

Best regards,