- Published on
Rewriting GraphQL Media URLs to Relative Paths (and Proxying Originals via Next.js)
- Authors

- Name
- Jorge Lusar
When consuming media from Sitecore Edge through GraphQL, responses typically contain absolute URLs:
https://edge.sitecorecloud.io/<tenant>/media/...
This works, but it introduces a few challenges:
- Exposing internal Edge URLs to the browser
- Difficulty swapping to a new media origin or CDN
- Inconsistent behavior across
live/previewenvironments - Lack of centralized caching or header control for media files
To solve this, we redesigned our media pipeline so that:
- GraphQL responses in live automatically rewrite absolute URLs into relative
/media/...paths - Next.js rewrites these to an API route
- The API route proxies the original asset from Sitecore Edge (or any configured origin), streaming the file and applying improved caching
The browser only ever sees /media/..., while the original asset is delivered unchanged.
Goals
We wanted a solution that:
- Keeps GraphQL usage untouched — only rewrites responses beneath the client factory
- Works only when
SITECORE_ENV=live - Allows fallback or rollback by toggling environment variables
- Streams media for performance and low server memory usage
- Applies long-lived caching headers if the upstream doesn't
- Makes media origin configurable (
MEDIA_ORIGIN)
1. Wrapping the GraphQL Client With a Response Transform
We extended the Sitecore JSS GraphQL client factory so we could intercept all GraphQL responses:
export const createGraphQLClientFactory = (config: JssConfig) => {
// Determine endpoint as usual...
const baseFactory = GraphQLRequestClient.createClientFactory(
clientConfig
) as GraphQLClientFactory;
const env = process.env.SITECORE_ENV || process.env.NEXT_PUBLIC_SITECORE_ENV;
const enableTransform = env === "live";
return enableTransform
? withResponseTransform(baseFactory, transformGraphQLResponse)
: baseFactory;
};
Why at the factory level?
Because every GraphQL consumer — components, services, APIs — goes through this factory.
This gives us a single point of control for media URL rewriting.
2. Intercepting request, rawRequest, and batchRequests
We wrapped the client in a JavaScript Proxy to intercept and modify GraphQL responses:
const withResponseTransform = (factory, transform) => {
return () => {
const client = factory();
return new Proxy(client, {
get(target, prop) {
if (prop === "request") {
return async (...args) => {
const result = await target.request(...args);
return safeTransform(transform, result);
};
}
if (prop === "rawRequest") {
return async (...args) => {
const result = await target.rawRequest(...args);
if (result?.data) {
result.data = safeTransform(transform, result.data);
}
return result;
};
}
if (prop === "batchRequests") {
return async (...args) => {
const result = await target.batchRequests(...args);
return safeTransform(transform, result);
};
}
return Reflect.get(target, prop);
},
});
};
};
safeTransform() ensures that no transform error can ever break GraphQL consumers.
3. Rewriting Absolute URLs to Relative Media Paths
Here’s the logic responsible for scanning GraphQL responses and rewriting all matching URL strings:
export const transformGraphQLResponse = (data: any): any => {
const pattern = process.env.MEDIA_REWRITE_PATTERN;
const replacement = process.env.MEDIA_REWRITE_REPLACEMENT || "";
const mediaRegex = pattern ? new RegExp(pattern, "g") : null;
if (!mediaRegex) return data;
const isMediaUrl = (s: string) =>
typeof s === "string" && (s.includes("/-/media/") || s.includes("/media/"));
const rewrite = (s: string) => s.replace(mediaRegex, replacement);
const visit = (node: any) => {
if (Array.isArray(node)) {
return node.map(visit);
}
if (node && typeof node === "object") {
for (const key of Object.keys(node)) {
const v = node[key];
if (typeof v === "string" && isMediaUrl(v)) {
node[key] = rewrite(v);
} else if (typeof v === "object") {
node[key] = visit(v);
}
}
}
return node;
};
return visit(data);
};
Environment variables controlling this behavior
MEDIA_REWRITE_PATTERN=https://edge.sitecorecloud.io/<tenant>/media/
MEDIA_REWRITE_REPLACEMENT=/media/
Example transform:
Input:
https://edge.sitecorecloud.io/tenant/media/image.jpg?rev=3
Output:
/media/image.jpg?rev=3
4. Next.js Rewrites: /media → /api/media
Because the browser now uses relative media URLs, we send them through a custom API route:
// next.config.js
async rewrites() {
return [
{
source: '/media/:path*',
destination: '/api/media/:path*',
},
];
}
This lets us apply proxying, security headers, and consistent caching — without modifying frontend code.
5. Streaming Proxy for Media Files
The final piece is the /api/media/[...path].ts route, which:
- Builds the upstream URL (keeps path + query)
- Streams the file back to the client
- Applies caching when missing
- Avoids buffering large files in memory
const DEFAULT_ORIGIN = process.env.MEDIA_ORIGIN || process.env.MEDIA_REWRITE_PATTERN || '';
const buildUpstreamUrl = (req) => {
const { path = [] } = req.query;
const base = DEFAULT_ORIGIN.replace(/\/$/, "");
const pathname = `/${Array.isArray(path) ? path.join("/") : path}`;
const qs = req.url?.includes("?") ? `?${req.url.split("?")[1]}` : "";
return `${base}${pathname}${qs}`;
};
The handler then streams the upstream response, forwarding relevant headers, and applying:
Cache-Control: public, max-age=31536000, immutable
when needed.
This makes media delivery significantly faster and more CDN-friendly.
6. Full Request Lifecycle Example
GraphQL returns:
https://edge.sitecorecloud.io/tenant/media/foo.jpg
Transform rewrites it to:
/media/foo.jpg
Browser requests:
GET /media/foo.jpg
Next.js rewrites to:
/api/media/foo.jpg
API route proxies to:
https://edge.sitecorecloud.io/tenant/media/foo.jpg
Response is streamed back to the browser.
7. Testing
Live mode:
SITECORE_ENV=live
MEDIA_REWRITE_PATTERN=https://edge.sitecorecloud.io/<tenant>/media/
MEDIA_REWRITE_REPLACEMENT=/media/
MEDIA_ORIGIN=https://edge.sitecorecloud.io/<tenant>/media
Verify:
- Image
srcattributes become/media/... - Network tab: hits
/api/media/... - Response headers look sane
- Files load correctly
Preview mode:
SITECORE_ENV=preview
Verify:
- GraphQL media URLs remain absolute
- No transforms occur
8. Rollback & Safety
This design is intentionally safe and reversible:
- Set
SITECORE_ENV != live→ transform turns off - Remove
MEDIA_REWRITE_PATTERN→ transform becomes a no-op - Revert Next.js rewrite → browser hits upstream URLs directly
- Remove the API route → Next.js rewrites will fail fast
Every component still receives the same GraphQL data structure either way.
9. Files Added or Modified
src/lib/graphql-client-factory/create.tsnext.config.jssrc/pages/api/media/[...path].ts.env(new media config variables)
