Fix duplicate first image in web clipper
This commit is contained in:
1 parent
2cc202ce03
commit
906b18e7a6
1 file changed
+85
-4
+85
-4
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user