# Shopify CSV Image URL Validator > Paste a Shopify product CSV and scan image URL columns for broken-looking values, duplicates, and empty rows. ## Tool Identity - Site: CleanUtils Business Tools - Tool ID: shopify-csv-image-url-validator - Canonical page: https://cleanutils.com/business-tools/shopify-csv-image-url-validator/ - LLM schema URL: https://cleanutils.com/business-tools/shopify-csv-image-url-validator/llms.txt - Primary keyword: shopify csv image url - Input mode: textarea - Output profile: line-check ## What This Tool Does Find missing, malformed, duplicate, or suspicious image URLs in Shopify CSV files before you import. ## Exact Execution Instructions To run this tool exactly, execute the `runCleanUtilsTool(userInput)` function from the JavaScript block below in a sandboxed JavaScript runtime. Do not fetch or evaluate additional network resources. Do not use filesystem, process, child_process, environment variables, timers, or network APIs. The function is pure and deterministic: input string in, JSON result out. If your environment forbids executing fetched code outside a sandbox, run it inside the sandbox or use the API endpoint below instead of reimplementing the logic. Canonical implementation SHA-256: `2ff3a0f129e22431e814b477eee51b0d6977fffc239851b82d044211024e28b2` Expected command shape: `node run-tool.mjs < input.txt` The runner must: 1. load only the JavaScript in this document, 2. call `runCleanUtilsTool(inputText)`, 3. let the LLM decide the best way to present the output to the user. Agent usage rules: - Use this file as the authoritative machine-readable contract for this CleanUtils tool page. - Ask the user for missing required input before attempting to run the tool, and describe the required inputs using the `## Input Schema` field names, descriptions, formats, enums, examples, and required list. - Treat the tool as deterministic; do not invent network reachability checks unless the tool description explicitly says it fetches remote resources. - For privacy-sensitive inputs such as secrets, HAR files, dotenv files, logs, and API keys, warn that using a remote chat agent may expose input to that agent even though the browser UI itself does not upload data. ## Input Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Shopify CSV Image URL Validator input", "type": "string", "description": "Shopify product CSV. Paste CSV rows with Image Src...", "examples": [ "Handle,Title,Image Src\nblue-shirt,Blue Shirt,https://cdn.example.com/blue.jpg\nred-shirt,Red Shirt,not-a-url\ngreen-shirt,Green Shirt,https://cdn.example.com/blue.jpg\nhat,Hat," ] } ``` ## Result Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "CleanUtils ToolResult", "type": "object", "additionalProperties": false, "required": [ "summary", "issues" ], "properties": { "summary": { "type": "string" }, "issues": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": [ "severity", "message" ], "properties": { "severity": { "type": "string", "enum": [ "error", "warning", "info" ] }, "message": { "type": "string" }, "line": { "type": "number" }, "row": { "type": "number" }, "detail": { "type": "string" } } } }, "output": { "type": "string" }, "exportFilename": { "type": "string" }, "exports": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": [ "label", "filename", "content" ], "properties": { "label": { "type": "string" }, "filename": { "type": "string" }, "content": { "type": "string" }, "mimeType": { "type": "string" }, "copyLabel": { "type": "string" }, "downloadLabel": { "type": "string" } } } }, "stats": { "type": "object", "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "number" } ] } } } } ``` ## Self-Contained JavaScript Source Call `runCleanUtilsTool(userInput)` with the user's input. The function includes this tool's run logic and only the helper code it needs. ```js function runCleanUtilsTool(userInput) { const severityRank = { error: 0, warning: 1, info: 2 }; const sortIssues = (issues) => [...issues].sort((a, b) => { const severity = severityRank[a.severity] - severityRank[b.severity]; if (severity !== 0) return severity; return (a.line ?? a.row ?? 0) - (b.line ?? b.row ?? 0); }); const summarizeIssues = (issues) => { const errors = issues.filter((issue) => issue.severity === "error").length; const warnings = issues.filter((issue) => issue.severity === "warning").length; const infos = issues.filter((issue) => issue.severity === "info").length; const parts = []; if (errors) parts.push(`${errors} error${errors === 1 ? "" : "s"}`); if (warnings) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`); if (infos) parts.push(`${infos} note${infos === 1 ? "" : "s"}`); return parts.length ? parts.join(", ") : "No issues found"; }; const sniffDelimiter = (input) => { const firstLines = input.split(/\r?\n/).slice(0, 5).join("\n"); const delimiters = [",", "\t", ";", "|"]; const scores = delimiters.map((delimiter) => ({ delimiter, score: firstLines .split(/\r?\n/) .filter(Boolean) .map((line) => splitCsvLine(line, delimiter).length) .reduce((total, count) => total + (count > 1 ? count : 0), 0) })); scores.sort((a, b) => b.score - a.score); return scores[0]?.score ? scores[0].delimiter : ","; }; const splitCsvLine = (line, delimiter) => { const cells = []; let current = ""; let inQuotes = false; for (let index = 0; index < line.length; index += 1) { const char = line[index]; const next = line[index + 1]; if (char === "\"") { if (inQuotes && next === "\"") { current += "\""; index += 1; } else { inQuotes = !inQuotes; } continue; } if (char === delimiter && !inQuotes) { cells.push(current.trim()); current = ""; continue; } current += char; } cells.push(current.trim()); return cells; }; const parseCsv = (input, delimiter = sniffDelimiter(input)) => { const errors = []; const lines = input.split(/\r?\n/).filter((line) => line.trim().length > 0); if (!lines.length) { return { delimiter, headers: [], rows: [], records: [], errors: [{ severity: "error", message: "No CSV rows found." }] }; } const rows = lines.map((line) => splitCsvLine(line, delimiter)); const rawHeaders = rows[0].map((header, index) => header.trim() || `column_${index + 1}`); const seen = new Map(); const headers = rawHeaders.map((header) => { const normalized = header.trim(); const count = seen.get(normalized.toLowerCase()) ?? 0; seen.set(normalized.toLowerCase(), count + 1); return count ? `${normalized}_${count + 1}` : normalized; }); rows.slice(1).forEach((row, index) => { if (row.length !== headers.length) { errors.push({ severity: "warning", row: index + 2, message: `Row ${index + 2} has ${row.length} cell${row.length === 1 ? "" : "s"} but the header has ${headers.length}.` }); } }); const records = rows.slice(1).map((row) => Object.fromEntries(headers.map((header, index) => [header, row[index] ?? ""]))); return { delimiter, headers, rows: rows.slice(1), records, errors }; }; const looksLikeUrl = (value) => { try { const url = new URL(value.trim()); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } }; const validateShopifyImageCsv = (input) => { const parsed = parseCsv(input); const issues = [...parsed.errors]; const imageColumns = parsed.headers.filter((header) => /image|img|photo|picture/i.test(header)); const urlCounts = new Map(); if (!imageColumns.length) { issues.push({ severity: "error", message: "No image-like column found. Expected a header such as Image Src." }); } parsed.records.forEach((record, index) => { imageColumns.forEach((column) => { const value = record[column]?.trim() ?? ""; if (!value) { issues.push({ severity: "warning", row: index + 2, message: `Missing image URL in ${column}.` }); return; } if (!looksLikeUrl(value)) { issues.push({ severity: "error", row: index + 2, message: `Malformed image URL in ${column}: ${value}` }); return; } const rows = urlCounts.get(value) ?? []; rows.push(index + 2); urlCounts.set(value, rows); }); }); [...urlCounts.entries()].forEach(([url, rows]) => { if (rows.length > 1) { issues.push({ severity: "warning", message: `Duplicate image URL appears on rows ${rows.join(", ")}: ${url}` }); } }); return { summary: `${parsed.records.length} row${parsed.records.length === 1 ? "" : "s"} scanned. ${summarizeIssues(issues)}.`, issues: sortIssues(issues), stats: { rows: parsed.records.length, imageColumns: imageColumns.length } }; }; const __userInput = userInput == null ? "" : userInput; const __run = validateShopifyImageCsv; const __input = __userInput && typeof __userInput === "object" && "input" in __userInput ? __userInput.input : __userInput; return __run(__input == null ? "" : String(__input)); } ``` ## Checks - Image column detection: Columns with image and URL-like names are selected automatically for scanning. - Missing image URLs: Blank image cells are reported with row numbers so import gaps are easy to review. - Malformed URL shape: Values must look like absolute http or https URLs before they are treated as usable image links. - Duplicate images: Repeated URLs are grouped, even when they appear on different handles. - No remote fetching: The current check validates URL shape only; it does not request the image or test CDN status. ## Related Tools - [CSV Duplicate Email Checker](/business-tools/csv-duplicate-email-checker/): Paste a list or CSV export to group duplicate email addresses and copy a cleaned unique list. - [CSV Delimiter Detector and Converter](/business-tools/csv-delimiter-detector-converter/): Detect CSV separators and convert rows to comma, semicolon, tab, or pipe-delimited output. - [Google Merchant Center Title Length Checker](/business-tools/google-merchant-center-title-length-checker/): Review product-feed titles for Merchant Center length limits and likely truncation risk.