Dynamic GraphQL Queries in Sitecore XM Cloud with Next.js
A practical guide to building dynamic, user-driven GraphQL queries for Sitecore XM Cloud with real-world examples from a production file manager component.
Start typing to search...
A practical guide to building dynamic, user-driven GraphQL queries for Sitecore XM Cloud with real-world examples from a production file manager component.
Static GraphQL queries work well for predictable data needs, but modern applications often require flexibility based on user interactions. When building Sitecore integrations, you’ll encounter scenarios where query parameters change based on user selections—navigating folder structures, filtering content, or loading paginated results. Hard-coding these queries for every possible path or filter combination is impractical and unmaintainable.
This guide demonstrates how to build dynamic GraphQL queries that respond to user actions in real-time. We’ll use a production file manager component as our example—a browser that lets users navigate Sitecore’s content tree, select items, and handle pagination dynamically. The patterns shown here apply to any scenario where query parameters depend on runtime conditions rather than build-time configuration.
Before building dynamic queries, ensure you have:
Required Setup:
# Environment variables (from authentication guide)
SITECORE_GRAPHQL_ENDPOINT=https://xmc-[instance].sitecorecloud.io/sitecore/api/graph/edge
SITECORE_API_KEY=your_api_key
Static Query (fixed at build time):
const query = `
query {
item(path: "/sitecore/content/Home", language: "en") {
children {
results {
name
}
}
}
}
`;
Dynamic Query (changes based on user input):
const query = `
query GetSitecoreItems($path: String!, $language: String!) {
item(path: $path, language: $language) {
children {
results {
name
}
}
}
}
`;
// Variables change based on user selection
const variables = {
path: userSelectedPath, // Changes dynamically
language: currentLanguage
};
File: src/pages/api/sitecore/graphql.ts
Purpose: Provides a server-side proxy for GraphQL requests. This keeps your API key secure (never exposed to the browser) and allows dynamic queries from the frontend without authentication complexity.
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { query, variables } = req.body;
if (!query) {
return res.status(400).json({ error: "GraphQL query is required" });
}
// Get Sitecore configuration from environment
const endpoint = process.env.SITECORE_GRAPHQL_ENDPOINT;
const apiKey = process.env.SITECORE_API_KEY;
if (!endpoint || !apiKey) {
return res.status(500).json({ error: "Sitecore not configured" });
}
// Execute GraphQL query against Sitecore
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
sc_apikey: apiKey,
},
body: JSON.stringify({
query,
variables: variables || {},
}),
});
if (!response.ok) {
throw new Error(`Sitecore API error: ${response.status}`);
}
const data = await response.json();
// Return the GraphQL response
res.status(200).json(data);
} catch (error) {
console.error("GraphQL proxy error:", error);
res.status(500).json({
error: "Failed to execute GraphQL query",
details: error instanceof Error ? error.message : "Unknown error",
});
}
}
What this does:
File: src/components/sitecore-file-manager.tsx
Purpose: A production-ready file browser that demonstrates dynamic GraphQL queries in action. Users can navigate Sitecore’s content tree, and each navigation action triggers a new GraphQL query with updated path parameters.
import { useState, useEffect } from "react";
export function SitecoreFileManager({
onSelectPath,
language = "en",
rootPath = "/sitecore/content"
}) {
// Dynamic query parameters
const [currentPath, setCurrentPath] = useState(rootPath);
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Pagination state
const [currentPage, setCurrentPage] = useState(0);
const [hasNextPage, setHasNextPage] = useState(false);
const [endCursor, setEndCursor] = useState(null);
const [totalCount, setTotalCount] = useState(0);
// UI state
const [highlightedItem, setHighlightedItem] = useState(null);
const [pathHistory, setPathHistory] = useState([rootPath]);
const ITEMS_PER_PAGE = 20;
// ... component logic
}
What this manages:
Purpose: Fetches items from Sitecore based on the current path. This function is called whenever the user navigates to a different folder, demonstrating how queries adapt to user actions.
const fetchItems = async (path, page = 0, append = false) => {
setLoading(true);
if (!append) {
setError(null);
}
try {
// Build dynamic GraphQL query with variables
const query = `
query GetSitecoreItems($path: String!, $language: String!, $first: Int!, $after: String) {
item(path: $path, language: $language) {
children(first: $first, after: $after) {
total
pageInfo {
hasNext
endCursor
}
results {
id
name
path
}
}
}
}
`;
// Variables change based on user's current location
const variables = {
path: path, // Dynamic: user's selected path
language: language, // Dynamic: current language
first: ITEMS_PER_PAGE, // Static: page size
after: append ? endCursor : null, // Dynamic: pagination cursor
};
// Call our proxy API route
const response = await fetch("/api/sitecore/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0]?.message || "GraphQL error");
}
// Process results
if (data.data?.item?.children) {
const children = data.data.item.children;
const newItems = children.results.map(item => ({
id: item.id,
name: item.name,
path: item.path || `${path}/${item.name}`,
hasChildren: true,
}));
// Append or replace items based on pagination
if (append) {
setItems(prev => [...prev, ...newItems]);
} else {
setItems(newItems);
}
// Update pagination state
setTotalCount(children.total || newItems.length);
setHasNextPage(children.pageInfo?.hasNext || false);
setEndCursor(children.pageInfo?.endCursor || null);
} else {
if (!append) {
setItems([]);
setTotalCount(0);
setHasNextPage(false);
}
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch items");
if (!append) {
setItems([]);
}
} finally {
setLoading(false);
}
};
What this does:
Purpose: Responds to user interactions by updating the current path and triggering new queries. These handlers demonstrate how user actions drive dynamic query execution.
// Trigger query when path changes
useEffect(() => {
setCurrentPage(0);
setHighlightedItem(null);
setEndCursor(null);
fetchItems(currentPath);
}, [currentPath, language]);
// Navigate into a folder
const handleNavigateInto = (item) => {
if (item.hasChildren && item.path) {
setCurrentPath(item.path); // Update path
setPathHistory(prev => [...prev, item.path]); // Track history
setHighlightedItem(null);
setCurrentPage(0);
setEndCursor(null);
// fetchItems will be called automatically by useEffect
}
};
// Navigate back to previous folder
const handleBackClick = () => {
if (pathHistory.length > 1) {
const newHistory = pathHistory.slice(0, -1);
const previousPath = newHistory[newHistory.length - 1];
setCurrentPath(previousPath); // Update path triggers new query
setPathHistory(newHistory);
setHighlightedItem(null);
setCurrentPage(0);
setEndCursor(null);
}
};
// Go to root folder
const handleGoToRoot = () => {
setCurrentPath(rootPath); // Update path triggers new query
setPathHistory([rootPath]);
setHighlightedItem(null);
setCurrentPage(0);
setEndCursor(null);
};
What this does:
Purpose: Loads more items as the user scrolls, providing a seamless browsing experience for large folders. This demonstrates dynamic pagination with cursor-based queries.
// Load more items when scrolling
const loadMoreItems = () => {
if (hasNextPage && !loading) {
const nextPage = currentPage + 1;
setCurrentPage(nextPage);
// Fetch next page with cursor (append to existing items)
fetchItems(currentPath, nextPage, true);
}
};
// Scroll event handler
<div
className="h-96 overflow-auto"
onScroll={(e) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 10;
if (isNearBottom && hasNextPage && !loading) {
loadMoreItems();
}
}}
>
{/* Items list */}
{items.map(item => (
<div key={item.id}>
{item.name}
</div>
))}
{/* Loading indicator */}
{loading && items.length > 0 && (
<div>Loading more items...</div>
)}
{/* Pagination info */}
{totalCount > 0 && (
<div>
Showing {items.length} of {totalCount} items
{hasNextPage && " (scroll down for more)"}
</div>
)}
</div>
What this does:
Purpose: Handles item selection and generates Sitecore-compatible internal link format. This shows how to work with Sitecore GUIDs and create proper link markup.
import { formatGuidWithHyphens } from "@/lib/guid-utils";
const handleSelectItem = () => {
if (highlightedItem) {
const itemId = highlightedItem.id;
const displayPath = highlightedItem.path || highlightedItem.name || "";
if (itemId) {
// Format GUID for Sitecore
const formattedId = formatGuidWithHyphens(itemId);
// Create Sitecore internal link format
const internalLink = `<link text="" anchor="" linktype="internal" class="" title="" target="_blank" querystring="" id="{${formattedId}}" />`;
// Pass both display path and internal link to parent
onSelectPath(displayPath, internalLink);
} else {
// Fallback to path only
onSelectPath(displayPath);
}
}
};
// UI for selection
<div>
{/* Highlighted item display */}
{highlightedItem && (
<div className="border-t p-3 bg-muted/20">
<div className="text-xs text-muted-foreground mb-1">Selected Item:</div>
<div className="text-sm font-mono break-all">{highlightedItem.path}</div>
</div>
)}
{/* Select button */}
<Button
onClick={handleSelectItem}
disabled={!highlightedItem}
>
Insert Item
</Button>
</div>
What this does:
File: src/pages/content-browser.tsx
Purpose: Demonstrates how to integrate the file manager component into a page, showing the complete flow from user interaction to data handling.
import { useState } from "react";
import { SitecoreFileManager } from "@/components/sitecore-file-manager";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function ContentBrowser() {
const [selectedPath, setSelectedPath] = useState("");
const [internalLink, setInternalLink] = useState("");
const handleSelectPath = (displayPath, linkMarkup) => {
setSelectedPath(displayPath);
if (linkMarkup) {
setInternalLink(linkMarkup);
}
};
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle>Sitecore Content Browser</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* File Manager */}
<div>
<h3 className="text-sm font-medium mb-2">Browse Content</h3>
<SitecoreFileManager
onSelectPath={handleSelectPath}
language="en"
rootPath="/sitecore/content"
/>
</div>
{/* Selected Item Display */}
<div>
<h3 className="text-sm font-medium mb-2">Selected Item</h3>
{selectedPath ? (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Path:</label>
<div className="p-2 bg-muted rounded text-sm font-mono">
{selectedPath}
</div>
</div>
{internalLink && (
<div>
<label className="text-xs text-muted-foreground">
Internal Link (Sitecore Format):
</label>
<div className="p-2 bg-muted rounded text-xs font-mono break-all">
{internalLink}
</div>
</div>
)}
</div>
) : (
<div className="text-sm text-muted-foreground">
No item selected. Browse and select an item from the tree.
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
);
}
What this demonstrates:
// ❌ WRONG - String interpolation in query
const query = `
query {
item(path: "${userPath}", language: "${lang}") {
children { results { name } }
}
}
`;
// ✅ CORRECT - Use GraphQL variables
const query = `
query GetItems($path: String!, $language: String!) {
item(path: $path, language: $language) {
children { results { name } }
}
}
`;
const variables = { path: userPath, language: lang };
// ✅ CORRECT - Comprehensive state handling
{loading && items.length === 0 ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error}</div>
) : items.length === 0 ? (
<div>No items found</div>
) : (
<div>{/* Render items */}</div>
)}
// ✅ CORRECT - Clean state on navigation
const handleNavigateInto = (item) => {
setCurrentPath(item.path);
setHighlightedItem(null); // Clear selection
setCurrentPage(0); // Reset pagination
setEndCursor(null); // Reset cursor
setError(null); // Clear errors
};
// ✅ CORRECT - Cursor-based pagination
const query = `
query GetItems($path: String!, $first: Int!, $after: String) {
item(path: $path) {
children(first: $first, after: $after) {
pageInfo {
hasNext
endCursor
}
results { id name }
}
}
}
`;
// ✅ CORRECT - Validate before querying
const fetchItems = async (path) => {
if (!path || typeof path !== 'string') {
setError("Invalid path");
return;
}
if (!path.startsWith('/sitecore/')) {
setError("Path must start with /sitecore/");
return;
}
// Proceed with query
};
Problem: String interpolation in queries causes security issues and breaks caching.
Solution: Always use GraphQL variables for dynamic values.
Problem: Navigating to a new folder shows old pagination state.
Solution: Reset currentPage and endCursor when path changes.
Problem: UI breaks when folder has no children.
Solution: Check for empty results and show appropriate message.
Problem: Making GraphQL calls directly from browser exposes credentials.
Solution: Always use a server-side proxy API route.
Problem: Scroll events trigger too many requests.
Solution: Check loading state before triggering new requests.
File: src/pages/api/debug/test-dynamic-query.ts
Purpose: Validates that dynamic queries work correctly with different parameters. This helps verify your GraphQL proxy and query structure before building UI components.
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { path = "/sitecore/content", language = "en" } = req.query;
try {
const query = `
query GetItems($path: String!, $language: String!) {
item(path: $path, language: $language) {
id
name
path
children(first: 5) {
total
results {
id
name
path
}
}
}
}
`;
const response = await fetch(
`${req.headers.host}/api/sitecore/graphql`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
variables: { path, language },
}),
}
);
const data = await response.json();
res.status(200).json({
success: !data.errors,
query: { path, language },
itemFound: !!data.data?.item,
childCount: data.data?.item?.children?.total || 0,
children: data.data?.item?.children?.results || [],
errors: data.errors || null,
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
Test it:
# Test default path
curl "http://localhost:3001/api/debug/test-dynamic-query"
# Test specific path
curl "http://localhost:3001/api/debug/test-dynamic-query?path=/sitecore/content/Home&language=en"
Building dynamic GraphQL queries in Sitecore XM Cloud transforms static content displays into interactive, user-driven experiences. The key to success lies in proper state management, using GraphQL variables instead of string interpolation, and implementing robust error handling for edge cases. The file manager example demonstrates these principles in action—each user interaction triggers a new query with updated parameters, pagination happens seamlessly through cursor-based loading, and the component gracefully handles empty folders or invalid paths.
The patterns shown here—server-side proxy for security, React state for tracking user position, and cursor-based pagination for performance—form the foundation for any dynamic Sitecore integration. Whether you’re building a content browser, search interface, or filtered list view, these same principles apply. Remember to always validate user input before querying, reset pagination state when navigation changes, and provide clear feedback during loading and error states for the best user experience.