Delivering Sitecore XM Content to a React + Vite Frontend via Experience Edge
If you’re running Sitecore Experience Manager (XM) on-premise and want a modern, framework-agnostic frontend, this is your stack: publish to Sitecore Experience Edge for XM, then pull content into a React + Vite SPA via GraphQL. No SitecoreAI (formerly XM Cloud) required.
A note on naming: Sitecore retired the XM Cloud brand at Symposium 2025. The SaaS platform is now called SitecoreAI. Throughout this post, any reference to the cloud-managed SaaS offering uses SitecoreAI. The on-premise product covered here remains Sitecore XM.
Contents
- 1 What This Architecture Actually Is
- 2 Prerequisites
- 3 Part 1: Sitecore CM Setup
- 4 Part 2: Understand the GraphQL Schema
- 5 Part 3: Why Vite — and What It Unlocks
- 6 Part 4: Scaffold the React + Vite App
- 7 Part 5: Build the Component Rendering Layer
- 8 Part 6: Wire Up Routing
- 9 Part 7: Querying Additional Content
- 10 Part 8: Local Development Workflow
- 11 Key Limitations
- 12 Summary
What This Architecture Actually Is
Before touching any code, understand what you’re wiring together.
Experience Edge for XM acts as a publishing target for your Sitecore content and media, and provides a GraphQL API that lets you traverse the content tree by ID or path, obtain a snapshot of the Layout Service output for a route for use with headless SDKs, and perform Boolean queries for items based on field values and other properties.
Your Sitecore CM server stays on-premise. The Connector connects a publishing target to the Sitecore-hosted Experience Edge Delivery Platform and publishes data including individual content items and their fields, a snapshot of the Layout Service output for any items with layout, and media library assets.
Your React + Vite app lives completely outside Sitecore. It fetches layout and content from Edge’s GraphQL endpoint and renders everything client-side. Sitecore Experience Edge is compatible with Sitecore XM and Sitecore XP 10.1 and higher.
Prerequisites
Before you begin, confirm you have:
- Sitecore XM 10.1 or later installed and running
- Sitecore Headless Services module installed on your CM role
- An active Sitecore Experience Edge subscription (licensed separately — contact your Sitecore account manager)
- Your Edge credentials:
client_id,client_secret,audience,authority_url,delivery_endpoint,cdn_uri,media_prefix - Node.js 18+ on your frontend dev machine
Part 1: Sitecore CM Setup
Step 1: Install the Experience Edge Connector
Go to the Downloads page for Sitecore Headless Rendering and choose the link for the package compatible with your version of XP.
In the Sitecore Launchpad, open Control Panel ? Administration ? Install a package. Upload the Experience Edge Connector .zip and install it. When the wizard completes, check Restart the Sitecore Client and Restart the Sitecore Server before closing.
Step 2: Add the Connection String
Open App_Config/ConnectionStrings.config on your CM server and add:
<add name="experienceedge"
connectionString="url=AUTHORITY_URL_HERE;
client_id=CLIENT_ID_HERE;
client_secret=CLIENT_SECRET_HERE;
audience=AUDIENCE_HERE;
delivery_endpoint=DELIVERY_ENDPOINT_HERE;
cdn_uri=CDN_URI_HERE;
media_prefix=MEDIA_PREFIX_HERE" />
You receive all of these values from Sitecore as part of your Edge subscription.
Step 3: Patch the Connector Config
Create a patch file at App_Config/Include/zzz/ExperienceEdge.Settings.config:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<settings>
<setting name="ExperienceEdge.DeliveryEndpoint"
patch:attribute="value"
value="https://edge.sitecorecloud.io/api/graphql/v1" />
<setting name="ExperienceEdge.CDN.Uri"
patch:attribute="value"
value="https://saas-cdn.marketingcontenthub.cloud" />
<setting name="ExperienceEdge.CDN.MediaPrefix"
patch:attribute="value"
value="your-tenant/media" />
</settings>
</sitecore>
</configuration>
Replace the values with those from your subscription.
Step 4: Create the Publishing Target
In Content Editor, navigate to /sitecore/system/Publishing targets. Create a new item (for example, Edge). Set Target database to experienceedge. This connects the publish UI to the Edge target used by the connector.
Check the Publish to Experience Edge checkbox on the new item, then restart your Sitecore instance.
Step 5: Publish and Verify
In the Sitecore Content Editor, trigger a Smart Publish targeting your Edge publishing target. To verify, go to https://edge.sitecorecloud.io/api/graphql/ide, obtain a token using your client_id and client_secret, and run a basic layout query against your home route. A response with a populated rendered field confirms it’s working.
Part 2: Understand the GraphQL Schema
Experience Edge exposes three primary query entry points you’ll use from React:
item— query an item by path or IDlayout— query an item by site and route path, returning its Layout Service JSON snapshotsearch— construct a Boolean field search query for finding items by field value or common properties
The layout query is the most important for page rendering. It returns the full Layout Service snapshot describing which components sit in which placeholders for a given route:
query LayoutQuery($site: String!, $routePath: String!, $language: String!) {
layout(site: $site, routePath: $routePath, language: $language) {
item {
rendered
}
}
}
rendered is a JSON blob — the complete Layout Service output — containing a sitecore.route object with placeholder definitions. Each placeholder holds an array of component renderings, each with componentName and fields.
For local development before publishing to Edge, the Sitecore Headless Services module includes a GraphQL endpoint that mirrors the schema and behavior of Experience Edge, enabling preview, editing, and local development without publishing to the live tenant. That endpoint lives at https://your-cm-host/sitecore/api/graph/edge.
Part 3: Why Vite — and What It Unlocks
This is worth pausing on, because the choice of Vite over something like Create React App or a Next.js setup is not cosmetic. It has meaningful implications for how you build, where you deploy, and what infrastructure you need.

What Vite Actually Does
Vite is a build tool and dev server, not a framework. In development, it serves your source files directly to the browser using native ES modules — there’s no bundle compilation step on every save. Hot module replacement (HMR) is near-instant because Vite only reprocesses the module that changed, not the entire dependency graph.
For production, Vite runs Rollup under the hood. The output of npm run build is a flat directory of static files: an index.html, a handful of chunked JavaScript files (named with content hashes for cache-busting), and your CSS and assets. That’s it. No Node.js process. No server runtime. No framework-specific adapter.
This matters for the Sitecore XM use case specifically because your content is already handled by Experience Edge. The frontend has nothing dynamic to compute at request time — it just needs to fetch from the Edge GraphQL API and render. A server runtime would be pure overhead.
The Hosting Freedom Vite Creates
Because npm run build produces static files, your hosting options are dramatically simpler than anything requiring a Node server:
CDN / Object Storage — The most cost-effective option for most teams. Deploy to AWS S3 + CloudFront, Azure Blob Storage + Azure CDN, or Google Cloud Storage + Cloud CDN. You’re paying for storage and egress, not compute. Configure a single page app redirect so all routes return index.html and React Router handles the path. No servers to patch, scale, or monitor.
Azure Static Web Apps — Microsoft’s purpose-built host for SPAs and static sites. Supports custom routing rules, built-in CI/CD via GitHub Actions, free SSL, and a global CDN. For Sitecore shops already in the Microsoft ecosystem, this is a natural fit. Free tier covers most internal and mid-traffic production sites.
Netlify / Vercel — Both support static deployments without a server function. Deploy by connecting your Git repo; every push to main triggers a build and atomic deployment. Preview deployments per pull request are built in. Worth noting that Vercel’s value-adds (edge functions, SSR) are irrelevant here — you’re using the static layer only, and it’s free up to generous traffic limits.
Nginx on a VM or bare metal — Drop the contents of dist/ into the web root, add a try_files $uri /index.html; rule in your site config, and you’re done. No Node, no npm, no runtime dependency. The binary that serves your Sitecore XM frontend is the same one that serves static files everywhere else in your stack. This is particularly relevant for organizations with existing on-premise infrastructure that can’t or won’t use cloud hosting — running Vite’s output on an internal Nginx server is no different from serving any other website.
IIS on Windows Server — For organizations running Sitecore on Windows infrastructure, IIS can serve the static output directly. Add a URL Rewrite rule to redirect all non-file requests to index.html. No .NET runtime needed, no application pool to manage — IIS in static file mode.
Docker — The build output is so small (typically under 2MB before assets) that packaging it in an nginx:alpine Docker image is trivial. A Dockerfile is essentially three lines: copy the dist/ output into the Nginx web root, add your SPA routing config, done. This makes the frontend deployable anywhere containers run — Kubernetes, AKS, ECS, or your own Docker host.
The common thread across all of these: no persistent server process is required. That means no Node.js version management in production, no process managers like PM2, no memory leaks to monitor, no cold starts. The frontend scales horizontally for free because CDN edges cache static files globally by default.
What This Means for Your Sitecore XM Stack
On-premise Sitecore XM already has significant infrastructure footprint: CM server, CD server (or Edge instead), SQL Server, Solr, potentially xConnect. Adding a Node.js server to run your frontend would add another moving part to that list.
With Vite’s static output pointing at Experience Edge for content, your frontend is fully decoupled from that infrastructure. It can be deployed and updated independently, rolled back instantly by swapping a CDN deployment, and scaled without touching the Sitecore environment. If the Sitecore CM goes offline for maintenance, your published frontend keeps serving content from Edge without interruption.
The One Trade-Off
The flat static output means no server-side rendering (SSR) and no server-side data fetching at request time. Every page load triggers a client-side GraphQL call to Experience Edge before content appears. For content-heavy public sites where SEO is critical, this is a meaningful limitation — crawlers will see the loading state before JavaScript executes.
If SSR matters for your project, the correct answer is not to bolt a Node server onto Vite — it’s to use a framework that has SSR built in, like Next.js (which is what the JSS Next.js SDK was designed for). For internal tools, authenticated portals, or sites where SEO is secondary to delivery simplicity and infrastructure cost, the Vite SPA approach is the right call.
Part 4: Scaffold the React + Vite App
Step 6: Create the Project
npm create vite@latest my-sitecore-app -- --template react-ts
cd my-sitecore-app
npm install
Install the only Sitecore-specific dependency you need — the JSS React field helpers for rendering Sitecore field types (Text, Image, RichText, Link). These are not the full JSS SDK; they’re lightweight rendering utilities:
npm install @sitecore-jss/sitecore-jss-react
npm install graphql-request
Step 7: Set Up Environment Variables
Create a .env file in the project root:
VITE_EDGE_ENDPOINT=https://edge.sitecorecloud.io/api/graphql/v1
VITE_EDGE_API_KEY=your-delivery-api-key
VITE_SITE_NAME=your-site-name
For local development against your CM preview endpoint:
VITE_EDGE_ENDPOINT=https://your-cm-host/sitecore/api/graph/edge
VITE_EDGE_API_KEY=your-jss-api-key
VITE_SITE_NAME=your-site-name
Vite exposes these to the browser as import.meta.env.VITE_*. The Edge Delivery API key is read-only and designed for client consumption. Never expose your OAuth client_secret in frontend env vars — that belongs server-side only.
Step 8: Create the GraphQL Client
Create src/lib/edgeClient.ts:
import { GraphQLClient } from 'graphql-request';
export const edgeClient = new GraphQLClient(
import.meta.env.VITE_EDGE_ENDPOINT,
{
headers: {
'sc-apikey': import.meta.env.VITE_EDGE_API_KEY,
},
}
);
Step 9: Create the Layout Query Hook
Create src/hooks/useSitecoreLayout.ts:
import { useState, useEffect } from 'react';
import { edgeClient } from '../lib/edgeClient';
const LAYOUT_QUERY = `
query LayoutQuery($site: String!, $routePath: String!, $language: String!) {
layout(site: $site, routePath: $routePath, language: $language) {
item {
rendered
}
}
}
`;
export function useSitecoreLayout(routePath: string, language = 'en') {
const [layoutData, setLayoutData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
edgeClient
.request(LAYOUT_QUERY, {
site: import.meta.env.VITE_SITE_NAME,
routePath,
language,
})
.then((data: any) => {
setLayoutData(data?.layout?.item?.rendered ?? null);
setLoading(false);
})
.catch((err: Error) => {
setError(err);
setLoading(false);
});
}, [routePath, language]);
return { layoutData, loading, error };
}
Part 5: Build the Component Rendering Layer
Step 10: Create Your Components
Create src/components/ContentBlock.tsx:
import { Text, RichText } from '@sitecore-jss/sitecore-jss-react';
interface ContentBlockProps {
fields: {
heading: { value: string };
content: { value: string };
};
}
export function ContentBlock({ fields }: ContentBlockProps) {
return (
<div className="content-block">
<Text tag="h2" field={fields.heading} />
<RichText field={fields.content} />
</div>
);
}
Step 11: Create the Component Factory
Create src/componentFactory.ts:
import { ContentBlock } from './components/ContentBlock';
import { HeroBanner } from './components/HeroBanner';
import { NavigationBar } from './components/NavigationBar';
const components: Record<string, React.ComponentType<any>> = {
ContentBlock,
HeroBanner,
NavigationBar,
};
export function componentFactory(componentName: string) {
return components[componentName] ?? null;
}
The key in this map must exactly match the rendering item name in Sitecore, which is what Experience Edge returns in the componentName field.
Step 12: Create the Placeholder Renderer
Create src/components/SitecorePlaceholder.tsx:
import { componentFactory } from '../componentFactory';
interface Rendering {
componentName: string;
fields: Record<string, any>;
placeholders?: Record<string, Rendering[]>;
uid: string;
}
interface SitecorePlaceholderProps {
name: string;
renderings: Rendering[];
}
export function SitecorePlaceholder({ renderings }: SitecorePlaceholderProps) {
return (
<>
{renderings.map((rendering) => {
const Component = componentFactory(rendering.componentName);
if (!Component) {
console.warn(`No component registered for: ${rendering.componentName}`);
return null;
}
return (
<Component
key={rendering.uid}
fields={rendering.fields}
rendering={rendering}
placeholders={rendering.placeholders}
/>
);
})}
</>
);
}
Step 13: Create the Route Handler
Create src/components/SitecoreRoute.tsx:
import { SitecorePlaceholder } from './SitecorePlaceholder';
interface SitecoreRouteProps {
layoutData: any;
}
export function SitecoreRoute({ layoutData }: SitecoreRouteProps) {
const route = layoutData?.sitecore?.route;
if (!route) return <div>Page not found</div>;
return (
<main>
<SitecorePlaceholder
name="jss-main"
renderings={route.placeholders?.['jss-main'] ?? []}
/>
</main>
);
}
Part 6: Wire Up Routing
Step 14: Install React Router
npm install react-router-dom
Step 15: Create the Page Component
Create src/pages/SitecorePage.tsx:
import { useParams } from 'react-router-dom';
import { useSitecoreLayout } from '../hooks/useSitecoreLayout';
import { SitecoreRoute } from '../components/SitecoreRoute';
export function SitecorePage() {
const { '*': routePath = '/' } = useParams();
const { layoutData, loading, error } = useSitecoreLayout(`/${routePath}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!layoutData) return <div>Page not found</div>;
return <SitecoreRoute layoutData={layoutData} />;
}
Step 16: Set Up the App Router
Update src/App.tsx:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { SitecorePage } from './pages/SitecorePage';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/*" element={<SitecorePage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Step 17: Configure Vite
Update vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api/graphql': {
target: 'https://edge.sitecorecloud.io',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/graphql/, '/api/graphql/v1'),
},
},
},
});
For any host serving the production build, configure a catch-all fallback to index.html.
For Nginx:
location / {
try_files $uri /index.html;
}
For IIS, add a URL Rewrite rule pointing all non-file requests to index.html. For S3 + CloudFront, set the error document to index.html with a 200 response code.
Part 7: Querying Additional Content
Step 18: Fetch Specific Items
When a component needs data beyond the layout snapshot — navigation menus, footers, or data-driven lists — query Edge directly. Create src/hooks/useSitecoreItem.ts:
import { useState, useEffect } from 'react';
import { edgeClient } from '../lib/edgeClient';
const ITEM_QUERY = `
query ItemQuery($path: String!, $language: String!) {
item(path: $path, language: $language) {
id
name
fields {
name
value
jsonValue
}
}
}
`;
export function useSitecoreItem(path: string, language = 'en') {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
edgeClient
.request(ITEM_QUERY, { path, language })
.then((res: any) => {
setData(res?.item ?? null);
setLoading(false);
})
.catch(() => setLoading(false));
}, [path, language]);
return { data, loading };
}
Use strongly-typed template inline fragments where you know the template structure:
query NavQuery($path: String!, $language: String!) {
item(path: $path, language: $language) {
... on NavigationRoot {
navigationTitle { value }
}
children {
results {
... on NavigationItem {
link { jsonValue }
label { value }
}
}
}
}
}
GraphQL types are only created for templates that follow the Helix content structure, meaning types are generated only for templates under Foundation, Feature, or Project paths. If your templates don’t follow Helix, use the generic field(name: "fieldname") { value } fallback.
Part 8: Local Development Workflow
Step 19: Use the Preview Endpoint
You should not publish to the live Edge tenant on every content change during development. The Sitecore Headless Services module includes a GraphQL endpoint that mirrors the schema and behavior of Experience Edge. Sitecore recommends using this endpoint in headless development even if you’re not yet using Experience Edge, to enable future compatibility.
Point a .env.local file at your CM preview endpoint:
VITE_EDGE_ENDPOINT=https://your-cm.dev.local/sitecore/api/graph/edge
VITE_EDGE_API_KEY=your-jss-api-key-from-sitecore
VITE_SITE_NAME=your-site-name
Then run:
npm run dev
Your app is live at http://localhost:5173, pulling layout data from your local CM and rendering in React. When you’re ready to test against real Edge, switch back to the production .env values.
Key Limitations
No Experience Editor. Authors manage content in the Sitecore Content Editor and preview via the Headless Preview endpoint. Visual in-context editing is not available in this architecture.
No server-side personalization at request time. The XM on-premise personalization engine runs at publish time and is baked into the Edge snapshot. Dynamic, per-visitor personalization requires the SitecoreAI cloud stack.
Default device layer only. Layout data in Experience Edge only supports the Default device layer in item presentation. Delivering content based on different devices such as mobile is now a front-end responsibility.
Content security is your responsibility. Experience Edge for XM does not enforce security constraints on Sitecore content. You must apply publishing restrictions to avoid publishing content that you do not want to be publicly accessible.
SEO requires additional strategy. Because this is a client-side SPA, crawlers see a loading shell before JavaScript executes. Pre-rendering tools (like vite-plugin-ssg or a separate static build step) or a CDN-level caching proxy can mitigate this if SEO is critical.
Summary
The complete request flow once deployed:
- User navigates to
/about - The CDN/static host returns
index.html(the SPA shell) - React Router matches the route,
SitecorePagemounts useSitecoreLayoutfires alayoutGraphQL query to Experience Edge- Edge returns the Layout Service snapshot in
rendered SitecoreRoutereadssitecore.route.placeholders["jss-main"]SitecorePlaceholderresolves eachcomponentNamevia the component factory and renders the matched React component with itsfields
The Sitecore CM is entirely out of the request path. Content is served from the globally distributed Edge CDN. The frontend runs as static files on whatever host you choose — a CDN bucket, a VM running Nginx, IIS, or a Docker container — with no Node.js runtime dependency in production.
