ReadabilityKit is a Swift Package for extracting clean, readable article content from web pages or raw HTML.
It is designed for Apple platforms and returns cleaned HTML, plain text, and article metadata (title, byline, excerpt).
- Async extraction from a URL via pluggable loaders (
URLSessionHTMLLoader,WebViewDOMLoader) - Direct extraction from raw HTML (
extract(fromHTML:url:)) - Multi-stage parsing pipeline (normalization, scoring, clustering, cleanup)
- Content scoring with dedicated scorer types
- Cluster-based main-content selection
- Output as both
contentHTMLandtextContent - Configurable extraction and media-preservation options (
ExtractionOptions)
- Swift 5.9+
- iOS 15+
- macOS 12+
- tvOS 15+
- watchOS 8+
Add ReadabilityKit to your Package.swift dependencies:
.dependencies: [
.package(url: "https://github.com/<your-org-or-user>/ReadabilityKit.git", branch: "main")
]Then add the product to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "ReadabilityKit", package: "ReadabilityKit")
]
)import Foundation
import ReadabilityKit
let extractor = ReadabilityExtractor()
let url = URL(string: "https://example.com/article")!
Task {
do {
let article = try await extractor.extract(from: url)
print(article.title)
print(article.textContent)
} catch {
print("Extraction failed: \(error)")
}
}By default, ReadabilityExtractor uses URLSessionHTMLLoader.
import ReadabilityKit
let extractor = ReadabilityExtractor(
loader: URLSessionHTMLLoader()
)If you want rendered DOM (after page load), use WebViewDOMLoader:
import ReadabilityKit
@MainActor
func makeExtractor() -> ReadabilityExtractor {
ReadabilityExtractor(loader: WebViewDOMLoader())
}You can also override the request user agent when loading from a URL:
let article = try await extractor.extract(
from: url,
userAgent: "MyApp/1.0 (https://example.com/bot)"
)import Foundation
import ReadabilityKit
let html = """
<html>
<head><title>Example</title></head>
<body><article><h1>Hello</h1><p>Readable content here...</p></article></body>
</html>
"""
let extractor = ReadabilityExtractor(options: .init(enableClustering: true))
let article = try extractor.extract(
fromHTML: html,
url: URL(string: "https://example.com/post")!
)
print(article.title)
print(article.contentHTML)ReadabilityExtractor follows this flow:
- Load HTML using the configured
URLLoadingimplementation. - Parse DOM with SwiftSoup.
- Run document cleaning passes (unsafe tags, break normalization, unlikely candidate removal).
- Score candidates using paragraph/class/link-density + density scoring.
- Select content via clustering (or best single node if clustering is disabled).
- Run element cleaning passes (junk blocks, lazy media, table cleanup, etc.).
- Build the final
Article.
Article contains:
url: URLtitle: Stringbyline: String?excerpt: String?contentHTML: StringtextContent: String
Use ExtractionOptions to tune behavior:
preserveHTMLkeepIframeskeepVideoskeepAudiowrapInArticleTagenableClusteringclusterTopNclusterMaxRankGapclusterMaxDepthDeltaclusterMinTokenJaccard
Example:
let options = ExtractionOptions(
preserveHTML: true,
keepIframes: false,
keepVideos: true,
keepAudio: true,
wrapInArticleTag: true,
enableClustering: true
)
let extractor = ReadabilityExtractor(options: options)ReadabilityKit throws ReadabilityError for common failures:
invalidResponsehttpStatus(Int)decodingFailedemptyHTMLparseFailednoReadableContent
Current source layout under Sources/ReadabilityKit:
Models/ArticleExtractionOptionsReadabilityError
Loading/URLLoadingURLSessionHTMLLoaderWebViewDOMLoader
Extraction/ReadabilityExtractor
Scoring/ElementScorerClassWeightScorerLinkDensityScorerParagraphScorerDensityScoring
Clustering/ClusteringCandidateClusteringEngine
Cleaning/CleaningPass,DocumentCleaningPass,ElementCleaningPass- Document and element cleaning pass implementations
Build:
swift buildRun tests:
swift test