索引から辿れないページをリストアップするスクリプト
概要
Cosense(旧Scrapbox)プロジェクトの索引ページからアクセスできないページを簡単に確認できるようになります。
指定した「索引」ページからリンクをたどって到達可能なページ群を求め、そこに含まれない(=到達できない)ページのみをリストアップしてポップアップウィンドウで表示します。
全てのページが索引から到達可能な場合、ポップアップウィンドウは開きません。
アルゴリズム
1. 索引ページの指定
indexPageTitle に索引となるページタイトルを設定します。ここからスタートして、リンクを再帰的にたどります。
2. データの取得
指定したプロジェクト名に対して、https://scrapbox.io/api/pages/:projectname/search/titles エンドポイントからページ情報を取得します。
応答ヘッダの X-Following-Id を利用して、ページ一覧を1000件ずつ順次取得する仕組みです。
3. リンク情報の解析
各ページに記載されている links 配列内のリンク先タイトルをすべてセットに保存し、どのページからも参照されているタイトルを記録します。
4. ページグラフの構築
全ページの情報から、ページタイトルをキーとした pagesMap を作成します。これにより任意のタイトルに対するページ情報に素早くアクセスできます。
5. 幅優先探索によるリンク辿り
キュー(queue)を用いて、索引ページから開始し、各ページ内の links 配列を辿って、到達可能なページ群(reachable)を求めています。
存在しないリンク先は無視し、すでに訪問済みの場合は再度追加しません。
6. 到達不可能なページの抽出
すべてのページ情報(allPages)の中から、到達可能なページ集合に含まれないページをフィルタで抽出します。
7. ポップアップウィンドウでの表示
結果一覧を HTML として組み立て、ポップアップウィンドウ内に書き出します。
参考
スクリプト
code:script.js
(async function() {
// プロジェクト名を設定してください
const projectName = "TOMIOKARIO";
const baseUrl = https://scrapbox.io/api/pages/${projectName}/search/titles;
// 索引ページのタイトルを設定してください(ここからリンクをたどります)
const indexPageTitle = "索引";
// 全ページ情報を格納する配列
let allPages = [];
// 次ページの取得用 URL(最初はベースURL)
let nextUrl = baseUrl;
// API の応答がなくなるまで全件取得(1000件ずつ)
while (nextUrl) {
try {
const response = await fetch(nextUrl);
if (!response.ok) {
console.error(HTTPエラー: ${response.status});
break;
}
const pages = await response.json();
allPages.push(...pages);
// 応答ヘッダ「X-Following-Id」から次回用のIDを取得
const followingId = response.headers.get('X-Following-Id');
if (followingId) {
nextUrl = ${baseUrl}?followingId=${followingId};
} else {
nextUrl = null;
}
} catch (error) {
console.error("データ取得中にエラーが発生しました:", error);
break;
}
}
// 全ページ情報を元に、タイトルをキーとしたマップ(辞書型)を作成
const pagesMap = {};
allPages.forEach(page => {
});
// 索引ページからリンクをたどって到達可能なページを集合で収集
const reachable = new Set();
const queue = [];
// 初期状態として索引ページが存在する場合はキューに追加
queue.push(indexPageTitle);
} else {
console.error(索引ページ "${indexPageTitle}" が存在しません。);
}
// 幅優先探索(BFS)で到達可能なページ群を求める
while (queue.length > 0) {
const currentTitle = queue.shift();
if (reachable.has(currentTitle)) continue;
reachable.add(currentTitle);
if (currentPage && Array.isArray(currentPage.links)) {
currentPage.links.forEach(linkTitle => {
if (pagesMaplinkTitle && !reachable.has(linkTitle)) { queue.push(linkTitle);
}
});
}
}
// プロジェクト内のすべてのページの中から、索引経由でたどり着けなかったページを抽出
const unreachablePages = allPages.filter(page => !reachable.has(page.title));
// 到達できないページがある場合のみポップアップを表示
if (unreachablePages.length > 0) {
let resultHtml = `
<html>
<head>
<meta charset="utf-8">
<title>到達できないページ一覧</title>
</head>
<body>
<h1>索引からたどって到達できないページ一覧</h1>
<ul>
`;
unreachablePages.forEach(page => {
resultHtml += <li>${page.title}</li>;
});
resultHtml += `
</ul>
</body>
</html>
`;
const popup = window.open("", "UnreachablePages", "width=400,height=600,scrollbars=yes");
if (popup) {
popup.document.open();
popup.document.write(resultHtml);
popup.document.close();
} else {
alert("ポップアップウィンドウの表示がブロックされました。ブラウザのポップアップブロック設定を確認してください。");
}
} else {
console.log("すべてのページは索引から到達可能です。");
}
})();