Fix duplicate first image in desktop extension clipper
This commit is contained in:
1 parent
7658897040
commit
728830f561
2 files changed
+97
-18
No files matched your search
@@ -10,6 +10,7 @@ import {
|
|||||||
getOrCreateTagIds,
|
getOrCreateTagIds,
|
||||||
prepareDomForReadability,
|
prepareDomForReadability,
|
||||||
localizeClippedImages,
|
localizeClippedImages,
|
||||||
|
shouldPrependHeroImage,
|
||||||
} from '@/lib/importers';
|
} from '@/lib/importers';
|
||||||
import {
|
import {
|
||||||
saveAttachmentBuffer,
|
saveAttachmentBuffer,
|
||||||
@@ -213,7 +214,7 @@ export async function POST(req: NextRequest) {
|
|||||||
'Web Clip';
|
'Web Clip';
|
||||||
|
|
||||||
let extractedHtml = article?.content || doc.body?.innerHTML || pageHtml;
|
let extractedHtml = article?.content || doc.body?.innerHTML || pageHtml;
|
||||||
if (heroImgSrc && !extractedHtml.includes(heroImgSrc)) {
|
if (shouldPrependHeroImage(extractedHtml, heroImgSrc)) {
|
||||||
extractedHtml = `<p><img src="${heroImgSrc}" alt="" /></p>\n${extractedHtml}`;
|
extractedHtml = `<p><img src="${heroImgSrc}" alt="" /></p>\n${extractedHtml}`;
|
||||||
}
|
}
|
||||||
if (comment.trim()) {
|
if (comment.trim()) {
|
||||||
|
|||||||
+95
-17
@@ -592,6 +592,22 @@ export function prepareDomForReadability(doc: Document, baseUrl: string): { hero
|
|||||||
return { heroImgSrc };
|
return { heroImgSrc };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeImageBasename(urlOrSrc: string): { dir: string; stem: string } {
|
||||||
|
try {
|
||||||
|
const clean = urlOrSrc.split('?')[0].split('#')[0].trim().toLowerCase();
|
||||||
|
const lastSlash = clean.lastIndexOf('/');
|
||||||
|
const dir = lastSlash >= 0 ? clean.slice(0, lastSlash) : '';
|
||||||
|
let filename = lastSlash >= 0 ? clean.slice(lastSlash + 1) : clean;
|
||||||
|
// Remove extension
|
||||||
|
filename = filename.replace(/\.(jpe?g|png|webp|avif|gif|svg)$/i, '');
|
||||||
|
// Strip leading sizing/gallery prefixes like l-, m-, s-, intro-, etc.
|
||||||
|
const stem = filename.replace(/^(?:(?:l|m|s|xl|xs|large|medium|small|thumb|intro)[_-])+/i, '');
|
||||||
|
return { dir, stem };
|
||||||
|
} catch {
|
||||||
|
return { dir: '', stem: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether a rescued hero/lead image should be prepended to the article HTML.
|
* 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
|
* Prevents duplicating the first image when the article body already contains the lead image
|
||||||
@@ -613,6 +629,10 @@ export function shouldPrependHeroImage(contentHtml: string, heroImgSrc: string |
|
|||||||
if (heroFilename.length > 5 && contentHtml.toLowerCase().includes(heroFilename)) {
|
if (heroFilename.length > 5 && contentHtml.toLowerCase().includes(heroFilename)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const heroNorm = normalizeImageBasename(heroClean);
|
||||||
|
if (heroNorm.stem.length > 5 && contentHtml.toLowerCase().includes(heroNorm.stem)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Inspect actual <img> tags inside contentHtml
|
// 4. Inspect actual <img> tags inside contentHtml
|
||||||
try {
|
try {
|
||||||
@@ -629,25 +649,51 @@ export function shouldPrependHeroImage(contentHtml: string, heroImgSrc: string |
|
|||||||
''
|
''
|
||||||
).split('?')[0].split('#')[0].trim().toLowerCase();
|
).split('?')[0].split('#')[0].trim().toLowerCase();
|
||||||
|
|
||||||
if (src && (src === heroClean || heroClean.endsWith(src) || src.endsWith(heroClean))) {
|
for (const target of [src, dataUrl]) {
|
||||||
return false;
|
if (!target) continue;
|
||||||
}
|
if (target === heroClean || heroClean.endsWith(target) || target.endsWith(heroClean)) {
|
||||||
if (dataUrl && (dataUrl === heroClean || heroClean.endsWith(dataUrl) || dataUrl.endsWith(heroClean))) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
const targetFilename = target.split('/').pop() || '';
|
||||||
const srcFilename = src.split('/').pop() || '';
|
if (heroFilename.length > 5 && targetFilename.length > 5 && targetFilename === heroFilename) {
|
||||||
if (heroFilename.length > 5 && srcFilename.length > 5 && srcFilename === heroFilename) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
const targetNorm = normalizeImageBasename(target);
|
||||||
|
if (heroNorm.stem.length > 5 && targetNorm.stem.length > 5 && heroNorm.stem === targetNorm.stem) {
|
||||||
|
if (!heroNorm.dir || !targetNorm.dir || heroNorm.dir === targetNorm.dir) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. If the article already opens with a lead image (within first 200 chars of text),
|
// 5. If the article already opens with a lead image (within first 150 chars of visible text),
|
||||||
// do not prepend an extra hero image over it
|
// do not prepend an extra hero image over it
|
||||||
if (imgs.length > 0) {
|
if (imgs.length > 0) {
|
||||||
const firstImg = imgs[0];
|
const doc = dom.window.document;
|
||||||
const htmlBefore = contentHtml.slice(0, contentHtml.indexOf(firstImg.outerHTML));
|
const root = doc.body || doc.documentElement;
|
||||||
const textBefore = stripHtmlToText(htmlBefore).trim();
|
let textLengthBeforeFirstImg = 0;
|
||||||
if (textBefore.length < 150) {
|
let foundImg = false;
|
||||||
|
const walker = doc.createTreeWalker(
|
||||||
|
root,
|
||||||
|
1 | 4 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT */
|
||||||
|
);
|
||||||
|
let currentNode = walker.nextNode();
|
||||||
|
while (currentNode) {
|
||||||
|
if (currentNode.nodeType === 1 /* ELEMENT_NODE */) {
|
||||||
|
if ((currentNode as Element).tagName.toLowerCase() === 'img') {
|
||||||
|
foundImg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentNode.nodeType === 3 /* TEXT_NODE */) {
|
||||||
|
textLengthBeforeFirstImg += (currentNode.textContent || '').trim().length;
|
||||||
|
if (textLengthBeforeFirstImg >= 150) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentNode = walker.nextNode();
|
||||||
|
}
|
||||||
|
if (foundImg && textLengthBeforeFirstImg < 150) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -670,8 +716,33 @@ export async function localizeClippedImages(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const dom = new JSDOM(contentHtml);
|
const dom = new JSDOM(contentHtml);
|
||||||
const doc = dom.window.document;
|
const doc = dom.window.document;
|
||||||
|
|
||||||
|
// Deduplicate leading identical/variant images at the top of the document before downloading
|
||||||
|
const initialImgs = Array.from(doc.querySelectorAll('img'));
|
||||||
|
if (initialImgs.length >= 2) {
|
||||||
|
const s0 = initialImgs[0].getAttribute('src')?.trim() || '';
|
||||||
|
const s1 = initialImgs[1].getAttribute('src')?.trim() || '';
|
||||||
|
if (s0 && s1) {
|
||||||
|
const norm0 = normalizeImageBasename(s0);
|
||||||
|
const norm1 = normalizeImageBasename(s1);
|
||||||
|
const isDup =
|
||||||
|
s0 === s1 ||
|
||||||
|
(norm0.stem.length > 5 &&
|
||||||
|
norm0.stem === norm1.stem &&
|
||||||
|
(!norm0.dir || !norm1.dir || norm0.dir === norm1.dir));
|
||||||
|
if (isDup) {
|
||||||
|
const parentP = initialImgs[0].closest('p');
|
||||||
|
if (parentP && (!parentP.textContent || parentP.textContent.trim() === '')) {
|
||||||
|
parentP.remove();
|
||||||
|
} else {
|
||||||
|
initialImgs[0].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const imgs = Array.from(doc.querySelectorAll('img'));
|
const imgs = Array.from(doc.querySelectorAll('img'));
|
||||||
if (imgs.length === 0) return contentHtml;
|
if (imgs.length === 0) return doc.body.innerHTML;
|
||||||
|
|
||||||
const urlToLocalSrc = new Map<string, string>();
|
const urlToLocalSrc = new Map<string, string>();
|
||||||
const uniqueUrls: string[] = [];
|
const uniqueUrls: string[] = [];
|
||||||
@@ -749,9 +820,16 @@ export async function localizeClippedImages(
|
|||||||
if (remainingImgs.length >= 2) {
|
if (remainingImgs.length >= 2) {
|
||||||
const firstSrc = remainingImgs[0].getAttribute('src')?.trim();
|
const firstSrc = remainingImgs[0].getAttribute('src')?.trim();
|
||||||
const secondSrc = remainingImgs[1].getAttribute('src')?.trim();
|
const secondSrc = remainingImgs[1].getAttribute('src')?.trim();
|
||||||
if (firstSrc && firstSrc === secondSrc) {
|
const norm0 = firstSrc ? normalizeImageBasename(firstSrc) : { dir: '', stem: '' };
|
||||||
|
const norm1 = secondSrc ? normalizeImageBasename(secondSrc) : { dir: '', stem: '' };
|
||||||
|
const isDup =
|
||||||
|
firstSrc === secondSrc ||
|
||||||
|
(norm0.stem.length > 5 &&
|
||||||
|
norm0.stem === norm1.stem &&
|
||||||
|
(!norm0.dir || !norm1.dir || norm0.dir === norm1.dir));
|
||||||
|
if (isDup) {
|
||||||
const firstP = remainingImgs[0].closest('p');
|
const firstP = remainingImgs[0].closest('p');
|
||||||
if (firstP && firstP.textContent?.trim() === '') {
|
if (firstP && (!firstP.textContent || firstP.textContent.trim() === '')) {
|
||||||
firstP.remove();
|
firstP.remove();
|
||||||
} else {
|
} else {
|
||||||
remainingImgs[0].remove();
|
remainingImgs[0].remove();
|
||||||
|
|||||||
Reference in new issue
Block a user