A common use case with a visual CMS is to have a region on your product page for adding more editorial about the given product — or for all products of a given type or collection. This could include content such as storytelling, imagery, videos, recommended products, and other merchandising.
A standard section model named product-editorial is all you need.
By default you don't need any fields. Instead, this example uses targeting to decide where the section should display; such as for a given product or any product of a certain collection.
Custom targeting or an e-commerce plugin can help provide the UI fields for picking product and collections where this section shows.
The Next.js shows and example in pages/products/[product].jsx:
import { BuilderComponent, builder } from '@builder.io/react';
// Replace with your Public API Key.
builder.init(YOUR_API_KEY);
export async function getStaticProps({ params }) {
// Get the product details from your ecom backend
const product = await getProduct(params.product)
const editorial = await builder
.get('product-editorial', {
userAttributes: {
// This helps you target different product editorials
// to different URL paths. Optionally add other properties to target
// on here too
urlPath: `/products/${params.product}`,
// Allow targeting a section by a specific product ID (or perhaps handle)
productId: product.id,
// Optionally, allow targeting any product in a collection
collection: product.collection
}
})
.toPromise();
return {
props: {
product,
editorial: editorial || null,
},
};
}
export default function Page({ editorial, product }) {
return (
<>
{/* Put your header here. */}
<YourHeader />
<YourProductInfo product={product} />
<BuilderComponent model="product-editorial" content={editorial} />
{/* Put the rest of your page here. */}
<YourFooter />
</>
);
}import React from "react";
import { builder } from "@builder.io/sdk";
import Head from "next/head";
import { RenderBuilderContent } from "@/components/builder";
// Replace with your Public API Key
builder.init(YOUR_API_KEY);
interface PageProps {
params: {
page: string[];
};
}
export default async function ProductEditorial(props: PageProps) {
const content = await builder
.get("product-editorial", {
prerender: false,
userAttributes: {
// This wil allow you to target different product editorials
// to different URL paths. Optionally add other properties to target
// on here too
urlPath: `/products/${props.params.product}`,
// Allow targeting a section by a specific product ID (or perhaps handle)
productId: props.params.product,
// Optionally, allow targeting any product in a collection
collection: props.params.collection
}
})
.toPromise();
return (
<>
<Head>
<title>{content?.data.title}</title>
</Head>
{/* Render the Builder page */}
<RenderBuilderContent content={content} />
{/* Put the rest of your page here. */}
<YourFooter />
</>
);
}Notice that RenderBuilderContent is a component you'd make, for example:
"use client";
import { BuilderComponent, useIsPreviewing } from "@builder.io/react";
import DefaultErrorPage from "next/error";
interface BuilderPageProps {
content: any;
}
export function RenderBuilderContent({ content }: BuilderPageProps) {
const isPreviewing = useIsPreviewing();
if (content || isPreviewing) {
return <BuilderComponent content={content} model="page" />;
}
return null;
}import { BuilderComponent, builder } from "@builder.io/react";
// Replace with your Public API Key.
builder.init(YOUR_API_KEY);
export default function ProductPage({ product }) {
const [editorial, setEditorial] = useState(null);
useEffect(() => {
builder
.get("product-editorial", {
userAttributes: {
// To allow targeting different editorials at different pages (URLs) and products
urlPath: window.location.pathname,
productId: product.id,
collection: product.collection
},
})
.toPromise()
.then((editorialData) => setEditorial(editorialData));
}, []);
return (
<>
{/* Put your header here. */}
<YourHeader />
<YourProductInfo product={product} />
{editorial && (
<BuilderComponent
model="product-editorial"
content={editorial}
/>
)}
{/* Put the rest of your page here. */}
<YourFooter />
</>
);
}builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { fetchOneEntry, getBuilderSearchParams } from '@builder.io/sdk-react';
// Replace 'getProduct' and 'YOUR_PUBLIC_API_KEY' with actual implementations and key
export const loader = async ({ params }) => {
const product = await getProduct(params.product); // Assuming getProduct is defined elsewhere
const content = await fetchOneEntry({
model: 'product-editorial',
apiKey: 'YOUR_PUBLIC_API_KEY', // <-- Replace with your Public API Key
options: getBuilderSearchParams(new URL(request.url).searchParams),
query: {
'data.productId': product.id,
'data.collection': product.collection,
// ... other targeting options
},
});
if (content) {
return json({ content, product });
}
return json({ content: null, product: null });
};
export default function Page() {
const { content, product } = useLoaderData();
return (
<div>
<YourHeader />
<YourProductInfo product={product} />
{/* Directly use the fetched content */}
{content && <div dangerouslySetInnerHTML={{ __html: content.data.html }} />}
<YourFooter />
</div>
);
}This example uses SvelteKit.
// src/routes/product/[productId].svelte
<script>
import { Content } from '@builder.io/svelte';
export let product;
export let editorial;
</script>
<main>
<!-- Render the editorial content -->
<Content model="product-editorial" content={editorial} />
<!-- Render the product info -->
<div>{product.name}</div>
<div>{product.description}</div>
<div>{product.price}</div>
</main>
<script context="module">
import { getProduct } from './_api';
import { getEditorial } from './_builder';
export async function load({ params }) {
const productId = params.productId;
const [product, editorial] = await Promise.all([
getProduct(productId),
getEditorial(productId),
]);
return { props: { product, editorial } };
}
</script>// src/routes/product/_api.js
export async function getProduct(productId) {
// Fetch the product data from your ecom backend
const res = await fetch(`/api/products/${productId}`);
const product = await res.json();
return product;
}// src/routes/product/_builder.js
import { fetchOneEntry, getBuilderSearchParams } from '@builder.io/sdk-svelte';
// Replace with your Public API Key.
const BUILDER_PUBLIC_API_KEY = YOUR_API_KEY;
export async function getEditorial(productId) {
const content = await fetchOneEntry({
model: 'product-editorial',
apiKey: BUILDER_PUBLIC_API_KEY,
options: getBuilderSearchParams(event.url.searchParams),
userAttributes: {
urlPath: `/products/${productId}`,
productId: productId,
},
});
return content;
}<template>
<div>
<!-- Put your header here. -->
<YourHeader />
<YourProductInfo :product="product" />
<!-- Display content if available -->
<div v-if="canShowContent" v-html="editorialHtml"></div>
<!-- Put the rest of your page here. -->
<YourFooter />
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import { fetchOneEntry, isPreviewing, getBuilderSearchParams } from '@builder.io/sdk-vue';
export default defineComponent({
setup() {
const apiKey = YOUR_PUBLIC_API_KEY; // Add your Public API Key
const product = ref(null); // Assuming you fetch this as shown in the asyncData example
const editorialHtml = ref(null);
const canShowContent = ref(false);
onMounted(async () => {
try {
// Assuming getProduct is a function that fetches product details
// from your backend based on params.product
product.value = await getProduct(params.product);
const editorial = await fetchOneEntry({
model: 'product-editorial',
apiKey: apiKey,
options: getBuilderSearchParams(new URL(location.href).searchParams),
query: {
'data.urlPath': `/products/${params.product}`,
'data.productId': product.value.id,
'data.collection': product.value.collection
}
});
if (editorial) {
// Assuming your editorial content has an HTML field
editorialHtml.value = editorial.data.html;
canShowContent.value = true;
}
} catch (error) {
console.error('Failed to fetch editorial content:', error);
}
// Adjust this based on how you determine previewing state
canShowContent.value = editorialHtml.value || isPreviewing({ apiKey });
});
return {
product,
editorialHtml,
canShowContent
};
}
});
</script><template>
<div>
<!-- Put your header here. -->
<YourHeader />
<YourProductInfo :product="product" />
<Content
v-if="canShowContent"
:model="productEditorial"
:content="content"
:api-key="apiKey"
/>
<!-- Put the rest of your page here. -->
<YourFooter />
</div>
</template>
<script>
import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';
export default {
components: { Content },
async asyncData({ params, error }) {
try {
// Replace 'YOUR_PUBLIC_API_KEY' with your Public API Key
const apiKey = 'YOUR_PUBLIC_API_KEY';
// Get the product details from your e-commerce backend
// Assuming getProduct is defined elsewhere
const product = await getProduct(params.product);
const editorial = await fetchOneEntry({
model: 'product-editorial',
apiKey: apiKey,
query: {
'data.urlPath': `/products/${params.product}`,
'data.productId': product.id,
'data.collection': product.collection,
},
});
return { product, editorial, apiKey };
} catch (err) {
console.error('Failed to fetch product details:', err);
error({ statusCode: 404, message: 'Product not found' });
}
},
data() {
return {
content: null,
canShowContent: false
};
},
mounted() {
this.canShowContent = this.editorial || isPreviewing({ apiKey: this.apiKey });
}
};
</script>Add the following to nuxt.config.js:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
module: ['@builder.io/sdk-vue/nuxt'],
});If you are using an SSR framework other than Nuxt, you must import the CSS by adding the following to your entry point, before rendering Builder content:
<script>
import '@builder.io/sdk-vue/css';
</script>Place the following into your .tsx file:
import { fetchOneEntry, Content, getBuilderSearchParams } from "@builder.io/sdk-qwik";
export const BUILDER_PUBLIC_API_KEY = YOUR_API_KEY; // <-- Add your Public API KEY here
export const BUILDER_MODEL = "product-editorial"; // <-- Add your section name here
export async function loader({ params }) {
const product = await fetchOneEntry(params.product)
const content = await getContent({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
ooptions: getBuilderSearchParams(url.searchParams),
userAttributes: {
urlPath: `/products/${product.id}`,
productId: product.id,
collection: product.collection,
},
});
return { content, product };
}
export default component$(({ content, product }) => {
return (
<>
{/* Put your header here. */}
<YourHeader />
<YourProductInfo product={product} />
<Resource
value={content}
onPending={() => <div>Loading...</div>}
onResolved={(content) => (
<Content
model={BUILDER_MODEL}
content={content}
apiKey={BUILDER_PUBLIC_API_KEY}
/>
)}
/>
{/* Put the rest of your page here. */}
<YourFooter />
</>
);
});<!-- src/app/product-page.component.html -->
<div *ngIf="product">
<!-- Put your header here. -->
<YourHeader />
<YourProductInfo [product]="product" />
<builder-content *ngIf="editorial" model="product-editorial" [content]="editorial">
</builder-content>
<!-- Put the rest of your page here. -->
<YourFooter />
</div>// src/app/product-page.component.ts
import { Component, OnInit } from '@angular/core';
import {
BuilderContent,
Content,
fetchOneEntry,
} from '@builder.io/sdk-angular';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-page',
standalone: true,
imports: [Content, CommonModule],
templateUrl: './product-page.component.html',
styleUrl: './product-page.component.css',
})
export class ProductPageComponent implements OnInit {
product: any;
editorial: BuilderContent | null = null;
constructor(private route: ActivatedRoute) {}
async ngOnInit() {
const urlPath = window.location.pathname || '';
const productId = this.route.snapshot.paramMap.get('productId');
if (productId) {
// Fetch produt details from ecommerce api
this.product = await fetch(
`https://fakestoreapi.com/products/${productId}`
).then((res) => res.json());
// Fetch editorial content
this.editorial = await fetchOneEntry({
apiKey: /* YOUR PUBLIC API KEY */,
model: 'product-editorial',
userAttributes: {
urlPath,
productId: this.product?.id,
collection: this.product?.category,
},
});
}
}
}<!-- src/app/product-page.component.html -->
<div *ngIf="product">
<!-- Put your header here. -->
<YourHeader />
<YourProductInfo [product]="product" />
<builder-content *ngIf="editorial" model="product-editorial" [content]="editorial">
</builder-content>
<!-- Put the rest of your page here. -->
<YourFooter />
</div>// src/app/product-page.component.ts
import { Component, OnInit } from '@angular/core';
import {
BuilderContent,
Content,
fetchOneEntry,
} from '@builder.io/sdk-angular';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-page',
standalone: true,
imports: [Content, CommonModule],
templateUrl: './product-page.component.html',
styleUrl: './product-page.component.css',
})
export class ProductPageComponent implements OnInit {
product: any;
editorial: BuilderContent | null = null;
constructor(private route: ActivatedRoute) {}
async ngOnInit() {
const urlPath = window.location.pathname || '';
const productId = this.route.snapshot.paramMap.get('productId');
if (productId) {
// Fetch produt details from ecommerce api
this.product = await fetch(
`https://fakestoreapi.com/products/${productId}`
).then((res) => res.json());
// Fetch editorial content
this.editorial = await fetchOneEntry({
apiKey: /* YOUR PUBLIC API KEY */,
model: 'product-editorial',
userAttributes: {
urlPath,
productId: this.product?.id,
collection: this.product?.category,
},
});
}
}
}In product-editorial.resolver.ts, add the following:
BuilderContent and fetchOneEntry from @builder.io/sdk-angular.userAttributes, by providing the URL path to fetch the editorial content.import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { fetchOneEntry } from '@builder.io/sdk-angular';
export const productEditorialResolver: ResolveFn<any> = async (
route: ActivatedRouteSnapshot
) => {
const productId = route.paramMap.get('id');
const urlPath = `/products/${productId}`;
const [product, editorial] = await Promise.all([
fetch(`https://fakestoreapi.com/products/${productId}`).then((res) =>
res.json()
),
fetchOneEntry({
apiKey: /* YOUR PUBLIC API KEY */,
model: 'product-editorial',
userAttributes: {
urlPath,
},
}),
]);
return { product, editorial };
};// in your product page component
import * as React from 'react';
import { graphql } from 'gatsby';
import { BuilderComponent } from '@builder.io/react';
function ProductPage({ data }) {
const product = data?.product;
const editorial = data?.editorial?.content;
return (
<>
{/* Put your header here. */}
<YourHeader />
<YourProductInfo product={product} />
<BuilderComponent model="product-editorial" content={editorial} />
{/* Put the rest of your page here. */}
<YourFooter />
</>
);
}
export default ProductPage;
export const query = graphql`
query($productId: String!) {
product(id: { eq: $productId }) {
id
title
description
price
// ...add other product fields you need
}
editorial: allBuilderModels(
filter: { modelId: { eq: "product-editorial" } }
) {
nodes {
content(target: { productId: { eq: $productId } })
}
}
}
`;Import Builder and fetch the product-editorial, making sure to replace YOUR_PUBLIC_API_KEY with your Builder Public API Key.
// Import necessary libraries
import SwiftUI
import BuilderIO
struct ContentView: View {
@ObservedObject var content: BuilderContentWrapper = BuilderContentWrapper()
init() {
self.getContent()
}
// Define getContent as a method to fetch content from Builder.io
func getContent() {
Content.getContent(model: "product-editorial",
apiKey: YOUR_PUBLIC_API_KEY, // Replace with your Public API key
url: "", // No URL is needed for sections
locale: "",
preview: "") { content in
// The completion block to be executed once getContent is completed
// Ideally in the main thread because it likely updates UI components
DispatchQueue.main.async {
// Calls changeContent on self.content with the new content
self.content.changeContent(content)
}
}
}
var body: some View {
VStack {
if let contentValue = content.content {
// Use the content to render your section view
// TO DO: Replace with code to render your section
RenderContent(content: contentValue, apiKey: YOUR_PUBLIC_API_KEY)
} else {
// Display a loading message if the content is not yet available
Text("Loading...")
}
// Handle live previewing
if Content.isPreviewing() {
// Display a 'Reload' button during content previews for manual refresh
Button("Reload") {
self.getContent()
}
}
}
}
}
import { Component } from '@angular/core';
import { BuilderContentService } from '@builder.io/angular';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-page',
template: `
<div *ngIf="product">
<!-- Put your header here. -->
<YourHeader />
<YourProductInfo [product]="product" />
<builder-component *ngIf="editorial" model="product-editorial" [content]="editorial"></builder-component>
<!-- Put the rest of your page here. -->
<YourFooter />
</div>
`,
})
export class ProductPageComponent {
product: any;
editorial: any;
constructor(private builderContentService: BuilderContentService, private route: ActivatedRoute) {}
async ngOnInit() {
const productId = this.route.snapshot.paramMap.get('productId');
// Get the product details from your ecom backend
this.product = await getProduct(productId);
// Get the editorial content
this.editorial = await this.builderContentService.getContent(
'product-editorial',
{
urlPath: `/products/${productId}`,
productId: this.product.id,
collection: this.product.collection
}
);
}
}© 2020 Builder.io, Inc.