Skip to content

Commit a1d0c90

Browse files
committed
fix(seo): 中身が全部 noindex の空ハブ (color/image) をインデックス対象から外す
カテゴリハブの掲載判定を「カテゴリ内の総ツール数」から 「index 対象ツール数」へ変更する。総数判定だと color(8件中0件) / image(1件中0件) のように中身が全ツール noindex のカテゴリが 実質空のハブとして index,follow で本番配信され、sitemap にも 載っていた (2026-09-18 に本番実測: 両URL 200 / sitemap に126回出現)。 registry.ts に listIndexableByCategory() を追加し、判定を使う 4か所すべてを同じ述語に統一する: - sitemap.ts : ハブの掲載 - generateStaticParams : ハブの生成 - siblings ナビ : 隣接ハブへの内部リンク - Footer.tsx : 全ページのカテゴリ導線 (揃えないと 404 リンクが全ページに出る) さらに generateMetadata に noindex を追加する。dynamicParams は 既定 true かつ output:"standalone" のため、sitemap と generateStaticParams から外すだけでは未生成ハブがオンデマンドで 200 を返し index,follow のまま残ってしまう。 実測: 223ツール / index対象63 / ハブ 8→6 (DROP は color と image のみ)。 注意: このツリーに node_modules が無く npm run build は未実行。 各ファイルの構文検査と、tsx によるレジストリ実挙動の確認のみ済み。 Claude-Session: https://claude.ai/code/session_01ENHPNZ5ngcdrTHJ2H6oAFd
1 parent b7ead2a commit a1d0c90

4 files changed

Lines changed: 32 additions & 7 deletions

File tree

site/src/app/[locale]/tools/category/[cat]/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
22
import type { Metadata } from "next";
33
import { setRequestLocale, getTranslations } from "next-intl/server";
44
import { Link } from "@/lib/i18n/navigation";
5-
import { listByCategory } from "@/lib/tools/registry";
5+
import { listByCategory, listIndexableByCategory } from "@/lib/tools/registry";
66
import { CATEGORY_DESCRIPTIONS } from "@/lib/tools/category-descriptions";
77
import { CATEGORY_CONFIG } from "@/lib/tools/categories";
88
import { ToolCard } from "@/components/tools/ToolCard";
@@ -15,7 +15,9 @@ const CATEGORIES = Object.keys(CATEGORY_CONFIG) as ToolCategory[];
1515

1616
export function generateStaticParams() {
1717
// Only generate hubs for categories that actually have tools (empty hubs are HCU-bait → 404).
18-
const populated = CATEGORIES.filter((c) => listByCategory(c).length > 0);
18+
// 判定は総数でなく index 対象の件数。総数だと color(8件中0件) / image(1件中0件) のように
19+
// 中身が全部 noindex のハブが index,follow で生成され、実質空のページになる。
20+
const populated = CATEGORIES.filter((c) => listIndexableByCategory(c).length > 0);
1921
return LOCALES.flatMap((locale) => populated.map((cat) => ({ locale, cat })));
2022
}
2123

@@ -33,11 +35,16 @@ export async function generateMetadata({
3335
const t = await getTranslations({ locale });
3436
const copy = getCopy(cat, locale);
3537
const cfg = CATEGORY_CONFIG[cat as ToolCategory];
38+
// sitemap / generateStaticParams から外しただけでは足りない。dynamicParams は既定 true なので
39+
// 未生成のハブもオンデマンドで 200 を返す。中身が全部 noindex のハブ (color / image) は
40+
// ここで明示的に noindex にしないと index,follow のまま残る (2026-09-18 実測)。
41+
const indexable = listIndexableByCategory(cat).length > 0;
3642
return buildMetadata({
3743
locale: locale as Locale,
3844
title: copy?.headline ?? `${cfg?.label ?? cat}${t("nav.tools")}`,
3945
description: copy?.body ?? t("site.description"),
4046
path: `/tools/category/${cat}`,
47+
noindex: !indexable,
4148
});
4249
}
4350

@@ -78,7 +85,9 @@ export default async function ToolCategoryPage({
7885
],
7986
};
8087

81-
const siblings = CATEGORIES.filter((c) => c !== cat && listByCategory(c).length > 0);
88+
// 隣接ハブへの内部リンク。生成されないハブ (index 対象0件) へのリンクは 404 になるため、
89+
// generateStaticParams と同じ判定を使う。
90+
const siblings = CATEGORIES.filter((c) => c !== cat && listIndexableByCategory(c).length > 0);
8291

8392
return (
8493
<div className="mx-auto max-w-6xl px-4 py-10">

site/src/app/sitemap.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MetadataRoute } from "next";
22
import { siteConfig } from "@/lib/config";
33
import { LOCALES, PROMPT_LOCALES, isIndexedLocale } from "@/lib/i18n/locales";
4-
import { listIndexableTools, listByCategory } from "@/lib/tools/registry";
4+
import { listIndexableTools, listIndexableByCategory } from "@/lib/tools/registry";
55
import { CATEGORY_CONFIG } from "@/lib/tools/categories";
66
import { listPrompts } from "@/lib/prompts/registry";
77
import type { ToolCategory } from "@/lib/tools/types";
@@ -91,8 +91,10 @@ export default function sitemap(): MetadataRoute.Sitemap {
9191

9292
// Tool category hubs — one per populated category, all locales (mirrors the page's
9393
// generateStaticParams). These concentrate internal-link authority on each category.
94+
// 判定は総数でなく index 対象の件数で行う。総数だと中身が全部 noindex の
95+
// カテゴリ (color / image) が実質空のハブとして sitemap に載ってしまう。
9496
const toolCats = (Object.keys(CATEGORY_CONFIG) as ToolCategory[]).filter(
95-
(c) => listByCategory(c).length > 0,
97+
(c) => listIndexableByCategory(c).length > 0,
9698
);
9799
for (const cat of toolCats) {
98100
for (const locale of IDX) {

site/src/components/layout/Footer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { siteConfig } from "@/lib/config";
44
import { isPromptLocale } from "@/lib/i18n/locales";
55
import { NewsletterForm } from "@/components/cross/NewsletterForm";
66
import { CATEGORY_CONFIG } from "@/lib/tools/categories";
7-
import { listByCategory } from "@/lib/tools/registry";
7+
import { listIndexableByCategory } from "@/lib/tools/registry";
88
import type { ToolCategory } from "@/lib/tools/types";
99

1010
export function Footer() {
@@ -67,8 +67,10 @@ export function Footer() {
6767
{t("nav.categories")}
6868
</p>
6969
<nav className="flex flex-wrap gap-x-6 gap-y-2">
70+
{/* 生成されないハブ (index 対象0件) へは全ページからリンクしないこと。
71+
総数で判定すると color / image への 404 リンクがサイト全体に出る。 */}
7072
{(Object.keys(CATEGORY_CONFIG) as ToolCategory[])
71-
.filter((cat) => listByCategory(cat).length > 0)
73+
.filter((cat) => listIndexableByCategory(cat).length > 0)
7274
.map((cat) => (
7375
<Link key={cat} href={`/tools/category/${cat}`} className="hover:underline">
7476
<span className="mr-1" aria-hidden>

site/src/lib/tools/registry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,18 @@ export function listByCategory(category: string): ToolMeta[] {
526526
return TOOLS.filter((m) => m.category === category);
527527
}
528528

529+
/**
530+
* カテゴリ内の index 対象ツールだけを返す。
531+
*
532+
* ハブを sitemap / generateStaticParams に載せるかの判定は、総数ではなく
533+
* **この件数**で行う。総数で判定すると、中身が全部 noindex のカテゴリ
534+
* (color=8件中0件, image=1件中0件) が `index, follow` のまま sitemap に載り、
535+
* 実質空のハブを Google に送信してしまう (2026-09-18 に本番で実測)。
536+
*/
537+
export function listIndexableByCategory(category: string): ToolMeta[] {
538+
return listByCategory(category).filter((m) => INDEXED_SLUGS.has(m.slug));
539+
}
540+
529541
export function getRelated(slug: string, limit = 5): ToolMeta[] {
530542
const tool = SLUG_INDEX.get(slug);
531543
if (!tool) return [];

0 commit comments

Comments
 (0)