Integrate Product Details
It can be helpful to keep product details in a CMS like Builder, in place of or in complement to an e-commerce platform.
In this way, you can do things such as extend your product info with additional rich content, localize product info, use A/B testing, set up scheduling, and configure targeting.
Model definition
First, create a structured data model named product-detailswith some fields:
| name | type | notes | ||||||
|
| Product Name | ||||||
|
| Rich text for your product detailed description | ||||||
|
| List with subfields:
| ||||||
|
| A unique URL handle for this product; for example, | ||||||
|
| Reference to a collection, either to your backend via an e-commerce plugin or to another data model in Builder that represents your product collections |
You can create as many additional fields as you like for additional product info.
Example Code
Below is an example in pages/products/[product].jsx:
import { builder } from '@builder.io/react';
// Replace with your Public API Key.
builder.init(YOUR_API_KEY);
export async function getStaticProps({ params }) {
const productDetails = await builder.get('product-details', {
query: {
// Query product details by its handle field
'data.handle': params.product
}
}).promise();
return {
props: {
productDetails: productDetails || null,
},
// Show a 404 page if no product is found
notFound: !productDetails,
revalidate: 5,
};
}
export default function Home({ productDetails }) {
return (
<>
<YourHeader />
<ProductDetails product={productDetails} />
</>
);
}builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})Create a file called pages/products/[product].tsx. This file handles the dynamic routing for individual product details.
import { builder } from '@builder.io/sdk';
// Replace with your Public API Key.
builder.init(YOUR_API_KEY);
export default async function Page(props) {
const productDetails = await builder.getAll('product-details', {
prerender: false,
query: {
// Query product details by its handle field
'data.handle': props?.params?.page?.join("/"),
}
});
return (
<>
<YourHeader />
<ProductDetails product={productDetails} />
</>
);
}builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})import { useEffect, useState } from "react";
import { builder } from "@builder.io/react";
// Put your API key here
builder.init(YOUR_API_KEY);
export default function App() {
const [productDetails, setProductDetails] = useState(null);
// Get the CMS data from Builder
useEffect(() => {
async function fetchContent() {
const productDetails = await builder.get("product-details", {
query: {
// Query product details by its handle field
"data.handle": "your-product-handle",
},
});
setProductDetails(productDetails);
}
fetchContent();
}, []);
return (
<>
<YourHeader />
<ProductDetails product={productDetails} />
</>
);
}builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})import { fetchOneEntry, getBuilderSearchParams } from '@builder.io/sdk-react';
import type { LoaderFunction } from '@remix-run/node';
export const loader: LoaderFunction = async ({ params }) => {
const productDetails = await fetchOneEntry({
model: 'product-details',
apiKey: 'YOUR_PUBLIC_API_KEY', // Replace with your API key
options: getBuilderSearchParams(new URL(request.url).searchParams),
query: {
'data.handle': params.handle,
},
});
return {
productDetails: productDetails || null,
};
};
import { useLoaderData } from '@remix-run/react';
import type { BuilderContent } from '@builder.io/sdk-react';
// Assuming YourHeader and ProductDetails are your custom components
import YourHeader from './YourHeader';
import ProductDetails from './ProductDetails';
interface LoaderData {
productDetails: BuilderContent | null;
}
export default function ProductPage() {
const { productDetails }: LoaderData = useLoaderData();
return (
<>
<YourHeader />
{productDetails && <ProductDetails product={productDetails} />}
</>
);
}
builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})<!-- src/routes/products/[slug]/+page.js -->
import { fetchOneEntry, getBuilderSearchParams } from '@builder.io/sdk-svelte';
import { BUILDER_PUBLIC_API_KEY } from '../../apiKey';
/** @type {import('./$types').PageServerLoad} */
export async function load({ params, url }) {
// fetch the product details by handle
const productDetails = await fetchOneEntry({
model: 'product-details',
apiKey: BUILDER_PUBLIC_API_KEY,
options: {
query: {
'data.handle': params.product,
},
...getBuilderSearchParams(url.searchParams),
},
});
return { productDetails };
}
<!-- src/routes/products/[slug]/index.svelte -->
<script>
import { isPreviewing, Content } from "@builder.io/sdk-svelte";
const apiKey = //ADD YOUR PUBLIC API KEY
const model = "product-details"
export let data;
const canShowContent = data?.productDetails || isPreviewing();
</script>
<main>
<ProductsPage>
{#if canShowContent}
<!-- Product Details model -->
<Content model={model} content={data?.productDetails} apiKey={apiKey} />
{:else}
<p>Loading...</p>
{/if}
</ProductsPage>
<!-- Additional content here -->
</RestOfThePage>
</main>
builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})<template>
<div>
<YourHeader />
<ProductDetails :product="productDetails" />
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import { fetchOneEntry, getBuilderSearchParams } from '@builder.io/sdk-vue';
export default defineComponent({
setup() {
const productDetails = ref(null);
onMounted(async () => {
try {
// Replace 'YOUR_PUBLIC_API_KEY' with your actual API Key and adjust the query as needed
const response = await fetchOneEntry({
model: 'product-details',
apiKey: 'YOUR_PUBLIC_API_KEY',
options: getBuilderSearchParams(new URL(location.href).searchParams),
query: {
'data.handle': 'your-product-handle',
},
});
productDetails.value = response;
} catch (error) {
console.error('Failed to fetch product details:', error);
productDetails.value = null;
}
});
return {
productDetails,
};
},
});
</script>
<template>
<div>
<YourHeader />
<ProductDetails :product="productDetails" />
</div>
</template>
<script>
import { fetchOneEntry } from '@builder.io/sdk-vue';
export default {
async asyncData({ params, $config }) {
try {
const productDetails = await fetchOneEntry({
model: 'product-details',
apiKey: YOUR_API_KEY
query: {
'data.handle': params.product,
},
});
return { productDetails };
} catch (error) {
console.error('Failed to fetch product details:', error);
return { productDetails: null };
}
},
};
</script>
import { component$, Resource, useResource$, getBuilderSearchParams } from "@builder.io/qwik";
import { DocumentHead, useLocation } from "@builder.io/qwik-city";
import { fetchOneEntry } from "@builder.io/sdk-qwik";
// Replace with your Public API Key.
export const apiKey = YOUR_API_KEY;
export default component$(() => {
const { pathname } = useLocation();
const handle = pathname.substring(1); // assuming the handle is in the URL path
const productDetailsResource = useResource$(() =>
fetchOneEntry({
model: "product-details",
apiKey: apiKey,
options: getBuilderSearchParams(url.searchParams),
query: {
"data.handle": handle,
},
})
);
return (
<Resource
value={productDetailsResource}
onPending={() => <>Loading...</>}
onRejected={(error) => <>Error: {error.message}</>}
onResolved={(productDetails) => (
<>
<DocumentHead>
<title>{productDetails.data.title}</title>
</DocumentHead>
<ProductDetails product={productDetails} />
</>
)}
/>
);
});
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { fetchOneEntry, type BuilderContent } from '@builder.io/sdk-angular';
@Component({
selector: 'app-product-details',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="productDetails">
<h1>{{ productDetails.data?.['name'] }}</h1>
<img
[src]="productDetails.data?.['image']"
[alt]="productDetails.data?.['name']"
width="400"
height="500"
/>
<p>{{ productDetails.data?.['collection'].value.data.copy }}</p>
<p>
Price:
{{ productDetails.data?.['collection'].value.data.price }}
</p>
</div>
<div *ngIf="!productDetails">
<p>Loading product details...</p>
</div>
`,
})
export class ProductDetailsComponent {
productDetails: BuilderContent | null = null;
apiKey: string = /* YOUR PUBLIC API KEY */;
async ngOnInit() {
this.productDetails = await fetchOneEntry({
model: 'product-details',
apiKey: this.apiKey,
query: {
'data.handle': /* YOUR HANDLE NAME */,
},
});
}
}import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BuilderContent } from '@builder.io/sdk-angular';
@Component({
selector: 'app-product-details',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="productDetails">
<h1>{{ productDetails.data?.['name'] }}</h1>
<img
[src]="productDetails.data?.['image']"
[alt]="productDetails.data?.['name']"
width="400"
height="500"
/>
<p>{{ productDetails.data?.['collection'].value.data.copy }}</p>
<p>
Price:
{{ productDetails.data?.['collection'].value.data.price }}
</p>
</div>
<div *ngIf="!productDetails">
<p>Loading product details...</p>
</div>
`,
})
export class ProductDetailsComponent {
productDetails: BuilderContent | null = null;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe((data: any) => {
this.productDetails = data.productDetails;
});
}
}Fetch productDetails in the resolver using fetchOneEntry() by providing the model, apiKey, and query with your handle.
// product-details.resolver.ts
import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { fetchOneEntry } from '@builder.io/sdk-angular';
export const productDetailsResolver: ResolveFn<any> = async (
route: ActivatedRouteSnapshot
) => {
const handle = route.paramMap.get('handle') || 'jacket'; //handle that you defined for your model
const productDetails = await fetchOneEntry({
model: 'product-details',
apiKey: /* YOUR PUBLIC API KEY */,
query: {
'data.handle': handle,
},
});
return productDetails;
};// src/pages/product.jsx
import * as React from 'react';
import { graphql } from 'gatsby';
function ProductPage({ data }) {
const productDetails = data?.builderModels?.productDetails?.[0]?.content?.data;
return (
<>
<YourHeader />
<ProductDetails product={productDetails} />
</>
);
}
export default ProductPage;
export const productQuery = graphql`
query($productHandle: String!) {
builderModels: allBuilderModels(
filter: { modelId: { eq: "product-details" } }
limit: 1
sort: { fields: [createdAt], order: DESC }
# pass the product handle as a variable to the query
filter: { content___data___handle: { eq: $productHandle } }
) {
productDetails: nodes {
content {
data
}
}
}
}
`;builder.get('your-model', {
query: {
'data.myCustomProperty.$eq': 'sale'
}
})import { Component, OnInit } from '@angular/core';
import { BuilderService } from '@builder.io/angular';
@Component({
selector: 'app-product-details',
templateUrl: './product-details.component.html',
styleUrls: ['./product-details.component.css']
})
export class ProductDetailsComponent implements OnInit {
productDetails: any;
constructor(private builder: BuilderService) { }
async ngOnInit(): Promise<void> {
this.productDetails = await this.builder.get('product-details', {
query: {
'data.handle': 'your-product-handle',
},
});
}
}<!-- app.component.html -->
<app-header></app-header>
<app-product-details [productDetails]="productDetails"></app-product-details>