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