Repository navigation
Cleaner article capture: strip in-body clutter, keep section breaks and captions #608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fredrivett
wants to merge
4
commits into
main
Choose a base branch
from
fredrivett/dropcaps-scraping-improvement
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
fc90186
Strip in-body clutter and keep dividers and captions when capturing a…
fredrivett 5eef64d
Add section drop caps and tighter captions to the article reader
fredrivett 7d98fc8
Apply cleanup guards to the matched element itself
fredrivett 32f8097
Guard reader drop caps and caption spacing with a story play test
fredrivett File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
109 changes: 109 additions & 0 deletions
109
app/src/components/article/article-detail-view.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import type { Meta, StoryObj } from "@storybook/nextjs"; | ||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
| import { expect, waitFor } from "storybook/test"; | ||
| import { highlightsQueryKey } from "@/lib/highlights/use-highlights"; | ||
| import { ArticleDetailView } from "./article-detail-view"; | ||
|
|
||
| const STORY_ITEM_ID = "story-article-view"; | ||
|
|
||
| const SIMPLE_CONTENT = `Most improvements are small. They compound quietly: a slightly faster | ||
| build, a slightly clearer name, a test that catches the thing before it ships. | ||
|
|
||
| Nobody notices any one of them. Everybody notices the sum. | ||
|
|
||
| ## Start with the annoyance | ||
|
|
||
| Pick the thing that irritates you every day and fix just that.`; | ||
|
|
||
| // Shaped like a captured magazine feature: section breaks (from the source's | ||
| // dividers) and a photo caption kept as a figcaption | ||
| const SECTIONED_CONTENT = `**The light was already going** by the time we reached the harbour. The boats had come in and the nets lay drying along the wall, and nobody seemed in a hurry to be anywhere else. | ||
|
|
||
| We sat at the end of the jetty and talked about nothing much until the lamps came on behind us. | ||
|
|
||
| * * * | ||
|
|
||
| **Morning came in grey.** The town was quieter than it had been the night before, and the first ferry left half empty, its wake spreading slowly across the flat water of the bay. | ||
|
|
||
| “It’s always like this after a storm,” the harbourmaster said. “Give it a day.” | ||
|
|
||
|  | ||
|
|
||
| <figcaption>PHOTO BY A PHOTOGRAPHER</figcaption> | ||
|
|
||
| * * * | ||
|
|
||
| **By the third day** the weather had turned, and the harbour filled again with the noise of engines and gulls and people calling to each other across the water.`; | ||
|
|
||
| // The real view fetches highlights; seed none so it never hits the network | ||
| function withQueryClient() { | ||
| const client = new QueryClient({ | ||
| defaultOptions: { queries: { staleTime: Number.POSITIVE_INFINITY } }, | ||
| }); | ||
| client.setQueryData(highlightsQueryKey(STORY_ITEM_ID), []); | ||
| return client; | ||
| } | ||
|
|
||
| // The article reader: captured markdown rendered with the reader typography | ||
| const meta = { | ||
| title: "Article/ArticleDetailView", | ||
| component: ArticleDetailView, | ||
| parameters: { layout: "fullscreen" }, | ||
| tags: ["autodocs"], | ||
| args: { | ||
| itemId: STORY_ITEM_ID, | ||
| content: SIMPLE_CONTENT, | ||
| originalName: "How to make everything slightly better", | ||
| enableTracking: false, | ||
| }, | ||
| decorators: [ | ||
| (Story) => ( | ||
| <QueryClientProvider client={withQueryClient()}> | ||
| <div className="flex"> | ||
| <Story /> | ||
| </div> | ||
| </QueryClientProvider> | ||
| ), | ||
| ], | ||
| } satisfies Meta<typeof ArticleDetailView>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = {}; | ||
|
|
||
| // Each section after a break opens with a drop cap; the opening paragraph | ||
| // stays plain and the caption is set apart from the body text. The play test | ||
| // guards the rendered structure the reader's sibling selectors rely on | ||
| // (`hr + p`, `p:has(+ figcaption)`), which only exists if captured markdown | ||
| // still renders breaks and kept captions as siblings of the paragraphs. | ||
| export const Sectioned: Story = { | ||
| args: { content: SECTIONED_CONTENT, originalName: "After the storm" }, | ||
| play: async ({ canvasElement }) => { | ||
| await waitFor(() => | ||
| expect(canvasElement.querySelector("article figcaption")).not.toBeNull(), | ||
| ); | ||
| const firstLetterFloat = (element: Element | null) => { | ||
| if (!element) throw new Error("missing paragraph"); | ||
| return getComputedStyle(element, "::first-letter").float; | ||
| }; | ||
|
|
||
| const sectionOpeners = canvasElement.querySelectorAll("article hr + p"); | ||
| expect(sectionOpeners).toHaveLength(2); | ||
| for (const opener of sectionOpeners) { | ||
| expect(firstLetterFloat(opener)).toBe("left"); | ||
| } | ||
| expect(firstLetterFloat(canvasElement.querySelector("article p"))).toBe( | ||
| "none", | ||
| ); | ||
|
|
||
| const caption = canvasElement.querySelector("article figcaption"); | ||
| const image = caption?.previousElementSibling?.querySelector("img"); | ||
| if (!caption || !image) throw new Error("caption isn't next to its image"); | ||
| const gap = | ||
| caption.getBoundingClientRect().top - | ||
| image.getBoundingClientRect().bottom; | ||
| expect(gap).toBeGreaterThanOrEqual(0); | ||
| expect(gap).toBeLessThan(16); | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
app/src/lib/__fixtures__/article-sectioned-magazine.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| <!doctype html> | ||
| <!-- | ||
| Synthetic fixture modelled on a WordPress magazine article layout (structure | ||
| and class names as captured from a real long-form feature; prose replaced | ||
| with placeholder text). Exercises the in-body clutter, SVG section dividers | ||
| and classed captions handled by article-cleanup.ts. | ||
| --> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <title>The Long Feature</title> | ||
| </head> | ||
| <body> | ||
| <header class="site-header"> | ||
| <nav> | ||
| <a href="/">Home</a> | ||
| <a class="menu-item__link menu-item__link--subscribe" href="/subscribe">Subscribe</a> | ||
| </nav> | ||
| </header> | ||
| <div class="article-sponsor-desktop"> | ||
| <a href="https://sponsor.example.com/">Presented by Sponsor Co</a> | ||
| </div> | ||
| <main> | ||
| <article class="article-body"> | ||
| <h1>The Long Feature</h1> | ||
| <div class="share"><a href="https://x.com/share">Share on X</a></div> | ||
| <div class="podcast-listen"> | ||
| <input type="checkbox" class="podcast-listen__checkbox" /> | ||
| <div class="podcast-listen__label"> | ||
| <label class="podcast-listen__label-toggle button">Listen on</label> | ||
| <div class="podcast-listen__label-name">Listen on:</div> | ||
| </div> | ||
| <div class="podcast-links"> | ||
| <a class="podcast-links__item" href="https://podcasts.example.com/">Apple Podcasts</a> | ||
| </div> | ||
| </div> | ||
| <div class="article-body__content"> | ||
| <div class="block subscribe-cta"> | ||
| <div class="subscribe-cta__text"> | ||
| <p><em><a href="/subscribe">Subscribe</a> to print for your office or home.</em></p> | ||
| </div> | ||
| <div class="block divider divider--style-line"><div class="divider__line"></div></div> | ||
| </div> | ||
|
|
||
| <p class="wp-block-paragraph">T<strong>he opening line.</strong> The first section begins here with enough words to read as a real paragraph of long-form prose, describing the scene in some detail so that the extractor treats it as the start of the article body rather than a fragment of navigation or furniture around it.</p> | ||
| <p class="wp-block-paragraph">A second paragraph continues the first section, adding more sustained prose about the people involved, the place where the interview happened, and the way the conversation unfolded over the course of a long afternoon in a quiet room overlooking the water outside.</p> | ||
| <p class="wp-block-paragraph">A third paragraph closes the first section with a quotation and a short reflection on what it might mean, keeping the prose going long enough that Readability scores this container as the main content of the page.</p> | ||
|
|
||
| <div class="block single-sponsor"> | ||
| <a href="https://sponsor.example.com/" class="single-sponsor__image"><img src="https://example.com/sponsor-banner.jpg" alt="" /></a> | ||
| </div> | ||
|
|
||
| <div class="block divider divider--style-icon"> | ||
| <svg width="38" height="20" viewBox="0 0 38 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 19 L19 1 L37 19" stroke="currentColor" /></svg> | ||
| </div> | ||
|
|
||
| <p class="dropcap wp-block-paragraph"><strong>The second section</strong> opens after an ornamental divider, picking the story up somewhere new with another long paragraph of prose that carries the reader forward through the next part of the narrative and introduces a new set of characters.</p> | ||
| <p class="wp-block-paragraph">More of the second section follows here, again written at length so the body reads as continuous prose, with details about the history of the subject and the decisions that shaped the years that followed the events described above.</p> | ||
|
|
||
| <div class="block read-more"> | ||
| <div class="read-more__title">Read More</div> | ||
| <div class="read-more__articles"> | ||
| <a class="read-more__article" href="/article/another-story/"> | ||
| <img src="https://example.com/another-story.jpg" alt="" /> | ||
| <h3>Another Story Entirely</h3> | ||
| <p>A short teaser for a different article that the publisher would like you to read next.</p> | ||
| <span>By Someone Else</span> | ||
| </a> | ||
| <a class="read-more__article" href="/article/a-third-story/"> | ||
| <img src="https://example.com/a-third-story.jpg" alt="" /> | ||
| <h3>A Third Story</h3> | ||
| <p>Another teaser line promoting a third, unrelated article from the same publisher.</p> | ||
| <span>By A Third Writer</span> | ||
| </a> | ||
| </div> | ||
| </div> | ||
|
|
||
| <p class="wp-block-paragraph">The second section continues after the promotional block, and this paragraph must survive cleanup intact, because it is part of the article itself and sits directly between two pieces of publisher furniture on the page.</p> | ||
|
|
||
| <div class="block single-image"> | ||
| <div class="single-image__image"><img src="https://example.com/photo.jpg" alt="" width="2560" height="1741" /></div> | ||
| <div class="single-image__caption"> | ||
| <div class="single-image__caption-short"><p>PHOTO BY A PHOTOGRAPHER</p></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="block divider divider--style-icon"> | ||
| <svg width="38" height="20" viewBox="0 0 38 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 19 L19 1 L37 19" stroke="currentColor" /></svg> | ||
| </div> | ||
|
|
||
| <p class="dropcap wp-block-paragraph"><strong>The third section</strong> begins after a second divider and runs to the end of the piece, with a final stretch of prose that brings the story to a close and returns to the scene described in the opening paragraph of the article.</p> | ||
| <p class="wp-block-paragraph">* * *</p> | ||
| <p class="wp-block-paragraph">A closing coda after an asterism break, written as one more long paragraph so that it reads as prose, ending the article on a quiet note that looks back over everything that came before it in the piece.</p> | ||
|
|
||
| <div class="block guest-subscribe-form"> | ||
| <p>Get the newsletter in your inbox every week.</p> | ||
| <form><input type="email" /><button>Sign up</button></form> | ||
| </div> | ||
| </div> | ||
| </article> | ||
| <section class="recommended-items"> | ||
| <h2>Recommended</h2> | ||
| <a href="/article/one/">One</a> | ||
| <a href="/article/two/">Two</a> | ||
| </section> | ||
| </main> | ||
| <footer class="site-footer"><p>© The Publisher</p></footer> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.