1import http from 'node:http';
2import { Actor } from 'apify';
3import { cleanUrl, cleanUrls } from './cleaner.js';
4
5const PPE_EVENT = 'url-clean';
6
7
8
9
10
11
12function normalizeItems(items) {
13 return items.map((item) => {
14 if (typeof item === 'string') {
15 return item;
16 }
17 if (item && typeof item === 'object' && 'input' in item) {
18 return String(item.input ?? '');
19 }
20 return String(item ?? '');
21 });
22}
23
24await Actor.init();
25
26if (Actor.config.get('metaOrigin') === 'STANDBY') {
27 startStandbyServer();
28} else {
29 await runBatchMode();
30 await Actor.exit();
31}
32
33
34
35
36
37function startStandbyServer() {
38 const port = Actor.config.get('containerPort');
39
40 const server = http.createServer(async (req, res) => {
41
42 if (req.headers['x-apify-container-server-readiness-probe']) {
43 res.writeHead(200, { 'Content-Type': 'application/json' });
44 res.end(JSON.stringify({ status: 'ready' }));
45 return;
46 }
47
48
49 if (req.method !== 'GET') {
50 res.writeHead(405, { 'Content-Type': 'application/json' });
51 res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
52 return;
53 }
54
55
56 const reqUrl = new URL(req.url, `http://${req.headers.host}`);
57 if (!reqUrl.searchParams.get('input')) {
58 res.writeHead(200, { 'Content-Type': 'text/html' });
59 res.end(getLandingPageHtml());
60 return;
61 }
62
63 try {
64 const result = handleStandbyRequest(reqUrl);
65 await Actor.pushData(result);
66
67
68 try {
69 await Actor.charge({ eventName: PPE_EVENT, count: 1 });
70 } catch (chargeErr) {
71 console.warn(`PPE charge failed: ${chargeErr.message}`);
72 }
73
74 res.writeHead(200, { 'Content-Type': 'application/json' });
75 res.end(JSON.stringify(result));
76 } catch (err) {
77 const statusCode = err.statusCode || 500;
78 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
79 res.end(JSON.stringify({ error: err.message }));
80 }
81 });
82
83 server.listen(port, () => {
84 console.log(`Superclean URLs Standby server listening on port ${port}`);
85 });
86}
87
88function handleStandbyRequest(reqUrl) {
89 const input = reqUrl.searchParams.get('input');
90
91 if (!input) {
92 const err = new Error("Provide an 'input' query parameter with the URL to clean.");
93 err.statusCode = 400;
94 throw err;
95 }
96
97 const style = reqUrl.searchParams.get('style') || 'full';
98 const forceHttps = reqUrl.searchParams.get('forceHttps') !== 'false';
99 const removeTracking = reqUrl.searchParams.get('removeTracking') !== 'false';
100
101 const validStyles = ['full', 'domain'];
102 if (!validStyles.includes(style)) {
103 const err = new Error(`Invalid style "${style}". Must be one of: ${validStyles.join(', ')}`);
104 err.statusCode = 400;
105 throw err;
106 }
107
108 const cleaned = cleanUrl(input, { style, forceHttps, removeTracking });
109
110 return {
111 id: 1,
112 input: cleaned.input,
113 output: cleaned.output,
114 domain: cleaned.domain,
115 protocol: cleaned.protocol,
116 path: cleaned.path,
117 query: cleaned.query,
118 hash: cleaned.hash,
119 valid: cleaned.valid,
120 confidence: Math.round(cleaned.confidence * 100) / 100,
121 };
122}
123
124
125
126
127
128async function runBatchMode() {
129 const input = await Actor.getInput();
130
131 let {
132 items: rawItems = [],
133 item,
134 style = 'full',
135 forceHttps = true,
136 removeTracking = true
137 } = input || {};
138
139
140 if (typeof item === 'string' && item.trim()) {
141 if (!Array.isArray(rawItems)) rawItems = [];
142 rawItems = [item.trim(), ...rawItems];
143 }
144
145
146 if (typeof rawItems === 'string') rawItems = [rawItems];
147
148
149 if (!Array.isArray(rawItems) || rawItems.length === 0) {
150 await Actor.fail('Input required: provide "items" (array of URLs) or "item" (single URL string). Example: {"items": ["https://example.com"]}');
151 return;
152 }
153
154
155 const items = normalizeItems(rawItems);
156
157 console.log(`Processing ${items.length} URL(s) with style: ${style}`);
158 console.log(`Options: forceHttps=${forceHttps}, removeTracking=${removeTracking}`);
159
160
161 const results = cleanUrls(items, {
162 style,
163 forceHttps,
164 removeTracking
165 });
166
167
168 let chargedCount = 0;
169 let chargeLimitReached = false;
170 for (const result of results) {
171 if (!chargeLimitReached) {
172 try {
173 const chargeResult = await Actor.charge({ eventName: PPE_EVENT, count: 1 });
174 chargedCount++;
175 if (chargeResult?.eventChargeLimitReached) {
176 chargeLimitReached = true;
177 console.log(`User spending limit reached after charging ${chargedCount}/${results.length} items`);
178 }
179 } catch (chargeErr) {
180 console.warn(`PPE charge failed: ${chargeErr.message}`);
181 }
182 }
183 }
184
185 await Actor.pushData(results);
186
187
188 const validCount = results.filter(r => r.valid).length;
189 const avgConfidence = results.length > 0
190 ? (results.reduce((sum, r) => sum + r.confidence, 0) / results.length).toFixed(2)
191 : 0;
192
193 console.log(`Processed ${results.length} URLs`);
194 console.log(`Valid format: ${validCount}, Invalid format: ${results.length - validCount}`);
195 console.log(`Average confidence: ${avgConfidence}`);
196}
197
198
199
200
201
202function getLandingPageHtml() {
203 return `<!DOCTYPE html>
204<html lang="en">
205<head>
206<meta charset="utf-8">
207<meta name="viewport" content="width=device-width, initial-scale=1">
208<title>Superclean URLs — Superlative</title>
209<link rel="preconnect" href="https://fonts.googleapis.com">
210<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
211<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
212<style>
213:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
214*{margin:0;padding:0;box-sizing:border-box}
215body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
216a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
217a:hover{color:#6ee7a0}
218.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
219.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
220@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
221@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
222.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
223header{padding:32px 0}
224.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
225.logo:hover{color:var(--color-text)}
226.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
227.logo-mark svg{width:16px;height:16px}
228.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
229.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
230.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
231h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
232h1 .highlight{color:var(--color-accent)}
233.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
234.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
235.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
236.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
237h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
238p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
239li{margin-bottom:6px}
240ul{margin:.5rem 0 1rem 1.25rem}
241strong{color:var(--color-text);font-weight:600}
242pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
243code{font-family:var(--font-display);font-size:13px}
244p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
245table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
246th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
247th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
248td{color:var(--color-text-muted)}
249.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
250.note{font-size:13px;color:var(--color-text-muted);margin-top:8px}
251.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
252.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
253.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
254.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
255.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
256.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
257footer{padding:48px 0;border-top:1px solid var(--color-border)}
258.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
259.footer-text{font-size:13px;color:var(--color-text-muted)}
260.footer-links{display:flex;gap:24px}
261.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
262.footer-links a:hover{color:var(--color-accent)}
263@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
264</style>
265</head>
266<body>
267<div class="grid-bg"></div>
268<div class="accent-line"></div>
269
270<div class="container">
271 <header>
272 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
273 <div class="logo-mark">
274 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
275 <polyline points="4 17 10 11 4 5"></polyline>
276 <line x1="12" y1="19" x2="20" y2="19"></line>
277 </svg>
278 </div>
279 Superlative
280 </a>
281 </header>
282
283 <section class="hero">
284 <div class="hero-label">Superclean</div>
285 <h1>Superclean <span class="highlight">URLs</span></h1>
286 <p class="hero-desc">Instant URL cleaning and normalization. Remove tracking params, fix protocols, extract domains. Sub-second responses via Standby API.</p>
287 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
288 </section>
289
290 <section class="section">
291 <div class="section-label">Features</div>
292 <h2>What this does</h2>
293 <ul>
294 <li><strong>Removes tracking</strong> — Strip UTM, fbclid, gclid, and 50+ other tracking params</li>
295 <li><strong>Normalizes format</strong> — Consistent protocol, lowercase domains, clean paths</li>
296 <li><strong>Extracts domains</strong> — Pull clean domain names from full URLs</li>
297 <li><strong>Validates URLs</strong> — Identify and flag invalid URL formats</li>
298 <li><strong>Batch mode</strong> — Process hundreds of URLs via the standard Actor run</li>
299 </ul>
300 </section>
301
302 <section class="section">
303 <div class="section-label">Getting Started</div>
304 <h2>Quick start</h2>
305 <p>Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
306
307 <p style="margin-top:16px"><strong>Clean a URL (full output):</strong></p>
308 <pre>curl "https://superlativetech--superclean-urls.apify.actor?token=YOUR_TOKEN&input=https://example.com/%3Futm_source%3Dlinkedin%26fbclid%3Dabc123"</pre>
309
310 <p><strong>Extract domain only:</strong></p>
311 <pre>curl "https://superlativetech--superclean-urls.apify.actor?token=YOUR_TOKEN&input=https://www.example.com/about&style=domain"</pre>
312
313 <p><strong>Keep tracking params:</strong></p>
314 <pre>curl "https://superlativetech--superclean-urls.apify.actor?token=YOUR_TOKEN&input=https://example.com/%3Futm_source%3Dx&removeTracking=false"</pre>
315 </section>
316
317 <section class="section">
318 <div class="section-label">Reference</div>
319 <h2>Query parameters</h2>
320 <table>
321 <tr><th>Parameter</th><th>Required</th><th>Description</th></tr>
322 <tr><td><code>input</code></td><td>Yes</td><td>URL to clean</td></tr>
323 <tr><td><code>style</code></td><td>No</td><td>Output format: <code>full</code> (default) or <code>domain</code></td></tr>
324 <tr><td><code>forceHttps</code></td><td>No</td><td>Convert http to https (default: <code>true</code>)</td></tr>
325 <tr><td><code>removeTracking</code></td><td>No</td><td>Remove tracking parameters (default: <code>true</code>)</td></tr>
326 <tr><td><code>token</code></td><td>Yes</td><td>Your Apify API token</td></tr>
327 </table>
328 </section>
329
330 <section class="section">
331 <div class="section-label">Response</div>
332 <h2>Response format</h2>
333 <pre>{
334 "id": 1,
335 "input": "https://example.com/?utm_source=linkedin",
336 "output": "https://example.com",
337 "domain": "example.com",
338 "protocol": "https",
339 "path": "",
340 "query": "",
341 "hash": "",
342 "valid": true,
343 "confidence": 0.9
344}</pre>
345 </section>
346
347 <section class="section">
348 <div class="section-label">Cost</div>
349 <h2>Pricing</h2>
350 <p>Pay-per-event pricing on the Apify platform:</p>
351 <table>
352 <tr><th>Tier</th><th>URLs</th><th>Per 1,000</th></tr>
353 <tr><td>Base</td><td>Any</td><td>$0.50</td></tr>
354 <tr><td><span class="badge">Bronze</span></td><td>100+</td><td>$0.45</td></tr>
355 <tr><td><span class="badge">Silver</span></td><td>1,000+</td><td>$0.40</td></tr>
356 <tr><td><span class="badge">Gold</span></td><td>10,000+</td><td>$0.35</td></tr>
357 </table>
358 </section>
359
360 <section class="section">
361 <div class="section-label">Auth</div>
362 <h2>Authentication</h2>
363 <p>Authenticate using either method:</p>
364 <ul>
365 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
366 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
367 </ul>
368 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
369 </section>
370
371 <section class="section">
372 <div class="section-label">Superlative</div>
373 <h2>More from Superlative</h2>
374 <div class="product-grid">
375 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
376 <div class="product-name">Company Names <span class="status">Live</span></div>
377 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
378 </a>
379 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
380 <div class="product-name">Job Titles <span class="status">Live</span></div>
381 <p class="product-desc">Standardize job titles for outreach sequences.</p>
382 </a>
383 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
384 <div class="product-name">Person Names <span class="status">Live</span></div>
385 <p class="product-desc">Clean and format person names for personalization.</p>
386 </a>
387 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
388 <div class="product-name">Product Names <span class="status">Live</span></div>
389 <p class="product-desc">Normalize product and brand names from exports.</p>
390 </a>
391 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="product-card">
392 <div class="product-name">Places <span class="status">Live</span></div>
393 <p class="product-desc">Parse and standardize location strings.</p>
394 </a>
395 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="product-card">
396 <div class="product-name">Phone Numbers <span class="status">Live</span></div>
397 <p class="product-desc">Format and validate phone numbers.</p>
398 </a>
399 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="product-card">
400 <div class="product-name">Supernet DNS Lookup <span class="status">Live</span></div>
401 <p class="product-desc">Look up DNS records for any domain.</p>
402 </a>
403 <a href="https://apify.com/superlativetech/http-api?fpr=8e9l1" class="product-card">
404 <div class="product-name">HTTP API <span class="status">Live</span></div>
405 <p class="product-desc">General-purpose HTTP request utility.</p>
406 </a>
407 </div>
408 </section>
409
410 <footer>
411 <div class="footer-content">
412 <p class="footer-text">© 2026 Superlative</p>
413 <div class="footer-links">
414 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
415 </div>
416 </div>
417 </footer>
418</div>
419
420</body>
421</html>`;
422}