Sitecore Marketplace - Game Changer In Sitecore AI Ecosystem
Hello everyone, in this blog we will explore how the Sitecore Marketplace has transformed the Sitecore AI ecosystem. We will also look at how to get started with development and build a practical application that will be hosted on Vercel. So, let’s get started.
If you’ve ever seen a content author navigate through multiple levels of the Sitecore AI content tree just to find a page they edited yesterday, you know how frustrating the experience can be. Similarly, when Sitecore developers work on stories and need to test their changes end-to-end, they often have to manually keep track of all the pages where a particular component has been added.
SC Bookmarks is a Sitecore Marketplace custom app that gives Page Builder authors the ability to pin frequently edited pages, see recently visited pages, and search through their bookmarks — all from a sidebar panel that lives right next to the canvas. No more drilling through the content tree.
In this post, I'll walk you through every step of building this app: from understanding how Marketplace apps work, to writing the code, to deploying on Vercel. If you're new to the Sitecore Marketplace, this is your end-to-end guide.
What We're Building
SC Bookmarks is a Pages Context Panel app — it appears as a sidebar to the left of the Page Builder canvas. Here's what it does? and how it will look when it is completed.
- Pin pages — Click "Pin This Page" to bookmark whatever page you're currently editing
- Recently visited — Automatically tracks pages you navigate to (in-memory, resets on reload)
- Search & filter — Find bookmarks quickly with debounced search
- One-click navigation — Click any bookmark to jump straight to that page in Page Builder
The tech stack:
| Layer | Technology |
|---|---|
| Framework | Next.js 15+ (App Router, TypeScript) |
| Hosting | Vercel |
| Persistence | Vercel KV (Redis) |
| SDK | @sitecore-marketplace-sdk/client + xmc |
| Styling | Tailwind CSS + Sitecore Blok design tokens |
| Icons | Lucide React |
How Sitecore Marketplace Apps Work
Before jumping into code, let's understand the architecture. This was the part I found most confusing when I started, so I want to be clear about it.
A Marketplace app is just a web application — built with any framework you like — that Sitecore loads inside a sandboxed iframe. Your app and Sitecore communicate through the Marketplace SDK, which is essentially a PostMessage API with a nice developer experience inspired by React Query and GraphQL.
Sitecore AI Page Builder
└── Pages Context Panel (iframe)
└── SC Bookmarks Next.js App
├── SDK Layer (useMarketplaceClient hook)
├── UI Layer (React components)
└── API Layer (/api/bookmarks → Vercel KV)
There are several extension points where your app can appear:
| Extension Point | Where It Shows Up |
|---|---|
| Pages Context Panel | Sidebar in Page Builder (what we're building) |
| Dashboard Widget | Tile on the Sitecore AI dashboard |
| Custom Field | Dialog for custom field types |
| Fullscreen | Full-page experience inside Pages |
| Standalone | Opens in its own browser tab from Cloud Portal |
For SC Bookmarks, the Pages Context Panel is the natural choice — we need live page context as the author navigates the content tree. Click on below highlighted jissaw icon and select your application.
Prerequisites
Before we start coding, make sure you have:
- Access to the Sitecore Cloud Portal with developer permissions
- Node.js 16+ and npm 10+
- A Vercel account (free tier works)
- A GitHub repository for deployment
- Basic familiarity with Next.js and TypeScript
Step 1: Register Your App in Cloud Portal
You need to register your app in the Sitecore Cloud Portal before you start coding, because the registration process gives you configuration values the SDK needs.
- Log into the Sitecore Cloud Portal
- Navigate to Developer Settings → Marketplace Apps
- Click "Create App" and choose Custom App
- Fill in the basics: app name ("SC Bookmarks"), description, and your localhost URL (
http://localhost:3000) for development - Select Pages Context Panel as your extension point
- Set the extension route to
/pages-contextpanel-extension - Provide a logo for your app.
- Activate the app.
- Now install your app in which project you want.
http://localhost:3000. You'll update this to your Vercel URL later when you deploy. The extension route (/pages-contextpanel-extension) must match the route in your Next.js app exactly what I have done which you can see in screenshot. I will also share the how to set this up from sratch as reference below as don't want to make this blog too long.
Step 2: Scaffold the Next.js Project
Let's create the project using the Next.js App Router:
npx create-next-app@latest sc-bookmarks --typescript --tailwind --app --src-dir
cd sc-bookmarks
Now install the Sitecore Marketplace SDK and other dependencies:
npm install @sitecore-marketplace-sdk/client @sitecore-marketplace-sdk/xmc
npm install @vercel/kv
npm install lucide-react
npm install clsx tailwind-merge
My project structure look like this. There might be additional files in my github repo, which will need some cleanup that I will do it later.
sc-bookmarks/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── global.css
│ │ ├── pages-contextpanel-extension/
│ │ │ └── page.tsx ← Main extension point
│ │ └── api/
│ │ └── bookmarks/
│ │ └── route.ts ← REST API for bookmarks
│ ├── components/ ← All UI components
│ ├── lib/
│ │ ├── types.ts ← TypeScript interfaces
│ │ ├── storage.ts ← Vercel KV helpers
│ │ ├── utils.ts ← cn() helper
│ │ └── constants.ts ← App constants
│ ├── types/
│ │ └── env.d.ts ← Environment type declarations
│ └── utils/
│ └── hooks/
│ ├── useMarketplaceClient.ts ← SDK initialization
│ ├── useBookmarks.ts ← Bookmark CRUD
│ └── useRecentPages.ts ← Recent pages tracking
├── next.config.ts
├── tailwind.config.js
└── package.json
Note: You can clone the Sitecore Marketplace app starter repository for quick spinning up instead of manually doing the above basic setup. Link
Step 3: Define Your Types
I always start with types. When the entire team (or your future self) can look at one file and understand the data shape, everything downstream gets easier. Create src/lib/types.ts:
Note: I will not add whole code as I will share github repo. For now, I will just add few example
// src/lib/types.ts
export interface Bookmark {
/** Sitecore item ID */
id: string;
/** Page display name */
name: string;
/** Content tree path */
path: string;
/** Language version */
language: string;
/** ISO timestamp when bookmarked */
pinnedAt: string;
/** Display order (lower = higher) */
sortOrder: number;
}
Also declare your environment variables in src/types/env.d.ts so TypeScript catches missing config early:
// src/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_APP_URL: string;
}
}
Step 4: The SDK Hook — Connecting to Sitecore
This is the heart of every Marketplace app. The useMarketplaceClient hook initializes the SDK, establishes the PostMessage channel with the parent Sitecore iframe, and gives you access to queries and mutations.
Now let's see what key queries are available in our page.tsx file:
client.query("application.context")— Fires once, returns your app's identity (app ID, org info, resource access). You need this to build unique storage keys per user/org.client.query("pages.context", { subscribe: true, ... })— Subscribes to live page changes. Every time the author clicks a different page in the content tree, youronSuccesscallback fires with the new page info (ID, name, language, presentation details).client.query("host.user")— Get logged in user information along with tenant name, organization name etc.
Step 5: The Storage Layer — Vercel KV
We need persistence for bookmarks — when an author pins a page, it should survive page reloads, browser restarts, and even device switches. Vercel KV (powered by Upstash Redis) is perfect for this: serverless, fast, and trivial to set up. I will share reference link how you can set up Upstash Redis quickly from Vercel dashboard in reference link.
Create the storage helpers in src/lib/storage.ts:
// src/lib/storage.ts
import { kv } from "@vercel/kv";
import type { Bookmark, BookmarkStore } from "./types";
/**
* Build a unique Redis key per user per app installation.
* Pattern: bookmarks:{appId}:{userId}
*/
export function buildKey(appId: string, userId: string): string {
return `bookmarks:${appId}:${userId}`;
}
/** Get all bookmarks for a user */
export async function getBookmarks(key: string): Promise<Bookmark[]> {
try {
/** Logic to Get Bookmarks */
} catch (err) {
console.error("KV read error:", err);
return [];
}
}
/** Save the full bookmarks array (overwrite) */
export async function saveBookmarks(
key: string,
bookmarks: Bookmark[]
): Promise<boolean> {
try {
/** Logic to Save Bookmarks */
} catch (err) {
console.error("KV write error:", err);
return false;
}
}
/** Add a bookmark (prevents duplicates by ID) */
export async function addBookmark(
key: string,
bookmark: Bookmark
): Promise<Bookmark[]> {
/** Logic to Add Bookmarks */
return updated;
}
/** Remove a bookmark by Sitecore item ID */
export async function removeBookmark(
key: string,
itemId: string
): Promise<Bookmark[]> {
/** Logic to Remove Bookmarks */
return updated;
}
/** Reorder bookmarks (receives array of IDs in new order) */
export async function reorderBookmarks(
key: string,
orderedIds: string[]
): Promise<Bookmark[]> {
/** Logic to Reorder Bookmarks */
return reordered;
}
The key pattern bookmarks:{appId}:{userId} ensures complete isolation — each user in each organization has their own bookmark list. The appId comes from application.context and the userId from the host user's identity.
Step 6: The API Route
Next.js App Router makes API routes clean. Create src/app/api/bookmarks/route.ts:
// src/app/api/bookmarks/route.ts
import { NextRequest, NextResponse } from "next/server";
import {
getBookmarks,
addBookmark,
removeBookmark,
reorderBookmarks,
buildKey,
} from "@/lib/storage";
import type { ApiResponse, Bookmark } from "@/lib/types";
function jsonResponse<T>(data: ApiResponse<T>, status = 200) {
return NextResponse.json(data, { status });
}
/** GET /api/bookmarks?appId=xxx&userId=yyy */
export async function GET(req: NextRequest) {
/** Get API Call logic */
}
/** POST /api/bookmarks — Add a bookmark */
export async function POST(req: NextRequest) {
/** POST API Call logic */
}
/** DELETE /api/bookmarks — Remove a bookmark */
export async function DELETE(req: NextRequest) {
/** Delete API Call logic */
}
/** PATCH /api/bookmarks — Reorder bookmarks */
export async function PATCH(req: NextRequest) {
/** Reorder API Call logic */
}
Every endpoint follows the same pattern: validate input, build the Redis key, call the storage helper, return a consistent { success, data, error } shape. This makes the client-side hooks straightforward to write.
Step 7: Client-Side Hooks
useBookmarks — CRUD Operations
This hook wraps the API calls and manages local state. Create src/utils/hooks/useBookmarks.ts:
// src/utils/hooks/useBookmarks.ts
"use client";
import { useState, useEffect, useCallback } from "react";
import type { Bookmark, ApiResponse } from "@/lib/types";
const API_BASE = "/api/bookmarks";
export function useBookmarks(appId: string | null, userId: string | null) {
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch bookmarks on mount (and when identity changes)
const fetchBookmarks = useCallback(async () => {
/* Call our next js route /api/bookmarks to fetch the data */
}, [appId, userId]);
useEffect(() => {
fetchBookmarks();
}, [fetchBookmarks]);
// Pin a page
const addBookmark = useCallback(
async (bookmark: Omit<Bookmark, "sortOrder">) => {
if (!appId || !userId) return;
/* Call our next js route /api/bookmarks to post the data */
},
[appId, userId]
);
// Unpin a page
const removeBookmark = useCallback(
async (itemId: string) => {
if (!appId || !userId) return;
/* Call our next js route /api/bookmarks to remove bookmark the data */
},
[appId, userId]
);
// Check if a page is already pinned
const isPinned = useCallback(
(itemId: string) => bookmarks.some((b) => b.id === itemId),
[bookmarks]
);
return {
bookmarks,
isLoading,
error,
addBookmark,
removeBookmark,
isPinned,
refresh: fetchBookmarks,
};
}
useRecentPages — In-Memory Tracking
Recent pages are intentionally not persisted. They reset on page reload — this is by design, since the "recent" list should reflect the current editing session, not historical visits. Create src/utils/hooks/useRecentPages.ts:
// src/utils/hooks/useRecentPages.ts
"use client";
import { useState, useCallback } from "react";
import type { RecentPage } from "@/lib/types";
import { MAX_RECENT_PAGES } from "@/lib/constants";
export function useRecentPages() {
const [recentPages, setRecentPages] = useState<RecentPage[]>([]);
const trackPage = useCallback(
(page: Omit<RecentPage, "visitedAt">) => {
/* logic for recent pages list */
},
[]
);
return { recentPages, trackPage };
}
And the constant in src/lib/constants.ts:
// src/lib/constants.ts
export const MAX_RECENT_PAGES = 10;
export const SEARCH_DEBOUNCE_MS = 300;
Step 8: Build the UI Components
The context panel is narrow — roughly 300-350px. Every component needs to respect this constraint. I use compact spacing (8/12/16px rhythm) and smaller font sizes (13-14px body, 12px secondary).
Utility Helper
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
PinButton Component
The Pin button sits at the top of the panel. It reads the current page from the SDK and toggles the bookmark state:
// src/components/PinButton.tsx
"use client";
import { Pin, PinOff } from "lucide-react";
import { cn } from "@/lib/utils";
interface PinButtonProps {
isPinned: boolean;
isLoading: boolean;
currentPageName: string | null;
onPin: () => void;
onUnpin: () => void;
}
export function PinButton({
isPinned,
isLoading,
currentPageName,
onPin,
onUnpin,
}: PinButtonProps) {
return (
/* html structure for pin with click handle logic */
);
}
SearchFilter Component
// src/components/SearchFilter.tsx
"use client";
import { useState, useEffect } from "react";
import { Search, X } from "lucide-react";
import { SEARCH_DEBOUNCE_MS } from "@/lib/constants";
interface SearchFilterProps {
onSearch: (query: string) => void;
}
export function SearchFilter({ onSearch }: SearchFilterProps) {
const [query, setQuery] = useState("");
useEffect(() => {
const timer = setTimeout(() => onSearch(query), SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
}, [query, onSearch]);
return (
/* html structure for search box and click handle logic */
);
}
BookmarkItem Component
// src/components/BookmarkItem.tsx
"use client";
import { Trash2, FileText } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Bookmark } from "@/lib/types";
interface BookmarkItemProps {
bookmark: Bookmark;
isCurrentPage: boolean;
onRemove: (id: string) => void;
onClick: (bookmark: Bookmark) => void;
}
export function BookmarkItem({
bookmark,
isCurrentPage,
onRemove,
onClick,
}: BookmarkItemProps) {
return (
/* Logic for html structure for bookmark item */
);
}
BookmarkList Component
// src/components/BookmarkList.tsx
"use client";
import type { Bookmark } from "@/lib/types";
import { BookmarkItem } from "./BookmarkItem";
import { EmptyState } from "./EmptyState";
interface BookmarkListProps {
bookmarks: Bookmark[];
currentPageId: string | null;
onRemove: (id: string) => void;
onClick: (bookmark: Bookmark) => void;
searchQuery: string;
}
export function BookmarkList({
bookmarks,
currentPageId,
onRemove,
onClick,
searchQuery,
}: BookmarkListProps) {
return(
/* logic and html structure for BookMark List */
);
}
EmptyState and LoadingSpinner
// src/components/EmptyState.tsx
import { Bookmark } from "lucide-react";
export function EmptyState({ message }: { message: string }) {
return (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Bookmark className="w-8 h-8 text-gray-300 mb-2" />
<p className="text-sm text-gray-500">{message}</p>
</div>
);
}
// src/components/LoadingSpinner.tsx
export function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-indigo-600
border-t-transparent rounded-full animate-spin" />
</div>
);
}
RecentPages Component
// src/components/RecentPages.tsx
"use client";
import { Clock, FileText } from "lucide-react";
import type { RecentPage, Bookmark } from "@/lib/types";
import { EmptyState } from "./EmptyState";
interface RecentPagesProps {
pages: RecentPage[];
onClick: (page: RecentPage) => void;
}
export function RecentPages({ pages, onClick }: RecentPagesProps) {
if (pages.length === 0) {
return <EmptyState message="Navigate to pages to see them here" />;
}
return (
/* Recent Pages Html logic */
);
}
BookmarkPanel — The Main Container
This orchestrates everything together:
// src/components/BookmarkPanel.tsx
"use client";
export function BookmarkPanel() {
return (
/* The main Html Contaniner for our whole market place app */
);
}
Step 9: The Extension Point Page
This is the route that Sitecore loads in its iframe. Create src/app/pages-contextpanel-extension/page.tsx:
// src/app/pages-contextpanel-extension/page.tsx
"use client";
import { BookmarkPanel } from "@/components/BookmarkPanel";
export default function PagesContextPanelExtension() {
return (
<main className="h-screen overflow-hidden bg-white">
<BookmarkPanel />
</main>
);
}
That's it — the page is just a wrapper. All the logic lives in the components and hooks. The "use client" directive is required because we're using React hooks and the browser's PostMessage API.
Step 10: Configure CSP Headers for Iframe Embedding
This is the step people forget, and then spend an hour wondering why their app shows a blank white panel in Sitecore. Your app loads inside an iframe, and the browser will block it unless you explicitly allow Sitecore's domains to frame your app.
Update next.config.ts:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Content-Security-Policy",
value: [
"frame-ancestors",
"https://*.sitecorecloud.io",
"https://*.sitecorecloud.app",
"https://*.sitecore-staging.cloud",
].join(" "),
},
],
},
];
},
};
export default nextConfig;
Also create vercel.json to ensure the same headers apply on Vercel's edge (Next.js config alone isn't always enough for all deployment modes):
// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "frame-ancestors https://*.sitecorecloud.io https://*.sitecorecloud.app https://*.sitecore-staging.cloud"
}
]
}
]
}
next.config.ts and vercel.json have matching frame-ancestors values.
Step 11: Set Up Vercel KV
Vercel's original KV product has been replaced by Vercel Marketplace Storage, which provisions Upstash Redis under the hood. Here's how to set it up:
- Go to your Vercel project dashboard
- Navigate to Storage tab
- Click "Connect Store" → choose Upstash Redis (KV)
- Select a region close to your users and create
- Vercel auto-injects
KV_URL,KV_REST_API_URL,KV_REST_API_TOKEN, andKV_REST_API_READ_ONLY_TOKENas environment variables
For local development, pull these variables down:
vercel env pull .env.local
Step 12: Test Locally
Run the development server:
npm run dev
A few things to understand about local testing:
- The UI will render at
http://localhost:3000/pages-contextpanel-extension, but SDK queries will fail because you're not inside the Sitecore AI iframe. You'll see the error state — this is expected and means your error handling works. - The API routes (
/api/bookmarks) work normally with Vercel KV as long as you've pulled your.env.local. - To test the full flow, you need to open it inside Sitecore AI (next section).
Step 13: Deploy to Vercel
Push your code to GitHub and deploy:
# Initialize git and push
git init
git add .
git commit -m "SC Bookmarks — initial build"
git remote add origin https://github.com/yourusername/sc-bookmarks.git
git push -u origin main
In the Vercel dashboard:
- Click "Add New" → "Project"
- Import your GitHub repo
- Vercel auto-detects Next.js — framework preset is set automatically
- The KV environment variables are already linked if you connected storage earlier
- Add
NEXT_PUBLIC_APP_URLwith your Vercel deployment URL - Click Deploy
After deployment, copy your production URL (e.g., https://{0}.vercel.app).
Note: I have used Vercel cli to deploy my project to Vercel. I will share how you can do that in reference blog link
Step 14: Update Cloud Portal & Install
Go back to the Sitecore Cloud Portal and update your app configuration:
- Replace the
localhost:3000URL with your Vercel production URL - Set the app logo URL (serve it from your app's
/publicfolder, e.g.,https://sc-bookmarks.vercel.app/icon.svg) - Activate the app (toggle in the top-right corner)
- Go to "My Apps" — your app should show as "TO BE INSTALLED"
- Click Install
Now open Sitecore AI Page Builder, click the jigsaw/extensions icon in the sidebar, and your SC Bookmarks panel will appear with live SDK data.
Debugging Tips
A few things I learned the hard way while building this:
Console logs go in the Sitecore tab, not localhost. When your app is running inside Sitecore AI, open DevTools in the browser tab where Sitecore is loaded. The iframe's console output shows up there, not in a separate localhost tab.
The SDK handshake can fail silently on first load. The iframe is still warming up when the SDK tries to connect. That's why the retry logic (3 attempts, 1 second apart) in the hook is essential — it covers the postMessage timing gap.
Presentation details is a JSON string. The pagesContext.pageInfo.presentationDetails value comes back as a raw string. Parse it with JSON.parse(presentationDetails || '{}') before using it.
You cannot navigate the content tree from the iframe. Your app is sandboxed. You can read page context and make SDK mutations (like navigating to a page by ID), but you cannot reach into the Page Builder DOM.
The Sitecore Marketplace is still new, and the ecosystem is growing fast. If you've been working with Sitecore and wondering how to get started with custom apps, I hope this walkthrough gives you a clear path. The concepts are straightforward once you internalize the core mental model: it's an iframe with a message channel, and the SDK makes that channel pleasant to use.
The full source code for SC Bookmarks is available on my GitHub. If you build something with the Marketplace, I'd love to see it — drop me a message or leave a comment below.
Happy building!
You can check my other blogs too if interested. Blog Website
References:










Comments
Post a Comment