Skip to content

Commit 168796f

Browse files
committed
fix(seo): 関連リンクの権威漏れと孤児ページを解消する
index 対象ページの関連リンクが noindex ページへ向いていた問題 (#3) と、 どこからも関連リンクを貰えない index 対象ページ (#4) をまとめて是正する。 #3 権威漏れ: getRelated に indexableOnly を追加し、index 対象ページでは index 対象だけを 関連に出す。実測で 378 本中 112 本 (29.6%) が noindex 先に向いていた。 既定は false のままとし、noindex ツールのページは従来どおり全ツールから 関連を出す (利用者向けの導線であり、クロール上の損が無いため)。 #4 孤児: fill をレジストリ登録順で埋めていたため、カテゴリ内の先頭付近ばかりが 関連に出て末尾のツールが孤児になっていた (#3 適用後も15件)。 明示 related の被リンク数 (index 対象ページからの分のみ) が少ない順に 埋めるよう変更する。同数なら slug 順で固定しビルド間で揺れないようにした。 ※被リンク数の集計で getRelated を呼ぶと相互再帰になるため明示 related のみ数える。 実測 (ビルド出力の HTML で検証): 関連リンク総数 378 → 374 noindex 先 112 → 0 関連が3件未満のページ 7 → 0 被related 0 のページ 21 → 0 (被リンク分布 min=1 / median=6 / max=14) noindex ページ160件は平均5.98件で従来どおり (関連0件は無し) 一覧の表示件数は 223 件のまま変更していない (ken 判断)。 Claude-Session: https://claude.ai/code/session_01ENHPNZ5ngcdrTHJ2H6oAFd
1 parent 687c41e commit 168796f

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

‎site/src/app/[locale]/tools/[slug]/page.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ export default async function ToolPage({
6060

6161
const t = await getTranslations();
6262
const tt = await getTranslations(`tools.${slug}`);
63-
const related = getRelated(slug, 6);
63+
// index 対象ページの関連リンクは index 対象だけに絞る(権威を noindex へ流さない)。
64+
// noindex ページ側は従来どおり全ツールから出す — 利用者向けの導線で、クロール上の損が無い。
65+
const related = getRelated(slug, 6, isIndexable(slug));
6466
const url = `${siteConfig.url}/${locale}/tools/${slug}`;
6567

6668
const faq = (tt.raw("faq") as { q: string; a: string }[]) ?? [];

‎site/src/lib/tools/registry.ts‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,15 +538,69 @@ export function listIndexableByCategory(category: string): ToolMeta[] {
538538
return listByCategory(category).filter((m) => INDEXED_SLUGS.has(m.slug));
539539
}
540540

541-
export function getRelated(slug: string, limit = 5): ToolMeta[] {
541+
/**
542+
* 関連ツール。`indexableOnly` で index 対象だけに絞れる。
543+
*
544+
* index 対象ページの関連リンクは noindex ページへ流してはいけない。実測で
545+
* 378 本中 112 本 (29.6%) が noindex 先に向いており、被リンクで得た権威が
546+
* インデックスされないページへ漏れていた (2026-09-18)。
547+
* 絞った上で同カテゴリの index 対象で埋め戻すと 374 本を確保でき、
548+
* 関連が 3 件未満になるページは 7 → 0 に減る(=情報量は落ちない)。
549+
*
550+
* 既定は false。noindex ツールのページでは noindex 同士を関連に出してよい
551+
* (利用者向けの導線であり、クロール上の損は無い)ため、絞るかは呼び出し側が決める。
552+
*/
553+
/**
554+
* 明示 related の被リンク数(index 対象ページからの分だけ)。
555+
*
556+
* fill の並び順に使う。登録順のまま埋めると、カテゴリ内の先頭付近のツールばかりが
557+
* 関連に出て、末尾のツールは**どこからも関連リンクを貰えない孤児**になる
558+
* (実測 15 件 / 2026-09-18)。被リンクが少ないものから先に埋めることで、
559+
* 手作業でリンクを捏造せずに孤児を解消する。
560+
*
561+
* ★ここで getRelated を呼ぶと相互再帰で無限ループになるため、**明示 related だけ**を数える。
562+
*/
563+
const INBOUND_FROM_INDEXED: Map<string, number> = (() => {
564+
const counts = new Map<string, number>();
565+
for (const m of TOOLS) counts.set(m.slug, 0);
566+
for (const m of TOOLS) {
567+
if (!INDEXED_SLUGS.has(m.slug)) continue; // 権威を流せるのは index 対象ページだけ
568+
for (const s of m.related) {
569+
if (s !== m.slug && counts.has(s)) counts.set(s, counts.get(s)! + 1);
570+
}
571+
}
572+
return counts;
573+
})();
574+
575+
/**
576+
* 関連ツール。`indexableOnly` で index 対象だけに絞れる。
577+
*
578+
* index 対象ページの関連リンクは noindex ページへ流してはいけない。実測で
579+
* 378 本中 112 本 (29.6%) が noindex 先に向いており、被リンクで得た権威が
580+
* インデックスされないページへ漏れていた (2026-09-18)。
581+
* 絞った上で同カテゴリの index 対象で埋め戻すと 374 本を確保でき、
582+
* 関連が 3 件未満になるページは 7 → 0 に減る(=情報量は落ちない)。
583+
*
584+
* 既定は false。noindex ツールのページでは noindex 同士を関連に出してよい
585+
* (利用者向けの導線であり、クロール上の損は無い)ため、絞るかは呼び出し側が決める。
586+
*/
587+
export function getRelated(slug: string, limit = 5, indexableOnly = false): ToolMeta[] {
542588
const tool = SLUG_INDEX.get(slug);
543589
if (!tool) return [];
590+
const eligible = (m: ToolMeta) => !indexableOnly || INDEXED_SLUGS.has(m.slug);
544591
const related = tool.related
545592
.map((s) => SLUG_INDEX.get(s))
546-
.filter((m): m is ToolMeta => Boolean(m));
593+
.filter((m): m is ToolMeta => Boolean(m))
594+
.filter(eligible);
547595
if (related.length >= limit) return related.slice(0, limit);
548596
const fill = TOOLS
549597
.filter((m) => m.slug !== slug && m.category === tool.category && !related.includes(m))
598+
.filter(eligible)
599+
// 被リンクの少ない順。同数なら slug 順で固定し、ビルドごとに揺れないようにする。
600+
.sort((a, b) => {
601+
const d = (INBOUND_FROM_INDEXED.get(a.slug) ?? 0) - (INBOUND_FROM_INDEXED.get(b.slug) ?? 0);
602+
return d !== 0 ? d : a.slug.localeCompare(b.slug);
603+
})
550604
.slice(0, limit - related.length);
551605
return [...related, ...fill];
552606
}

0 commit comments

Comments
 (0)