@aryanbansal-launch/edge-utils 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/launch-help.js +191 -0
- package/dist/ai/gemini.js +153 -0
- package/dist/utils/inject.js +45 -0
- package/package.json +20 -5
- package/readme.md +826 -46
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const colors = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
bright: '\x1b[1m',
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
cyan: '\x1b[36m',
|
|
9
|
+
blue: '\x1b[34m',
|
|
10
|
+
magenta: '\x1b[35m',
|
|
11
|
+
dim: '\x1b[2m'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
console.log(`
|
|
15
|
+
${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════════════╗
|
|
16
|
+
║ 🚀 Launch Edge Utils - Complete Reference Guide ║
|
|
17
|
+
╚════════════════════════════════════════════════════════════════════╝${colors.reset}
|
|
18
|
+
|
|
19
|
+
${colors.bright}${colors.yellow}📦 AVAILABLE METHODS${colors.reset}
|
|
20
|
+
${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
|
|
21
|
+
|
|
22
|
+
${colors.bright}${colors.green}1. Security & Access Control${colors.reset}
|
|
23
|
+
|
|
24
|
+
${colors.cyan}blockAICrawlers(request, bots?)${colors.reset}
|
|
25
|
+
Block AI crawlers and bots from accessing your site
|
|
26
|
+
${colors.dim}Parameters:${colors.reset}
|
|
27
|
+
- request: Request object
|
|
28
|
+
- bots: string[] (optional) - Custom bot list
|
|
29
|
+
${colors.dim}Default Blocked Bots:${colors.reset}
|
|
30
|
+
claudebot, gptbot, googlebot, bingbot, ahrefsbot,
|
|
31
|
+
yandexbot, semrushbot, mj12bot, facebookexternalhit, twitterbot
|
|
32
|
+
${colors.dim}Returns:${colors.reset} Response | null
|
|
33
|
+
${colors.dim}Example:${colors.reset}
|
|
34
|
+
const response = blockAICrawlers(request);
|
|
35
|
+
if (response) return response;
|
|
36
|
+
|
|
37
|
+
${colors.cyan}blockDefaultDomains(request, options?)${colors.reset}
|
|
38
|
+
Block access via default Launch domains (*.contentstackapps.com)
|
|
39
|
+
${colors.dim}Parameters:${colors.reset}
|
|
40
|
+
- request: Request object
|
|
41
|
+
- options: { domainToBlock?: string }
|
|
42
|
+
${colors.dim}Returns:${colors.reset} Response | null
|
|
43
|
+
${colors.dim}Example:${colors.reset}
|
|
44
|
+
const response = blockDefaultDomains(request);
|
|
45
|
+
if (response) return response;
|
|
46
|
+
|
|
47
|
+
${colors.cyan}ipAccessControl(request, options)${colors.reset}
|
|
48
|
+
Whitelist or blacklist IPs at the edge
|
|
49
|
+
${colors.dim}Parameters:${colors.reset}
|
|
50
|
+
- request: Request object
|
|
51
|
+
- options: { allow?: string[], deny?: string[] }
|
|
52
|
+
${colors.dim}Returns:${colors.reset} Response | null
|
|
53
|
+
${colors.dim}Example:${colors.reset}
|
|
54
|
+
const response = ipAccessControl(request, {
|
|
55
|
+
allow: ["203.0.113.10", "198.51.100.5"]
|
|
56
|
+
});
|
|
57
|
+
if (response) return response;
|
|
58
|
+
|
|
59
|
+
${colors.cyan}protectWithBasicAuth(request, options)${colors.reset}
|
|
60
|
+
Add Basic Authentication to protect staging/dev environments
|
|
61
|
+
${colors.dim}Parameters:${colors.reset}
|
|
62
|
+
- request: Request object
|
|
63
|
+
- options: {
|
|
64
|
+
hostnameIncludes: string,
|
|
65
|
+
username: string,
|
|
66
|
+
password: string,
|
|
67
|
+
realm?: string
|
|
68
|
+
}
|
|
69
|
+
${colors.dim}Returns:${colors.reset} Promise<Response> | null
|
|
70
|
+
${colors.dim}Example:${colors.reset}
|
|
71
|
+
const auth = await protectWithBasicAuth(request, {
|
|
72
|
+
hostnameIncludes: "staging.myapp.com",
|
|
73
|
+
username: "admin",
|
|
74
|
+
password: "securepass123"
|
|
75
|
+
});
|
|
76
|
+
if (auth && auth.status === 401) return auth;
|
|
77
|
+
|
|
78
|
+
${colors.bright}${colors.green}2. Routing & Redirects${colors.reset}
|
|
79
|
+
|
|
80
|
+
${colors.cyan}redirectIfMatch(request, options)${colors.reset}
|
|
81
|
+
Conditional redirects based on path and method
|
|
82
|
+
${colors.dim}Parameters:${colors.reset}
|
|
83
|
+
- request: Request object
|
|
84
|
+
- options: {
|
|
85
|
+
path: string,
|
|
86
|
+
method?: string,
|
|
87
|
+
to: string,
|
|
88
|
+
status?: number
|
|
89
|
+
}
|
|
90
|
+
${colors.dim}Returns:${colors.reset} Response | null
|
|
91
|
+
${colors.dim}Example:${colors.reset}
|
|
92
|
+
const redirect = redirectIfMatch(request, {
|
|
93
|
+
path: "/old-page",
|
|
94
|
+
to: "/new-page",
|
|
95
|
+
status: 301
|
|
96
|
+
});
|
|
97
|
+
if (redirect) return redirect;
|
|
98
|
+
|
|
99
|
+
${colors.bright}${colors.green}3. Next.js Optimization${colors.reset}
|
|
100
|
+
|
|
101
|
+
${colors.cyan}handleNextJS_RSC(request, options)${colors.reset}
|
|
102
|
+
Fix Next.js React Server Components header issues
|
|
103
|
+
${colors.dim}Parameters:${colors.reset}
|
|
104
|
+
- request: Request object
|
|
105
|
+
- options: { affectedPaths: string[] }
|
|
106
|
+
${colors.dim}Returns:${colors.reset} Promise<Response> | null
|
|
107
|
+
${colors.dim}Example:${colors.reset}
|
|
108
|
+
const rsc = await handleNextJS_RSC(request, {
|
|
109
|
+
affectedPaths: ["/shop", "/about", "/products"]
|
|
110
|
+
});
|
|
111
|
+
if (rsc) return rsc;
|
|
112
|
+
|
|
113
|
+
${colors.bright}${colors.green}4. Geo-Location${colors.reset}
|
|
114
|
+
|
|
115
|
+
${colors.cyan}getGeoHeaders(request)${colors.reset}
|
|
116
|
+
Extract geo-location data from Launch headers
|
|
117
|
+
${colors.dim}Parameters:${colors.reset}
|
|
118
|
+
- request: Request object
|
|
119
|
+
${colors.dim}Returns:${colors.reset} {
|
|
120
|
+
country: string | null,
|
|
121
|
+
region: string | null,
|
|
122
|
+
city: string | null,
|
|
123
|
+
latitude: string | null,
|
|
124
|
+
longitude: string | null
|
|
125
|
+
}
|
|
126
|
+
${colors.dim}Example:${colors.reset}
|
|
127
|
+
const geo = getGeoHeaders(request);
|
|
128
|
+
if (geo.country === "US") {
|
|
129
|
+
// Custom logic for US visitors
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
${colors.bright}${colors.green}5. Response Utilities${colors.reset}
|
|
133
|
+
|
|
134
|
+
${colors.cyan}jsonResponse(body, init?)${colors.reset}
|
|
135
|
+
Create JSON responses easily
|
|
136
|
+
${colors.dim}Parameters:${colors.reset}
|
|
137
|
+
- body: Record<string, unknown>
|
|
138
|
+
- init: ResponseInit (optional)
|
|
139
|
+
${colors.dim}Returns:${colors.reset} Response
|
|
140
|
+
${colors.dim}Example:${colors.reset}
|
|
141
|
+
return jsonResponse({ status: "ok", data: [...] });
|
|
142
|
+
|
|
143
|
+
${colors.cyan}passThrough(request)${colors.reset}
|
|
144
|
+
Forward request to origin server
|
|
145
|
+
${colors.dim}Parameters:${colors.reset}
|
|
146
|
+
- request: Request object
|
|
147
|
+
${colors.dim}Returns:${colors.reset} Promise<Response>
|
|
148
|
+
${colors.dim}Example:${colors.reset}
|
|
149
|
+
return passThrough(request);
|
|
150
|
+
|
|
151
|
+
${colors.bright}${colors.green}6. Configuration${colors.reset}
|
|
152
|
+
|
|
153
|
+
${colors.cyan}generateLaunchConfig(options)${colors.reset}
|
|
154
|
+
Generate launch.json configuration programmatically
|
|
155
|
+
${colors.dim}Parameters:${colors.reset}
|
|
156
|
+
- options: {
|
|
157
|
+
redirects?: LaunchRedirect[],
|
|
158
|
+
rewrites?: LaunchRewrite[],
|
|
159
|
+
cache?: { cachePriming?: { urls: string[] } }
|
|
160
|
+
}
|
|
161
|
+
${colors.dim}Returns:${colors.reset} LaunchConfig
|
|
162
|
+
${colors.dim}Example:${colors.reset}
|
|
163
|
+
const config = generateLaunchConfig({
|
|
164
|
+
redirects: [{ source: "/old", destination: "/new", statusCode: 301 }],
|
|
165
|
+
cache: { cachePriming: { urls: ["/", "/about"] } }
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
${colors.bright}${colors.yellow}🛠️ CLI COMMANDS${colors.reset}
|
|
169
|
+
${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
|
|
170
|
+
|
|
171
|
+
${colors.cyan}npx create-launch-edge${colors.reset}
|
|
172
|
+
Initialize edge functions with boilerplate code
|
|
173
|
+
Creates: functions/[proxy].edge.js
|
|
174
|
+
|
|
175
|
+
${colors.cyan}npx launch-config${colors.reset}
|
|
176
|
+
Interactive CLI to manage launch.json
|
|
177
|
+
Configure: redirects, rewrites, cache priming
|
|
178
|
+
|
|
179
|
+
${colors.cyan}npx launch-help${colors.reset}
|
|
180
|
+
Display this help guide
|
|
181
|
+
|
|
182
|
+
${colors.bright}${colors.yellow}📚 QUICK LINKS${colors.reset}
|
|
183
|
+
${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
|
|
184
|
+
|
|
185
|
+
${colors.blue}Documentation:${colors.reset} https://github.com/AryanBansal-launch/launch-edge-utils
|
|
186
|
+
${colors.blue}NPM Package:${colors.reset} https://www.npmjs.com/package/@aryanbansal-launch/edge-utils
|
|
187
|
+
${colors.blue}Launch Docs:${colors.reset} https://www.contentstack.com/docs/developers/launch
|
|
188
|
+
|
|
189
|
+
${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
|
|
190
|
+
`);
|
|
191
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a complete Gemini AI Chatbot widget script to be injected at the Edge.
|
|
3
|
+
*/
|
|
4
|
+
export function getGeminiChatbotScript(options) {
|
|
5
|
+
const { apiKey, botName = "Gemini Assistant", welcomeMessage = "Hi! How can I help you today?", primaryColor = "#1a73e8", } = options;
|
|
6
|
+
return `
|
|
7
|
+
<script>
|
|
8
|
+
(function() {
|
|
9
|
+
// Create styles
|
|
10
|
+
const style = document.createElement('style');
|
|
11
|
+
style.textContent = \`
|
|
12
|
+
#gemini-chat-widget {
|
|
13
|
+
position: fixed;
|
|
14
|
+
bottom: 20px;
|
|
15
|
+
right: 20px;
|
|
16
|
+
z-index: 9999;
|
|
17
|
+
font-family: sans-serif;
|
|
18
|
+
}
|
|
19
|
+
#gemini-chat-button {
|
|
20
|
+
width: 60px;
|
|
21
|
+
height: 60px;
|
|
22
|
+
border-radius: 50%;
|
|
23
|
+
background-color: ${primaryColor};
|
|
24
|
+
color: white;
|
|
25
|
+
border: none;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
font-size: 24px;
|
|
32
|
+
}
|
|
33
|
+
#gemini-chat-window {
|
|
34
|
+
display: none;
|
|
35
|
+
width: 350px;
|
|
36
|
+
height: 500px;
|
|
37
|
+
background: white;
|
|
38
|
+
border-radius: 12px;
|
|
39
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
margin-bottom: 15px;
|
|
43
|
+
}
|
|
44
|
+
#gemini-chat-header {
|
|
45
|
+
padding: 15px;
|
|
46
|
+
background: ${primaryColor};
|
|
47
|
+
color: white;
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
}
|
|
52
|
+
#gemini-chat-messages {
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 15px;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
background: #f7f7f7;
|
|
57
|
+
}
|
|
58
|
+
.gemini-msg {
|
|
59
|
+
margin-bottom: 10px;
|
|
60
|
+
padding: 8px 12px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
max-width: 80%;
|
|
63
|
+
word-wrap: break-word;
|
|
64
|
+
}
|
|
65
|
+
.gemini-msg-bot { background: white; align-self: flex-start; border: 1px solid #eee; }
|
|
66
|
+
.gemini-msg-user { background: ${primaryColor}; color: white; align-self: flex-end; margin-left: auto; }
|
|
67
|
+
#gemini-chat-input-area {
|
|
68
|
+
padding: 10px;
|
|
69
|
+
border-top: 1px solid #eee;
|
|
70
|
+
display: flex;
|
|
71
|
+
}
|
|
72
|
+
#gemini-chat-input {
|
|
73
|
+
flex: 1;
|
|
74
|
+
border: 1px solid #ddd;
|
|
75
|
+
padding: 8px;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
outline: none;
|
|
78
|
+
}
|
|
79
|
+
\`;
|
|
80
|
+
document.head.appendChild(style);
|
|
81
|
+
|
|
82
|
+
// Create HTML
|
|
83
|
+
const container = document.createElement('div');
|
|
84
|
+
container.id = 'gemini-chat-widget';
|
|
85
|
+
container.innerHTML = \`
|
|
86
|
+
<div id="gemini-chat-window">
|
|
87
|
+
<div id="gemini-chat-header">
|
|
88
|
+
<span>${botName}</span>
|
|
89
|
+
<button onclick="document.getElementById('gemini-chat-window').style.display='none'" style="background:none; border:none; color:white; cursor:pointer;">×</button>
|
|
90
|
+
</div>
|
|
91
|
+
<div id="gemini-chat-messages">
|
|
92
|
+
<div class="gemini-msg gemini-msg-bot">${welcomeMessage}</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div id="gemini-chat-input-area">
|
|
95
|
+
<input type="text" id="gemini-chat-input" placeholder="Type a message...">
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<button id="gemini-chat-button">💬</button>
|
|
99
|
+
\`;
|
|
100
|
+
document.body.appendChild(container);
|
|
101
|
+
|
|
102
|
+
const btn = document.getElementById('gemini-chat-button');
|
|
103
|
+
const win = document.getElementById('gemini-chat-window');
|
|
104
|
+
const input = document.getElementById('gemini-chat-input');
|
|
105
|
+
const msgContainer = document.getElementById('gemini-chat-messages');
|
|
106
|
+
|
|
107
|
+
btn.onclick = () => {
|
|
108
|
+
win.style.display = win.style.display === 'flex' ? 'none' : 'flex';
|
|
109
|
+
if (win.style.display === 'flex') input.focus();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
async function sendMessage() {
|
|
113
|
+
const text = input.value.trim();
|
|
114
|
+
if (!text) return;
|
|
115
|
+
|
|
116
|
+
input.value = '';
|
|
117
|
+
appendMessage('user', text);
|
|
118
|
+
|
|
119
|
+
const loadingMsg = appendMessage('bot', '...');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
contents: [{ parts: [{ text }] }]
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
const botReply = data.candidates[0].content.parts[0].text;
|
|
131
|
+
loadingMsg.textContent = botReply;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
loadingMsg.textContent = 'Sorry, I am having trouble connecting.';
|
|
134
|
+
}
|
|
135
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendMessage(role, text) {
|
|
139
|
+
const div = document.createElement('div');
|
|
140
|
+
div.className = 'gemini-msg gemini-msg-' + role;
|
|
141
|
+
div.textContent = text;
|
|
142
|
+
msgContainer.appendChild(div);
|
|
143
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
144
|
+
return div;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
input.onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };
|
|
148
|
+
})();
|
|
149
|
+
</script>
|
|
150
|
+
\`;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to inject HTML/scripts into a Response body using TransformStream.
|
|
3
|
+
* This is high-performance as it streams the modification without loading the full body into memory.
|
|
4
|
+
*/
|
|
5
|
+
class HTMLRewriterStream extends TransformStream {
|
|
6
|
+
constructor(contentToInject, position = 'before-body-end') {
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const decoder = new TextDecoder();
|
|
9
|
+
super({
|
|
10
|
+
transform(chunk, controller) {
|
|
11
|
+
let html = decoder.decode(chunk);
|
|
12
|
+
if (position === 'before-body-end' && html.includes('</body>')) {
|
|
13
|
+
html = html.replace('</body>', `${contentToInject}</body>`);
|
|
14
|
+
}
|
|
15
|
+
else if (position === 'after-head-start' && html.includes('<head>')) {
|
|
16
|
+
html = html.replace('<head>', `<head>${contentToInject}`);
|
|
17
|
+
}
|
|
18
|
+
controller.enqueue(encoder.encode(html));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Injects a script or HTML snippet into a Response.
|
|
25
|
+
* @param response The original Response object
|
|
26
|
+
* @param options Injection options
|
|
27
|
+
*/
|
|
28
|
+
export async function injectScript(response, options) {
|
|
29
|
+
const contentType = response.headers.get('content-type');
|
|
30
|
+
// Only inject into HTML responses
|
|
31
|
+
if (!contentType || !contentType.includes('text/html')) {
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
34
|
+
const contentToInject = options.scriptUrl
|
|
35
|
+
? `<script src="${options.scriptUrl}" async defer></script>`
|
|
36
|
+
: (options.script || '');
|
|
37
|
+
if (!contentToInject)
|
|
38
|
+
return response;
|
|
39
|
+
const rewriter = new HTMLRewriterStream(contentToInject, options.position);
|
|
40
|
+
return new Response(response.body?.pipeThrough(rewriter), {
|
|
41
|
+
status: response.status,
|
|
42
|
+
statusText: response.statusText,
|
|
43
|
+
headers: response.headers,
|
|
44
|
+
});
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryanbansal-launch/edge-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"main": "dist/index.js",
|
|
15
15
|
"bin": {
|
|
16
|
-
"create-launch-edge": "
|
|
17
|
-
"launch-config": "
|
|
16
|
+
"create-launch-edge": "bin/launch-init.js",
|
|
17
|
+
"launch-config": "bin/launch-config.js",
|
|
18
|
+
"launch-help": "bin/launch-help.js"
|
|
18
19
|
},
|
|
19
20
|
"exports": {
|
|
20
21
|
".": "./dist/index.js"
|
|
@@ -24,6 +25,20 @@
|
|
|
24
25
|
"bin"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
|
-
"build": "tsc"
|
|
28
|
-
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"contentstack",
|
|
33
|
+
"launch",
|
|
34
|
+
"edge-functions",
|
|
35
|
+
"edge",
|
|
36
|
+
"middleware",
|
|
37
|
+
"security",
|
|
38
|
+
"nextjs",
|
|
39
|
+
"redirect",
|
|
40
|
+
"geo-location",
|
|
41
|
+
"basic-auth",
|
|
42
|
+
"ip-access"
|
|
43
|
+
]
|
|
29
44
|
}
|
package/readme.md
CHANGED
|
@@ -7,47 +7,73 @@ A comprehensive toolkit for [Contentstack Launch](https://www.contentstack.com/d
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 📋 Table of Contents
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
- [Quick Start](#-quick-start)
|
|
13
|
+
- [Usage Flow](#-usage-flow)
|
|
14
|
+
- [Complete API Reference](#-complete-api-reference)
|
|
15
|
+
- [Real-World Examples](#-real-world-examples)
|
|
16
|
+
- [CLI Commands](#-cli-commands)
|
|
17
|
+
- [Platform Support](#-platform-support)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ⚡ Quick Start
|
|
22
|
+
|
|
23
|
+
### Step 1: Install the Package
|
|
13
24
|
|
|
14
25
|
```bash
|
|
15
|
-
# Install the package
|
|
16
26
|
npm install @aryanbansal-launch/edge-utils
|
|
27
|
+
```
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
### Step 2: Initialize Edge Functions
|
|
30
|
+
|
|
31
|
+
Run this command from your **project root** (where `package.json` is located):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
19
34
|
npx create-launch-edge
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
This
|
|
37
|
+
This automatically creates:
|
|
38
|
+
- `functions/` directory
|
|
39
|
+
- `functions/[proxy].edge.js` with production-ready boilerplate
|
|
40
|
+
|
|
41
|
+
### Step 3: Customize & Deploy
|
|
42
|
+
|
|
43
|
+
1. Open `functions/[proxy].edge.js`
|
|
44
|
+
2. Customize the utilities based on your needs
|
|
45
|
+
3. Deploy to Contentstack Launch
|
|
46
|
+
|
|
47
|
+
**Need help?** Run:
|
|
48
|
+
```bash
|
|
49
|
+
npx launch-help
|
|
50
|
+
```
|
|
23
51
|
|
|
24
52
|
---
|
|
25
53
|
|
|
26
|
-
##
|
|
54
|
+
## 🔄 Usage Flow
|
|
27
55
|
|
|
28
|
-
###
|
|
29
|
-
- **[Block AI Crawlers](https://www.contentstack.com/docs/developers/launch/blocking-ai-crawlers)**: Automatically detects and rejects requests from known scrapers (GPTBot, ClaudeBot, etc.) to protect your content and server resources.
|
|
30
|
-
- **[Restricted Default Domains](https://www.contentstack.com/docs/developers/launch/blocking-default-launch-domains-from-google-search)**: By default, Launch provides a `*.contentstackapps.com` domain. This utility forces visitors to your custom domain, which is essential for SEO (preventing duplicate content) and professional branding.
|
|
31
|
-
- **[IP Access Control](https://www.contentstack.com/docs/developers/launch/ip-based-access-control-using-edge-functions)**: Create a lightweight firewall at the edge to whitelist internal teams or block malicious IPs before they hit your application logic.
|
|
56
|
+
### Understanding Edge Functions
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
- **[RSC Header Fix](https://www.contentstack.com/docs/developers/launch/handling-nextjs-rsc-issues-on-launch)**: Next.js React Server Components (RSC) use a special `rsc` header. Sometimes, proxies or caches can incorrectly serve RSC data when a full page load is expected. This utility detects these edge cases and strips the header to ensure the correct response type is served.
|
|
58
|
+
Edge Functions in Contentstack Launch act as **middleware** that runs before requests reach your origin server. Think of them as a chain of checks and transformations:
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
```
|
|
61
|
+
User Request → Edge Function → Your Application
|
|
62
|
+
```
|
|
39
63
|
|
|
40
|
-
###
|
|
41
|
-
- **Declarative Redirects**: Handle complex, logic-based redirects at runtime.
|
|
42
|
-
- **Runtime vs Config**:
|
|
43
|
-
- Use **`launch.json`** ([Static Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)) for high-performance, simple path-to-path mapping.
|
|
44
|
-
- Use **`redirectIfMatch`** (this library) for dynamic redirects that require logic, such as checking cookies, headers, or geo-location before redirecting.
|
|
64
|
+
### Basic Pattern
|
|
45
65
|
|
|
46
|
-
|
|
66
|
+
Every utility follows this pattern:
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
```javascript
|
|
69
|
+
const result = utilityFunction(request, options);
|
|
70
|
+
if (result) return result; // Early return if condition met
|
|
71
|
+
// Continue to next check...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Complete Handler Example
|
|
49
75
|
|
|
50
|
-
|
|
76
|
+
Here's how utilities work together in `functions/[proxy].edge.js`:
|
|
51
77
|
|
|
52
78
|
```javascript
|
|
53
79
|
import {
|
|
@@ -55,74 +81,828 @@ import {
|
|
|
55
81
|
handleNextJS_RSC,
|
|
56
82
|
blockAICrawlers,
|
|
57
83
|
ipAccessControl,
|
|
84
|
+
protectWithBasicAuth,
|
|
58
85
|
redirectIfMatch,
|
|
59
86
|
getGeoHeaders,
|
|
87
|
+
jsonResponse,
|
|
60
88
|
passThrough
|
|
61
89
|
} from "@aryanbansal-launch/edge-utils";
|
|
62
90
|
|
|
63
91
|
export default async function handler(request, context) {
|
|
64
|
-
// 1
|
|
65
|
-
//
|
|
92
|
+
// 1️⃣ SECURITY LAYER
|
|
93
|
+
// Block default domains (SEO best practice)
|
|
66
94
|
const domainCheck = blockDefaultDomains(request);
|
|
67
95
|
if (domainCheck) return domainCheck;
|
|
68
96
|
|
|
69
|
-
//
|
|
70
|
-
|
|
97
|
+
// Block AI bots and crawlers
|
|
98
|
+
const botCheck = blockAICrawlers(request);
|
|
99
|
+
if (botCheck) return botCheck;
|
|
100
|
+
|
|
101
|
+
// IP-based access control
|
|
102
|
+
const ipCheck = ipAccessControl(request, {
|
|
103
|
+
allow: ["203.0.113.10", "198.51.100.5"]
|
|
104
|
+
});
|
|
105
|
+
if (ipCheck) return ipCheck;
|
|
106
|
+
|
|
107
|
+
// 2️⃣ AUTHENTICATION LAYER
|
|
108
|
+
// Protect staging environments
|
|
109
|
+
const auth = await protectWithBasicAuth(request, {
|
|
110
|
+
hostnameIncludes: "staging.myapp.com",
|
|
111
|
+
username: "admin",
|
|
112
|
+
password: "securepass123"
|
|
113
|
+
});
|
|
114
|
+
if (auth && auth.status === 401) return auth;
|
|
115
|
+
|
|
116
|
+
// 3️⃣ FRAMEWORK FIXES
|
|
117
|
+
// Fix Next.js RSC header issues
|
|
71
118
|
const rscCheck = await handleNextJS_RSC(request, {
|
|
72
|
-
affectedPaths: ["/shop", "/about"]
|
|
119
|
+
affectedPaths: ["/shop", "/products", "/about"]
|
|
73
120
|
});
|
|
74
121
|
if (rscCheck) return rscCheck;
|
|
75
122
|
|
|
76
|
-
//
|
|
123
|
+
// 4️⃣ ROUTING LAYER
|
|
124
|
+
// Handle redirects
|
|
125
|
+
const redirect = redirectIfMatch(request, {
|
|
126
|
+
path: "/old-page",
|
|
127
|
+
to: "/new-page",
|
|
128
|
+
status: 301
|
|
129
|
+
});
|
|
130
|
+
if (redirect) return redirect;
|
|
131
|
+
|
|
132
|
+
// 5️⃣ PERSONALIZATION
|
|
133
|
+
// Get user's location
|
|
134
|
+
const geo = getGeoHeaders(request);
|
|
135
|
+
if (geo.country === "US") {
|
|
136
|
+
console.log(`US visitor from ${geo.city}, ${geo.region}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 6️⃣ CUSTOM API ENDPOINTS
|
|
140
|
+
const url = new URL(request.url);
|
|
141
|
+
if (url.pathname === "/api/health") {
|
|
142
|
+
return jsonResponse({
|
|
143
|
+
status: "healthy",
|
|
144
|
+
region: geo.region,
|
|
145
|
+
timestamp: Date.now()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 7️⃣ DEFAULT: Pass to origin
|
|
150
|
+
return passThrough(request);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 📚 Complete API Reference
|
|
157
|
+
|
|
158
|
+
### 🛡️ Security & Access Control
|
|
159
|
+
|
|
160
|
+
#### `blockAICrawlers(request, bots?)`
|
|
161
|
+
|
|
162
|
+
Block AI crawlers and bots from accessing your site.
|
|
163
|
+
|
|
164
|
+
**Parameters:**
|
|
165
|
+
- `request` (Request) - The incoming request object
|
|
166
|
+
- `bots` (string[], optional) - Custom list of bot user-agents to block
|
|
167
|
+
|
|
168
|
+
**Returns:** `Response | null`
|
|
169
|
+
- Returns `403 Forbidden` if bot detected
|
|
170
|
+
- Returns `null` if no bot detected (continue processing)
|
|
171
|
+
|
|
172
|
+
**Default Blocked Bots:**
|
|
173
|
+
The following bots are blocked by default (case-insensitive):
|
|
174
|
+
- `claudebot` - Anthropic's Claude AI crawler
|
|
175
|
+
- `gptbot` - OpenAI's GPT crawler
|
|
176
|
+
- `googlebot` - Google's web crawler
|
|
177
|
+
- `bingbot` - Microsoft Bing's crawler
|
|
178
|
+
- `ahrefsbot` - Ahrefs SEO crawler
|
|
179
|
+
- `yandexbot` - Yandex search engine crawler
|
|
180
|
+
- `semrushbot` - SEMrush SEO tool crawler
|
|
181
|
+
- `mj12bot` - Majestic SEO crawler
|
|
182
|
+
- `facebookexternalhit` - Facebook's link preview crawler
|
|
183
|
+
- `twitterbot` - Twitter's link preview crawler
|
|
184
|
+
|
|
185
|
+
**Example:**
|
|
186
|
+
```javascript
|
|
187
|
+
// Use default bot list
|
|
188
|
+
const response = blockAICrawlers(request);
|
|
189
|
+
if (response) return response;
|
|
190
|
+
|
|
191
|
+
// Custom bot list
|
|
192
|
+
const response = blockAICrawlers(request, [
|
|
193
|
+
"gptbot",
|
|
194
|
+
"claudebot",
|
|
195
|
+
"my-custom-bot"
|
|
196
|
+
]);
|
|
197
|
+
if (response) return response;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Use Cases:**
|
|
201
|
+
- Protect content from AI scraping
|
|
202
|
+
- Reduce server load from aggressive crawlers
|
|
203
|
+
- Comply with content usage policies
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
#### `blockDefaultDomains(request, options?)`
|
|
208
|
+
|
|
209
|
+
Block access via default Launch domains (`*.contentstackapps.com`).
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
- `request` (Request) - The incoming request object
|
|
213
|
+
- `options` (object, optional)
|
|
214
|
+
- `domainToBlock` (string) - Custom domain to block (default: "contentstackapps.com")
|
|
215
|
+
|
|
216
|
+
**Returns:** `Response | null`
|
|
217
|
+
- Returns `403 Forbidden` if default domain detected
|
|
218
|
+
- Returns `null` if custom domain used
|
|
219
|
+
|
|
220
|
+
**Example:**
|
|
221
|
+
```javascript
|
|
222
|
+
// Block default Launch domain
|
|
223
|
+
const response = blockDefaultDomains(request);
|
|
224
|
+
if (response) return response;
|
|
225
|
+
|
|
226
|
+
// Block custom domain
|
|
227
|
+
const response = blockDefaultDomains(request, {
|
|
228
|
+
domainToBlock: "myolddomain.com"
|
|
229
|
+
});
|
|
230
|
+
if (response) return response;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Use Cases:**
|
|
234
|
+
- Force users to custom domain for SEO
|
|
235
|
+
- Prevent duplicate content indexing
|
|
236
|
+
- Professional branding
|
|
237
|
+
|
|
238
|
+
**Learn More:** [Blocking Default Launch Domains](https://www.contentstack.com/docs/developers/launch/blocking-default-launch-domains-from-google-search)
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
#### `ipAccessControl(request, options)`
|
|
243
|
+
|
|
244
|
+
Whitelist or blacklist IPs at the edge.
|
|
245
|
+
|
|
246
|
+
**Parameters:**
|
|
247
|
+
- `request` (Request) - The incoming request object
|
|
248
|
+
- `options` (object)
|
|
249
|
+
- `allow` (string[], optional) - Whitelist of allowed IPs
|
|
250
|
+
- `deny` (string[], optional) - Blacklist of denied IPs
|
|
251
|
+
|
|
252
|
+
**Returns:** `Response | null`
|
|
253
|
+
- Returns `403 Forbidden` if IP blocked
|
|
254
|
+
- Returns `null` if IP allowed
|
|
255
|
+
|
|
256
|
+
**Example:**
|
|
257
|
+
```javascript
|
|
258
|
+
// Whitelist specific IPs (only these can access)
|
|
259
|
+
const response = ipAccessControl(request, {
|
|
260
|
+
allow: ["203.0.113.10", "198.51.100.5"]
|
|
261
|
+
});
|
|
262
|
+
if (response) return response;
|
|
263
|
+
|
|
264
|
+
// Blacklist specific IPs
|
|
265
|
+
const response = ipAccessControl(request, {
|
|
266
|
+
deny: ["192.0.2.100", "192.0.2.101"]
|
|
267
|
+
});
|
|
268
|
+
if (response) return response;
|
|
269
|
+
|
|
270
|
+
// Combine both (deny takes precedence)
|
|
271
|
+
const response = ipAccessControl(request, {
|
|
272
|
+
allow: ["203.0.113.0/24"], // Allow subnet
|
|
273
|
+
deny: ["203.0.113.50"] // Except this one
|
|
274
|
+
});
|
|
275
|
+
if (response) return response;
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Use Cases:**
|
|
279
|
+
- Restrict admin panels to office IPs
|
|
280
|
+
- Block malicious IPs
|
|
281
|
+
- Create staging environment access control
|
|
282
|
+
|
|
283
|
+
**Learn More:** [IP-Based Access Control](https://www.contentstack.com/docs/developers/launch/ip-based-access-control-using-edge-functions)
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
#### `protectWithBasicAuth(request, options)`
|
|
288
|
+
|
|
289
|
+
Add Basic Authentication to protect environments.
|
|
290
|
+
|
|
291
|
+
**Parameters:**
|
|
292
|
+
- `request` (Request) - The incoming request object
|
|
293
|
+
- `options` (object)
|
|
294
|
+
- `hostnameIncludes` (string) - Protect URLs containing this hostname
|
|
295
|
+
- `username` (string) - Username for authentication
|
|
296
|
+
- `password` (string) - Password for authentication
|
|
297
|
+
- `realm` (string, optional) - Auth realm name (default: "Protected Area")
|
|
298
|
+
|
|
299
|
+
**Returns:** `Promise<Response> | null`
|
|
300
|
+
- Returns `401 Unauthorized` if auth fails
|
|
301
|
+
- Returns authenticated response if credentials valid
|
|
302
|
+
- Returns `null` if hostname doesn't match
|
|
303
|
+
|
|
304
|
+
**Example:**
|
|
305
|
+
```javascript
|
|
306
|
+
// Protect staging environment
|
|
307
|
+
const auth = await protectWithBasicAuth(request, {
|
|
308
|
+
hostnameIncludes: "staging.myapp.com",
|
|
309
|
+
username: "admin",
|
|
310
|
+
password: "securepass123",
|
|
311
|
+
realm: "Staging Environment"
|
|
312
|
+
});
|
|
313
|
+
if (auth && auth.status === 401) return auth;
|
|
314
|
+
|
|
315
|
+
// Protect specific path pattern
|
|
316
|
+
const url = new URL(request.url);
|
|
317
|
+
if (url.pathname.startsWith("/admin")) {
|
|
318
|
+
const auth = await protectWithBasicAuth(request, {
|
|
319
|
+
hostnameIncludes: url.hostname,
|
|
320
|
+
username: "admin",
|
|
321
|
+
password: "adminpass"
|
|
322
|
+
});
|
|
323
|
+
if (auth && auth.status === 401) return auth;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Use Cases:**
|
|
328
|
+
- Protect staging/dev environments
|
|
329
|
+
- Quick password protection for demos
|
|
330
|
+
- Restrict access to admin areas
|
|
331
|
+
|
|
332
|
+
**Security Note:** For production, use proper authentication systems. Basic Auth credentials are base64-encoded, not encrypted.
|
|
333
|
+
|
|
334
|
+
**Learn More:** [Password Protection for Environments](https://www.contentstack.com/docs/developers/launch/password-protection-for-environments)
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### 🔀 Routing & Redirects
|
|
339
|
+
|
|
340
|
+
#### `redirectIfMatch(request, options)`
|
|
341
|
+
|
|
342
|
+
Conditional redirects based on path and method.
|
|
343
|
+
|
|
344
|
+
**Parameters:**
|
|
345
|
+
- `request` (Request) - The incoming request object
|
|
346
|
+
- `options` (object)
|
|
347
|
+
- `path` (string) - Path to match
|
|
348
|
+
- `to` (string) - Destination path
|
|
349
|
+
- `method` (string, optional) - HTTP method to match (e.g., "GET", "POST")
|
|
350
|
+
- `status` (number, optional) - HTTP status code (default: 301)
|
|
351
|
+
|
|
352
|
+
**Returns:** `Response | null`
|
|
353
|
+
- Returns redirect response if path matches
|
|
354
|
+
- Returns `null` if no match
|
|
355
|
+
|
|
356
|
+
**Example:**
|
|
357
|
+
```javascript
|
|
358
|
+
// Simple redirect
|
|
359
|
+
const redirect = redirectIfMatch(request, {
|
|
360
|
+
path: "/old-page",
|
|
361
|
+
to: "/new-page",
|
|
362
|
+
status: 301 // Permanent redirect
|
|
363
|
+
});
|
|
364
|
+
if (redirect) return redirect;
|
|
365
|
+
|
|
366
|
+
// Temporary redirect
|
|
367
|
+
const redirect = redirectIfMatch(request, {
|
|
368
|
+
path: "/maintenance",
|
|
369
|
+
to: "/coming-soon",
|
|
370
|
+
status: 302 // Temporary redirect
|
|
371
|
+
});
|
|
372
|
+
if (redirect) return redirect;
|
|
373
|
+
|
|
374
|
+
// Method-specific redirect
|
|
375
|
+
const redirect = redirectIfMatch(request, {
|
|
376
|
+
path: "/api/old-endpoint",
|
|
377
|
+
method: "POST",
|
|
378
|
+
to: "/api/v2/endpoint",
|
|
379
|
+
status: 308 // Permanent redirect (preserves method)
|
|
380
|
+
});
|
|
381
|
+
if (redirect) return redirect;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Use Cases:**
|
|
385
|
+
- SEO-friendly URL changes
|
|
386
|
+
- Migrate old URLs to new structure
|
|
387
|
+
- A/B testing redirects
|
|
388
|
+
- Maintenance mode redirects
|
|
389
|
+
|
|
390
|
+
**When to Use:**
|
|
391
|
+
- **Edge Functions (this utility)**: Dynamic redirects requiring logic (cookies, headers, geo)
|
|
392
|
+
- **launch.json**: Static path-to-path redirects (better performance)
|
|
393
|
+
|
|
394
|
+
**Learn More:** [Edge URL Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### ⚛️ Next.js Optimization
|
|
399
|
+
|
|
400
|
+
#### `handleNextJS_RSC(request, options)`
|
|
401
|
+
|
|
402
|
+
Fix Next.js React Server Components header issues.
|
|
403
|
+
|
|
404
|
+
**Parameters:**
|
|
405
|
+
- `request` (Request) - The incoming request object
|
|
406
|
+
- `options` (object)
|
|
407
|
+
- `affectedPaths` (string[]) - Array of paths with RSC issues
|
|
408
|
+
|
|
409
|
+
**Returns:** `Promise<Response> | null`
|
|
410
|
+
- Returns modified response if RSC issue detected
|
|
411
|
+
- Returns `null` if no issue
|
|
412
|
+
|
|
413
|
+
**Example:**
|
|
414
|
+
```javascript
|
|
415
|
+
// Fix specific pages
|
|
416
|
+
const rsc = await handleNextJS_RSC(request, {
|
|
417
|
+
affectedPaths: ["/shop", "/products", "/about"]
|
|
418
|
+
});
|
|
419
|
+
if (rsc) return rsc;
|
|
420
|
+
|
|
421
|
+
// Fix all dynamic routes
|
|
422
|
+
const rsc = await handleNextJS_RSC(request, {
|
|
423
|
+
affectedPaths: ["/blog", "/products", "/categories"]
|
|
424
|
+
});
|
|
425
|
+
if (rsc) return rsc;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**What It Does:**
|
|
429
|
+
Next.js RSC uses a special `rsc: 1` header. Sometimes, caches incorrectly serve RSC data (JSON) when a full page load is expected. This utility detects these cases and strips the header to ensure correct response type.
|
|
430
|
+
|
|
431
|
+
**Use Cases:**
|
|
432
|
+
- Fix "JSON showing instead of page" issues
|
|
433
|
+
- Resolve RSC caching problems
|
|
434
|
+
- Ensure proper page hydration
|
|
435
|
+
|
|
436
|
+
**Learn More:** [Handling Next.js RSC Issues](https://www.contentstack.com/docs/developers/launch/handling-nextjs-rsc-issues-on-launch)
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### 📍 Geo-Location
|
|
441
|
+
|
|
442
|
+
#### `getGeoHeaders(request)`
|
|
443
|
+
|
|
444
|
+
Extract geo-location data from Launch headers.
|
|
445
|
+
|
|
446
|
+
**Parameters:**
|
|
447
|
+
- `request` (Request) - The incoming request object
|
|
448
|
+
|
|
449
|
+
**Returns:** Object with geo data
|
|
450
|
+
```typescript
|
|
451
|
+
{
|
|
452
|
+
country: string | null, // ISO country code (e.g., "US")
|
|
453
|
+
region: string | null, // Region/state code (e.g., "CA")
|
|
454
|
+
city: string | null, // City name (e.g., "San Francisco")
|
|
455
|
+
latitude: string | null, // Latitude coordinate
|
|
456
|
+
longitude: string | null // Longitude coordinate
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Example:**
|
|
461
|
+
```javascript
|
|
462
|
+
// Get user location
|
|
463
|
+
const geo = getGeoHeaders(request);
|
|
464
|
+
|
|
465
|
+
// Country-based logic
|
|
466
|
+
if (geo.country === "US") {
|
|
467
|
+
console.log(`US visitor from ${geo.city}, ${geo.region}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Region-specific content
|
|
471
|
+
if (geo.country === "US" && geo.region === "CA") {
|
|
472
|
+
// Show California-specific content
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Distance-based logic
|
|
476
|
+
if (geo.latitude && geo.longitude) {
|
|
477
|
+
const userLat = parseFloat(geo.latitude);
|
|
478
|
+
const userLon = parseFloat(geo.longitude);
|
|
479
|
+
// Calculate distance to store, etc.
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Redirect based on location
|
|
483
|
+
const geo = getGeoHeaders(request);
|
|
484
|
+
if (geo.country === "FR") {
|
|
485
|
+
return Response.redirect("https://fr.mysite.com", 302);
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Use Cases:**
|
|
490
|
+
- Personalize content by location
|
|
491
|
+
- Show region-specific pricing
|
|
492
|
+
- Redirect to country-specific sites
|
|
493
|
+
- Display nearest store locations
|
|
494
|
+
- Comply with regional regulations
|
|
495
|
+
|
|
496
|
+
**Learn More:** [Geolocation Headers in Launch](https://www.contentstack.com/docs/developers/launch/geolocation-headers-in-launch)
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### 📤 Response Utilities
|
|
501
|
+
|
|
502
|
+
#### `jsonResponse(body, init?)`
|
|
503
|
+
|
|
504
|
+
Create JSON responses easily.
|
|
505
|
+
|
|
506
|
+
**Parameters:**
|
|
507
|
+
- `body` (object) - JSON-serializable object
|
|
508
|
+
- `init` (ResponseInit, optional) - Additional response options (status, headers, etc.)
|
|
509
|
+
|
|
510
|
+
**Returns:** `Response` with `Content-Type: application/json`
|
|
511
|
+
|
|
512
|
+
**Example:**
|
|
513
|
+
```javascript
|
|
514
|
+
// Simple JSON response
|
|
515
|
+
return jsonResponse({ status: "ok", message: "Success" });
|
|
516
|
+
|
|
517
|
+
// With custom status
|
|
518
|
+
return jsonResponse(
|
|
519
|
+
{ error: "Not found" },
|
|
520
|
+
{ status: 404 }
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// With custom headers
|
|
524
|
+
return jsonResponse(
|
|
525
|
+
{ data: [...] },
|
|
526
|
+
{
|
|
527
|
+
status: 200,
|
|
528
|
+
headers: {
|
|
529
|
+
"Cache-Control": "max-age=3600",
|
|
530
|
+
"X-Custom-Header": "value"
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// API endpoint example
|
|
536
|
+
const url = new URL(request.url);
|
|
537
|
+
if (url.pathname === "/api/user") {
|
|
538
|
+
const geo = getGeoHeaders(request);
|
|
539
|
+
return jsonResponse({
|
|
540
|
+
user: "john_doe",
|
|
541
|
+
location: {
|
|
542
|
+
country: geo.country,
|
|
543
|
+
city: geo.city
|
|
544
|
+
},
|
|
545
|
+
timestamp: Date.now()
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Use Cases:**
|
|
551
|
+
- Create API endpoints at the edge
|
|
552
|
+
- Return structured error messages
|
|
553
|
+
- Build serverless functions
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
#### `passThrough(request)`
|
|
558
|
+
|
|
559
|
+
Forward request to origin server.
|
|
560
|
+
|
|
561
|
+
**Parameters:**
|
|
562
|
+
- `request` (Request) - The incoming request object
|
|
563
|
+
|
|
564
|
+
**Returns:** `Promise<Response>` from origin
|
|
565
|
+
|
|
566
|
+
**Example:**
|
|
567
|
+
```javascript
|
|
568
|
+
// Default: pass everything through
|
|
569
|
+
return passThrough(request);
|
|
570
|
+
|
|
571
|
+
// After all checks
|
|
572
|
+
export default async function handler(request, context) {
|
|
573
|
+
// ... all your checks ...
|
|
574
|
+
|
|
575
|
+
// If nothing matched, pass to origin
|
|
576
|
+
return passThrough(request);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Use Cases:**
|
|
581
|
+
- Default fallback after edge logic
|
|
582
|
+
- Forward requests that don't need edge processing
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
### ⚙️ Configuration
|
|
587
|
+
|
|
588
|
+
#### `generateLaunchConfig(options)`
|
|
589
|
+
|
|
590
|
+
Generate `launch.json` configuration programmatically.
|
|
591
|
+
|
|
592
|
+
**Parameters:**
|
|
593
|
+
- `options` (object)
|
|
594
|
+
- `redirects` (LaunchRedirect[], optional)
|
|
595
|
+
- `rewrites` (LaunchRewrite[], optional)
|
|
596
|
+
- `cache` (object, optional)
|
|
597
|
+
- `cachePriming` (object)
|
|
598
|
+
- `urls` (string[])
|
|
599
|
+
|
|
600
|
+
**Returns:** `LaunchConfig` object
|
|
601
|
+
|
|
602
|
+
**Types:**
|
|
603
|
+
```typescript
|
|
604
|
+
interface LaunchRedirect {
|
|
605
|
+
source: string;
|
|
606
|
+
destination: string;
|
|
607
|
+
statusCode?: number;
|
|
608
|
+
response?: {
|
|
609
|
+
headers?: Record<string, string>;
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
interface LaunchRewrite {
|
|
614
|
+
source: string;
|
|
615
|
+
destination: string;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Example:**
|
|
620
|
+
```javascript
|
|
621
|
+
import { generateLaunchConfig } from "@aryanbansal-launch/edge-utils";
|
|
622
|
+
import fs from "fs";
|
|
623
|
+
|
|
624
|
+
const config = generateLaunchConfig({
|
|
625
|
+
redirects: [
|
|
626
|
+
{
|
|
627
|
+
source: "/old-blog/:slug",
|
|
628
|
+
destination: "/blog/:slug",
|
|
629
|
+
statusCode: 301
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
source: "/products",
|
|
633
|
+
destination: "/shop",
|
|
634
|
+
statusCode: 308
|
|
635
|
+
}
|
|
636
|
+
],
|
|
637
|
+
rewrites: [
|
|
638
|
+
{
|
|
639
|
+
source: "/api/:path*",
|
|
640
|
+
destination: "https://api.mybackend.com/:path*"
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
cache: {
|
|
644
|
+
cachePriming: {
|
|
645
|
+
urls: ["/", "/about", "/products", "/contact"]
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Write to launch.json
|
|
651
|
+
fs.writeFileSync("launch.json", JSON.stringify(config, null, 2));
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Use Cases:**
|
|
655
|
+
- Generate config from CMS data
|
|
656
|
+
- Automate bulk redirects
|
|
657
|
+
- Dynamic configuration management
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
## 🎯 Real-World Examples
|
|
662
|
+
|
|
663
|
+
### Example 1: E-Commerce Site
|
|
664
|
+
|
|
665
|
+
```javascript
|
|
666
|
+
export default async function handler(request, context) {
|
|
667
|
+
const url = new URL(request.url);
|
|
668
|
+
const geo = getGeoHeaders(request);
|
|
669
|
+
|
|
670
|
+
// 1. Block bots to reduce costs
|
|
77
671
|
const botCheck = blockAICrawlers(request);
|
|
78
672
|
if (botCheck) return botCheck;
|
|
79
673
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
674
|
+
// 2. Geo-based redirects
|
|
675
|
+
if (geo.country === "UK" && !url.hostname.includes("uk.")) {
|
|
676
|
+
return Response.redirect(`https://uk.myshop.com${url.pathname}`, 302);
|
|
677
|
+
}
|
|
83
678
|
|
|
84
|
-
//
|
|
679
|
+
// 3. Maintenance mode for specific regions
|
|
680
|
+
if (geo.country === "US" && url.pathname.startsWith("/checkout")) {
|
|
681
|
+
return jsonResponse(
|
|
682
|
+
{ error: "Checkout temporarily unavailable in your region" },
|
|
683
|
+
{ status: 503 }
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// 4. Product redirects
|
|
85
688
|
const redirect = redirectIfMatch(request, {
|
|
86
|
-
path: "/
|
|
87
|
-
to: "/new-
|
|
689
|
+
path: "/products/old-sku-123",
|
|
690
|
+
to: "/products/new-sku-456",
|
|
88
691
|
status: 301
|
|
89
692
|
});
|
|
90
693
|
if (redirect) return redirect;
|
|
91
694
|
|
|
92
|
-
|
|
695
|
+
return passThrough(request);
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Example 2: Multi-Environment Setup
|
|
700
|
+
|
|
701
|
+
```javascript
|
|
702
|
+
export default async function handler(request, context) {
|
|
703
|
+
const url = new URL(request.url);
|
|
704
|
+
|
|
705
|
+
// 1. Protect staging with Basic Auth
|
|
706
|
+
if (url.hostname.includes("staging")) {
|
|
707
|
+
const auth = await protectWithBasicAuth(request, {
|
|
708
|
+
hostnameIncludes: "staging",
|
|
709
|
+
username: "team",
|
|
710
|
+
password: process.env.STAGING_PASSWORD || "defaultpass"
|
|
711
|
+
});
|
|
712
|
+
if (auth && auth.status === 401) return auth;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 2. Restrict dev environment to office IPs
|
|
716
|
+
if (url.hostname.includes("dev")) {
|
|
717
|
+
const ipCheck = ipAccessControl(request, {
|
|
718
|
+
allow: ["203.0.113.0/24"] // Office IP range
|
|
719
|
+
});
|
|
720
|
+
if (ipCheck) return ipCheck;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 3. Block default domain on production
|
|
724
|
+
if (url.hostname.includes("myapp.com")) {
|
|
725
|
+
const domainCheck = blockDefaultDomains(request);
|
|
726
|
+
if (domainCheck) return domainCheck;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return passThrough(request);
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Example 3: Next.js App with API Routes
|
|
734
|
+
|
|
735
|
+
```javascript
|
|
736
|
+
export default async function handler(request, context) {
|
|
737
|
+
const url = new URL(request.url);
|
|
93
738
|
const geo = getGeoHeaders(request);
|
|
94
|
-
|
|
95
|
-
|
|
739
|
+
|
|
740
|
+
// 1. Fix RSC issues on dynamic pages
|
|
741
|
+
const rscCheck = await handleNextJS_RSC(request, {
|
|
742
|
+
affectedPaths: ["/blog", "/products", "/categories"]
|
|
743
|
+
});
|
|
744
|
+
if (rscCheck) return rscCheck;
|
|
745
|
+
|
|
746
|
+
// 2. Edge API endpoints
|
|
747
|
+
if (url.pathname === "/api/geo") {
|
|
748
|
+
return jsonResponse({
|
|
749
|
+
country: geo.country,
|
|
750
|
+
region: geo.region,
|
|
751
|
+
city: geo.city
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (url.pathname === "/api/health") {
|
|
756
|
+
return jsonResponse({
|
|
757
|
+
status: "healthy",
|
|
758
|
+
timestamp: Date.now(),
|
|
759
|
+
region: geo.region
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 3. Block bots from expensive pages
|
|
764
|
+
if (url.pathname.startsWith("/search")) {
|
|
765
|
+
const botCheck = blockAICrawlers(request);
|
|
766
|
+
if (botCheck) return botCheck;
|
|
96
767
|
}
|
|
97
768
|
|
|
98
|
-
// 7. 🚀 Pass through to Origin
|
|
99
769
|
return passThrough(request);
|
|
100
770
|
}
|
|
101
771
|
```
|
|
102
772
|
|
|
103
773
|
---
|
|
104
774
|
|
|
105
|
-
##
|
|
775
|
+
## 🛠️ CLI Commands
|
|
106
776
|
|
|
107
|
-
|
|
777
|
+
### `npx create-launch-edge`
|
|
108
778
|
|
|
779
|
+
Initialize edge functions with production-ready boilerplate.
|
|
780
|
+
|
|
781
|
+
**What it does:**
|
|
782
|
+
1. Checks you're in project root (where `package.json` exists)
|
|
783
|
+
2. Creates `functions/` directory if needed
|
|
784
|
+
3. Generates `functions/[proxy].edge.js` with example code
|
|
785
|
+
|
|
786
|
+
**Usage:**
|
|
787
|
+
```bash
|
|
788
|
+
cd /path/to/your/project
|
|
789
|
+
npx create-launch-edge
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Output:**
|
|
793
|
+
```
|
|
794
|
+
🚀 create-launch-edge: Contentstack Launch Initializer
|
|
795
|
+
|
|
796
|
+
✨ New: Created /functions directory
|
|
797
|
+
✨ New: Created /functions/[proxy].edge.js
|
|
798
|
+
|
|
799
|
+
🎉 Setup Complete!
|
|
800
|
+
|
|
801
|
+
Next Steps:
|
|
802
|
+
1. Open functions/[proxy].edge.js
|
|
803
|
+
2. Customize your redirects, auth, and RSC paths
|
|
804
|
+
3. Deploy your project to Contentstack Launch
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
### `npx launch-config`
|
|
810
|
+
|
|
811
|
+
Interactive CLI to manage `launch.json` configuration.
|
|
812
|
+
|
|
813
|
+
**What it does:**
|
|
814
|
+
- Add/manage redirects
|
|
815
|
+
- Configure rewrites
|
|
816
|
+
- Set up cache priming URLs
|
|
817
|
+
- Preserves existing configuration
|
|
818
|
+
|
|
819
|
+
**Usage:**
|
|
109
820
|
```bash
|
|
110
821
|
npx launch-config
|
|
111
822
|
```
|
|
112
823
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
824
|
+
**Interactive Prompts:**
|
|
825
|
+
```
|
|
826
|
+
🚀 Launch Configuration Generator
|
|
827
|
+
|
|
828
|
+
Do you want to add a Redirect? (y/n): y
|
|
829
|
+
Source path (e.g., /source): /old-page
|
|
830
|
+
Destination path (e.g., /destination): /new-page
|
|
831
|
+
Status code (default 308): 301
|
|
832
|
+
✔ Redirect added.
|
|
833
|
+
|
|
834
|
+
Do you want to add a Redirect? another? (y/n): n
|
|
835
|
+
|
|
836
|
+
Do you want to add a Rewrite? (y/n): y
|
|
837
|
+
Source path (e.g., /api/*): /api/*
|
|
838
|
+
Destination URL: https://backend.myapp.com/api/*
|
|
839
|
+
✔ Rewrite added.
|
|
840
|
+
|
|
841
|
+
Do you want to add Cache Priming URLs? (y/n): y
|
|
842
|
+
Note: Only relative paths are supported. No Regex/Wildcards.
|
|
843
|
+
Enter URLs separated by commas (e.g., /home,/about,/shop): /,/about,/products
|
|
844
|
+
|
|
845
|
+
✅ Successfully updated launch.json!
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**Learn More:**
|
|
849
|
+
- [Static Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
|
|
850
|
+
- [Cache Priming](https://www.contentstack.com/docs/developers/launch/cache-priming)
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
### `npx launch-help`
|
|
855
|
+
|
|
856
|
+
Display complete reference guide with all methods and examples.
|
|
857
|
+
|
|
858
|
+
**Usage:**
|
|
859
|
+
```bash
|
|
860
|
+
npx launch-help
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**Shows:**
|
|
864
|
+
- All available methods with parameters
|
|
865
|
+
- Return types and examples
|
|
866
|
+
- CLI commands
|
|
867
|
+
- Quick links to documentation
|
|
117
868
|
|
|
118
869
|
---
|
|
119
870
|
|
|
120
871
|
## 🌐 Platform Support
|
|
121
872
|
|
|
122
|
-
This library is exclusively optimized for **[Contentstack Launch](https://www.contentstack.com/docs/developers/launch)**. It assumes an environment where
|
|
873
|
+
This library is exclusively optimized for **[Contentstack Launch](https://www.contentstack.com/docs/developers/launch)**. It assumes an environment where:
|
|
874
|
+
- Standard Web APIs (`Request`, `Response`, `fetch`) are available
|
|
875
|
+
- Edge runtime environment
|
|
876
|
+
- Contentstack Launch geo-location headers
|
|
877
|
+
|
|
878
|
+
**Not compatible with:**
|
|
879
|
+
- Node.js servers
|
|
880
|
+
- Traditional hosting platforms
|
|
881
|
+
- Other edge platforms (Cloudflare Workers, Vercel Edge, etc.)
|
|
882
|
+
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
## 🤝 Contributing
|
|
886
|
+
|
|
887
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
888
|
+
|
|
889
|
+
**Repository:** https://github.com/AryanBansal-launch/launch-edge-utils
|
|
123
890
|
|
|
124
891
|
---
|
|
125
892
|
|
|
126
893
|
## 📄 License
|
|
127
894
|
|
|
128
895
|
Distributed under the MIT License. See `LICENSE` for more information.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## 🔗 Useful Links
|
|
900
|
+
|
|
901
|
+
- **[Contentstack Launch Documentation](https://www.contentstack.com/docs/developers/launch)**
|
|
902
|
+
- **[Edge Functions Guide](https://www.contentstack.com/docs/developers/launch/edge-functions)**
|
|
903
|
+
- **[NPM Package](https://www.npmjs.com/package/@aryanbansal-launch/edge-utils)**
|
|
904
|
+
- **[GitHub Repository](https://github.com/AryanBansal-launch/launch-edge-utils)**
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
**Made with ❤️ for the Contentstack Launch community**
|