Tencent/WeKnora · warning

no readable content extracted

Error message

no readable content extracted

What it means

Returned when readability successfully parsed the HTML but its scoring algorithm found no readable article node (article.Node == nil). The readability heuristic requires a minimum amount of text-bearing, structured content; pages that are too small, JS-shell single-page apps, or pure link lists yield no candidate article.

Source

Thrown at internal/datasource/connector/rss/client.go:115

}

// extractArticle fetches an article page and returns the readability-cleaned
// main content as HTML, plus the extracted title (may be empty). Returns an
// error if the page can't be fetched or no readable content is found, so the
// caller can fall back to feed-provided content.
func (c *client) extractArticle(ctx context.Context, articleURL string) (contentHTML, title string, err error) {
	body, err := c.fetch(ctx, articleURL, maxArticleSize, false)
	if err != nil {
		return "", "", err
	}

	pageURL, _ := url.Parse(articleURL)
	article, err := readability.FromReader(bytes.NewReader(body), pageURL)
	if err != nil {
		return "", "", fmt.Errorf("readability parse: %w", err)
	}
	if article.Node == nil {
		return "", "", fmt.Errorf("no readable content extracted")
	}

	var buf bytes.Buffer
	if err := article.RenderHTML(&buf); err != nil {
		return "", "", fmt.Errorf("render article html: %w", err)
	}
	return buf.String(), article.Title(), nil
}

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Fall back to the feed-provided content (item.Content / item.Description) — resolveItem is designed to do exactly this; ensure that fallback path is in place.
  2. For JS-only sites, no static fetch will work; either exclude the item or use feed-provided content.
  3. Detect login/paywall stubs (small body size, keywords like 'subscribe') and skip extraction for those domains.
  4. Consider a rendering proxy (headless browser service) if full text from SPAs is a hard requirement.

Example fix

// before
contentHTML, _, err := cli.extractArticle(ctx, item.Link)
if err != nil {
    return item.Summary
}
// after
contentHTML, _, err := cli.extractArticle(ctx, item.Link)
if err != nil || utf8.RuneCountInString(contentHTML) < 200 {
    return coalesce(item.Content, item.Description, item.Title)
}
Defensive patterns

Strategy: fallback

Validate before calling

func looksLikeArticle(html string) bool {
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        return false
    }
    text := strings.TrimSpace(doc.Find("body").Text())
    return utf8.RuneCountInString(text) > 200
}

Try / catch

contentHTML, title, err := cli.extractArticle(ctx, item.Link)
if err != nil || contentHTML == "" {
    // SPA/paywall/landing page — no readable node
    contentHTML = coalesce(item.Content, item.Description)
    title = item.Title
}

Prevention

When it happens

Trigger: The article page is a JavaScript-rendered SPA whose HTML contains no server-rendered text; the page has less content than readability's scoring thresholds; the URL redirected to a homepage, login page, or paywall stub; the page is a bare link aggregator with no body text.

Common situations: Modern news sites serving empty shells to non-browser clients; paywalled articles returning a stub page; feeds whose item links point to landing pages instead of articles; very short link-list posts (e.g. linkblogs).

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/63368ffd57c88fad. Report an issue: GitHub.