· বাংলাদেশ সময়
Next.js ইন্টিগ্রেশন
Next.js শপ EcomSolveBD-তে কানেক্ট করুন
- ১
অ্যাকাউন্ট খুলুন
আপনি- ecomsolvebd.com এ রেজিস্টার / লগইন
- ড্যাশবোর্ডে ঢুকুন
- ২
Website → Next.js
আপনি- Website → Connect / Create (কানেক্ট / তৈরি)
- “আপনার কি ইতিমধ্যে … ওয়েবসাইট আছে?” → হ্যাঁ
- Custom / Headless কার্ড → Next.js
- শপ URL দিন — https://myshop.com
- চাইলে GA4 ID (G-…)
- Connect Next.js চাপুন
Tracking Merchant ID + Webhook Secret কপি করুন — Secret এখনই সেভ করুন।
- ৩
ডেভেলপারকে পাঠান
আপনি- Tracking Merchant ID
- Webhook Secret
- লাইভ শপ URL
- ৪
ডেভেলপার: SDK কন্ট্রাক্ট
ডেভেলপার- npm install @ecomsolvebd/next
- Routes: collect + attribution-config + order-status (+ order-create)
- Root layout: EsbTracker funnelAuto={false} + EsbPageViews
- lib/esb-track.ts → সব ১৯টা ব্রাউজার ইভেন্ট (নিচের টেবিল)
- Checkout সফল → সার্ভার থেকে postOrder()
- app/feed/[channel]/route.ts + ProductFeedProvider → ৪টা XML ফিড
ইভেন্ট টেবিলের প্রতিটি ইভেন্ট ও প্রোডাক্ট ফিড বাধ্যতামূলক — শুধু page_view বা অর্ডার দিয়ে কাজ শেষ নয়। নিচের চেকলিস্ট ডেভেলপার/AI-কে দিন।
↓ SDK রেফারেন্স - ৫
টেস্ট করুন
দুজনে- প্রোডাক্ট দেখুন → কার্টে যোগ/বাদ → কার্ট → চেকআউট → প্রতিটি ফিল্ড পূরণ → অর্ডার
- Dashboard → Events: টেবিলের প্রতিটি ইভেন্ট এসেছে কিনা মিলিয়ে নিন
- টেস্ট অর্ডার → Dashboard → Orders; SaaS স্ট্যাটাস বদলান → Next.js চেক
- https://yoursite.com/feed/products.xml খুলুন → Dashboard → Products → Feed URL → Fetch now
SDK রেফারেন্স
@ecomsolvebd/next — App Router
ইন্টিগ্রেশন SDK — path কন্ট্রাক্ট + factories। স্টোর বিল্ড (কার্ট, ORM, CMS, UI) হোস্টের। ডেমো শপ কপি করবেন না; কন্ট্রাক্ট ইমপ্লিমেন্ট করুন।
কন্ট্রাক্ট
Path contract (host must expose these; package does not inject files):
POST|GET /api/ecomsolvebd/collect
GET|POST /api/ecomsolvebd/attribution-config
POST /api/ecomsolvebd/order-status
Exports:
@ecomsolvebd/next → buildOrderPayload, OrderWebhookPoster, …
@ecomsolvebd/next/client → EsbTracker (sets window.esbTrack)
@ecomsolvebd/next/route-handlers → create*RouteHandlers factories
Also required (sections below): all browser events via lib/esb-track.ts and
GET /feed/{products|facebook|tiktok|google}.xml via app/feed/[channel]/route.ts.
Out of scope for this SDK: your catalog UI, cart store, ORM, CMS, checkout UX.
Prefix paths with src/ when using src/app.
Identity: never fabricate visitor ids. Forward Cookie via buildOrderPayload(..., { cookieHeader }).১) ইনস্টল npm install @ecomsolvebd/next
2) Environment
Merchant Key + Webhook Secret ড্যাশবোর্ড Connect থেকে। Secret শুধু সার্ভার রানটাইমে।
.envECOMSOLVEBD_API_BASE=https://api.ecomsolvebd.com ECOMSOLVEBD_MERCHANT_KEY=<tracking_merchant_id> ECOMSOLVEBD_WEBHOOK_SECRET=<webhook_secret> ECOMSOLVEBD_GA4_MEASUREMENT_ID=G-XXXXXXXX # optional NEXT_PUBLIC_STORE_URL=https://your-store.example # ECOMSOLVEBD_DEPLOY_ENV=staging # when connected via staging dashboard
3) Browser tracker
EsbTracker transport মাউন্ট করে (window.esbTrack)। funnelAuto={false} = হোস্ট ইভেন্ট ওয়্যারিং।
root layout// Root layout — path may be app/layout.tsx or src/app/layout.tsx import { EsbTracker } from "@ecomsolvebd/next/client"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <EsbTracker merchantKey={process.env.ECOMSOLVEBD_MERCHANT_KEY!} // Public store origin (scheme + host). Used to build same-origin collect URL. storeBaseUrl={process.env.NEXT_PUBLIC_STORE_URL!} apiBase={process.env.ECOMSOLVEBD_API_BASE} deployEnv={process.env.ECOMSOLVEBD_DEPLOY_ENV} ga4MeasurementId={process.env.ECOMSOLVEBD_GA4_MEASUREMENT_ID} // You fire every event via lib/esb-track.ts (see events section) funnelAuto={false} /> {/* <Suspense fallback={null}><EsbPageViews /></Suspense> — SPA route-change page_view */} {children} </body> </html> ); }4) Collect proxy
First-party Auto ladder: browser → same-origin → API। Thin wrapper only.
app/api/ecomsolvebd/collect/route.ts// app/api/ecomsolvebd/collect/route.ts import { createCollectRouteHandlers } from "@ecomsolvebd/next/route-handlers"; export const { POST, GET } = createCollectRouteHandlers({ merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!, apiBase: process.env.ECOMSOLVEBD_API_BASE, deployEnv: process.env.ECOMSOLVEBD_DEPLOY_ENV, });5) Attribution config proxy
Same-origin attribution config — path ঠিক রাখুন।
app/api/ecomsolvebd/attribution-config/route.ts// app/api/ecomsolvebd/attribution-config/route.ts import { createAttributionConfigRouteHandlers } from "@ecomsolvebd/next/route-handlers"; export const { GET, POST } = createAttributionConfigRouteHandlers({ merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!, apiBase: process.env.ECOMSOLVEBD_API_BASE, deployEnv: process.env.ECOMSOLVEBD_DEPLOY_ENV, });6) Inbound order status
SaaS → store। findOrder / updateOrder আপনার persistence-এ ম্যাপ — ORM বাধ্যতামূলক নয়।
app/api/ecomsolvebd/order-status/route.ts// app/api/ecomsolvebd/order-status/route.ts (or src/app/…) import { createOrderStatusRouteHandlers } from "@ecomsolvebd/next/route-handlers"; // Wire find/update to YOUR persistence (Prisma, Drizzle, SQL, REST CMS, …). // The factory only needs: load by orderNumber → read status → write status. export const { POST } = createOrderStatusRouteHandlers({ webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!, allowedStatuses: ["pending", "confirmed", "delivered", "cancelled", "returned"], findOrder: async (orderNumber) => { // return await db.orders.findByNumber(orderNumber) return null; // replace — null → handler reports not found }, getCurrentStatus: (order) => { // return order.status return (order as { status?: string }).status; }, updateOrder: async (order, update) => { // Prefer business key orderNumber over PK type (Int vs String/cuid). // await db.orders.updateStatus(order.orderNumber, update.status) void order; void update; }, });7) Order ingest
Checkout success call site (যেখানেই অর্ডার কমিট হয়)। Ads Purchase নয়।
call site · server// Call from YOUR checkout success path (Route Handler, Server Action, etc.) // Exact file path depends on your storefront — not prescribed by this SDK. import { cookies } from "next/headers"; import { buildOrderPayload, OrderWebhookPoster } from "@ecomsolvebd/next"; // 1) Persist the order in your own database first, then: const poster = new OrderWebhookPoster({ merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!, webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!, apiBase: process.env.ECOMSOLVEBD_API_BASE, }); const payload = buildOrderPayload( { orderNumber: order.number, // Use your store currency (ISO 4217) — do not hardcode currency: order.currency, customer: { fullName: order.customerName, // name phone: order.phone, // email: order.email, }, items: order.lines.map((l) => ({ sku: l.sku, title: l.title, quantity: l.qty, unitPrice: String(l.unitPrice), // productId: l.productId, // variantTitle: l.variantTitle, })), // Shipping / address (uncomment when your checkout collects them) // district: order.district, // upazila: order.upazila, // address: order.address, // notes: order.notes, // Money (flat fields — not a nested totals object) // shippingTotal: String(order.shippingTotal), // discountTotal: String(order.discountTotal), // taxTotal: String(order.taxTotal), }, { cookieHeader: (await cookies()).toString() }, ); try { await poster.postOrder(payload); } catch (err) { console.error("[ecomsolvebd] postOrder failed", err); } // Ingest only — ads Purchase is sent later from the EcomSolveBD dashboard / courier flow.
নোট
- Client export: EsbTracker (@ecomsolvebd/next/client) — অন্য নাম নেই।
- Route handlers হোস্ট অ্যাপে thin wrappers; লজিক প্যাকেজে।
- বাধ্যতামূলক: সব ব্রাউজার ইভেন্ট (lib/esb-track.ts) ও প্রোডাক্ট ফিড (app/feed/[channel]/route.ts) — নিচের দুই সেকশন।
- NEXT_PUBLIC_STORE_URL = পাবলিক origin (https://shop.example) — proxy URL বানাতে লাগে।
আরও ফিচার
অর্ডার স্ট্যাটাস
EcomSolveBD ও Next.js অ্যাডমিন স্ট্যাটাস সিঙ্ক করে।
- · SaaS → Next.js: order-status route handler
- · Next.js → SaaS: status webhook (প্যাকেজ)
- · Dashboard Create Order → Next.js: v1.0.1+ (`POST /api/ecomsolvebd/order-create`)
- · ম্যাপ: pending · confirmed · delivered · cancelled · returned
প্রোডাক্ট ফিড
চারটা XML (বাধ্যতামূলক) — প্রোডাক্ট Next.js/API-তেই ম্যানেজ। সেটআপ নিচে।
- /feed/products.xml — EcomSolveBD ক্যাটালগ
- /feed/facebook.xml — Meta
- /feed/tiktok.xml — TikTok (RSS/XML)
- /feed/google.xml — Google Merchant
First-party ট্র্যাকিং
ইভেন্ট আপনার ডোমেইন দিয়ে — অ্যাড ব্লকারে কম হারায়।
- · Auto: same-origin /api/ecomsolvebd/collect → API
- · Dashboard: CNAME → Verify DNS → tracking.{domain}
- · ডেভ: proxy routes; owner: DNS (কোড নয়)
SDK রেফারেন্স
সব ইভেন্ট — কখন, কী পেলোড, কোথায় কোড
Transport (provided by EsbTracker in the root layout):
window.esbTrack(eventName, props?) → same-origin /api/ecomsolvebd/collect
window.esbBindCheckoutForm(selector, map?) → optional form auto-bind
"esb:ready" window event → fired once the transport exists
EsbTracker itself sends ONE hit when it mounts: landing_page_view on the visitor's first-ever visit
(no esb_first_touch yet — once per visitor, no page_view on that hit), page_view on every later load.
Never fire landing_page_view yourself. Everything else is yours:
<EsbTracker funnelAuto={false} … /> → no guessing from CSS/URLs; you call every event
via lib/esb-track.ts (below), which queues calls made before "esb:ready".
Required events (fire each one whose UI moment exists on the site):
page_view · view_item · add_to_cart · remove_from_cart · view_cart · begin_checkout
add_shipping_info · add_payment_info · checkout_form_started · first_name_added
last_name_added · phone_number_added · email_address_added · address_added · city_added
state_added · postal_code_added · country_added · checkout_form_completed
Orders: server webhook (OrderWebhookPoster.postOrder) — never a browser "purchase".নিয়ম: নিচের তালিকার প্রতিটি ইভেন্ট বাধ্যতামূলক — যদি সেই UI মুহূর্ত/ফিল্ড আপনার সাইটে থাকে।
- আগে সাইটের প্রোডাক্ট পেজ, কার্ট, চেকআউট ও চেকআউট ফর্মের প্রতিটি ইনপুট লিস্ট করুন; তারপর প্রতিটির সাথে এই টেবিলের ইভেন্ট ম্যাপ করুন।
- “শর্তসাপেক্ষ” ইভেন্ট শুধু তখনই বাদ দিন যখন সাইটে সেই জিনিসটাই নেই (যেমন পোস্ট কোড ফিল্ড নেই) — কারণটা কোডে কমেন্ট করে রাখুন।
- item_id = প্রোডাক্ট ফিডের <id> = অর্ডার আইটেমের productId — তিন জায়গায় একই ID, নইলে রিপোর্টে প্রোডাক্ট মিলবে না।
- currency = ISO কোড (BDT), value/price = number (string নয়)।
Next.js: EsbTracker মাউন্ট হলে window.esbTrack তৈরি হয়। নিচের esb-track.ts হেল্পার দিয়ে কল করুন — tracker রেডি হওয়ার আগের কল কিউ হয়, হারায় না। EsbTracker-এ funnelAuto={false} রাখলে সব ইভেন্ট হোস্টকেই কল করতে হবে।
| ইভেন্ট | কখন ফায়ার করবেন | পেলোড | বাধ্যতামূলক? |
|---|---|---|---|
| ব্রাউজ ও কার্ট | |||
| landing_page_view | ভিজিটরের একদম প্রথম ভিজিটের প্রথম হিট (esb_first_touch তখনো নেই) — প্রতি ভিজিটরে জীবনে একবারই; সেই হিটে page_view যাবে না। Next.js EsbTracker / Laravel @ecomsolvebdTracker / Custom বুটস্ট্র্যাপ / Woo-Shopify ট্র্যাকার নিজেই পাঠায় — নিজে আর কখনো পাঠাবেন না | — | হ্যাঁ |
| page_view | প্রথম ভিজিটের landing হিট বাদে প্রতিটি পেজ লোড ও SPA রুট পরিবর্তনে (পরের ভিজিট, পরের পেজ — সব page_view) | — | হ্যাঁ |
| view_item | প্রোডাক্ট ডিটেইল পেজ/কুইক-ভিউ দেখানোর সময় (প্রতি প্রোডাক্টে একবার) | ecommerce { currency, value, items[1] } | হ্যাঁ |
| add_to_cart | কার্টে যোগ সফল হওয়ার পর (প্রতিটি add, Buy Now সহ) | ecommerce { currency, value = price × qty, items[1] } | হ্যাঁ |
| remove_from_cart | কার্ট থেকে লাইন সরালে বা qty কমালে | ecommerce { currency, value, items[1] (removed qty) } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| view_cart | কার্ট পেজ / কার্ট ড্রয়ার খোলার সময় | ecommerce { currency, value = subtotal, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| চেকআউট | |||
| begin_checkout | চেকআউট পেজ/স্টেপ খোলার সময় (প্রতি চেকআউট সেশনে একবার) | ecommerce { currency, value, items[] } | হ্যাঁ |
| add_shipping_info | ডেলিভারি এরিয়া/শিপিং মেথড বাছাই করলে | ecommerce { currency, value, shipping_tier, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| add_payment_info | পেমেন্ট মেথড (COD, bKash…) বাছাই করলে | ecommerce { currency, value, payment_type, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| চেকআউট ফর্ম ও ফিল্ড | |||
| checkout_form_started | চেকআউট ফর্মে প্রথম ফোকাস/টাইপ (একবার) | {} | হ্যাঁ |
| first_name_added | First name / পূর্ণ নাম ফিল্ড পূরণ হলে (change/blur) — আসল নাম পাঠান (Checkout drop-off-এ Customer) | { name, field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| last_name_added | Last name ফিল্ড পূরণ হলে — আসল মান পাঠান (সার্ভার first + last জোড়া দেয়) | { last_name, field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| phone_number_added | মোবাইল নম্বর পূরণ হলে — আসল নম্বর পাঠান (abandon recovery) | { phone } | হ্যাঁ |
| email_address_added | ইমেইল পূরণ হলে — আসল ইমেইল পাঠান | { email } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| address_added | ঠিকানা ফিল্ড পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| city_added | City / জেলা (district) বাছাই বা পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| state_added | State / বিভাগ / থানা-উপজেলা বাছাই হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| postal_code_added | পোস্ট কোড পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| country_added | দেশ বাছাই হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| checkout_form_completed | ফর্ম সাবমিট (Place order চাপলে) | {} | হ্যাঁ |
Device & technology — প্রতিটা ইভেন্টে
Device, OS with Version, Device model, Browser — Details reports, সব ফিল্টার, অর্ডার জার্নি আর অ্যাডস Purchase এই চারটা ফিল্ড থেকে আসে। EsbTracker (@ecomsolvebd/next v1.0.2+) নিজেই প্রতিটা ইভেন্টে পাঠায় — আলাদা কোড লাগবে না। নিজের ট্র্যাকার বানালে deviceContextFromUserAgent() ব্যবহার করুন।
// Sent on EVERY event (same labels as the WooCommerce tracker)
{
"device": "Mobile", // Mobile | Tablet | Desktop | TV
"device_model": "iPhone", // "Samsung A52", "Windows NT 10.0" — omit if unknown
"os": "iOS 18.7", // "Android 14", "Windows 10/11", "macOS 14.5"
"browser": "Safari 18.7.5" // "Chrome 155.0.0.0", "Samsung Internet 25.0"
}সাইট না পাঠালে EcomSolveBD ক্রেতার user-agent থেকে নিজে ভরে দেয় (তাই proxy অবশ্যই X-Client-User-Agent পাঠাবে); সাইটের পাঠানো মান কখনো বদলানো হয় না।
ব্রাউজার থেকে কখনো পাঠাবেন না
purchase— অর্ডার যায় সার্ভার webhook দিয়ে; অ্যাডস Purchase EcomSolveBD নিজে পাঠায়।checkout_form_abandoned— সার্ভার নিজে লেখে (ফোন/ইমেইলের পর idle টাইমার)।click,scroll— সার্ভার ফেলে দেয়।
Abandoned checkout কীভাবে তৈরি হয়
- ক্রেতা চেকআউটে ফোন বা ইমেইল লিখলে phone_number_added { phone } / email_address_added { email } পাঠান — আসল মানসহ। মান ছাড়া পাঠালে টাইমার চালু হয় না।
- সার্ভার ঐ ভিজিটরের (esb_vid) জন্য idle টাইমার চালু করে (ডিফল্ট ১০ মিনিট)। এরপর যেকোনো ইভেন্ট (page_view, অন্য ফিল্ড…) এলে টাইমার আবার শুরু থেকে গোনে।
- অর্ডার webhook এলে টাইমার বাতিল হয় — কিন্তু শুধু তখনই, যখন webhook-এর trackingUserId = ব্রাউজারের esb_vid কুকি। ভুল বা খালি হলে ক্রেতা অর্ডার দিলেও ১০ মিনিট পর তাকে abandoned দেখাবে।
- অর্ডার না এলে টাইমার শেষে সার্ভার নিজে checkout_form_abandoned লেখে → Dashboard-এর Checkout drop-off / Abandon recovery-তে নাম, ফোন ও ইমেইলসহ দেখা যায়। নাম আসে first_name_added { name } (+ last_name_added { last_name }) থেকে।
- অর্ডার পাঠান চেকআউটের সেই request থেকেই (যেখানে esb_vid কুকি আছে), অথবা esb_vid অর্ডারে সেভ করে পরে পাঠান। কুকি ছাড়া queue/cron থেকে পাঠালে visitor মিলবে না।
কোড — ধাপে ধাপে (সবগুলো লাগবে)
1) ইভেন্ট হেল্পার (একবার তৈরি করুন)
সব ইভেন্টের জন্য একটাই ফাইল। tracker রেডি হওয়ার আগের কল কিউতে থাকে। CURRENCY আপনার স্টোরের কারেন্সি দিন।
lib/esb-track.ts// lib/esb-track.ts — import only from client components ("use client") // One helper for EVERY event. Calls made before EsbTracker is ready are queued. type Props = Record<string, unknown>; type EsbWindow = Window & { esbTrack?: (event: string, props?: Props) => boolean }; export type EsbItem = { item_id: string; // SAME id as product feed <id> and order item productId item_name: string; price: number; quantity?: number; item_variant?: string; item_category?: string; }; export type EsbField = | "first_name" | "last_name" | "phone" | "email" | "address" | "city" | "state" | "postal_code" | "country"; const CURRENCY = "BDT"; // your store currency (ISO 4217) const queue: Array<[string, Props | undefined]> = []; let waiting = false; function send(event: string, props?: Props) { if (typeof window === "undefined") return; const w = window as EsbWindow; if (w.esbTrack) { w.esbTrack(event, props); return; } queue.push([event, props]); if (waiting) return; waiting = true; window.addEventListener( "esb:ready", () => { while (queue.length) { const [e, p] = queue.shift()!; (window as EsbWindow).esbTrack?.(e, p); } }, { once: true }, ); } function ecommerce(items: EsbItem[], extra?: Props): Props { const clean = items.map((it) => ({ item_id: String(it.item_id), item_name: String(it.item_name), price: Number(it.price), quantity: Number(it.quantity ?? 1), ...(it.item_variant ? { item_variant: it.item_variant } : {}), ...(it.item_category ? { item_category: it.item_category } : {}), })); const value = clean.reduce((s, it) => s + it.price * it.quantity, 0); return { ecommerce: { currency: CURRENCY, value, items: clean, ...(extra ?? {}) } }; } const FIELD_EVENT: Record<EsbField, string> = { first_name: "first_name_added", last_name: "last_name_added", phone: "phone_number_added", email: "email_address_added", address: "address_added", city: "city_added", state: "state_added", postal_code: "postal_code_added", country: "country_added", }; let formStarted = false; const sentFieldValues = new Set<string>(); export const esb = { pageView: () => send("page_view"), viewItem: (item: EsbItem) => send("view_item", ecommerce([{ ...item, quantity: 1 }])), addToCart: (item: EsbItem) => send("add_to_cart", ecommerce([item])), removeFromCart: (item: EsbItem) => send("remove_from_cart", ecommerce([item])), viewCart: (items: EsbItem[]) => send("view_cart", ecommerce(items)), beginCheckout: (items: EsbItem[]) => send("begin_checkout", ecommerce(items)), addShippingInfo: (items: EsbItem[], shippingTier: string) => send("add_shipping_info", ecommerce(items, { shipping_tier: shippingTier })), addPaymentInfo: (items: EsbItem[], paymentType: string) => send("add_payment_info", ecommerce(items, { payment_type: paymentType })), checkoutFormStarted: () => { if (formStarted) return; formStarted = true; send("checkout_form_started", {}); }, /** Call on change/blur of a checkout input with its current value. */ field: (field: EsbField, value: string) => { const v = String(value ?? "").trim(); if (!v) return; const event = FIELD_EVENT[field]; const key = `${event}:${v.toLowerCase()}`; if (sentFieldValues.has(key)) return; // same value already sent sentFieldValues.add(key); esb.checkoutFormStarted(); if (field === "phone") return send(event, { phone: v }); if (field === "email") return send(event, { email: v }); if (field === "first_name") return send(event, { name: v, field_value_len: v.length }); if (field === "last_name") return send(event, { last_name: v, field_value_len: v.length }); send(event, { field_value_len: v.length }); }, checkoutFormCompleted: () => send("checkout_form_completed", {}), };2) page_view — রুট পরিবর্তন
প্রথম লোডের হিট EsbTracker নিজে পাঠায় (ভিজিটরের একদম প্রথম ভিজিটে landing_page_view, পরে page_view); SPA নেভিগেশনের জন্য এই কম্পোনেন্ট।
components/esb-page-views.tsx// components/esb-page-views.tsx — SPA route changes → page_view (EsbTracker already sent the first hit) "use client"; import { useEffect, useRef } from "react"; import { usePathname, useSearchParams } from "next/navigation"; import { esb } from "@/lib/esb-track"; export function EsbPageViews() { const pathname = usePathname(); const search = useSearchParams(); const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; // initial load → EsbTracker sent landing_page_view / page_view on mount return; } esb.pageView(); }, [pathname, search]); return null; } // app/layout.tsx — next to <EsbTracker … /> // import { Suspense } from "react"; // <Suspense fallback={null}><EsbPageViews /></Suspense> // useSearchParams needs Suspense3) view_item — প্রোডাক্ট পেজ
সার্ভার PDP থেকে প্রোডাক্ট ডাটা দিয়ে ছোট client কম্পোনেন্ট রেন্ডার করুন।
components/track-view-item.tsx// Product page — a tiny client component rendered by your (server) PDP "use client"; import { useEffect } from "react"; import { esb } from "@/lib/esb-track"; export function TrackViewItem({ id, name, price, variant }: { id: string; name: string; price: number; variant?: string; }) { useEffect(() => { esb.viewItem({ item_id: id, item_name: name, price, item_variant: variant }); }, [id, name, price, variant]); return null; } // app/product/[slug]/page.tsx (server): // <TrackViewItem id={String(product.id)} name={product.name} price={Number(product.salePrice ?? product.price)} />4) add_to_cart · remove_from_cart · view_cart
কার্ট লজিক যেখানে আছে সেখানেই কল — বাটন যত জায়গায় থাকুক (কার্ড, PDP, Buy Now) সব কভার হবে।
your cart store / handlers// Inside YOUR cart store / handlers (Zustand, Redux, Context, Server Action result, …) import { esb } from "@/lib/esb-track"; async function onAddToCart(product, qty) { await addLine(product.id, qty); // your logic esb.addToCart({ item_id: String(product.id), item_name: product.name, price: product.price, quantity: qty }); } function onRemoveLine(line, removedQty = line.qty) { removeLine(line.id); // your logic (also when qty is decreased) esb.removeFromCart({ item_id: String(line.productId), item_name: line.name, price: line.price, quantity: removedQty }); } // Cart page/drawer opened: // useEffect(() => { if (open) esb.viewCart(lines.map(toItem)); }, [open]);5) begin_checkout · add_shipping_info · add_payment_info
চেকআউট খোলা, ডেলিভারি এরিয়া/শিপিং বাছাই, পেমেন্ট মেথড বাছাই।
checkout page// Checkout page (client component) "use client"; import { useEffect } from "react"; import { esb, type EsbItem } from "@/lib/esb-track"; export function CheckoutTracking({ items }: { items: EsbItem[] }) { useEffect(() => { esb.beginCheckout(items); // once per checkout visit }, []); // eslint-disable-line react-hooks/exhaustive-deps return null; } // Shipping zone / method selected (e.g. "inside_dhaka" | "outside_dhaka"): // onChange={(z) => { setZone(z); esb.addShippingInfo(items, z); }} // Payment method selected (e.g. "cod" | "bkash"): // onChange={(m) => { setPayment(m); esb.addPaymentInfo(items, m); }}6) চেকআউট ফর্ম — ১১টা ফর্ম/ফিল্ড ইভেন্ট
checkout_form_started, প্রতিটি ফিল্ডের *_added, checkout_form_completed। সাইটে যে ইনপুট আছে তার প্রতিটিতে esb.field()।
checkout form// Checkout form — call esb.field() for EVERY input that exists (any input names work) <form onFocus={() => esb.checkoutFormStarted()} onSubmit={(e) => { esb.checkoutFormCompleted(); handleSubmit(e); }} > <input name="fullName" onBlur={(e) => esb.field("first_name", e.target.value)} /> <input name="lastName" onBlur={(e) => esb.field("last_name", e.target.value)} /> <input name="phone" type="tel" onBlur={(e) => esb.field("phone", e.target.value)} /> <input name="email" type="email" onBlur={(e) => esb.field("email", e.target.value)} /> <textarea name="address" onBlur={(e) => esb.field("address", e.target.value)} /> <select name="district" onChange={(e) => esb.field("city", e.target.value)} /> <select name="thana" onChange={(e) => esb.field("state", e.target.value)} /> <input name="postcode" onBlur={(e) => esb.field("postal_code", e.target.value)} /> <select name="country" onChange={(e) => esb.field("country", e.target.value)} /> </form> // Field → event: first_name→first_name_added · last_name→last_name_added · phone→phone_number_added // email→email_address_added · address→address_added · city (district)→city_added // state (division / thana / upazila)→state_added · postal_code→postal_code_added · country→country_added // Phone, email and name send the real value ({ phone } / { email } / { name } / { last_name }) — // required for abandoned-checkout recovery (Customer, phone, email in Checkout drop-off). // ALTERNATIVE (no per-input code): window.esbBindCheckoutForm(selector, fieldMap) // - matches inputs by their name attribute (lowercased; "billing[city]" → "city") // - passing fieldMap REPLACES the defaults — list every input: // window.esbBindCheckoutForm("form#checkout", { // fullname: "first_name_added", lastname: "last_name_added", address: "address_added", // district: "city_added", thana: "state_added", postcode: "postal_code_added", country: "country_added", // }); // phone/email are detected automatically from type/name/placeholder // - call it after the form is in the DOM AND after "esb:ready" (it returns false otherwise)7) অর্ডার — সার্ভার webhook
অর্ডার DB-তে সেভ হওয়ার পর সার্ভার থেকে। productId = item_id = ফিড id।
call site · server// Server — after YOUR order is committed (Route Handler, Server Action, worker, …) import { cookies } from "next/headers"; import { buildOrderPayload, OrderWebhookPoster } from "@ecomsolvebd/next"; const poster = new OrderWebhookPoster({ merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!, webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!, apiBase: process.env.ECOMSOLVEBD_API_BASE, }); const payload = buildOrderPayload( { orderNumber: String(order.number), // stable storefront order number currency: String(order.currency), // ISO 4217, e.g. "BDT" customer: { fullName: order.customerName, phone: order.phone, email: order.email }, items: order.lines.map((line) => ({ productId: String(line.productId), // SAME id as event item_id + feed <id> sku: line.sku, title: line.title, variantTitle: line.variantTitle, quantity: line.qty, unitPrice: String(line.unitPrice), })), district: order.district, upazila: order.upazila, address: order.address, notes: order.notes, shippingTotal: String(order.shippingTotal), discountTotal: String(order.discountTotal ?? 0), }, { cookieHeader: (await cookies()).toString() }, // forwards esb_vid → links order to the visitor ); try { await poster.postOrder(payload); } catch (err) { console.error("[ecomsolvebd] postOrder failed", err); // never fail the shopper's checkout } // Ingest only — ads Purchase is sent later from EcomSolveBD (confirm / courier).
সব কল-সাইট একনজরে
import { esb } from "@/lib/esb-track"; // client components only
// Map YOUR models → { item_id, item_name, price, quantity }.
const toItem = (p, qty = 1) => ({
item_id: String(p.id), // same id as feed <id>
item_name: p.name,
price: Number(p.price), // the price the buyer sees (sale price if any)
quantity: qty,
item_variant: p.variantName, // optional
});
// 1) Product page — once when the product is shown
esb.viewItem(toItem(product));
// 2) Add to cart — after the add succeeds (also on "Buy now")
esb.addToCart(toItem(product, qty));
// 3) Remove from cart / qty decreased — removed quantity only
esb.removeFromCart(toItem(line.product, removedQty));
// 4) Cart page or cart drawer opened
esb.viewCart(cart.lines.map((l) => toItem(l.product, l.qty)));
// 5) Checkout page/step opened (once)
esb.beginCheckout(cart.lines.map((l) => toItem(l.product, l.qty)));
// 6) Delivery area / shipping method chosen (e.g. "inside_dhaka")
esb.addShippingInfo(cartItems, shippingZone);
// 7) Payment method chosen (e.g. "cod", "bkash")
esb.addPaymentInfo(cartItems, paymentMethod);
// 8) Checkout form — every input that exists, on change/blur:
// <input name="name" onBlur={(e) => esb.field("first_name", e.target.value)} />
// <input name="lastName" onBlur={(e) => esb.field("last_name", e.target.value)} />
// <input name="phone" onBlur={(e) => esb.field("phone", e.target.value)} />
// <input name="email" onBlur={(e) => esb.field("email", e.target.value)} />
// <textarea name="address" onBlur={(e) => esb.field("address", e.target.value)} />
// <select name="district" onChange={(e) => esb.field("city", e.target.value)} />
// <select name="thana" onChange={(e) => esb.field("state", e.target.value)} />
// <input name="zip" onBlur={(e) => esb.field("postal_code", e.target.value)} />
// <select name="country" onChange={(e) => esb.field("country", e.target.value)} />
// (field() also fires checkout_form_started on the first filled input;
// call esb.checkoutFormStarted() on the form's first focus as well.)
// 9) Place order clicked (form submit) — before/while you call your order API
esb.checkoutFormCompleted();
// The order itself goes to EcomSolveBD from your SERVER (order webhook) — not from here.SDK রেফারেন্স · বাধ্যতামূলক
প্রোডাক্ট ফিড — সেটআপ
EcomSolveBD-এর প্রোডাক্ট লিস্ট, প্রফিট/কস্ট, ইনবক্স প্রোডাক্ট কার্ড ও অ্যাড ক্যাটালগ এই ফিড থেকেই আসে — এটা ঐচ্ছিক নয়।
- চারটা পাবলিক GET URL (লগইন/মিডলওয়্যার/লোকেল রিডাইরেক্ট ছাড়া): /feed/products.xml (EcomSolveBD), /feed/facebook.xml, /feed/tiktok.xml, /feed/google.xml।
- সব active/published প্রোডাক্ট আসবে — খালি লিস্ট বা placeholder ডাটা নয়। ভ্যারিয়েশন থাকলে প্রতিটি ভ্যারিয়েন্ট আলাদা row (id = parentId-variantId)।
- id = ইভেন্টের item_id = অর্ডারের productId। price = ক্রেতা যে দাম দেয় (সেল প্রাইস থাকলে সেটা)।
- ইমেজ ও লিংক সম্পূর্ণ https URL (relative path নয়)। Meta/TikTok/Google ফিডে ইমেজ নেই বা price ≤ 0 হলে সেই প্রোডাক্ট বাদ দিন।
- XML escape করুন (& < > " ')। description-এ HTML নয়, plain text।
- কস্ট (buying_cost ইত্যাদি) ফিডে দেবেন না — EcomSolveBD-তে সেট করা কস্ট সিঙ্কে মুছে যায় না।
1) Feed route (একটা ফাইল, চারটা URL)
ফোল্ডারের নাম [channel] — [channel].xml নয়। /feed/* পাবলিক রাখুন (middleware/auth/i18n বাদ)।
app/feed/[channel]/route.ts// app/feed/[channel]/route.ts (src/app/feed/[channel]/route.ts with a src/ dir) // One file serves all four URLs: /feed/products.xml · /feed/facebook.xml · /feed/tiktok.xml · /feed/google.xml // The [channel] segment receives "products.xml" etc.; the package strips ".xml". // Do NOT name the folder "[channel].xml" and no next.config rewrite is needed. import { createFeedRouteHandlers } from "@ecomsolvebd/next/route-handlers"; import { productFeedProvider } from "@/lib/esb-product-feed"; export const dynamic = "force-dynamic"; // always read the live catalog export const { GET } = createFeedRouteHandlers({ storeOrigin: process.env.NEXT_PUBLIC_STORE_URL!, // https://your-store.example currency: process.env.ECOMSOLVEBD_FEED_CURRENCY || "BDT", storeName: process.env.ECOMSOLVEBD_FEED_STORE_NAME || "My Shop", provider: productFeedProvider, }); // middleware.ts / proxy.ts: make sure /feed/* is NOT behind auth, i18n or locale redirects // (e.g. add "feed" to the matcher's excluded paths).2) ProductFeedProvider — আসল ক্যাটালগ থেকে
db.product… লাইনটা আপনার ডাটা সোর্সে বদলান; বাকি ম্যাপিং রাখুন। খালি array রেখে দেবেন না।
lib/esb-product-feed.ts// lib/esb-product-feed.ts — read YOUR real catalog (Prisma, Drizzle, CMS, REST API, …) import type { FeedCatalogProduct, FeedChannelItem, ProductFeedProvider } from "@ecomsolvebd/next"; const ORIGIN = process.env.NEXT_PUBLIC_STORE_URL!.replace(/\/$/, ""); const CURRENCY = process.env.ECOMSOLVEBD_FEED_CURRENCY || "BDT"; const BRAND = process.env.ECOMSOLVEBD_FEED_STORE_NAME || "My Shop"; const abs = (u?: string | null) => (!u ? "" : /^https?:\/\//i.test(u) ? u : ORIGIN + "/" + u.replace(/^\//, "")); const plain = (html?: string | null) => String(html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); /** One row per sellable unit. Expand variants here (id = `${productId}-${variantId}`). */ async function loadRows() { // ↓ replace with your query — ONLY active/published products, ALL of them (no paging limit) const products = await db.product.findMany({ where: { isActive: true }, include: { category: true, images: true, variants: true }, }); return products.flatMap((p) => { const base = { title: p.name, description: plain(p.description) || p.name, category: p.category?.name ?? "", image: abs(p.images[0]?.url ?? p.thumbnail), url: `${ORIGIN}/product/${p.slug}`, // your real PDP URL pattern }; if (p.variants.length === 0) { return [{ ...base, id: String(p.id), sku: p.sku ?? String(p.id), price: Number(p.salePrice ?? p.price), stock: p.stock }]; } return p.variants.map((v) => ({ ...base, id: `${p.id}-${v.id}`, title: `${p.name} — ${v.name}`, sku: v.sku ?? `${p.id}-${v.id}`, image: abs(v.image) || base.image, price: Number(v.salePrice ?? v.price), stock: v.stock, })); }); } export const productFeedProvider: ProductFeedProvider = { // → /feed/products.xml (EcomSolveBD catalog). Include every row, even out of stock. async catalogProducts(): Promise<FeedCatalogProduct[]> { const rows = await loadRows(); return rows.map((r) => ({ id: r.id, // = event item_id = order productId title: r.title, price: r.price.toFixed(2), currency: CURRENCY, category: r.category, image: r.image, stockStatus: r.stock === null || r.stock > 0 ? "in_stock" : "out_of_stock", stockQty: r.stock === null ? "unlimited" : String(Math.max(0, r.stock)), productUrl: r.url, sku: r.sku, })); }, // → facebook.xml / tiktok.xml / google.xml. The package does NOT filter — skip bad rows here. async channelItems(): Promise<FeedChannelItem[]> { const rows = await loadRows(); return rows .filter((r) => r.price > 0 && /^https:\/\//i.test(r.image)) .map((r) => { const inStock = r.stock === null || r.stock > 0; return { id: r.id, skuId: r.sku, title: r.title.slice(0, 150), description: r.description.slice(0, 5000), availabilityMeta: inStock ? "in stock" : "out of stock", availabilityGoogle: inStock ? "in_stock" : "out_of_stock", price: `${r.price.toFixed(2)} ${CURRENCY}`, link: r.url, imageLink: r.image, brand: BRAND, productType: r.category || "General", }; }); }, };3) যাচাই + ড্যাশবোর্ডে বসানো
ডিপ্লয়ের পর URL খুলে দেখুন, তারপর Dashboard → Products → Feed URL → Save → Fetch now।
verify# Replace with your public store origin curl -sI https://your-store.example/feed/products.xml | head -n 5 # HTTP 200 + application/xml curl -s https://your-store.example/feed/products.xml | head -n 40 # real <product> rows curl -s https://your-store.example/feed/facebook.xml | head -n 30 curl -s https://your-store.example/feed/google.xml | head -n 30 # Then in EcomSolveBD: # Dashboard → Products → Feed URL = https://your-store.example/feed/products.xml # → Save → Fetch now → products appear in the list (auto re-fetch interval on the same card) # Ads catalogs: # Meta Commerce Manager → Data sources → Scheduled feed → /feed/facebook.xml # TikTok Ads → Catalog → Data feed → /feed/tiktok.xml # Google Merchant Center → Products → Feeds → Scheduled fetch → /feed/google.xml
সঠিক আউটপুট — /feed/products.xml
<?xml version="1.0" encoding="UTF-8"?>
<products generated_at="2026-10-23T10:00:00Z" currency="BDT">
<product>
<id>1042</id> <!-- required · same as event item_id + order productId -->
<title>Cotton Panjabi — Navy</title> <!-- required -->
<price>1450.00</price> <!-- required · selling price, number only -->
<currency>BDT</currency>
<category>Panjabi</category>
<image>https://shop.example/img/1042.jpg</image> <!-- absolute https -->
<sku>PJ-NAVY-42</sku>
<product_url>https://shop.example/product/cotton-panjabi-navy</product_url>
<stock>in_stock</stock> <!-- in_stock | out_of_stock -->
<stock_qty>25</stock_qty> <!-- integer, or "unlimited"; 0 = out of stock -->
</product>
</products>সঠিক আউটপুট — facebook/tiktok/google
<?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:g="http://base.google.com/ns/1.0"> <channel> <title>My Shop Product Feed</title> <link>https://shop.example/</link> <description>Product Feed</description> <item> <g:id>1042</g:id> <g:title>Cotton Panjabi — Navy</g:title> <g:description>Plain-text description (no HTML)</g:description> <g:link>https://shop.example/product/cotton-panjabi-navy</g:link> <g:image_link>https://shop.example/img/1042.jpg</g:image_link> <g:brand>My Shop</g:brand> <g:condition>new</g:condition> <g:availability>in stock</g:availability> <!-- google.xml: in_stock / out_of_stock --> <g:price>1450.00 BDT</g:price> <g:product_type>Men > Panjabi</g:product_type> <!-- google.xml only: <g:identifier_exists>no</g:identifier_exists> when no GTIN/MPN --> <!-- tiktok.xml only: <sku_id>PJ-NAVY-42</sku_id> --> </item> </channel> </rss>
শেষ চেকলিস্ট — ডেভেলপার / AI এজেন্টের জন্য
ডেভেলপার বা AI এজেন্টকে এই চেকলিস্টটা হুবহু দিন — শেষে প্রতিটি ইভেন্ট কোন ফাইলে ফায়ার হচ্ছে তার রিপোর্ট চাইবে।
EcomSolveBD × Next.js — implementation checklist (do ALL items; do not skip silently)
1. Inventory the site first: product page, quick view, cart page/drawer, checkout page,
shipping selector, payment selector, and EVERY checkout form input (name attribute + meaning).
2. Install the base (env, tracker, collect/attribution proxies, order webhook, order-status) per this page.
3. Create the event helper from this page and fire EVERY event in the events table at its UI moment:
(landing_page_view = tracker/bootstrap sends it once on the visitor's first-ever load — do not fire it yourself)
page_view, view_item, add_to_cart, remove_from_cart, view_cart, begin_checkout,
add_shipping_info, add_payment_info, checkout_form_started, first_name_added, last_name_added,
phone_number_added, email_address_added, address_added, city_added, state_added,
postal_code_added, country_added, checkout_form_completed.
If the site has no such UI/field, skip that event and write a code comment explaining why.
4. Use ONE product id everywhere: event item_id = feed <id> = order item productId.
Every event also carries device, device_model, os, browser (Device & technology section) —
the tracker/bootstrap adds them; never strip them from the collect body.
5. Product feed: Add app/feed/[channel]/route.ts + the next.config rewrite + a real ProductFeedProvider (no empty arrays, no placeholders) so /feed/products.xml, /feed/facebook.xml, /feed/tiktok.xml and /feed/google.xml list every active product.
6. Order: post the order from the SERVER after it is saved (never a browser "purchase").
trackingUserId MUST be the browser's esb_vid cookie (read it in the checkout request, or store it
on the order) — otherwise buyers who ordered still appear as abandoned checkouts.
phone_number_added / email_address_added must carry the real { phone } / { email } value,
first_name_added the real { name } and last_name_added the real { last_name } (Customer column).
Names, phones and emails stay in EcomSolveBD — they are not forwarded to GA4 / Meta / TikTok.
7. Verify (report results for each):
- Open the site, view a product, add/remove from cart, open cart, open checkout, fill every field,
place a test order.
- Dashboard → Events / Realtime: every event above appears with correct item_id, price, quantity.
- Dashboard → Orders: the test order is there with the same productId.
- curl -s https://<store>/feed/products.xml | head -40 → HTTP 200, real products, absolute https images.
- Dashboard → Products → Feed URL = https://<store>/feed/products.xml → Save → Fetch now → products listed.
8. Final report: a table "event → file:line where it fires" + list of skipped events with reasons.সাধারণ প্রশ্ন
- কোডিং জানি না — পারব?
- হ্যাঁ। ধাপ ১–৩ আপনি। ধাপ ৪ ডেভেলপার।
- প্রোডাক্ট EcomSolveBD থেকে এডিট?
- না। প্রোডাক্ট Next.js/API-তেই।
- অর্ডার কীভাবে আসে?
- চেকআউট সফলে সার্ভার webhook — ব্রাউজার purchase নয়। Dashboard Create Order (v1.0.1+) Next.js অ্যাডমিনেও যায়।
- Secret হারিয়ে গেলে?
- Website → Custom / Headless → Next.js (open to manage) → আবার Connect, নতুন Secret।
সমস্যা হলে
page_view / ecommerce আসছে না
Collect proxy + EsbTracker। বাকি সব ইভেন্ট: lib/esb-track.ts-এর esb.*() কল প্রতিটি UI মুহূর্তে।
tracker-config CORS লাল
Collect same-origin proxy দিয়ে চলে; tracker-config API Origin reflect করে (SaaS)। Hard-refresh করে আবার দেখুন।
অর্ডার আসছে না
Connect Next.js আগে। Secret মিলিয়ে postOrder() লগ চেক করুন।
স্ট্যাটাস সিঙ্ক নয়
order-status route + HTTPS base URL + order_number মিলছে কিনা।
· বাংলাদেশ সময়