|
| 1 | +# Implementation Steps: Content Extractor |
| 2 | + |
| 3 | +## Step 1: `ExtractedContent` record |
| 4 | + |
| 5 | +- [x] Record with `title`, `excerpt`, `body`, `author`, `categories`, `publishedDate`, `previewImage`, `locale`; `categories` normalized to non-null |
| 6 | + |
| 7 | +**Related behaviors:** all (return shape) |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Step 2: `ContentExtractor` — body extraction |
| 12 | + |
| 13 | +- [x] `@Component` taking `ContentLocaleResolver` + `ObjectMapper` |
| 14 | +- [x] Read metadata first, then strip `script`/`style`/`noscript`/`template` + comments (so JSON-LD survives) |
| 15 | +- [x] Container selection: `contentSelector` comma list (first non-empty match), `body` = whole document, readability fallback (semantic tag, else largest div/section) when nothing matches |
| 16 | +- [x] Remove `contentExclude` selectors from the container |
| 17 | +- [x] Whitespace-normalized, block-separated body (`\n\n` between paragraphs); fallback to collapsed container text |
| 18 | + |
| 19 | +**Related behaviors:** selector container; comma fallback; body selector; scripts/styles removed; contentExclude; whitespace normalized; readability fallback; empty container |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## Step 3: `ContentExtractor` — metadata extraction |
| 24 | + |
| 25 | +- [x] title: `og:title` → `<title>` → first `<h1>` |
| 26 | +- [x] excerpt: `description` → `og:description` |
| 27 | +- [x] previewImage: `og:image` |
| 28 | +- [x] publishedDate: `article:published_time` → `<time datetime>` → JSON-LD `datePublished` |
| 29 | +- [x] author: `author`/`article:author` meta → JSON-LD `author.name` |
| 30 | +- [x] categories: `article:tag` metas → JSON-LD `keywords` |
| 31 | +- [x] locale: from `ContentLocaleResolver` (path rule) |
| 32 | +- [x] JSON-LD parsed via Jackson, following arrays and `@graph` |
| 33 | +- [x] Graceful degradation to null when fields are absent |
| 34 | + |
| 35 | +**Related behaviors:** OpenGraph metadata; JSON-LD date/author; article tags; locale; metadata independent of selector; missing metadata |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Step 4: Tests |
| 40 | + |
| 41 | +- [x] `ContentExtractorTest` (14 cases) — body selection/cleaning + metadata + edge cases |
| 42 | + |
| 43 | +**Acceptance criteria:** |
| 44 | +- [x] All tests pass (`mvn test`); build green |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Behavior Coverage |
| 49 | + |
| 50 | +| Scenario | Layer | Covered in Step | |
| 51 | +|----------|-------|-----------------| |
| 52 | +| Selector picks the article container | Backend | Step 4 (`selectorPicksArticleContainer`) | |
| 53 | +| Comma-separated fallback list, first match wins | Backend | Step 4 (`commaSeparatedFallbackFirstMatchWins`) | |
| 54 | +| body selector takes the whole document | Backend | Step 4 (`bodySelectorTakesWholeDocument`) | |
| 55 | +| Scripts and styles are always removed | Backend | Step 4 (`scriptsAndStylesAreAlwaysRemoved`) | |
| 56 | +| contentExclude removes boilerplate | Backend | Step 4 (`contentExcludeRemovesBoilerplate`) | |
| 57 | +| Whitespace is normalized | Backend | Step 4 (`whitespaceIsNormalized`) | |
| 58 | +| Readability fallback when no selector matches | Backend | Step 4 (`readabilityFallbackWhenNoSelectorMatches`) | |
| 59 | +| OpenGraph metadata is read | Backend | Step 4 (`openGraphMetadataIsRead`) | |
| 60 | +| JSON-LD fallback for date/author | Backend | Step 4 (`jsonLdFallbackForDateAndAuthor`) | |
| 61 | +| Categories from article tags | Backend | Step 4 (`categoriesFromArticleTags`) | |
| 62 | +| Locale derived from path | Backend | Step 4 (`localeDerivedFromPath`) | |
| 63 | +| Metadata read regardless of content selector | Backend | Step 4 (`metadataReadRegardlessOfContentSelector`) | |
| 64 | +| Missing metadata degrades gracefully | Backend | Step 4 (`missingMetadataDegradesGracefully`) | |
| 65 | +| Empty content container | Backend | Step 4 (`emptyContentContainerFallsBack`) | |
| 66 | + |
| 67 | +All scenarios are backend; there is no frontend in this spec. |
| 68 | + |
| 69 | +## Notes |
| 70 | + |
| 71 | +- Excerpt open question: kept `null` when no `description`/`og:description` (no body-snippet fallback) — the behavior allows null. |
| 72 | +- Markdown vs plaintext: plaintext body kept (no `flexmark` dependency added). |
0 commit comments