Clean and structure extracted text

Extraction gives you a wall of text. Here you turn it into a record with a language, keywords, a summary and a search key, and learn where each of those steps quietly gets things wrong.

Extracted text is raw material, not data. A page run through Website to Text gives you one long string: the article, yes, but also a cookie notice, a "related stories" rail, and a footer with the copyright year. Before it is useful to anything downstream you need to know what language it is in, what it is about, what it says in brief, and how someone will find it again. Each of those is one API call. The work is in knowing when each call's answer can be trusted.

The target is a record like this, built from one page:

record.json
{
  "url": "https://example.com/post",
  "language": "en",
  "languageSure": true,
  "summary": "Two or three sentences lifted from the text.",
  "keywords": ["crawler", "server", "robots"],
  "searchKey": "polite crawling respects robotstxt caches responses",
  "text": "the cleaned body text, kept for reprocessing"
}

Keep text. Every field above it is derived, and the day you change how you derive them you will want to rebuild from the source rather than re-fetch every page.

When you have HTML, not a URL

Most of this course extracts from a URL. Sometimes the HTML arrives another way: an RSS item's description field, a CMS export, a webhook from a form builder, a page you rendered yourself. Sending that to HTML to Text costs 2 credits and skips the fetch entirely.

Try it liveHTML to Text2 credits
POSThttps://api.apiverve.com/v1/htmltotext
curl -X POST "https://api.apiverve.com/v1/htmltotext" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<nav>Home | About</nav><article><h1>Release notes</h1><p>Version 2.4 adds retries.</p><p>Version 2.3 fixed a crash.</p></article><footer>Copyright 2026</footer>"}'
Sign in or create a free account to run this with your own key.

Look at the joins between elements in text. wordCount and characterCount come back on every plan; detectedLanguage is premium.

Run that and read text closely. When we called it, the result was Home | AboutRelease notesVersion 2.4 adds retries.Version 2.3 fixed a crash.Copyright 2026. Two things to notice. Block elements are joined with no whitespace, so About and Release fuse into one token, and so do crash. and Copyright. And the navigation and footer came through, because extractionMethod was markup: on a fragment this small there is no article body to isolate.

Both are fixable before the call, cheaply and locally. Remove the chrome you know you don't want, and put a newline after every block-level closing tag so words cannot fuse:

prep-html.js
const BLOCK = /<\/(p|div|h[1-6]|li|tr|td|th|section|article|header|footer|blockquote|pre|br)\s*>/gi;

export function prepHtml(html) {
  return html
    .replace(/<(nav|footer|aside|script|style|form)[\s\S]*?<\/\1>/gi, '')
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(BLOCK, (m) => m + '\n');
}

A fused token is the kind of bug nobody notices until search stops working: a user searches for "release" and the one page that mentions it is indexed under aboutrelease. Fix it at the boundary, before anything else sees the text.

Detect the language, and know when not to believe it

Language decides everything after it. Keyword extraction drops stopwords, and stopwords are language-specific. A summariser that ranks sentences tuned for English will produce something odd from Finnish. A search index needs to know which stemmer to apply. So the first enrichment call is Language Detector, at 2 credits.

Try it liveLanguage Detector2 credits
POSThttps://api.apiverve.com/v1/languagedetector
curl -X POST "https://api.apiverve.com/v1/languagedetector" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Le service est temporairement indisponible. Merci de réessayer plus tard."}'
Sign in or create a free account to run this with your own key.

Compare the confidence of the first and second entries in detectedLanguages, not the absolute number.

The answer is correct: primaryCode is fr. Now look at the confidences we got: French 0.300, Latin 0.246, Portuguese 0.235. A correct answer at 0.30. If you had written if (confidence > 0.8), you would throw away nearly every real detection. These scores are relative weights spread across candidate languages, not probabilities of being right. A German sentence came back at 0.376 against Dutch at 0.299, also correct.

Now send the single word Gift. We got vietnamese at 0.227, with Cebuano at 0.219 right behind it. The word is English (and German, where it means poison). There was no signal to find, and the detector still returned a primary language, because that is what the field is for.

So the rule that works is about the margin, plus a minimum length:

  • Below about 20 characters, don't ask; mark the language as undetermined.
  • Accept the top answer when it leads the runner-up by a clear margin. In our calls, real text led by 0.05 or more; the one-word guess led by under 0.01.
  • When the margin is thin, fall back to a declared language if you have one. Website to Text returns the page's own language field, which is what the publisher said the page is in.

The confidenceLevel label (low, medium, high) is premium, so do not build on it if you are on a free key. Also handle an empty code: some candidates in detectedLanguages have no ISO 639-1 code (Cebuano came back with ""), so a primaryCode of "" should be treated as undetermined too.

Detect on a slice, not the whole page. The first 1,000 or so characters of body text are plenty, and sending less is faster.

Check yourself

Language Detector returns primaryCode "en" with detectedLanguages confidences of 0.24, 0.23 and 0.20 for a 15-character product name. What should your pipeline record?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales