Metehan: Visual Query Fan-Out Analysis in Screaming Frog
- th3s3rp4nt
- 27. Okt. 2025
- 7 Min. Lesezeit
Key Takeaways:
Metehan developed a script to run via Screaming Frog that analysis meaningful images of websites automatically
Google’s investing heavily in visual understanding. Their visual search fan-out technique runs multiple queries in the background based on comprehensive image analysis.
With this tool you can check whether your images provide meaning to search engines and LLMs to add value to your content OR rather whether your visuals meet your product or brand identity



Here’s what happens when you run it:
Find the important images – It skips logos, UI elements, tracking pixels and focuses on product images, content images, galleries
Comprehensive visual analysis – The AI identifies everything: primary subjects, secondary objects, visible text, colors, materials, styles, implied use cases
Generate search queries – For each element and combination of elements, it creates queries real people might actually search
Code for the full analysis in Screaming Frogs custom JS executions:
// Screaming Frog Custom JavaScript - Visual Query Fan-out using OpenAI Vision API
// Enhanced version with per-image query breakdown
const OPENAI_API_KEY = 'sk-proj-xxxxx'; // Replace with your OpenAI API key
const MODEL = 'gpt-4o-mini'; // Options: gpt-4o, gpt-4o-mini, gpt-4-turbo
// Configuration
const CONFIG = {
maxImages: 5, // Limit images to process
minImageSize: 200, // Minimum dimension in pixels
maxTokens: 1000, // Max response tokens per image
mainContentSelectors: [ // Selectors for main content areas
'main img',
'article img',
'.product-image img',
'.product-gallery img',
'.content img',
'#content img',
'.post-content img',
'[role="main"] img',
'.product img',
'.gallery img',
'[data-testid*="product"] img',
'.pdp-image img', // Product detail page
'.product-photo img'
]
};
// Helper to check if image is in main content area
function isMainContentImage(img) {
// Check if image matches any main content selector
for (const selector of CONFIG.mainContentSelectors) {
if (img.matches(selector)) {
return true;
}
}
// Check parent containers
const parent = img.closest('main, article, .product, .content, #content');
if (parent) return true;
// Check if it's a product image by URL/attributes
const src = (img.src || '').toLowerCase();
const className = (img.className || '').toLowerCase();
if (src.includes('product') || src.includes('item') ||
className.includes('product') || className.includes('gallery')) {
return true;
}
// Skip UI elements
if (src.includes('logo') || src.includes('icon') || src.includes('sprite') ||
src.includes('pixel') || src.includes('tracking') || src.includes('analytics')) {
return false;
}
return false;
}
// Get absolute URL for image
function getAbsoluteUrl(url) {
if (!url) return null;
if (url.startsWith('data:')) return null; // Skip data URLs
if (url.startsWith('http://') || url.startsWith('https://')) return url;
if (url.startsWith('//')) return window.location.protocol + url;
if (url.startsWith('/')) return window.location.origin + url;
return new URL(url, window.location.href).href;
}
// Extract main content images from page
function extractMainContentImages() {
const images = [];
const processedUrls = new Set();
const allImages = document.querySelectorAll('img');
for (let img of allImages) {
if (!isMainContentImage(img)) continue;
if (img.naturalWidth >= CONFIG.minImageSize && img.naturalHeight >= CONFIG.minImageSize) {
const src = getAbsoluteUrl(img.src || img.dataset.src || img.dataset.lazySrc);
if (!src || processedUrls.has(src)) continue;
processedUrls.add(src);
images.push({
url: src,
alt: img.alt || '',
title: img.title || '',
width: img.naturalWidth,
height: img.naturalHeight
});
if (images.length >= CONFIG.maxImages) break;
}
}
return images;
}
// Call OpenAI Vision API with image URL directly
function analyzeImageWithOpenAI(imageUrl, imageInfo) {
const prompt = 'Analyze this e-commerce/website image and provide a comprehensive visual search fan-out analysis.\n\n' +
'Tasks:\n' +
'1. Identify ALL objects, products, and visual elements in the image\n' +
'2. Extract any visible text, brands, or labels\n' +
'3. Determine the image context (product shot, lifestyle, detail view, etc.)\n' +
'4. Generate search queries that users might use to find similar items\n\n' +
'For each detected object/product, provide:\n' +
'- Specific descriptive label (e.g., "men\'s blue denim jacket" not just "jacket")\n' +
'- Visual attributes (color, material, style, pattern, size)\n' +
'- Category classification\n' +
'- Brand if identifiable\n' +
'- 3-5 search queries someone might use to find this item\n\n' +
'Return your analysis as a JSON object with this exact structure:\n' +
'{\n' +
' "objects": [\n' +
' {\n' +
' "label": "specific item name",\n' +
' "category": "category",\n' +
' "attributes": {\n' +
' "color": "color",\n' +
' "material": "material",\n' +
' "style": "style",\n' +
' "brand": "brand if visible"\n' +
' },\n' +
' "search_queries": ["query1", "query2", "query3"]\n' +
' }\n' +
' ],\n' +
' "text_detected": ["any visible text"],\n' +
' "scene_analysis": {\n' +
' "type": "product/lifestyle/studio",\n' +
' "composition": "description",\n' +
' "purpose": "hero/gallery/detail"\n' +
' },\n' +
' "fan_out_queries": ["top search query 1", "top search query 2", "top search query 3", "top search query 4", "top search query 5"]\n' +
'}';
try {
const requestData = {
model: MODEL,
messages: [
{
role: "user",
content: [
{
type: "text",
text: prompt
},
{
type: "image_url",
image_url: {
url: imageUrl,
detail: "high" // "low", "high", or "auto"
}
}
]
}
],
max_tokens: CONFIG.maxTokens,
temperature: 0.2,
response_format: { type: "json_object" }
};
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.openai.com/v1/chat/completions', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + OPENAI_API_KEY);
xhr.send(JSON.stringify(requestData));
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.choices && response.choices[0] && response.choices[0].message) {
const content = response.choices[0].message.content;
try {
return JSON.parse(content);
} catch (e) {
// Try to clean and parse
const cleaned = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
return JSON.parse(cleaned);
}
}
} else {
const error = JSON.parse(xhr.responseText);
throw new Error(error.error?.message || 'API request failed');
}
} catch (e) {
return null;
}
}
// Main execution
try {
const startTime = Date.now();
const images = extractMainContentImages();
if (images.length === 0) {
return seoSpider.data('No main content images found on page. Checked ' + CONFIG.mainContentSelectors.length + ' content selectors.');
}
const results = [];
let processedCount = 0;
let totalObjects = 0;
const errors = [];
const allQueries = new Set();
// Process each image with OpenAI
for (let i = 0; i < images.length; i++) {
const image = images[i];
// Call OpenAI Vision API directly with URL
const analysis = analyzeImageWithOpenAI(image.url, image);
if (analysis) {
processedCount++;
if (analysis.objects) {
totalObjects += analysis.objects.length;
}
// Collect all queries
if (analysis.fan_out_queries) {
analysis.fan_out_queries.forEach(q => allQueries.add(q));
}
if (analysis.objects) {
analysis.objects.forEach(obj => {
if (obj.search_queries) {
obj.search_queries.forEach(q => allQueries.add(q));
}
});
}
results.push({
image: image,
data: analysis
});
} else {
errors.push('Failed to analyze: ' + image.url);
}
}
// Prepare output
let output = '=== OPENAI VISION QUERY FAN-OUT ANALYSIS ===\n\n';
output += 'URL: ' + window.location.href + '\n';
output += 'Model: ' + MODEL + '\n';
output += 'Processing Time: ' + (Date.now() - startTime) + 'ms\n\n';
output += '=== SUMMARY ===\n';
output += '• Images Found: ' + images.length + '\n';
output += '• Images Processed: ' + processedCount + '\n';
output += '• Total Objects Detected: ' + totalObjects + '\n';
output += '• Unique Queries Generated: ' + allQueries.size + '\n';
output += '• API Errors: ' + errors.length + '\n\n';
if (processedCount > 0) {
// Show detailed per-image analysis with queries
output += '=== PER-IMAGE ANALYSIS WITH QUERY FAN-OUT ===\n';
output += '━'.repeat(60) + '\n';
results.forEach((result, idx) => {
if (result.data) {
output += '\n📷 IMAGE ' + (idx + 1) + '\n';
output += '━'.repeat(60) + '\n';
// Image details
output += '📍 URL: ' + result.image.url.substring(result.image.url.lastIndexOf('/') + 1).substring(0, 60) + '\n';
output += '📏 Dimensions: ' + result.image.width + 'x' + result.image.height + 'px\n';
if (result.image.alt) {
output += '🏷️ Alt Text: "' + result.image.alt + '"\n';
}
// Scene Analysis
if (result.data.scene_analysis) {
output += '\n🎬 Scene Analysis:\n';
output += ' • Type: ' + result.data.scene_analysis.type + '\n';
output += ' • Purpose: ' + result.data.scene_analysis.purpose + '\n';
if (result.data.scene_analysis.composition) {
output += ' • Composition: ' + result.data.scene_analysis.composition + '\n';
}
}
// Detected objects
if (result.data.objects && result.data.objects.length > 0) {
output += '\n🔍 Detected Objects (' + result.data.objects.length + '):\n';
result.data.objects.forEach((obj, objIdx) => {
output += ' ' + (objIdx + 1) + '. ' + obj.label;
if (obj.category) output += ' [' + obj.category + ']';
output += '\n';
if (obj.attributes) {
const attrs = [];
Object.entries(obj.attributes).forEach(([key, val]) => {
if (val && val !== 'null' && val !== 'unknown') {
attrs.push(key + ': ' + val);
}
});
if (attrs.length > 0) {
output += ' Attributes: ' + attrs.join(', ') + '\n';
}
}
});
}
// Text detected
if (result.data.text_detected && result.data.text_detected.length > 0) {
output += '\n📝 Text/Brands Detected:\n';
result.data.text_detected.forEach(text => {
output += ' • ' + text + '\n';
});
}
// Collect all queries for this image
const imageQueries = new Set();
// Add fan-out queries
if (result.data.fan_out_queries) {
result.data.fan_out_queries.forEach(q => imageQueries.add(q));
}
// Add object-specific queries
if (result.data.objects) {
result.data.objects.forEach(obj => {
if (obj.search_queries) {
obj.search_queries.forEach(q => imageQueries.add(q));
}
});
}
// Display all queries for this image
if (imageQueries.size > 0) {
output += '\n🎯 Visual Query Fan-Out for This Image (' + imageQueries.size + ' queries):\n';
const imageQueriesArray = Array.from(imageQueries);
// Group queries by type if possible
const topQueries = result.data.fan_out_queries || [];
const objectQueries = [];
if (result.data.objects) {
result.data.objects.forEach(obj => {
if (obj.search_queries) {
obj.search_queries.forEach(q => {
if (!topQueries.includes(q)) {
objectQueries.push(q);
}
});
}
});
}
if (topQueries.length > 0) {
output += ' 📌 Primary Queries:\n';
topQueries.forEach((query, i) => {
output += ' ' + (i + 1) + '. "' + query + '"\n';
});
}
if (objectQueries.length > 0) {
output += ' 📦 Object-Specific Queries:\n';
objectQueries.slice(0, 10).forEach((query, i) => {
output += ' ' + (i + 1) + '. "' + query + '"\n';
});
}
}
output += '\n' + '─'.repeat(60) + '\n';
}
});
// Show aggregated top queries
output += '\n=== TOP QUERIES ACROSS ALL IMAGES ===\n';
const queriesArray = Array.from(allQueries);
queriesArray.slice(0, 15).forEach((query, idx) => {
output += (idx + 1) + '. "' + query + '"\n';
});
}
if (errors.length > 0) {
output += '\n=== ERRORS ===\n';
errors.forEach(err => {
output += '• ' + err + '\n';
});
output += '\nNote: Check API key and rate limits if errors occur.\n';
}
return seoSpider.data(output);
} catch (error) {
return seoSpider.error('Script Error: ' + error.toString() + '\n\nMake sure your OpenAI API key is valid.');
}
Sources:





