Skip to content

fix(graph): clean migrate content extraction (description + boilerplate + asset filter) - #3

Merged
devrishik merged 5 commits into
masterfrom
fix/graph-migrate-extraction
Aug 11, 2026
Merged

devrishik merged 5 commits into
masterfrom
fix/graph-migrate-extraction

Conversation

@devrishik

@devrishik devrishik commented Aug 11, 2026

Copy link
Copy Markdown

What this fixes

zola graph migrate ran for real against https://curriculo.me and committed 29 pages whose markdown (and 19/29 front-matter descriptions) were polluted with WordPress theme chrome — nav runs, the search widget, avatar/author lines, the cookie banner, and a mashed-title "toolkit" link list. The same dirty text also flowed into the knowledge graph (content_hash, Page.summary, and topics::enrich_one input), so topics were extracted from nav/footer text. The run also hard-failed on https://ats.curriculo.me/favicon.ico (Firecrawl SCRAPE_UNSUPPORTED_FILE_ERROR → HTTP 500), which tripped the failure threshold for an otherwise fine crawl.

Root cause: onlyMainContent: true was already set, but this theme lacks clean <main>/<article> semantics, so Firecrawl returns essentially the whole body. The migrate driver then took its description from summarize(raw_body) — i.e. the body's first junk line.

This PR fixes extraction in five TDD'd changes. Scope is this repo only — no landing-website content is touched.

Before / after

Front-matter description (content/ai-resume-builder/blogs/how-ats-works-2026/index.md)

Before (the bug — description came from the body's first line):

description = "Hit enter to search or ESC to closeSearch [Close Search](https://curriculo.me/ai-resume-builder/blogs/how-ats-works-2026/#) [ATS Optimization](https://curriculo"

After (description comes from source metadata — Yoast description, then og:description, then ogDescription, whitespace-collapsed to one line):

description = "Learn how applicant tracking systems really work in 2026 — parsing, scoring, and AI ranking."

Body excerpt (same page)

Before — opened with chrome and closed with the footer:

Hit enter to search or ESC to closeSearch

[Close Search](https://curriculo.me/.../how-ats-works-2026/#)

[ATS Optimization](https://curriculo.me/.../category/ats-optimization/) [Resume Tips](https://curriculo.me/.../category/resume-tips/)

# How ATS Really Works in 2026 — Parsing, Scoring & AI Ranking Explained_**Disclosure:** This article was produced by Curriculo Inc.…_

Ready to build your resume?

_Next Post_

### You May Also Like
… (related-post cards, avatar images) …

## The complete resume toolkit

- [AI Resume BuilderBuild an ATS-ready resume that gets past the filters.](https://curriculo.me/ai-resume-builder/)
- [FeaturesKeyword matching, formatting checks, and AI rewrite.](https://curriculo.me/.../features/)

[Close Menu](https://curriculo.me/.../how-ats-works-2026/#)

We use cookies to improve your experience and analyze site traffic. [Privacy Policy](https://curriculo.me/privacy/)

RejectAccept

After — opens at the heading, closes at the last real section, all chrome gone:

# How ATS Really Works in 2026 — Parsing, Scoring & AI Ranking Explained

Learn how applicant tracking systems really work in 2026 — from document parsing to AI-powered ranking. Understand why 75% of resumes fail ATS screening and how to optimize yours.

![How ATS applicant tracking systems work in 2026 …](https://curriculo.me/wp-content/uploads/2026/03/featured_existing_01.png.webp)

_Reviewed by the Curriculo Engineering Team_

## What Is an Applicant Tracking System (ATS)?_**Disclosure:** This article was produced by Curriculo Inc., which develops AI resume building and ATS products._

(The in-content "Ready to build your resume?" CTA is kept on purpose — conservatism is the hard requirement: never truncate real article prose. Everything from _Next Post_ onward is dropped.)

Changes per task

  • T2 — src/cmd/graph/clean.rs (new): strip_boilerplate(md) — the deterministic backstop to Firecrawl's excludeTags. Three passes: (1) drop wp-content/litespeed/avatar author lines anywhere; (2) drop leading chrome (search-widget strings, link-only nav runs, logo+brand mash, bare breadcrumb words) until the first heading/prose line; (3) truncate at the first trailing footer marker (_Next Post_, ### You May Also Like, [Close Menu], cookie banner / RejectAccept, or a mashed-title link list item). 16 unit tests pin every rule, including a realistic polluted fixture where all article prose + the featured image + the disclosure survive.
  • T1 — description from source metadata: FetchedPage gains pub description, populated from the Firecrawl response (first non-empty of metadata.description, og:description, ogDescription), whitespace-collapsed to one line. migrate uses it for the front-matter description and TopicInput.description, falling back to summarize(cleaned body) only when the site exposes no meta description. scrape_payload(url) is extracted as a network-free, testable seam.
  • T4 — cleaned body used everywhere: migrate cleans once then uses that value for the written body, content_hash, Page.summary, and TopicInput.body. No call site sees the raw fetched markdown.
  • T3 — skip non-HTML asset URLs: sitemap::is_asset_url flags .ico .png .jpg .jpeg .gif .webp .svg .avif .css .js .json .xml .pdf .zip .mp4 .webm .woff .woff2 .ttf (case-insensitive, query/fragment ignored). migrate filters them out before the --max cap (so the cap yields N real pages) and logs the skipped count; skipped assets are not failures.
  • T5 — regression guard: a faithful polluted body driven through the migrate write path with MockFetcher asserts the written index.md contains none of Hit enter to search, Close Search, Close Menu, RejectAccept, wp-content/litespeed, and that description equals the metadata description.

Defense in depth: Firecrawl is now also sent excludeTags for structural chrome + common WordPress theme selectors, but clean.rs is the deterministic guarantee — neither layer is trusted alone (see the new scrape_payload test).

Constraints honored

  • refresh.rs hard rule untouched — Firecrawl is still migrate-only; refresh.rs does not import the firecrawl module.
  • Graph JSON SCHEMA_VERSION and schema are unchanged.
  • All tests are offline (existing MockFetcher extended with a description param; no network).
  • No landing-website content edited.

Test gate

  • cargo test --workspaceall green (81 bin tests incl. 56 graph::, plus all component crates).
  • cargo fmt --checkclean (the prior feat(graph)/feat(translate) commits landed unformatted and the release CI doesn't gate on fmt; commit style: cargo fmt is a purely mechanical rustfmt 1.95 pass, no logic change).
  • cargo clippy — no new warnings (the 5 remaining are pre-existing in topics.rs/translate.rs/components/*).

⚠️ Orchestrator follow-up (not in this PR)

This PR only fixes extraction. Before the re-migrate of curriculo.me, the orchestrator must:

  1. Cut a new curriculo-tech/zola release tag off this branch.
  2. Bump ZOLA_VERSION and ZOLA_BIN_URL in landing-website to that tag.
  3. Re-run zola graph migrate --from https://curriculo.me --force (the --force re-crawl discards the polluted graph) and retire the duplicate curated pages.

Commits are per-task (T2, T1+T4, T3, T5, style) for easy review.

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph migrations by removing common website navigation, author details, footer content, and related-post clutter from imported Markdown.
    • Excluded static assets from sitemap crawls so migration limits focus on actual pages.
    • Improved generated descriptions using page metadata, with cleaned-content fallback.
    • Preserved meaningful headings, images, captions, references, and page content during cleanup.
  • Tests

    • Added coverage for content cleanup, metadata handling, asset filtering, and regression scenarios.

New clean.rs exposes strip_boilerplate(md): deterministic backstop to
Firecrawl's onlyMainContent/excludeTags ask. Three passes:
1. drop wp-content/litespeed/avatar author lines anywhere;
2. drop leading chrome (search widget, link-only nav runs, logo+brand
   mash, bare breadcrumb words) until the first heading/prose line;
3. truncate at the first trailing footer marker (_Next Post_, You May
   Also Like heading, [Close Menu], cookie banner/RejectAccept, or a
   mashed-title link list item).

Conservatism is the hard rule: never eat real prose. 16 tests pin every
rule, including a realistic polluted fixture (condensed from a real
curriculo.me page) where chrome is removed and all article prose +
featured image + disclosure survive.
…T1, T4)

T1 — front-matter description from source metadata, not the body:
- FetchedPage gains pub description, populated from the Firecrawl response
  (first non-empty of metadata.description, og:description, ogDescription),
  whitespace-collapsed to a single line so TOML stays valid.
- migrate uses fetched.description for the front-matter description and
  TopicInput.description; falls back to summarize(cleaned body) only when the
  site exposes no meta description.
- scrape_payload() extracted as a testable seam: alongside onlyMainContent it
  now sends excludeTags for structural chrome + common WordPress theme
  selectors (clean.rs is still the deterministic backstop).

T4 — cleaned body used everywhere:
- migrate cleans once (clean::strip_boilerplate) then uses that value for the
  written body, content_hash, Page.summary, and TopicInput.body. No call site
  sees raw fetched markdown.

MockFetcher.with gains a description param; existing tests updated.
The live run hard-failed on https://ats.curriculo.me/favicon.ico
(Firecrawl SCRAPE_UNSUPPORTED_FILE_ERROR → HTTP 500), which counted as a
failure and tripped the run's fail-through for an otherwise fine crawl.

sitemap::is_asset_url(url) flags static-asset extensions (.ico .png .jpg .jpeg
.gif .webp .svg .avif .css .js .json .xml .pdf .zip .mp4 .webm .woff .woff2
.ttf), case-insensitive, ignoring query string + fragment. migrate applies it
to the whole sitemap BEFORE the --max cap (so the cap yields N real pages) and
logs the skipped count; skipped assets are not counted as failures.
Drives a faithful polluted curriculo.me body (search widget, nav/category
link runs, wp-content/litespeed avatar line, _Next Post_ / You May Also
Like / mashed-title toolkit / Close Menu / cookie banner / RejectAccept)
through the migrate write path with MockFetcher + a clean Yoast metadata
description, then asserts the written index.md:

- front-matter description == the metadata description (not 'Hit enter to
  search…' from the polluted body);
- none of the chrome markers survive (Hit enter to search, Close Search,
  Close Menu, RejectAccept, wp-content/litespeed, _Next Post_, You May
  Also Like, We use cookies);
- real article prose + featured image + disclosure survive the clean.

Offline (MockFetcher), no network.
Mechanical rustfmt pass. No logic changes. The graph/ translate modules
landed unformatted (the release CI builds but does not gate on fmt); this
brings the whole crate to cargo fmt --check clean under the runner's
toolchain (nixpkgs cargo/rustfmt 1.95) so the migrate-extraction PR can
meet its fmt-clean gate.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The graph migration now excludes static sitemap assets, retrieves normalized Firecrawl descriptions, removes recognized WordPress theme boilerplate, and uses cleaned Markdown for summaries, hashes, topics, and written pages. Unit, integration, and regression tests cover these changes.

Changes

Migration quality

Layer / File(s) Summary
Markdown cleaning and parsing
src/cmd/graph/clean.rs, src/cmd/graph/html_to_md.rs, src/cmd/graph/mod.rs
Adds deterministic boilerplate removal, conservative preservation rules, Markdown list spacing, and cleanup tests.
Firecrawl metadata and exclusions
src/cmd/graph/firecrawl.rs
Adds FetchedPage.description, metadata precedence and normalization, expanded excludeTags, mock support, and related tests.
Sitemap asset filtering
src/cmd/graph/sitemap.rs
Adds case-insensitive static-asset detection that ignores query strings and fragments, with positive and negative tests.
Migration output and regression coverage
src/cmd/graph/migrate.rs, GLM-BRIEF-MIGRATE-QUALITY.md, src/cmd/graph/openrouter.rs, src/cmd/graph/refresh.rs, src/cmd/graph/schema.rs, src/cmd/graph/topics.rs, src/cmd/translate.rs
Filters assets before --max, cleans fetched Markdown before downstream use, resolves descriptions from metadata or cleaned content, and adds migration regression tests. The brief and remaining files contain formatting-only updates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • curriculo-tech/zola#2: Introduces the graph migration and related Firecrawl, sitemap, and graph modules extended by this change.

Poem

A rabbit found theme crumbs in a page,
And swept them neatly off the stage.
Firecrawl brought descriptions bright,
While clean Markdown stayed in sight.
Assets hopped away before the crawl—
Good content now grows tall!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main migration fixes: content cleaning, description extraction, and asset filtering.
Docstring Coverage ✅ Passed Docstring coverage is 96.97% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/graph-migrate-extraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@GLM-BRIEF-MIGRATE-QUALITY.md`:
- Around line 19-22: Add the text language identifier to the fenced example
containing the description lines, changing its opening fence to use text while
preserving the example contents.

In `@src/cmd/graph/clean.rs`:
- Around line 117-127: Restrict the breadcrumb predicate used by
strip_boilerplate to a finite set of verified labels or require adjacent
navigation evidence, instead of accepting arbitrary one-token text such as
“Abstract” or “Introduction.” Preserve ordinary article headings, and add a
regression test confirming a one-token heading without “#” reaches the page,
hash, summary, or topic inputs unchanged.
- Around line 169-178: Update is_mashed_title_list_item and the truncation logic
around it so one matching list item cannot trigger footer removal; require
multiple adjacent mashed-title links or an explicit footer marker before
truncating content. Add a regression test covering a legitimate camel-case
reference link such as “Configure iPhoneApp integration” and verify the article
remains intact.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b1d15e9-1e46-4e1e-b384-a42fdb64d3ed

📥 Commits

Reviewing files that changed from the base of the PR and between c444290 and e31b5ae.

📒 Files selected for processing (12)
  • GLM-BRIEF-MIGRATE-QUALITY.md
  • src/cmd/graph/clean.rs
  • src/cmd/graph/firecrawl.rs
  • src/cmd/graph/html_to_md.rs
  • src/cmd/graph/migrate.rs
  • src/cmd/graph/mod.rs
  • src/cmd/graph/openrouter.rs
  • src/cmd/graph/refresh.rs
  • src/cmd/graph/schema.rs
  • src/cmd/graph/sitemap.rs
  • src/cmd/graph/topics.rs
  • src/cmd/translate.rs

Comment on lines +19 to +22
```
description = "Hit enter to search or ESC to closeSearch [Close Search](https://…/#) [Resume Tips](https://…)"
description = "![CurriculoATS](https://curriculo.me/wp-content/uploads/2026/04/curriculo-logo-144.webp)CurriculoATS [Features](https://curriculo.me/features/) [AI Screening](h"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced example.

markdownlint-cli2 reports MD040 because Line 19 opens a fence without an info string. Use text to keep the documented lint gate clean.

Proposed fix
-```
+```text
 description = "Hit enter to search or ESC to closeSearch [Close Search](https://…/#) [Resume Tips](https://…)"
 description = "![CurriculoATS](https://curriculo.me/wp-content/uploads/2026/04/curriculo-logo-144.webp)CurriculoATS [Features](https://…)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
description = "Hit enter to search or ESC to closeSearch [Close Search](https://…/#) [Resume Tips](https://…)"
description = "![CurriculoATS](https://curriculo.me/wp-content/uploads/2026/04/curriculo-logo-144.webp)CurriculoATS [Features](https://curriculo.me/features/) [AI Screening](h"
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GLM-BRIEF-MIGRATE-QUALITY.md` around lines 19 - 22, Add the text language
identifier to the fenced example containing the description lines, changing its
opening fence to use text while preserving the example contents.

Source: Linters/SAST tools

Comment thread src/cmd/graph/clean.rs
Comment on lines +117 to +127
t.len() <= 12
&& !t.contains(' ')
&& !t.contains('\t')
&& !t.contains('[')
&& !t.contains('!')
&& !t.contains('#')
&& !t.contains('.')
&& !t.contains(',')
&& !t.contains(':')
&& !t.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not remove arbitrary one-token leading content.

Line 117 classifies valid leading content such as Abstract or Introduction as a breadcrumb. strip_boilerplate then deletes that content before it reaches the written page, hash, summary, or topic input.

Match a finite set of verified breadcrumb labels, or require adjacent navigation evidence. Add a regression test where a one-token article heading without # survives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/graph/clean.rs` around lines 117 - 127, Restrict the breadcrumb
predicate used by strip_boilerplate to a finite set of verified labels or
require adjacent navigation evidence, instead of accepting arbitrary one-token
text such as “Abstract” or “Introduction.” Preserve ordinary article headings,
and add a regression test confirming a one-token heading without “#” reaches the
page, hash, summary, or topic inputs unchanged.

Comment thread src/cmd/graph/clean.rs
Comment on lines +169 to +178
fn is_mashed_title_list_item(t: &str) -> bool {
if !t.starts_with("- [") {
return false;
}
let Some(caps) = single_link_re().captures(t) else {
return false;
};
let text = caps.get(1).map(|m| m.as_str()).unwrap_or("");
mash_re().is_match(text)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a footer list run before truncating content.

A single normal reference item such as - [Configure iPhoneApp integration](...) matches the lowercase-to-uppercase rule. Line 153 then treats it as a footer marker and removes the rest of the article.

The requirement specifies a run of mashed-title links. Detect multiple adjacent matching items, or require an explicit footer marker before truncation. Add a preservation test for a legitimate camel-case reference link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/graph/clean.rs` around lines 169 - 178, Update
is_mashed_title_list_item and the truncation logic around it so one matching
list item cannot trigger footer removal; require multiple adjacent mashed-title
links or an explicit footer marker before truncating content. Add a regression
test covering a legitimate camel-case reference link such as “Configure
iPhoneApp integration” and verify the article remains intact.

@devrishik
devrishik merged commit 9032319 into master Aug 11, 2026
1 check passed
@devrishik
devrishik deleted the fix/graph-migrate-extraction branch August 11, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant