Why CORS for Static Files in Next.js Sitecore Projects Matters
Cross-Origin Resource Sharing (CORS) defines whether browser JavaScript can read responses from another domain. In Next.js + Sitecore projects, getting CORS right is critical because you must:
- Secure static files (JS, CSS, fonts, images) served by Next.js or CDN
- Enable API communication between your app and Sitecore (Layout Service, Media API, etc.)
- Avoid wildcard policies () that fail security audits and penetration tests
Note: CORS only governs whether a response is exposed to browser JavaScript. It does not stop hotlinking (other sites embedding your images or scripts). To prevent that, you need CDN rules, signed URLs, or middleware.
The Sitecore Challenge: Balancing Static Asset Security and CMS Integration
Dual CORS Requirements
In a Next.js + Sitecore setup, you must address CORS in two areas:
- App-to-Sitecore Communication
- Layout Service API calls
- Media item requests
- Content management operations
- Static File Security
- JavaScript chunks & ES modules (may require CORS if loaded cross-domain)
- Fonts (browsers enforce CORS on fonts from another domain)
- Images & CSS (only need CORS if accessed via JS APIs like
fetch
or<canvas>
) - PWA assets (service workers, manifest, etc.)
The Plugin Ecosystem
Sitecore JSS includes a CORS header plugin that handles CMS integration:
// src/lib/next-config/plugins/cors-header.js
const corsHeaderPlugin = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
async headers() {
return [
{
source: "/_next/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: config.sitecoreApiHost, // Could be wildcard!
},
],
},
];
},
});
};
The problem: This plugin might use wildcards or overly permissive origins.
📋 Implementation Strategy
1. Audit Current Configuration
First, identify all CORS configurations in your project:
# Search for CORS configurations
grep -r "Access-Control-Allow-Origin" src/
grep -r "cors" src/
grep -r "\\*" src/ | grep -i cors
2. Three-Layer Approach
CORS in a Next.js + Sitecore project happens at three layers:
- Next.js Main Config → controls static files (/_next, fonts, PWA assets)
- CORS Plugin → controls Sitecore JSS integration (Layout Service, Media API)
- Sitecore API Key → controls backend API access (explicit origins in Sitecore)
Image1: Each layer contributes headers or allowed origins, and together they form a unified CORS policy across Next.js static files and Sitecore APIs.
3. Priority System
// Priority order for allowed origins:
1. process.env.NEXTAUTH_URL // App-specific domain
2. config.sitecoreApiHost // Sitecore CMS domain
3. '<https://app.domain.com>' // Fallback production domain
Code Implementation
Step 0: Environment Configuration
Add environment-specific app origins.
.env.local / .env.qa / .env.production
# Environment-specific CORS origins
NEXTAUTH_URL=https://app-d.domain.com # Development
# NEXTAUTH_URL=https://app-q.domain.com # QA
# NEXTAUTH_URL=https://app.domain.com # Production
# Ensure required environment variables have fallbacks
ANYLINE_OCR_LICENSE_KEY=your-license-key-or-empty
LIVECHAT_LICENSE_KEY=your-license-key-or-empty
Step 1: Update Main Next.js Configuration
File: next.config.js
// Security headers for Vercel conformance
async headers() {
return [
// API endpoints - Restrictive CORS
{
source: '/api/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization, X-Requested-With',
},
],
},
// Next.js static assets - Secure CORS
{
source: '/_next/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
// Public directory assets - Organized by type
{
source: '/images/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
{
source: '/icons/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
{
source: '/fonts/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
// Global security headers
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' <https://trusted-domains.com>",
"style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>",
"font-src 'self' <https://fonts.gstatic.com>",
"img-src 'self' data: blob: https: http:",
"connect-src 'self' https: wss:",
"frame-src 'self' <https://trusted-domains.com>",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Server',
value: '', // Hide server information
},
{
key: 'X-Powered-By',
value: '', // Hide technology stack
},
],
},
];
}
Step 2: Update CORS Plugin for Sitecore (optional)
File: src/lib/next-config/plugins/cors-header.js
const config = require("../../../temp/config");
/**
* Enhanced CORS plugin for Sitecore integration
* Prioritizes security while maintaining CMS functionality
* @param {import('next').NextConfig} nextConfig
*/
const corsHeaderPlugin = (nextConfig = {}) => {
if (!config.sitecoreApiHost) {
return nextConfig;
}
// Multi-level fallback for allowed origins
const allowedOrigin =
process.env.NEXTAUTH_URL || // 1. App domain (highest priority)
config.sitecoreApiHost.replace(/\\/$/, "") || // 2. Sitecore CMS domain
"<https://yourapp.com>"; // 3. Production fallback
return Object.assign({}, nextConfig, {
async headers() {
const extendHeaders =
typeof nextConfig.headers === "function"
? await nextConfig.headers()
: [];
return [
...(await extendHeaders),
{
source: "/_next/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: allowedOrigin,
},
{
key: "Access-Control-Allow-Methods",
value: "GET, HEAD, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Cache-Control",
},
],
},
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: allowedOrigin,
},
{
key: "Access-Control-Allow-Credentials",
value: "true",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization, X-Requested-With",
},
],
},
];
},
});
};
module.exports = corsHeaderPlugin;
Step 3: Update Sitecore API Key Configuration
File: src/PWA.Serialization/serialization/APIKey/API Keys/www.yml
SharedFields:
- Hint: CORS Origins
Value: https://app.domain.com,https://app-q.domain.com,https://app-d.domain.com
Common Pitfalls & Solutions
1. Invalid Regex Patterns
❌ Problem:
// This will cause Next.js to fail
source: '/((?!api/|_next/|healthz|sitecore/api/|-/).*\\\\.(js|css|png))',
Error:
Error parsing regex: Capturing groups are not allowed at position 46
✅ Solution:
// Use simple path patterns instead
source: '/images/:path*',
source: '/fonts/:path*',
source: '/icons/:path*',
2. Missing Environment Variables
❌ Problem:
env: {
ANYLINE_OCR_LICENSE_KEY: process.env.ANYLINE_OCR_LICENSE_KEY, // undefined = error
}
✅ Solution:
env: {
ANYLINE_OCR_LICENSE_KEY: process.env.ANYLINE_OCR_LICENSE_KEY || '',
LIVECHAT_LICENSE_KEY: process.env.LIVECHAT_LICENSE_KEY || '',
}
3. CORS Header Conflicts
❌ Problem:
// Main config and plugin both setting headers for same route
// Can cause unpredictable behavior
✅ Solution:
// Ensure consistent priority order in both configurations
const allowedOrigin = process.env.NEXTAUTH_URL || fallback;
4. Overly Restrictive CORS
❌ Problem:
// Blocking legitimate Sitecore requests
{
key: 'Access-Control-Allow-Origin',
value: '<https://app.domain.com>', // Too restrictive for CMS
}
✅ Solution:
// Allow both app and CMS domains
const allowedOrigin = process.env.NEXTAUTH_URL || config.sitecoreApiHost;
Testing & Validation
1. Browser Developer Tools
Test CORS Headers:
// In browser console
fetch("<https://yourapp.com/_next/static/chunks/main.js>", {
method: "GET",
mode: "cors",
}).then((response) => {
console.log(
"CORS Headers:",
response.headers.get("Access-Control-Allow-Origin")
);
});
2. curl Commands
Test Static File CORS:
# Test image CORS
curl -H "Origin: <https://malicious-site.com>" \\
-H "Access-Control-Request-Method: GET" \\
-H "Access-Control-Request-Headers: X-Requested-With" \\
-X OPTIONS \\
<https://yourapp.com/images/logo.png>
# Should return 403 or no CORS headers
Test Legitimate Access:
# Test from allowed origin
curl -H "Origin: <https://app.domain.com>" \\
-H "Access-Control-Request-Method: GET" \\
-X OPTIONS \\
<https://yourapp.com/images/logo.png>
# Should return proper CORS headers
🎯 Best Practices
1. Environment-Specific Configuration
// Use environment variables for flexibility
const getCorsOrigin = () => {
const env = process.env.NODE_ENV || "development";
const origins = {
development: "<https://app-d.domain.com>",
staging: "<https://app-q.domain.com>",
production: "<https://app.domain.com>",
};
return process.env.NEXTAUTH_URL || origins[env] || origins.production;
};
2. Organized Route Patterns
// Group related static assets
const staticFileRoutes = [
{ source: "/images/:path*", type: "media" },
{ source: "/icons/:path*", type: "media" },
{ source: "/fonts/:path*", type: "fonts" },
{ source: "/_next/:path*", type: "build" },
];
const corsHeaders = staticFileRoutes.map((route) => ({
source: route.source,
headers: [
{
key: "Access-Control-Allow-Origin",
value: getCorsOrigin(),
},
{
key: "Access-Control-Allow-Methods",
value: route.type === "build" ? "GET, HEAD, OPTIONS" : "GET, HEAD",
},
],
}));
3. Monitoring and Logging
// Add CORS logging middleware
const corsLogger = (req, res, next) => {
const origin = req.headers.origin;
const allowedOrigin = getCorsOrigin();
if (origin && origin !== allowedOrigin) {
console.warn(`CORS: Blocked request from ${origin} to ${req.path}`);
}
next();
};
4. Documentation and Maintenance
/**
* CORS Configuration for Static Files
*
* Purpose: Secure static assets while maintaining Sitecore integration
*
* Routes covered:
* - /_next/:path* (Next.js build assets)
* - /images/:path* (Public images)
* - /icons/:path* (App icons)
* - /fonts/:path* (Web fonts)
*
* Allowed origins:
* - Development: <https://app-d.domain.com>
* - QA: <https://app-q.domain.com>
* - Production: <https://app.domain.com>
*
* Last updated: December 2024
* Next review: March 2025
*/
Bringing It All Together: A Unified CORS Policy for Next.js + Sitecore
Implementing CORS in a Next.js + Sitecore project isn’t just about flipping a header — it’s about coordinating three layers:
- Next.js config for static assets and fonts
- Sitecore JSS CORS plugin for Layout Service and media APIs
- Sitecore API Key origins for backend access
When aligned, these give you a unified CORS policy that protects assets, secures CMS communication, and passes security audits without breaking functionality.
Key takeaways:
- Avoid wildcards — always use explicit origins.
- Handle static CORS with simple, fixed headers; handle API CORS dynamically with preflight support.
- Use Sitecore serialization to manage API Key CORS origins consistently across environments.
- Add hotlink protection (Media Request Protection, CDN rules, middleware) if bandwidth theft is a concern.
- Test with browser dev tools,
curl
, and automated tests to confirm the right headers are applied.
By following this layered approach, your project will have a secure, predictable, and maintainable CORS configuration — one that protects static assets and Sitecore APIs while keeping your Next.js PWA fast and reliable.
*Pro Tip: Regular security audits should include CORS policy reviews to ensure your configuration remains secure as your application evolves.*