Fix duplicate first image in web clipper

This commit is contained in:
chrisf committed 2026-10-26 11:02:19 -06:00
1 parent 2cc202ce03
commit 906b18e7a6
1 file changed
+85 -4
+85 -4
View File
@@ -586,6 +586,72 @@ export function prepareDomForReadability(doc: Document, baseUrl: string): { hero
return { heroImgSrc };
}
/**
* Determines whether a rescued hero/lead image should be prepended to the article HTML.
* Prevents duplicating the first image when the article body already contains the lead image
* or when query parameters, CDNs, or ampersand encodings differ slightly from the article img src.
*/
export function shouldPrependHeroImage(contentHtml: string, heroImgSrc: string | null): boolean {
if (!heroImgSrc) return false;
// 1. Literal exact match
if (contentHtml.includes(heroImgSrc)) return false;
// 2. Base URL match (strip query string and hash)
const heroClean = heroImgSrc.split('?')[0].split('#')[0].trim().toLowerCase();
if (!heroClean) return false;
if (contentHtml.toLowerCase().includes(heroClean)) return false;
// 3. Filename match if filename is distinctive (> 5 chars)
const heroFilename = heroClean.split('/').pop() || '';
if (heroFilename.length > 5 && contentHtml.toLowerCase().includes(heroFilename)) {
return false;
}
// 4. Inspect actual <img> tags inside contentHtml
try {
const dom = new JSDOM(contentHtml);
const imgs = Array.from(dom.window.document.querySelectorAll('img'));
for (const img of imgs) {
const src = (img.getAttribute('src') || '').split('?')[0].split('#')[0].trim().toLowerCase();
const dataUrl = (
img.getAttribute('data-img-url') ||
img.getAttribute('data-src') ||
img.getAttribute('data-lazy-src') ||
img.getAttribute('data-original') ||
''
).split('?')[0].split('#')[0].trim().toLowerCase();
if (src && (src === heroClean || heroClean.endsWith(src) || src.endsWith(heroClean))) {
return false;
}
if (dataUrl && (dataUrl === heroClean || heroClean.endsWith(dataUrl) || dataUrl.endsWith(heroClean))) {
return false;
}
const srcFilename = src.split('/').pop() || '';
if (heroFilename.length > 5 && srcFilename.length > 5 && srcFilename === heroFilename) {
return false;
}
}
// 5. If the article already opens with a lead image (within first 200 chars of text),
// do not prepend an extra hero image over it
if (imgs.length > 0) {
const firstImg = imgs[0];
const htmlBefore = contentHtml.slice(0, contentHtml.indexOf(firstImg.outerHTML));
const textBefore = stripHtmlToText(htmlBefore).trim();
if (textBefore.length < 150) {
return false;
}
}
} catch {
return false;
}
return true;
}
/**
* Downloads remote <img src="https://..."> images in the clipped HTML,
* stores them locally as Attachments on the Note, and rewrites the HTML
@@ -672,6 +738,21 @@ export async function localizeClippedImages(
}
}
// Deduplicate consecutive identical leading images (e.g. hero image prepended before an article that already has it)
const remainingImgs = Array.from(doc.querySelectorAll('img'));
if (remainingImgs.length >= 2) {
const firstSrc = remainingImgs[0].getAttribute('src')?.trim();
const secondSrc = remainingImgs[1].getAttribute('src')?.trim();
if (firstSrc && firstSrc === secondSrc) {
const firstP = remainingImgs[0].closest('p');
if (firstP && firstP.textContent?.trim() === '') {
firstP.remove();
} else {
remainingImgs[0].remove();
}
}
}
return doc.body.innerHTML;
}
@@ -697,8 +778,8 @@ export async function importHtmlNote(
'Web Clip';
let contentHtml = parsed?.content || doc.body?.innerHTML || htmlString;
if (heroImgSrc && !contentHtml.includes(heroImgSrc)) {
contentHtml = `<p><img src="${escapeHtml(heroImgSrc)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
if (shouldPrependHeroImage(contentHtml, heroImgSrc)) {
contentHtml = `<p><img src="${escapeHtml(heroImgSrc!)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
}
const userId = await getCurrentUserId();
@@ -793,8 +874,8 @@ export async function clipUrlToNote(
title = options.title?.trim() || url;
}
contentHtml = `<p><strong><a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(title)}</a></strong></p>\n<p><em>(This site is protected by bot verification. To clip full article text and graphics, use the Everclone Web Clipper extension directly from your browser tab.)</em></p>\n<p>Source: <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a></p>`;
} else if (heroImgSrc && !contentHtml.includes(heroImgSrc)) {
contentHtml = `<p><img src="${escapeHtml(heroImgSrc)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
} else if (shouldPrependHeroImage(contentHtml, heroImgSrc)) {
contentHtml = `<p><img src="${escapeHtml(heroImgSrc!)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
}
const userId = await getCurrentUserId();