> ## Content Index
> Fetch the complete content index at: https://www.gowthamaraja.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Building Production-Ready Sitecore Marketplace Apps with Next.js
- URL: https://www.gowthamaraja.com/building-production-ready-sitecore-marketplace-apps-with-next-js/
- Published: 2026-02-23T04:30:00.000Z
- Updated: 2026-03-06T15:28:55.000Z
- Author: Gowtham
- Tags: Sitecore Marketplace, Next.js, Sitecore, SitecoreAI

In [Part 1](https://www.gowthamaraja.com/sitecore-marketplace-getting-started-nextjs), we built our first Marketplace app and got it running in XM Cloud. Now it's time to take things to the next level—building production-ready apps that actually solve real problems.

We'll explore all four extension types, integrate with XM Cloud APIs, and write code that you can confidently deploy to production.

## Table of Contents

1. [Recap: Where We Left Off](#recap-where-we-left-off)
2. [Understanding Extension Points in Depth](#understanding-extension-points-in-depth)
3. [Building a Standalone Application](#building-a-standalone-application)
4. [Creating Dashboard Widgets](#creating-dashboard-widgets)
5. [Building Custom Field Extensions](#building-custom-field-extensions)
6. [Working with XM Cloud APIs](#working-with-xm-cloud-apis)
7. [Fetching Real Content Data](#fetching-real-content-data)
8. [Handling API Errors Gracefully](#handling-api-errors-gracefully)
9. [State Management Best Practices](#state-management-best-practices)
10. [What's Next?](#whats-next)

---

## Recap: Where We Left Off

In Part 1, we created a basic analytics dashboard that:

- Initialized the Marketplace SDK
- Fetched application context
- Displayed mock statistics
- Ran successfully in XM Cloud Portal

Now we'll enhance this foundation with real functionality and explore other extension types.

---

## Understanding Extension Points in Depth

Sitecore Marketplace offers four extension points. Each serves different use cases and user workflows.

![Sitecore Extension Points](https://storage.ghost.io/c/01/c5/01c5f86b-1a7c-400e-8513-2371e48d18f0/content/images/2026/02/extension-points-overview.webp)

### 1\. Standalone Application

**Best For:**

- Admin dashboards and reporting tools
- Bulk content operations
- Configuration interfaces
- Complex workflows that need dedicated space

**User Access:** Via "My Apps" in the main navigation

**Screen Real Estate:** Full page within the portal

### 2\. Full-Screen Experience

**Best For:**

- Content migration tools
- Immersive workflows
- Multi-step wizards
- Tasks requiring maximum focus

**User Access:** Takes over the entire portal interface

**Screen Real Estate:** Complete viewport

### 3\. Dashboard Widgets

**Best For:**

- At-a-glance metrics
- Quick status indicators
- Action buttons for common tasks
- Real-time notifications

**User Access:** Site dashboard cards

**Screen Real Estate:** Compact cards (typically 300-400px wide)

### 4\. Page Builder Extensions

**Custom Fields:**

- New field types (color pickers, icon selectors, etc.)
- Enhanced input controls
- Validation and formatting

**Context Panels:**

- Content suggestions
- SEO analysis
- AI-powered recommendations
- Workflow helpers

**User Access:** Content editor sidebar or field locations

**Screen Real Estate:** Varies by type

---

## Building a Standalone Application

Let's enhance our analytics dashboard from Part 1 with real functionality.

### Advanced Features

We'll add:

- Real-time data updates
- Multiple chart types
- Date range filtering
- Export functionality

### Enhanced Dashboard Component

Update `src/app/standalone/page.tsx`:

```typescript
"use client";

import { useMarketplaceClient } from "@/hooks/useMarketplaceClient";
import { ApplicationContext } from "@sitecore-marketplace-sdk/client";
import { useEffect, useState } from "react";
import { initializeXMC } from "@/lib/xmcClient";

interface ContentStats {
  totalItems: number;
  publishedToday: number;
  draftItems: number;
  scheduledPublishing: number;
  lastUpdated: Date;
}

interface ActivityItem {
  id: string;
  action: string;
  user: string;
  timestamp: Date;
}

export default function AnalyticsDashboard() {
  const { client, error, isInitialized, isLoading } = useMarketplaceClient();
  const [appContext, setAppContext] = useState<ApplicationContext | null>(null);
  const [stats, setStats] = useState<ContentStats | null>(null);
  const [activity, setActivity] = useState<ActivityItem[]>([]);
  const [refreshing, setRefreshing] = useState(false);

  // Fetch application context
  useEffect(() => {
    if (!isInitialized || !client) return;

    async function fetchContext() {
      try {
        const response = await client.query("application.context");
        setAppContext(response.data);
      } catch (err) {
        console.error("Failed to fetch context:", err);
      }
    }

    fetchContext();
  }, [client, isInitialized]);

  // Fetch content statistics
  useEffect(() => {
    if (!appContext || !client) return;

    fetchStats();

    // Auto-refresh every 30 seconds
    const interval = setInterval(fetchStats, 30000);
    return () => clearInterval(interval);
  }, [appContext, client]);

  async function fetchStats() {
    if (!client) return;
    
    setRefreshing(true);
    
    try {
      const xmc = await initializeXMC(client);

      // GraphQL query for content statistics
      const query = `
        query GetContentStats {
          search(
            where: {
              AND: [
                { name: "_path", value: "/sitecore/content/", operator: CONTAINS }
              ]
            }
            first: 1000
          ) {
            total
            results {
              id
              name
              updated
              published: field(name: "__Published") {
                value
              }
            }
          }
        }
      `;

      const response = await xmc.graphQL.query({ query });
      const items = response.data.search.results;
      
      // Calculate statistics
      const today = new Date();
      today.setHours(0, 0, 0, 0);

      const publishedToday = items.filter((item: any) => {
        const updateDate = new Date(item.updated);
        return updateDate >= today;
      }).length;

      setStats({
        totalItems: response.data.search.total,
        publishedToday,
        draftItems: items.filter((i: any) => !i.published?.value).length,
        scheduledPublishing: 0, // Would need workflow API
        lastUpdated: new Date(),
      });

      // Fetch recent activity
      const recentItems = items
        .slice(0, 5)
        .map((item: any) => ({
          id: item.id,
          action: "Updated",
          user: "Content Editor",
          timestamp: new Date(item.updated),
        }));

      setActivity(recentItems);

    } catch (err) {
      console.error("Failed to fetch stats:", err);
      // Fallback to mock data
      setStats({
        totalItems: 1247,
        publishedToday: 23,
        draftItems: 156,
        scheduledPublishing: 8,
        lastUpdated: new Date(),
      });
    } finally {
      setRefreshing(false);
    }
  }

  if (isLoading) {
    return <LoadingScreen />;
  }

  if (error) {
    return <ErrorScreen error={error} />;
  }

  return (
    <div className="min-h-screen bg-gray-50 p-6">
      <div className="max-w-7xl mx-auto">
        {/* Header with refresh */}
        <div className="flex items-center justify-between mb-8">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">
              Content Analytics
            </h1>
            <p className="text-gray-600 mt-1">
              {appContext?.organization?.name || "Your Organization"}
            </p>
          </div>
          
          <button
            onClick={fetchStats}
            disabled={refreshing}
            className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
          >
            <RefreshIcon className={refreshing ? "animate-spin" : ""} />
            Refresh
          </button>
        </div>

        {/* Stats Grid */}
        {stats && (
          <>
            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
              <StatCard
                title="Total Items"
                value={stats.totalItems.toLocaleString()}
                icon="📊"
                trend="+12%"
                trendUp={true}
              />
              <StatCard
                title="Published Today"
                value={stats.publishedToday.toString()}
                icon="✅"
                trend="+5"
                trendUp={true}
              />
              <StatCard
                title="Draft Items"
                value={stats.draftItems.toString()}
                icon="📝"
                trend="-3"
                trendUp={false}
              />
              <StatCard
                title="Scheduled"
                value={stats.scheduledPublishing.toString()}
                icon="⏰"
                trend="→"
                trendUp={null}
              />
            </div>

            <p className="text-sm text-gray-500 mb-8">
              Last updated: {stats.lastUpdated.toLocaleTimeString()}
            </p>
          </>
        )}

        {/* Recent Activity */}
        <div className="bg-white rounded-lg shadow">
          <div className="px-6 py-4 border-b border-gray-200">
            <h2 className="text-xl font-semibold text-gray-900">
              Recent Activity
            </h2>
          </div>
          <div className="divide-y divide-gray-200">
            {activity.map((item) => (
              <div key={item.id} className="px-6 py-4 flex items-center justify-between">
                <div>
                  <p className="font-medium text-gray-900">{item.action}</p>
                  <p className="text-sm text-gray-500">by {item.user}</p>
                </div>
                <p className="text-sm text-gray-500">
                  {formatTimeAgo(item.timestamp)}
                </p>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// Helper components
function StatCard({ title, value, icon, trend, trendUp }: any) {
  const trendColor = trendUp === true ? "text-green-600" : 
                     trendUp === false ? "text-red-600" : "text-gray-600";

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex items-center justify-between mb-4">
        <span className="text-3xl">{icon}</span>
        <span className={`text-sm font-medium ${trendColor}`}>
          {trend}
        </span>
      </div>
      <h3 className="text-gray-500 text-sm font-medium mb-2">{title}</h3>
      <p className="text-3xl font-bold text-gray-900">{value}</p>
    </div>
  );
}

function RefreshIcon({ className }: { className?: string }) {
  return (
    <svg className={`w-5 h-5 ${className}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
    </svg>
  );
}

function formatTimeAgo(date: Date): string {
  const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
  
  if (seconds < 60) return "just now";
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  return `${Math.floor(seconds / 86400)}d ago`;
}

function LoadingScreen() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-50">
      <div className="text-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
        <p className="mt-4 text-gray-600">Loading dashboard...</p>
      </div>
    </div>
  );
}

function ErrorScreen({ error }: { error: Error }) {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-50">
      <div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
        <h2 className="text-red-800 font-semibold text-lg mb-2">
          ⚠️ Error Loading Dashboard
        </h2>
        <p className="text-red-600 text-sm">{error.message}</p>
      </div>
    </div>
  );
}

```

---

## Creating Dashboard Widgets

Dashboard widgets are compact, focused components perfect for at-a-glance information.

### Widget Design Principles

1. **Keep it focused**: One primary metric or action
2. **Make it actionable**: Include a CTA button
3. **Update in real-time**: Show live data when possible
4. **Be responsive**: Work on different screen sizes

### Building a Content Approval Widget

Create `src/app/dashboard-widget/page.tsx`:

```typescript
"use client";

import { useMarketplaceClient } from "@/hooks/useMarketplaceClient";
import { useEffect, useState } from "react";

interface ApprovalStats {
  pending: number;
  approved: number;
  rejected: number;
}

export default function ApprovalWidget() {
  const { client, isInitialized } = useMarketplaceClient();
  const [stats, setStats] = useState<ApprovalStats>({
    pending: 0,
    approved: 0,
    rejected: 0,
  });

  useEffect(() => {
    if (!isInitialized || !client) return;

    async function fetchApprovals() {
      // In production, fetch from workflow API
      setStats({
        pending: 8,
        approved: 45,
        rejected: 2,
      });
    }

    fetchApprovals();
    
    // Refresh every minute
    const interval = setInterval(fetchApprovals, 60000);
    return () => clearInterval(interval);
  }, [client, isInitialized]);

  return (
    <div className="p-4 bg-white rounded-lg h-full">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-semibold text-gray-900">
          Content Approvals
        </h3>
        <span className="text-2xl">📋</span>
      </div>
      
      <div className="space-y-3">
        <ApprovalRow
          label="Pending Review"
          count={stats.pending}
          color="orange"
          pulse={stats.pending > 0}
        />
        <ApprovalRow
          label="Approved Today"
          count={stats.approved}
          color="green"
        />
        <ApprovalRow
          label="Rejected"
          count={stats.rejected}
          color="red"
        />
      </div>
      
      <button className="mt-4 w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
        View All Workflows
      </button>
    </div>
  );
}

function ApprovalRow({ label, count, color, pulse }: any) {
  const colors = {
    orange: "bg-orange-100 text-orange-700",
    green: "bg-green-100 text-green-700",
    red: "bg-red-100 text-red-700",
  };

  return (
    <div className="flex items-center justify-between py-2">
      <span className="text-sm text-gray-600">{label}</span>
      <span className={`
        px-3 py-1 rounded-full text-sm font-semibold
        ${colors[color]}
        ${pulse ? 'animate-pulse' : ''}
      `}>
        {count}
      </span>
    </div>
  );
}

```

**Register this widget:**

- Extension point: Dashboard widgets
- Deployment URL: `http://localhost:3000/dashboard-widget`

---

## Building Custom Field Extensions

Custom fields let you add new input types to the content editor.

### Building a Color Picker Field

Create `src/app/custom-field/page.tsx`:

```typescript
"use client";

import { useMarketplaceClient } from "@/hooks/useMarketplaceClient";
import { useEffect, useState } from "react";

const PRESET_COLORS = [
  { name: "Primary Blue", value: "#3B82F6" },
  { name: "Success Green", value: "#10B981" },
  { name: "Warning Orange", value: "#F59E0B" },
  { name: "Danger Red", value: "#EF4444" },
  { name: "Purple", value: "#8B5CF6" },
  { name: "Pink", value: "#EC4899" },
  { name: "Teal", value: "#14B8A6" },
  { name: "Gray", value: "#6B7280" },
];

export default function ColorPickerField() {
  const { client, isInitialized } = useMarketplaceClient();
  const [selectedColor, setSelectedColor] = useState("#3B82F6");
  const [customColor, setCustomColor] = useState("#3B82F6");

  useEffect(() => {
    if (!isInitialized || !client) return;

    async function initField() {
      try {
        // Get current field value
        const context = await client.query("field.context");
        if (context.data?.value) {
          setSelectedColor(context.data.value);
          setCustomColor(context.data.value);
        }
      } catch (err) {
        console.error("Failed to get field context:", err);
      }
    }

    initField();
  }, [client, isInitialized]);

  const updateColor = async (color: string) => {
    setSelectedColor(color);
    setCustomColor(color);

    if (!client) return;

    try {
      // Update field value in Sitecore
      await client.query("field.setValue", { value: color });
    } catch (err) {
      console.error("Failed to update field:", err);
    }
  };

  return (
    <div className="p-4 max-w-md">
      {/* Color Preview */}
      <div className="mb-6">
        <div
          className="w-full h-24 rounded-lg border-2 border-gray-200 shadow-sm"
          style={{ backgroundColor: selectedColor }}
        />
        <p className="text-center mt-3 text-lg font-mono font-semibold text-gray-700">
          {selectedColor}
        </p>
      </div>

      {/* Preset Colors */}
      <div className="mb-6">
        <label className="block text-sm font-medium text-gray-700 mb-3">
          Preset Colors
        </label>
        <div className="grid grid-cols-4 gap-3">
          {PRESET_COLORS.map((color) => (
            <button
              key={color.value}
              onClick={() => updateColor(color.value)}
              className={`
                h-12 rounded-lg transition-all
                ${selectedColor === color.value 
                  ? 'ring-2 ring-blue-500 ring-offset-2 scale-110' 
                  : 'hover:scale-105'}
              `}
              style={{ backgroundColor: color.value }}
              title={color.name}
            />
          ))}
        </div>
      </div>

      {/* Custom Color Input */}
      <div>
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Custom Color
        </label>
        <div className="flex gap-3">
          <input
            type="color"
            value={customColor}
            onChange={(e) => setCustomColor(e.target.value)}
            className="h-12 w-20 rounded-lg cursor-pointer"
          />
          <button
            onClick={() => updateColor(customColor)}
            className="flex-1 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 font-medium"
          >
            Apply Custom Color
          </button>
        </div>
      </div>
    </div>
  );
}

```

**To use this field:**

1. Enable "Custom Field" extension point
2. In Sitecore, add field to content template
3. Set field type to "Marketplace Type → Plugin"
4. Select your app from dropdown

---

## Working with XM Cloud APIs

The XMC package gives you access to powerful content operations.

### Initializing the XMC Client

Create `src/lib/xmcClient.ts`:

```typescript
import { XMC } from "@sitecore-marketplace-sdk/xmc";
import { ClientSDK } from "@sitecore-marketplace-sdk/client";

let xmcInstance: XMC | null = null;

export async function initializeXMC(marketplaceClient: ClientSDK): Promise<XMC> {
  if (xmcInstance) return xmcInstance;

  xmcInstance = new XMC({ client: marketplaceClient });
  await xmcInstance.init();
  
  console.log("✅ XM Cloud API client initialized");
  return xmcInstance;
}

export function getXMC(): XMC {
  if (!xmcInstance) {
    throw new Error("XMC not initialized");
  }
  return xmcInstance;
}

```

### Common API Operations

```typescript
// Fetch content items
const query = `
  query GetItems {
    item(path: "/sitecore/content/home") {
      id
      name
      children {
        results {
          id
          name
          path
        }
      }
    }
  }
`;

const response = await xmc.graphQL.query({ query });

// Create a new item
const createMutation = `
  mutation CreateItem($input: CreateItemInput!) {
    createItem(input: $input) {
      id
      name
    }
  }
`;

await xmc.graphQL.query({
  query: createMutation,
  variables: {
    input: {
      name: "New Page",
      templateId: "template-id-here",
      parent: "parent-item-id"
    }
  }
});

```

---

## Fetching Real Content Data

Let's implement a practical content browser component.

```typescript
interface ContentItem {
  id: string;
  name: string;
  path: string;
  template: string;
  hasChildren: boolean;
}

export function ContentBrowser() {
  const { client } = useMarketplaceClient();
  const [items, setItems] = useState<ContentItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [currentPath, setCurrentPath] = useState("/sitecore/content");

  async function loadItems(path: string) {
    if (!client) return;
    
    setLoading(true);
    
    try {
      const xmc = await initializeXMC(client);
      
      const query = `
        query GetChildren($path: String!) {
          item(path: $path) {
            id
            name
            children {
              results {
                id
                name
                path
                template {
                  name
                }
                hasChildren
              }
            }
          }
        }
      `;

      const response = await xmc.graphQL.query({
        query,
        variables: { path }
      });

      setItems(response.data.item.children.results);
      setCurrentPath(path);
    } catch (err) {
      console.error("Failed to load items:", err);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="p-6">
      <div className="mb-4">
        <p className="text-sm text-gray-600">Current: {currentPath}</p>
      </div>
      
      {loading ? (
        <div className="text-center py-8">Loading...</div>
      ) : (
        <div className="space-y-2">
          {items.map((item) => (
            <div
              key={item.id}
              onClick={() => item.hasChildren && loadItems(item.path)}
              className="p-3 bg-white rounded border hover:bg-gray-50 cursor-pointer"
            >
              <p className="font-medium">{item.name}</p>
              <p className="text-sm text-gray-500">{item.template}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

```

---

## Handling API Errors Gracefully

Production apps need robust error handling.

### Error Boundary Component

```typescript
import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="p-6 bg-red-50 border border-red-200 rounded-lg">
          <h2 className="text-lg font-semibold text-red-800 mb-2">
            Something went wrong
          </h2>
          <p className="text-red-600 text-sm">
            {this.state.error?.message}
          </p>
          <button
            onClick={() => this.setState({ hasError: false, error: null })}
            className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
          >
            Try Again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

```

### API Error Handler

```typescript
export async function handleAPICall<T>(
  apiCall: () => Promise<T>,
  fallbackValue?: T
): Promise<T> {
  try {
    return await apiCall();
  } catch (error) {
    console.error("API call failed:", error);
    
    if (fallbackValue !== undefined) {
      return fallbackValue;
    }
    
    throw error;
  }
}

// Usage
const data = await handleAPICall(
  () => xmc.graphQL.query({ query }),
  [] // fallback to empty array
);

```

---

## State Management Best Practices

For complex apps, consider using a state management library.

### Using React Context

```typescript
import { createContext, useContext, useState, ReactNode } from 'react';

interface AppState {
  stats: ContentStats | null;
  setStats: (stats: ContentStats) => void;
  refreshData: () => Promise<void>;
}

const AppContext = createContext<AppState | null>(null);

export function AppProvider({ children }: { children: ReactNode }) {
  const [stats, setStats] = useState<ContentStats | null>(null);

  const refreshData = async () => {
    // Refresh logic here
  };

  return (
    <AppContext.Provider value={{ stats, setStats, refreshData }}>
      {children}
    </AppContext.Provider>
  );
}

export function useAppState() {
  const context = useContext(AppContext);
  if (!context) throw new Error("useAppState must be used within AppProvider");
  return context;
}

```

---

## What's Next?

You now have the skills to build production-ready Marketplace apps with:

- All four extension types
- Real XM Cloud API integration
- Proper error handling
- State management

In **Part 3**, we'll cover:

- Deploying to production (Vercel, Netlify, Azure)
- Performance optimization techniques
- Security best practices
- Monitoring and analytics
- Publishing to public Marketplace

### Quick Checklist

Before deploying, make sure you have:

- Error boundaries in place
- Loading states for all async operations
- Fallback data for API failures
- Responsive design tested
- Console.log statements removed
- TypeScript errors resolved

**Next:** [Part 3 - Deploying and Optimizing Marketplace Apps →](https://www.gowthamaraja.com/sitecore-marketplace-production-apps-nextjs)

---

*Found this helpful? Follow for Part 3 where we deploy to production!*

*Have questions? Drop them in the comments below.*