-
{profile.name}
+
{profile.name}
{getProfileSummary(profile)}
diff --git a/client/src/utils/businessProfile.js b/client/src/utils/businessProfile.js
index 43cfded..96f6e39 100644
--- a/client/src/utils/businessProfile.js
+++ b/client/src/utils/businessProfile.js
@@ -14,6 +14,16 @@ function firstNonEmptyText(...values) {
return '';
}
+function getScrapedLogoUrl(entity = {}) {
+ const branding = entity?.scrapeArtifacts?.json?.branding;
+
+ return firstNonEmptyText(
+ branding?.primaryLogoUrl,
+ Array.isArray(branding?.logoCandidates) ? branding.logoCandidates[0] : '',
+ Array.isArray(branding?.logos) ? branding.logos[0] : ''
+ );
+}
+
function extractDomainName(domain) {
if (!domain) return '';
if (typeof domain === 'string') return normalizeText(domain);
@@ -170,11 +180,14 @@ export function getBusinessTagline(entity) {
export function getBusinessImage(entity) {
const relevantImage = normalizeList(entity?.relevantImagePaths)[0];
- if (relevantImage) return relevantImage;
+ const scrapedLogo = getScrapedLogoUrl(entity);
return (
- entity?.imageUrl
- || entity?.logoUrl
+ entity?.logoUrl
+ || scrapedLogo
+ || entity?.imageUrl
+ || entity?.previewImagePath
+ || relevantImage
|| entity?.brandImageUrl
|| entity?.image
|| ''
diff --git a/server/routes/businesses.js b/server/routes/businesses.js
index ee8ccd8..5636ccf 100644
--- a/server/routes/businesses.js
+++ b/server/routes/businesses.js
@@ -143,6 +143,7 @@ const RUNTIME_TOKEN_LABELS = {
};
const CURL_DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
const DETERMINISTIC_SENDER_ID_KEYS = ['sender_id', 'senderId', 'sender', 'sender_code', 'senderCode'];
+const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
function createHttpError(status, message, extra = {}) {
const err = new Error(message);
@@ -218,6 +219,23 @@ function validateSenderId(senderId) {
return null;
}
+function buildAutomaticProfileName(source = {}) {
+ const senderId = normalizeSenderId(
+ source?.provider?.senderId
+ || source?.senderId
+ );
+
+ return senderId || PENDING_SENDER_ID_PROFILE_NAME;
+}
+
+function syncAutomaticProfileName(profile = {}) {
+ if (profile?.isAutoNamed !== true) return profile;
+
+ profile.name = buildAutomaticProfileName(profile);
+ profile.isAutoNamed = true;
+ return profile;
+}
+
function getSenderIdFromStructuredValue(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
@@ -558,9 +576,10 @@ function getProfileDisplayValue(profile = {}, key) {
}
function serializeProfile(profile = {}) {
- const hydratedProfile = hydrateProfile(profile);
+ const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
return {
...hydratedProfile,
+ isAutoNamed: hydratedProfile.isAutoNamed === true,
rawCurl: undefined,
rawCurlTemplate: undefined,
profileInputValues: undefined,
@@ -607,7 +626,7 @@ function sanitizeStoredCurlAnalysis(profile = {}) {
}
function persistableProfile(profile = {}) {
- const hydratedProfile = hydrateProfile(profile);
+ const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
const normalizedAuthKey = firstNonEmptyResolvedText(
hydratedProfile.profileInputValues?.authKey,
hydratedProfile.provider?.authKey,
@@ -616,6 +635,7 @@ function persistableProfile(profile = {}) {
return {
id: hydratedProfile.id,
name: normalizeText(hydratedProfile.name),
+ isAutoNamed: hydratedProfile.isAutoNamed === true,
rawCurl: getStoredCurlTemplate(hydratedProfile),
isDefault: hydratedProfile.isDefault === true,
provider: {
@@ -791,22 +811,124 @@ function normalizeUrlList(value) {
});
}
+const LOGO_URL_POSITIVE_REGEX = /(?:^|[\/_.-])(logo|wordmark|brandmark|site-logo|header-logo)(?:[\/_.-]|$)/i;
+const LOGO_URL_ICON_REGEX = /(?:^|[\/_.-])(favicon|apple-touch-icon|mask-icon|mstile|icon)(?:[\/_.-]|$)/i;
+const LOGO_URL_NEGATIVE_REGEX = /(hero|banner|product|products|collection|collections|slide|carousel|thumbnail|thumb|promo|cover|background)/i;
+const LOGO_URL_SOCIAL_REGEX = /(facebook|instagram|twitter|linkedin|youtube|pinterest|avatar|profile)/i;
+
+function isAbsoluteHttpUrl(value) {
+ const normalized = normalizeText(value);
+ if (!normalized) return false;
+
+ try {
+ const url = new URL(normalized);
+ return url.protocol === 'http:' || url.protocol === 'https:';
+ } catch {
+ return false;
+ }
+}
+
+function scoreLogoCandidateUrl(url, sources = new Set()) {
+ const normalized = normalizeText(url).toLowerCase();
+ if (!isAbsoluteHttpUrl(normalized)) return -100;
+
+ let score = 0;
+
+ if (sources.has('branding')) score += 70;
+ if (sources.has('model')) score += 45;
+ if (sources.has('relevant') && (LOGO_URL_POSITIVE_REGEX.test(normalized) || LOGO_URL_ICON_REGEX.test(normalized))) score += 12;
+ if (sources.has('crawl') && (LOGO_URL_POSITIVE_REGEX.test(normalized) || LOGO_URL_ICON_REGEX.test(normalized))) score += 6;
+
+ if (LOGO_URL_POSITIVE_REGEX.test(normalized)) score += 30;
+ if (LOGO_URL_ICON_REGEX.test(normalized)) score += 18;
+ if (normalized.endsWith('.svg') || normalized.includes('.svg?')) score += 12;
+ if (normalized.endsWith('.png') || normalized.includes('.png?')) score += 6;
+ if (normalized.endsWith('.webp') || normalized.includes('.webp?')) score += 3;
+
+ if (LOGO_URL_NEGATIVE_REGEX.test(normalized)) score -= 35;
+ if (LOGO_URL_SOCIAL_REGEX.test(normalized)) score -= 25;
+ if (/\/products?\//i.test(normalized) || /\/collections?\//i.test(normalized)) score -= 20;
+
+ return score;
+}
+
+function collectLogoCandidates(crawlSummary = {}, brandContext = {}) {
+ const candidatesByUrl = new Map();
+ let order = 0;
+
+ function append(values, source) {
+ normalizeUrlList(values).forEach((url) => {
+ if (!isAbsoluteHttpUrl(url)) return;
+
+ if (!candidatesByUrl.has(url)) {
+ candidatesByUrl.set(url, {
+ url,
+ sources: new Set(),
+ order,
+ });
+ order += 1;
+ }
+
+ candidatesByUrl.get(url).sources.add(source);
+ });
+ }
+
+ append([brandContext?.logoUrl], 'model');
+ append([crawlSummary?.branding?.primaryLogoUrl], 'branding');
+ append(crawlSummary?.branding?.logos, 'branding');
+ append(crawlSummary?.branding?.logoCandidates, 'branding');
+ append(brandContext?.relevantImageUrls, 'relevant');
+ append(crawlSummary?.topImages, 'crawl');
+
+ return Array.from(candidatesByUrl.values())
+ .map((candidate) => ({
+ ...candidate,
+ score: scoreLogoCandidateUrl(candidate.url, candidate.sources),
+ }))
+ .sort((left, right) => right.score - left.score || left.order - right.order);
+}
+
+function selectCanonicalLogoSourceUrl(crawlSummary = {}, brandContext = {}) {
+ const bestCandidate = collectLogoCandidates(crawlSummary, brandContext)[0];
+ if (!bestCandidate || bestCandidate.score <= 0) return '';
+
+ if (
+ bestCandidate.sources.has('model')
+ || bestCandidate.sources.has('branding')
+ || LOGO_URL_POSITIVE_REGEX.test(bestCandidate.url)
+ || LOGO_URL_ICON_REGEX.test(bestCandidate.url)
+ ) {
+ return bestCandidate.url;
+ }
+
+ return '';
+}
+
function getBusinessPreviewSummary(source = {}) {
const taglines = Array.isArray(source?.taglines)
? source.taglines.map((tagline) => normalizeText(tagline)).filter(Boolean)
: [];
+ const logoUrl = normalizeText(source?.logoUrl);
const relevantImagePaths = Array.isArray(source?.relevantImagePaths)
? source.relevantImagePaths.map((path) => normalizeText(path)).filter(Boolean)
: [];
return {
previewTagline: taglines[0] || '',
- previewImagePath: relevantImagePaths[0] || '',
+ previewImagePath: logoUrl || relevantImagePaths[0] || '',
};
}
-function mergeBusinessSummary(baseBusiness = {}, context = null) {
- const previewSummary = getBusinessPreviewSummary(context || baseBusiness);
+function mergeBusinessSummary(baseBusiness = {}, context = null, crawlSummary = null) {
+ const logoUrl = firstNonEmptyText(
+ context?.logoUrl,
+ baseBusiness?.logoUrl,
+ selectCanonicalLogoSourceUrl(crawlSummary, context || baseBusiness)
+ );
+ const previewSummary = getBusinessPreviewSummary({
+ ...(context || baseBusiness),
+ logoUrl,
+ });
const relevantImagePaths = normalizeUrlList(
Array.isArray(baseBusiness?.relevantImagePaths) && baseBusiness.relevantImagePaths.length
? baseBusiness.relevantImagePaths
@@ -815,8 +937,9 @@ function mergeBusinessSummary(baseBusiness = {}, context = null) {
return {
...baseBusiness,
+ logoUrl,
previewTagline: normalizeText(baseBusiness.previewTagline) || previewSummary.previewTagline,
- previewImagePath: normalizeText(baseBusiness.previewImagePath) || previewSummary.previewImagePath,
+ previewImagePath: logoUrl || normalizeText(baseBusiness.previewImagePath) || previewSummary.previewImagePath,
relevantImagePaths,
};
}
@@ -897,7 +1020,12 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
const existingBusiness = await findBusinessByApplicationId(merchantId, applicationId);
if (existingBusiness) {
const existingContext = await fetchJSON(businessRoot(merchantId, existingBusiness.businessId), 'context').catch(() => null);
- const mergedBusiness = existingContext ? { ...existingContext } : mergeBusinessSummary(existingBusiness);
+ const mergedBusiness = existingContext
+ ? {
+ ...existingContext,
+ ...mergeBusinessSummary(existingBusiness, existingContext, crawlSummary),
+ }
+ : mergeBusinessSummary(existingBusiness, null, crawlSummary);
return {
business: {
...mergedBusiness,
@@ -913,13 +1041,29 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
const bizRoot = businessRoot(merchantId, businessId);
const imagesFolder = `${bizRoot}/images`;
const imagePaths = [];
+ const uploadedImageBySourceUrl = new Map();
const imageCandidates = normalizeUrlList(brandContext?.relevantImageUrls);
for (let i = 0; i < Math.min(imageCandidates.length, 6); i += 1) {
const uploaded = await uploadImageFromUrl(imageCandidates[i], imagesFolder, `image_${i + 1}`);
- if (uploaded) imagePaths.push(uploaded);
+ if (uploaded) {
+ imagePaths.push(uploaded);
+ uploadedImageBySourceUrl.set(imageCandidates[i], uploaded);
+ }
}
+ const selectedLogoSourceUrl = selectCanonicalLogoSourceUrl(crawlSummary, brandContext);
+ const logoUploadUrl = selectedLogoSourceUrl
+ ? (
+ uploadedImageBySourceUrl.get(selectedLogoSourceUrl)
+ || await uploadImageFromUrl(selectedLogoSourceUrl, imagesFolder, 'logo')
+ )
+ : '';
+ const logoUrl = normalizeText(logoUploadUrl || selectedLogoSourceUrl);
+ const relevantImagePaths = normalizeUrlList(
+ logoUrl ? [logoUrl, ...imagePaths] : imagePaths
+ );
+
let domain = normalizeText(crawlSummary?.domain);
if (!domain) {
try {
@@ -940,7 +1084,8 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
tone: brandContext.tone || 'professional',
taglines: Array.isArray(brandContext.taglines) ? brandContext.taglines : [],
colors: Array.isArray(brandContext.colors) ? brandContext.colors : [],
- relevantImagePaths: imagePaths,
+ logoUrl,
+ relevantImagePaths,
aboutSummary: normalizeText(brandContext.aboutSummary) || extractAboutSummary(crawlSummary),
websiteUrl,
crawlStats: crawlSummary?.siteStats || {},
@@ -959,6 +1104,7 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
applicationId,
brandName: contextJson.brandName,
domain: contextJson.domain,
+ logoUrl: contextJson.logoUrl,
previewTagline: previewSummary.previewTagline,
previewImagePath: previewSummary.previewImagePath,
relevantImagePaths: normalizeUrlList(contextJson.relevantImagePaths),
@@ -970,7 +1116,7 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
return {
business: {
...contextJson,
- scrapeArtifacts: buildScrapeArtifacts(crawlSummary, imagePaths),
+ scrapeArtifacts: buildScrapeArtifacts(crawlSummary, relevantImagePaths),
},
reusedExistingBusiness: false,
};
@@ -2456,6 +2602,8 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
}
profile.updatedAt = now;
+ profile.isAutoNamed = baseProfile.isAutoNamed === true
+ || (!Object.prototype.hasOwnProperty.call(baseProfile, 'isAutoNamed') && !normalizeText(baseProfile.name));
profile.provider = normalizeProvider({
...profile.provider,
authKey: '',
@@ -2464,7 +2612,7 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
profile.rawCurl = getStoredCurlTemplate(profile);
profile.rawCurlTemplate = profile.rawCurl;
- return profile;
+ return syncAutomaticProfileName(profile);
}
function collectProfileInputPatch(payload = {}) {
@@ -2518,7 +2666,7 @@ function applyProfileInputPatch(profile, patch = {}) {
}, profile.updatedAt);
profile.profileInputValues = normalizeProfileInputValues(profile.profileInputValues);
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
- return profile;
+ return syncAutomaticProfileName(profile);
}
function validateProfileInputPatch(patch = {}) {
@@ -2554,9 +2702,14 @@ async function validateCurlAndExtractProvider(rawCurl) {
throw createHttpError(422, senderIdError);
}
+ const normalizedProvider = normalizeProvider({
+ ...provider,
+ ...(resolvedSenderId ? { senderId: resolvedSenderId } : {}),
+ });
+
return {
...validation,
- provider,
+ provider: normalizedProvider,
};
} catch (err) {
if (err.status) throw err;
@@ -2573,15 +2726,23 @@ router.get('/', async (req, res) => {
const businesses = await getIndex(merchantId);
const hydratedBusinesses = await Promise.all(
businesses.map(async (business) => {
- const hasPreviewSummary = normalizeText(business.previewTagline) || normalizeText(business.previewImagePath);
+ const hasPreviewSummary = normalizeText(business.previewTagline)
+ || normalizeText(business.previewImagePath)
+ || normalizeText(business.logoUrl);
const hasRelevantImagePaths = Array.isArray(business.relevantImagePaths) && business.relevantImagePaths.length > 0;
+ const hasLogoUrl = normalizeText(business.logoUrl);
- if (hasPreviewSummary && hasRelevantImagePaths) {
+ if (hasPreviewSummary && (hasRelevantImagePaths || hasLogoUrl)) {
return mergeBusinessSummary(business);
}
- const context = await fetchJSON(businessRoot(merchantId, business.businessId), 'context').catch(() => null);
- return mergeBusinessSummary(business, context);
+ const root = businessRoot(merchantId, business.businessId);
+ const [context, crawlSummary] = await Promise.all([
+ fetchJSON(root, 'context').catch(() => null),
+ fetchJSON(root, 'crawl_summary').catch(() => null),
+ ]);
+
+ return mergeBusinessSummary(business, context, crawlSummary);
})
);
@@ -2684,12 +2845,13 @@ router.get('/:businessId', async (req, res) => {
if (!context) return res.status(404).json({ error: 'Business not found' });
if (!crawlSummary) {
- return res.json(context);
+ return res.json(mergeBusinessSummary(context));
}
+ const business = mergeBusinessSummary(context, null, crawlSummary);
res.json({
- ...context,
- scrapeArtifacts: buildScrapeArtifacts(crawlSummary, context.relevantImagePaths),
+ ...business,
+ scrapeArtifacts: buildScrapeArtifacts(crawlSummary, business.relevantImagePaths),
});
} catch (err) {
res.status(500).json({ error: err.message });
@@ -2993,9 +3155,6 @@ router.get('/:businessId/global-sms/profiles/:profileId/delete-impact', async (r
router.post('/:businessId/global-sms/profiles', async (req, res) => {
try {
const { name, rawCurl, setActive } = req.body;
- if (!normalizeText(name)) {
- return res.status(400).json({ error: 'name is required' });
- }
if (!normalizeText(rawCurl)) {
return res.status(400).json({ error: 'rawCurl is required' });
}
@@ -3007,11 +3166,13 @@ router.post('/:businessId/global-sms/profiles', async (req, res) => {
const { profileData } = await getProfileState(bizRoot);
const now = new Date().toISOString();
const normalizedCurl = normalizeText(rawCurl);
+ const normalizedRequestedName = normalizeText(name);
const validation = await validateCurlAndExtractProvider(normalizedCurl);
const newProfile = buildStoredProfileFromValidation({
id: uuidv4(),
- name: normalizeText(name),
+ ...(normalizedRequestedName ? { name: normalizedRequestedName } : {}),
+ isAutoNamed: !normalizedRequestedName,
isDefault: false,
createdAt: now,
updatedAt: now,
@@ -3053,7 +3214,10 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
const profile = profileData.profiles.find(p => p.id === profileId);
if (!profile) return res.status(404).json({ error: 'Profile not found' });
- if (name !== undefined) profile.name = normalizeText(name);
+ if (name !== undefined) {
+ profile.name = normalizeText(name);
+ profile.isAutoNamed = false;
+ }
applyProfileInputPatch(profile, profilePatch);
await saveGlobalSmsProfiles(bizRoot, profileData);
diff --git a/server/services/crawlSummary.js b/server/services/crawlSummary.js
index 5f69dd6..d968ebf 100644
--- a/server/services/crawlSummary.js
+++ b/server/services/crawlSummary.js
@@ -6,6 +6,38 @@ function normalizeList(value) {
return Array.isArray(value) ? value : [];
}
+function isAbsoluteHttpUrl(value) {
+ const normalized = normalizeText(value);
+ if (!normalized) return false;
+
+ try {
+ const parsed = new URL(normalized);
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
+ } catch {
+ return false;
+ }
+}
+
+function collectImageUrls(value, bucket = []) {
+ if (Array.isArray(value)) {
+ value.forEach((entry) => collectImageUrls(entry, bucket));
+ return bucket;
+ }
+
+ if (typeof value === 'string') {
+ if (isAbsoluteHttpUrl(value)) bucket.push(value);
+ return bucket;
+ }
+
+ if (!value || typeof value !== 'object') {
+ return bucket;
+ }
+
+ const url = normalizeText(value.url || value.href || value.src || value.secure_url);
+ if (isAbsoluteHttpUrl(url)) bucket.push(url);
+ return bucket;
+}
+
function uniqueStrings(values) {
const seen = new Set();
return normalizeList(values)
@@ -76,6 +108,46 @@ function dedupeLinks(links) {
});
}
+function scoreLogoCandidate(url) {
+ const normalized = normalizeText(url).toLowerCase();
+ if (!normalized) return -100;
+
+ let score = 0;
+
+ if (/logo|brandmark|wordmark|logomark/.test(normalized)) score += 40;
+ if (/favicon|apple-touch-icon|android-chrome|mstile|site-icon|siteicon|icon/.test(normalized)) score += 25;
+ if (/header|navbar|nav/.test(normalized)) score += 8;
+
+ if (/hero|banner|carousel|slider|product|collection|catalog|lookbook/.test(normalized)) score -= 35;
+ if (/social|facebook|instagram|twitter|linkedin|youtube|pinterest|avatar|profile/.test(normalized)) score -= 25;
+ if (/sprite|tracking|pixel|placeholder/.test(normalized)) score -= 40;
+
+ return score;
+}
+
+function rankLogoCandidates(values) {
+ return uniqueStrings(values)
+ .map((url, index) => ({
+ index,
+ score: scoreLogoCandidate(url),
+ url,
+ }))
+ .sort((left, right) => right.score - left.score || left.index - right.index)
+ .map((entry) => entry.url);
+}
+
+function extractMetadataImageCandidates(metadata = {}) {
+ const candidates = [];
+
+ Object.entries(metadata || {}).forEach(([key, value]) => {
+ const normalizedKey = normalizeText(key).toLowerCase();
+ if (!/(image|logo|icon|favicon|thumbnail|apple)/.test(normalizedKey)) return;
+ collectImageUrls(value, candidates);
+ });
+
+ return rankLogoCandidates(candidates).filter((url) => scoreLogoCandidate(url) > 0);
+}
+
function summarizePage(page, pageType) {
const metadata = page?.metadata && typeof page.metadata === 'object' ? page.metadata : {};
@@ -101,7 +173,7 @@ function buildRepresentativeTextBlocks(homepage, aboutPage, productPages) {
}));
}
-function flattenBranding(homepage) {
+function flattenBranding(homepage, topImages = []) {
const branding = homepage?.branding && typeof homepage.branding === 'object' ? homepage.branding : {};
const colorEntries = [];
const logos = [];
@@ -142,12 +214,22 @@ function flattenBranding(homepage) {
const brandName = normalizeText(branding.brandName || branding.brand_name || branding.name);
if (brandName) brandNames.push(brandName);
+ const metadataImageCandidates = extractMetadataImageCandidates(homepage?.metadata || {});
+ const topLogoCandidates = rankLogoCandidates(topImages).filter((url) => scoreLogoCandidate(url) > 0);
+ const logoCandidates = uniqueStrings([
+ ...logos,
+ ...metadataImageCandidates,
+ ...topLogoCandidates,
+ ]);
+
return {
colors: uniqueStrings(colorEntries.map((entry) => entry.hex)),
labeledColors: colorEntries.filter((entry, index, values) => (
values.findIndex((candidate) => candidate.name === entry.name && candidate.hex === entry.hex) === index
)),
logos: uniqueStrings(logos),
+ logoCandidates,
+ primaryLogoUrl: logoCandidates[0] || '',
brandNames: uniqueStrings(brandNames),
};
}
@@ -188,7 +270,7 @@ function buildCrawlSummary(data = {}, startUrlOverride = '') {
...normalizeList(aboutRaw?.images),
...productRawPages.flatMap((page) => normalizeList(page?.images)),
]).slice(0, 60);
- const branding = flattenBranding(homepageRaw);
+ const branding = flattenBranding(homepageRaw, topImages);
return {
startUrl,
diff --git a/server/services/openai2.js b/server/services/openai2.js
index 789fe20..7ee7fa4 100644
--- a/server/services/openai2.js
+++ b/server/services/openai2.js
@@ -1006,6 +1006,7 @@ async function parseBrandContext(scrapedData = {}) {
` "tone": "one of ${BRAND_CONTEXT_TONE_OPTIONS.join(', ')}",`,
' "taglines": ["up to 3 strings"],',
' "colors": ["hex colors only"],',
+ ' "logoUrl": "absolute http(s) URL for the best company logo or empty string",',
' "relevantImageUrls": ["3-5 absolute http(s) image URLs only"],',
' "aboutSummary": "2-4 concise customer-facing sentences"',
'}',
@@ -1014,6 +1015,8 @@ async function parseBrandContext(scrapedData = {}) {
'- No markdown.',
'- No explanatory prose.',
'- Do not copy the About page verbatim.',
+ '- `logoUrl` must be a real brand logo or wordmark when one is available. Prefer storefront/header logos from branding_json.logos.',
+ '- If no credible logo is present, return an empty string for `logoUrl`.',
'- Exclude icons, tracking pixels, and data URLs from images.',
'',
`start_url: ${String(scrapedData.startUrl || '')}`,
@@ -1037,12 +1040,14 @@ async function parseBrandContext(scrapedData = {}) {
});
const normalizedTone = normalizeText(String(result.tone || '')).toLowerCase();
+ const normalizedLogoUrl = normalizeText(String(result.logoUrl || ''));
return {
brandName: normalizeText(String(result.brandName || '')) || 'Unknown Brand',
tone: BRAND_CONTEXT_TONE_OPTIONS.includes(normalizedTone) ? normalizedTone : 'professional',
taglines: sanitizeStringArray(result.taglines, { maxItems: 3 }),
colors: sanitizeStringArray(result.colors),
+ logoUrl: isAbsoluteHttpUrl(normalizedLogoUrl) ? normalizedLogoUrl : '',
relevantImageUrls: sanitizeStringArray(result.relevantImageUrls, { maxItems: 5, allowUrlsOnly: true }),
aboutSummary: normalizeText(String(result.aboutSummary || '')),
};