Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/src/app/[locale]/tools/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default async function ToolPage({
const tt = await getTranslations(`tools.${slug}`);
// index 対象ページの関連リンクは index 対象だけに絞る(権威を noindex へ流さない)。
// noindex ページ側は従来どおり全ツールから出す — 利用者向けの導線で、クロール上の損が無い。
const related = getRelated(slug, 6, isIndexableIn(locale, slug));
const related = getRelated(slug, 6, isIndexableIn(locale, slug), locale);
const url = `${siteConfig.url}/${locale}/tools/${slug}`;

const faq = (tt.raw("faq") as { q: string; a: string }[]) ?? [];
Expand Down
33 changes: 33 additions & 0 deletions site/src/lib/tools/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,36 @@ describe("tool registry consistency", () => {
}
});
});


describe("SEO links match the locale-specific indexability policy", () => {
it("does not recommend noindex tool URLs from indexable exception locales", async () => {
const { getRelated, isIndexableIn } = await import("./registry");
const cases = [
["th", "date-calculator"],
["tr", "grade-calculator"],
["ar", "length-converter"],
["ru", "pressure-converter"],
["es", "due-date-calculator"],
["fr", "speed-converter"],
["en", "workdays-calculator"],
["ja", "date-calculator"],
] as const;
for (const [locale, slug] of cases) {
expect(isIndexableIn(locale, slug), `${locale}/${slug} should be indexable`).toBe(true);
const related = getRelated(slug, 6, true, locale);
for (const target of related) {
expect(target.slug).not.toBe(slug);
expect(isIndexableIn(locale, target.slug),
`${locale}/${slug} should not link to noindex ${target.slug}`).toBe(true);
}
expect(new Set(related.map((tool) => tool.slug)).size).toBe(related.length);
}
});
it("leaves ordinary user navigation unrestricted on noindex pages", async () => {
const { getRelated } = await import("./registry");
const old = getRelated("date-calculator", 6, false);
const localized = getRelated("date-calculator", 6, false, "th");
expect(localized.map((x) => x.slug)).toEqual(old.map((x) => x.slug));
});
});
14 changes: 12 additions & 2 deletions site/src/lib/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,23 @@ const INBOUND_FROM_INDEXED: Map<string, number> = (() => {
* 既定は false。noindex ツールのページでは noindex 同士を関連に出してよい
* (利用者向けの導線であり、クロール上の損は無い)ため、絞るかは呼び出し側が決める。
*/
export function getRelated(slug: string, limit = 5, indexableOnly = false): ToolMeta[] {
export function getRelated(
slug: string,
limit = 5,
indexableOnly = false,
locale?: string,
): ToolMeta[] {
const tool = SLUG_INDEX.get(slug);
if (!tool) return [];
const eligible = (m: ToolMeta) => !indexableOnly || INDEXED_SLUGS.has(m.slug);
// Pages indexed as locale-specific exceptions must NOT link to related
// tools that are noindex in that locale. CORE_TOOLS_BY_DEMAND covers en/ja
// broadly but does not imply indexability for ar/th/tr/ru/es/fr.
const eligible = (m: ToolMeta) => !indexableOnly ||
(locale ? isIndexableIn(locale, m.slug) : INDEXED_SLUGS.has(m.slug));
const related = tool.related
.map((s) => SLUG_INDEX.get(s))
.filter((m): m is ToolMeta => Boolean(m))
.filter((m) => m.slug !== slug)
.filter(eligible);
if (related.length >= limit) return related.slice(0, limit);
const fill = TOOLS
Expand Down
Loading