commercetools Frontend Integration: ארכיטקטורה ופרקטיקה
ל-commercetools אין ממשק משתמש מובנה — רק API. משמעות הדבר היא שהפרונטאנד חייב להיבנות מאפס, ובחירת הטכנולוגיה משפיעה ישירות על ביצועים ועלויות תחזוקה. המערכת האקולוגית הבוגרת ביותר נוצרה סביב Next.js עם @commercetools/platform-sdk. במשך יותר מ-5 שנים, יישמנו יותר מ-20 פרויקטים headless — מחנויות איקומרס ועד פורטלי B2B — ולמדנו את הלקחים הקשים שלא תצטרכו לחזור עליהם. נקודות כאב אופייניות: שגיאת ConcurrentModification בעבודה עם עגלת הקניות, מחירים שאבדו עקב priceCurrency שגוי, וחיפוש איטי ללא קאש. כדי להימנע ממלכודות אלה, נעבור על החלטות ארכיטקטוניות מרכזיות ונראה כיצד ISR נותן TTFB מהיר פי 5 מ-SSR מסורתי, וסנכרון דרך Subscriptions מעדכן חיפוש בשניות במקום שעות.
למה Headless commercetools הוא אתגר לפרונטאנד
בניגוד ל-CMS מונוליטיים, commercetools אינו מספק רינדור או קאש. כל לוגיקת התצוגה נופלת על הפרונטאנד. זה נותן חופש אך דורש ארכיטקטורה נכונה: הפרדת קליינטים (שרת וקליינט), יצירת דפים סטטיים אינקרמנטלית, וטיפול בקונפליקטי גרסאות. צוות לא מוכן מגיע לעיתים קרובות לשאילתות N+1, TTFB גבוה, ואובדן נתוני עגלה.
SDK ואתחול קליינט
npm install @commercetools/platform-sdk @commercetools/sdk-client-v2 \
@commercetools/sdk-middleware-auth @commercetools/sdk-middleware-http \
@commercetools/sdk-middleware-queueשלושה קליינטים לשלושה הקשרים:
// lib/ctpClient.ts
import { createClient } from "@commercetools/sdk-client-v2";
import { createApiBuilderFromCtpClient } from "@commercetools/platform-sdk";
function buildClient(authMiddleware: Middleware) {
return createApiBuilderFromCtpClient(
createClient({
middlewares: [
authMiddleware,
createQueueMiddleware({ concurrency: 5 }),
createHttpMiddleware({
host: `https://api.${process.env.CTP_REGION}.commercetools.com`,
}),
],
})
).withProjectKey({ projectKey: process.env.CTP_PROJECT_KEY! });
}
export const serverApiRoot = buildClient(
createAuthMiddlewareForClientCredentialsFlow({
host: `https://auth.${process.env.CTP_REGION}.commercetools.com`,
projectKey: process.env.CTP_PROJECT_KEY!,
credentials: {
clientId: process.env.CTP_SERVER_CLIENT_ID!,
clientSecret: process.env.CTP_SERVER_CLIENT_SECRET!,
},
scopes: [`view_products:${process.env.CTP_PROJECT_KEY}`],
})
);
הקליינט בצד השרת משמש ב-npm install @commercetools/platform-sdk @commercetools/sdk-client-v2 \ @commercetools/sdk-middleware-auth @commercetools/sdk-middleware-http \ @commercetools/sdk-middleware-queue / RSC. הקליינט בצד הלקוח (עם טוקן משתמש) משמש רק בדפדפן.
ISR פותר את בעיית התמחור הדינמי
אם מפרסמים את הקטלוג באופן סטטי לחלוטין, המחירים מתיישנים. אנו משתמשים ב-Incremental Static Regeneration עם // lib/ctpClient.ts import { createClient } from "@commercetools/sdk-client-v2"; import { createApiBuilderFromCtpClient } from "@commercetools/platform-sdk"; function buildClient(authMiddleware: Middleware) { return createApiBuilderFromCtpClient( createClient({ middlewares: [ authMiddleware, createQueueMiddleware({ concurrency: 5 }), createHttpMiddleware({ host: `https://api.${process.env.CTP_REGION}.commercetools.com`, }), ], }) ).withProjectKey({ projectKey: process.env.CTP_PROJECT_KEY! }); } export const serverApiRoot = buildClient( createAuthMiddlewareForClientCredentialsFlow({ host: `https://auth.${process.env.CTP_REGION}.commercetools.com`, projectKey: process.env.CTP_PROJECT_KEY!, credentials: { clientId: process.env.CTP_SERVER_CLIENT_ID!, clientSecret: process.env.CTP_SERVER_CLIENT_SECRET!, }, scopes: [`view_products:${process.env.CTP_PROJECT_KEY}`], }) ); עבור דפי מוצר. זה נותן TTFB של 80 אלפיות השנייה וטריות מחירים שלא עולה על 5 דקות. עבור קטלוגים עם עדכונים תכופים — קאש Redis על גבי SDK עם invalidation דרך webhook.
// app/catalog/[slug]/page.tsx (App Router)
import { serverApiRoot } from "@/lib/ctpClient";
export async function generateStaticParams() {
const products = await serverApiRoot
.productProjections()
.get({
queryArgs: {
limit: 500,
staged: false,
where: 'masterData(published = true)',
},
})
.execute();
return products.body.results.map((p) => ({
slug: p.slug["ru"],
}));
}
export default async function ProductPage({
params,
}: {
params: { slug: string };
}) {
const result = await serverApiRoot
.productProjections()
.get({
queryArgs: {
where: `slug(ru = "${params.slug}")`,
expand: ["productType", "categories[*]"],
priceCurrency: "RUB",
priceChannel: "channel-key=storefront-ru",
},
})
.execute();
const product = result.body.results[0];
if (!product) notFound();
return <ProductDetail product={product} />;
} עגלת קניות: State בצד הלקוח + API
העגלה נשמרת ב-commercetools — getStaticProps נשמר בקוקי. ללא שכפול ב-localStorage.
// hooks/useCart.ts
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { browserApiRoot } from "@/lib/ctpClientBrowser";
import Cookies from "js-cookie";
export function useCart() {
const queryClient = useQueryClient();
const cartId = Cookies.get("cart_id");
const { data: cart } = useQuery({
queryKey: ["cart", cartId],
queryFn: async () => {
if (!cartId) return null;
return (await browserApiRoot.carts().withId({ ID: cartId }).get().execute()).body;
},
enabled: !!cartId,
});
const addToCart = useMutation({
mutationFn: async ({
productId,
variantId,
quantity,
}: {
productId: string;
variantId: number;
quantity: number;
}) => {
if (!cartId) {
const newCart = await browserApiRoot.carts().post({
body: {
currency: "RUB",
store: { typeId: "store", key: "web-ru" },
lineItems: [{ productId, variantId, quantity }],
},
}).execute();
Cookies.set("cart_id", newCart.body.id, { expires: 30 });
return newCart.body;
}
return (await browserApiRoot.carts().withId({ ID: cartId }).post({
body: {
version: cart!.version,
actions: [
{ action: "addLineItem", productId, variantId, quantity },
],
},
}).execute()).body;
},
onSuccess: (updatedCart) => {
queryClient.setQueryData(["cart", updatedCart.id], updatedCart);
},
});
return { cart, addToCart };
} חיפוש עם סנכרון Algolia
commercetools אינו מספק חיפוש טקסט מלא ברמת הרלוונטיות של Algolia. פתרון פרודוקטיבי הוא סנכרון דרך Subscriptions:
// subscriptions/algolia-sync.ts
// Commercetools Subscription → SQS → Lambda → Algolia
export async function handler(event: SQSEvent) {
for (const record of event.Records) {
const message = JSON.parse(record.body);
const { notificationType, resourceTypeId, resourceUserProvidedIdentifiers } = message;
if (resourceTypeId !== "product") continue;
const product = await serverApiRoot
.products()
.withId({ ID: message.resource.id })
.get({ queryArgs: { expand: ["productType"] } })
.execute();
if (notificationType === "ResourceDeleted") {
await algoliaIndex.deleteObject(message.resource.id);
} else {
await algoliaIndex.saveObject(transformForAlgolia(product.body));
}
}
} אימות לקוח ומיזוג עגלות
להתחברות אנו משתמשים ב-Customer SDK. לאחר אימות מוצלח אנו ממזגים את העגלה האנונימית עם עגלת המשתמש — זה סטנדרטי עבור commercetools. פרטי היישום ב-Next.js Route Handlers עם קוקיז httpOnly.
קליינט בצד השרת לעומת קליינט בצד הלקוח
| מאפיין | קליינט בצד השרת | קליינט בצד הלקוח |
|---|---|---|
| סוג אימות | Client Credentials (service-to-service) | Password Flow (טוקן משתמש) |
| היקף | קטלוג מלא, מחירים, מלאי | רק נתוני לקוח נוכחיים |
| קאש | ISR/SSG עם revalidate | ללא (רק React Query client) |
| אבטחה | משתני סביבה, לעולם לא בדפדפן | טוקן בקוקי httpOnly |
| ביצועים | TTFB נמוך מאוד (80-150 אלפיות השנייה) | תלוי ברשת (200-400 אלפיות השנייה) |
איך להימנע מטעויות אינטגרציה: בעיות נפוצות והתהליך שלנו
-
409 ConcurrentModification —
revalidate: 300מיושן. פתרון: ניסיון חוזר עם אובייקט מעודכן. אנו מוסיפים ניסיון חוזר אוטומטי. -
400 InvalidInput על העגלה — Extension שהופעל דחה את הפעולה, קרא
// app/catalog/[slug]/page.tsx (App Router) import { serverApiRoot } from "@/lib/ctpClient"; export async function generateStaticParams() { const products = await serverApiRoot .productProjections() .get({ queryArgs: { limit: 500, staged: false, where: 'masterData(published = true)', }, }) .execute(); return products.body.results.map((p) => ({ slug: p.slug["ru"] })); } export default async function ProductPage({ params, }: { params: { slug: string }; }) { const result = await serverApiRoot .productProjections() .get({ queryArgs: { where: `slug(ru = "${params.slug}")`, expand: ["productType", "categories[*]"], priceCurrency: "RUB", priceChannel: "channel-key=storefront-ru", }, }) .execute(); const product = result.body.results[0]; if (!product) notFound(); return <ProductDetail product={product} />; }. -
מחירים לא מוצגים — חסר
cartIdו-// hooks/useCart.ts import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { browserApiRoot } from "@/lib/ctpClientBrowser"; import Cookies from "js-cookie"; export function useCart() { const queryClient = useQueryClient(); const cartId = Cookies.get("cart_id"); const { data: cart } = useQuery({ queryKey: ["cart", cartId], queryFn: async () => { if (!cartId) return null; return (await browserApiRoot.carts().withId({ ID: cartId }).get().execute()).body; }, enabled: !!cartId, }); const addToCart = useMutation({ mutationFn: async ({ productId, variantId, quantity, }: { productId: string; variantId: number; quantity: number; }) => { if (!cartId) { const newCart = await browserApiRoot.carts().post({ body: { currency: "RUB", store: { typeId: "store", key: "web-ru" }, lineItems: [{ productId, variantId, quantity }], }, }).execute(); Cookies.set("cart_id", newCart.body.id, { expires: 30 }); return newCart.body; } return (await browserApiRoot.carts().withId({ ID: cartId }).post({ body: { version: cart!.version, actions: [{ action: "addLineItem", productId, variantId, quantity }], }, }).execute()).body; }, onSuccess: (updatedCart) => { queryClient.setQueryData(["cart", updatedCart.id], updatedCart); }, }); return { cart, addToCart }; }. -
Slug לא נמצא — המוצר לא פורסם (
// subscriptions/algolia-sync.ts // Commercetools Subscription → SQS → Lambda → Algolia export async function handler(event: SQSEvent) { for (const record of event.Records) { const message = JSON.parse(record.body); const { notificationType, resourceTypeId, resourceUserProvidedIdentifiers } = message; if (resourceTypeId !== "product") continue; const product = await serverApiRoot .products() .withId({ ID: message.resource.id }) .get({ queryArgs: { expand: ["productType"] } }) .execute(); if (notificationType === "ResourceDeleted") { await algoliaIndex.deleteObject(message.resource.id); } else { await algoliaIndex.saveObject(transformForAlgolia(product.body)); } } }במקוםversion).
שלבים ולוחות זמנים
| שלב | מה אנחנו עושים | משך |
|---|---|---|
| ניתוח | ביקורת ארכיטקטורה נוכחית, היקף אינטגרציה, הגדרת סביבות commercetools | 2-3 ימים |
| עיצוב | סכמת נתונים, סוגי מוצרים, טקסונומיה, נקודות קצה של SDK | 3-5 ימים |
| יישום | הגדרת קליינט, קטלוג ISR, עגלה, אימות, חיפוש | 10-15 ימים |
| בדיקות | בדיקות אינטגרציה, בדיקות עומס (N+1, קאש) | 3-4 ימים |
| פריסה | CI/CD, ניטור, invalidation קאש, תיעוד | 2-3 ימים |
מה כלול
- תיעוד: סכמת נקודות קצה, תיאורי סוגים, דוגמאות בקשות.
- אישורים: client_id/secret לקליינט שרת, password flow ללקוחות.
- הדרכה: הדגמת שימוש ב-SDK, הסבר על שגיאות נפוצות.
- תמיכה: שבועיים לאחר הפריסה — תיקוני באגים וייעוץ.
למה לבחור בנו
- 5+ שנות ניסיון עם commercetools ופלטפורמות headless.
- 20+ פרויקטים מוצלחים לאיקומרס ו-B2B.
- אחריות: Code Review לפני כל פריסה, כיסוי בדיקות >80%.
- הסמכה: המהנדסים שלנו עברו הכשרה רשמית של commercetools.
הפחתת עלויות תשתית ב-30-50% בזכות ISR והאצת זמן היציאה לשוק עם פתרונות מוכנים — תוצאות אמיתיות שאנו מגבים במדדים. קבלו ייעוץ לפרויקט שלכם. נעריך את ההיקף ונציע את הפתרון האופטימלי.







