URLからマークダウンファイルを生成する仕組みを解説する。
URL
↓ fetch API または curl
HTML文字列
↓ jsdom
DOMツリー (window.document)
↓ @mozilla/readability
本文HTML (ナビゲーション等を除去)
↓ turndown
Markdown文字列
↓ fs.writeFileSync
ファイル出力
役割: HTML文字列からブラウザと同等のDOMツリーを構築する
なぜ必要か: @mozilla/readabilityはブラウザのdocumentオブジェクトを前提としている。Node.jsにはDOMがないため、jsdomで仮想的なDOM環境を作る。
使用箇所 (src/pipeline.mts):
import { JSDOM } from 'jsdom';
const dom = new JSDOM(input.html, { url: input.url });
// dom.window.document でブラウザと同じdocumentオブジェクトが得られるJSDOMコンストラクタのオプション:
- 第1引数: HTML文字列
url: ベースURLを設定。相対リンクの解決に使用される
内部の仕組み:
- HTML5パーサー(parse5)でHTMLをパース
- whatwg-urlでURL処理
- cssstyleでCSSOM実装
- DOMのほぼ全API(querySelector, getElementByIdなど)を実装
役割: Webページから本文コンテンツを抽出する(Firefoxのリーダービュー機能と同じアルゴリズム)
なぜ必要か: Webページにはナビゲーション、サイドバー、広告、フッターなど本文以外の要素が多い。これらを除去して記事本文だけを取り出す。
使用箇所 (src/pipeline.mts):
import { Readability } from '@mozilla/readability';
const reader = new Readability(dom.window.document);
const article = reader.parse();parse()の戻り値:
{
title: string; // 記事タイトル
content: string; // 処理済みHTML(本文のみ)
textContent: string; // プレーンテキスト版
excerpt: string; // 記事の抜粋
byline: string; // 著者情報
siteName: string; // サイト名
publishedTime: string; // 公開日時
}抽出アルゴリズムの概要:
-
候補要素のスコアリング
<p>,<div>,<article>などの要素に初期スコアを付与- クラス名・ID名でスコア調整(
article,content→ 加点、sidebar,nav→ 減点)
-
テキスト密度の計算
- 各要素内のテキスト量、リンク密度、句読点の数を分析
- リンクだらけの要素(ナビゲーション)は低スコア
-
最高スコア要素の選択
- スコアが最も高い要素を本文コンテナとして採用
-
クリーンアップ
- 不要な要素(
<script>,<style>,<nav>など)を除去 - 空の要素を削除
- 相対URLを絶対URLに変換
- 不要な要素(
役割: HTMLをMarkdown形式に変換する
使用箇所 (src/pipeline.mts):
import TurndownService from 'turndown';
const turndown = new TurndownService({
headingStyle: 'atx', // # 形式の見出し
codeBlockStyle: 'fenced', // ``` 形式のコードブロック
bulletListMarker: '-', // リストのマーカー
});
const markdown = turndown.turndown(input.article.content);変換ルール(デフォルト):
| HTML | Markdown |
|---|---|
<h1> |
# |
<h2> |
## |
<strong>, <b> |
**text** |
<em>, <i> |
*text* |
<a href="url"> |
[text](url) |
<img src="url"> |
 |
<code> |
`code` |
<pre><code> |
```code``` |
<ul><li> |
- item |
<ol><li> |
1. item |
<blockquote> |
> quote |
内部の仕組み:
- HTMLをDOMとしてパース(内部でdomino使用)
- DOMツリーを深さ優先で走査
- 各ノードに対応するルールを適用
- テキストノードはエスケープ処理(
*,_,[など) - 結果を連結してMarkdown文字列を生成
オプション詳細:
headingStyle:'atx'(#形式)or'setext'(下線形式)codeBlockStyle:'fenced'(```)or'indented'(4スペース)bulletListMarker:'-','+','*'のいずれか
役割: HTTPリクエストを送信してHTMLを取得
const response = await fetch(url);
const html = await response.text();Node.js 18以降でグローバルに利用可能。内部実装はundici。
役割: curlコマンドでHTMLを取得(--curlオプション使用時)
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
const { stdout } = await execAsync(`curl -sL "${url}"`);なぜ必要か: fetch APIがブロックされる環境(プロキシ、ファイアウォール等)でも、curlコマンドが使える場合がある。curlはシステムのプロキシ設定を自動で参照する。
オプション:
-s: サイレントモード(プログレス非表示)-L: リダイレクトを追跡
役割: ファイルの読み書き
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
writeFileSync(path, content, 'utf-8');役割: ファイルパスの操作
import { join } from 'node:path';
const outputDir = join(import.meta.dirname, '..', 'output');import.meta.dirnameはNode.js 20.11以降で利用可能。ESモジュールにおける__dirname相当。
1. CLI引数からURL取得
process.argv[2] → "https://example.com/article"
2. HTTPリクエスト
fetch(url) → Response { status: 200, ... }
response.text() → "<html><head>..."
3. DOM構築
new JSDOM(html, { url })
→ {
window: {
document: Document { ... }
}
}
4. 本文抽出
new Readability(document).parse()
→ {
title: "記事タイトル",
content: "<div id=\"readability-page-1\">...</div>",
...
}
5. Markdown変換
turndownService.turndown(content)
→ "# 見出し\n\n本文テキスト..."
6. フロントマター付与
"---\ntitle: ...\n---\n" + markdown
7. ファイル出力
writeFileSync("output/20260123-123456_記事タイトル.md", fullMarkdown)