From 0806665b9466b0400a5007e068de29fbd71aea7e Mon Sep 17 00:00:00 2001 From: Jason Shellen Date: Thu, 13 Aug 2026 16:37:33 -0700 Subject: [PATCH] Fix hero image eaten by its own dedup pass; launch on Explore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reader fixes for 0.5.1: - The image dedup pass in renderArticle pre-seeds the hero thumbnail URL into its seen-set, then walked every img INCLUDING the hero — deleting the hero as a duplicate of itself (and the matching body lead image right after). Most visible after "Fetch full content" adds an og:image to the frontmatter: the refetched article lost its top image entirely. The pass now skips images inside .article-hero. Fixes the re-fetch image wipe; heroes render at all again. - On launch the dashboard (Explore) renders but the sidebar nav kept its hardcoded "All Items" highlight. Default the active nav to Explore in the markup and re-sync it after init renders the hub. Fixes #110 Both covered by a new Playwright spec (hero-image.spec.ts) verified red on the old code and green on the fix. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AdXEsWQTadb9qbTQjLkMqn --- src/viewer-html.ts | 2 +- tests/e2e/hero-image.spec.ts | 53 ++++++++++++++++++++++++++++++++++++ viewer.html | 4 +-- viewer/04-article.js | 5 ++++ viewer/13-init.js | 7 ++++- 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/hero-image.spec.ts diff --git a/src/viewer-html.ts b/src/viewer-html.ts index 68810528..1baf1060 100644 --- a/src/viewer-html.ts +++ b/src/viewer-html.ts @@ -1,3 +1,3 @@ // Auto-generated by scripts/embed-viewer.ts — do not edit // Source: viewer.html + viewer.css + viewer/*.js + @fontsource/* woff2 -export const VIEWER_HTML = "\n\n\n\n\nPull Read\n\n\n\n\n\n\n\n\n\n\n\n\n\nSkip to content\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n
\n\n
\n \n Pull Read\n \n
\n
\n\n
\n \n\n \n\n
\n
\n Sources\n \n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n \n \n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n
\n\n\n\n\n"; +export const VIEWER_HTML = "\n\n\n\n\nPull Read\n\n\n\n\n\n\n\n\n\n\n\n\n\nSkip to content\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n
\n\n
\n \n Pull Read\n \n
\n
\n\n
\n \n\n \n\n
\n
\n Sources\n \n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n \n \n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n
\n\n\n\n\n"; diff --git a/tests/e2e/hero-image.spec.ts b/tests/e2e/hero-image.spec.ts new file mode 100644 index 00000000..ad02a846 --- /dev/null +++ b/tests/e2e/hero-image.spec.ts @@ -0,0 +1,53 @@ +// ABOUTME: Regression test for the article hero image surviving image dedup. +// ABOUTME: The dedup pass pre-seeds the hero URL; it must skip the hero itself. + +import { test, expect } from '@playwright/test'; + +// Article with a frontmatter image AND the same image repeated in the body — +// the exact shape produced by "Fetch full content" adding an og:image. +const ARTICLE = `--- +title: "Hero Dedup Regression" +url: https://example.com/hero-test +bookmarked: 2026-08-12T10:00:00.000Z +domain: example.com +image: https://example.com/lead.jpg?w=1200 +source: extracted +--- + +![Lead](https://example.com/lead.jpg?w=800 "Lead") + +Some body text that follows the lead image. + +![Other](https://example.com/other.jpg) +`; + +test('hero image survives dedup; body duplicate is removed', async ({ page }) => { + await page.goto('/'); + await page.waitForFunction(() => typeof (window as any).renderArticle === 'function'); + + await page.evaluate((text) => { + (window as any).renderArticle(text, 'hero-test.md'); + }, ARTICLE); + + // The hero must exist and still contain its image (it was previously + // removed as a "duplicate" of its own pre-seeded URL). + const heroImg = page.locator('.article-hero img'); + await expect(heroImg).toHaveCount(1); + await expect(heroImg).toHaveAttribute('src', /lead\.jpg/); + + // The body copy of the same image (same path, different params) is deduped… + const bodyLeads = page.locator('#content .content-wrap img[src*="lead.jpg"]'); + await expect(bodyLeads).toHaveCount(0); + + // …while distinct body images are untouched. + const otherImg = page.locator('#content img[src*="other.jpg"]'); + await expect(otherImg).toHaveCount(1); +}); + +test('launch highlights Explore in the sidebar nav (#110)', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('.sidebar-nav-item'); + const active = page.locator('.sidebar-nav-item.active'); + await expect(active).toHaveCount(1); + await expect(active).toHaveAttribute('data-nav', 'explore'); +}); diff --git a/viewer.html b/viewer.html index c01aff94..3f27d373 100644 --- a/viewer.html +++ b/viewer.html @@ -62,8 +62,8 @@