diff --git a/app/[locale]/developers/tools/constants.ts b/app/[locale]/developers/tools/constants.ts index 44f1f5b01dd..4fac04ac9a9 100644 --- a/app/[locale]/developers/tools/constants.ts +++ b/app/[locale]/developers/tools/constants.ts @@ -11,35 +11,34 @@ import { import { TagProps } from "@/components/ui/tag" -import type { DeveloperToolCategorySlug } from "./types" +import { + DEV_TOOL_CATEGORY_SLUG_LIST, + DEV_TOOL_CATEGORY_SLUGS, + type DeveloperToolCategorySlug, +} from "@/data/developerTools" -export const DEV_TOOL_CATEGORY_SLUGS: Record< - string, - DeveloperToolCategorySlug +const DEV_TOOL_CATEGORY_VISUALS: Record< + DeveloperToolCategorySlug, + { Icon: LucideIcon; tag: TagProps["status"] } > = { - "Cross-Chain & Interoperability": "interoperability", - "Transaction & Wallet Infrastructure": "transactions", - "Data, Analytics & Tracing": "analytics", - "Education & Community Resources": "education", - "Client Libraries & SDKs (Front-End)": "sdks", - "Smart Contract Development & Toolchains": "contracts", - "Security, Testing & Formal Verification": "security", + interoperability: { Icon: SendToBack, tag: "accent-a" }, + transactions: { Icon: ArrowLeftRight, tag: "accent-b" }, + analytics: { Icon: ChartSpline, tag: "accent-c" }, + education: { Icon: GraduationCap, tag: "primary" }, + sdks: { Icon: Package, tag: "tag-green" }, + contracts: { Icon: CodeXml, tag: "tag-yellow" }, + security: { Icon: Shield, tag: "tag-red" }, } -export const DEV_TOOL_CATEGORIES = [ - { slug: "interoperability", Icon: SendToBack, tag: "accent-a" }, - { slug: "transactions", Icon: ArrowLeftRight, tag: "accent-b" }, - { slug: "analytics", Icon: ChartSpline, tag: "accent-c" }, - { slug: "education", Icon: GraduationCap, tag: "primary" }, - { slug: "sdks", Icon: Package, tag: "tag-green" }, - { slug: "contracts", Icon: CodeXml, tag: "tag-yellow" }, - { slug: "security", Icon: Shield, tag: "tag-red" }, -] as const satisfies { - slug: string +export { DEV_TOOL_CATEGORY_SLUGS } + +export const DEV_TOOL_CATEGORIES: { + slug: DeveloperToolCategorySlug Icon: LucideIcon tag: TagProps["status"] -}[] +}[] = DEV_TOOL_CATEGORY_SLUG_LIST.map((slug) => ({ + slug, + ...DEV_TOOL_CATEGORY_VISUALS[slug], +})) -export const VALID_CATEGORY_SLUGS = new Set( - DEV_TOOL_CATEGORIES.map(({ slug }) => slug) -) +export const VALID_CATEGORY_SLUGS = new Set(DEV_TOOL_CATEGORY_SLUG_LIST) diff --git a/app/sitemap.ts b/app/sitemap.ts index 8e24caf7124..614a0b25cc8 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -10,29 +10,37 @@ export default async function sitemap(): Promise { const pages = await getAllPagesWithTranslations() const entries: MetadataRoute.Sitemap = [] + const seenUrls = new Set() for (const { slug, translatedLocales } of pages) { const normalizedSlug = slug.startsWith("/") ? slug : `/${slug}` + const alternates = + translatedLocales.length > 0 + ? { + languages: { + "x-default": getFullUrl(DEFAULT_LOCALE, normalizedSlug), + ...Object.fromEntries( + translatedLocales.map((locale) => [ + locale, + getFullUrl(locale, normalizedSlug), + ]) + ), + }, + } + : undefined for (const locale of translatedLocales) { const url = getFullUrl(locale, normalizedSlug) - // Drop the `/en` root entry to avoid duplicating `/` - // This happens when slug is "/" and locale is default - if ( - locale === DEFAULT_LOCALE && - (normalizedSlug === "/" || normalizedSlug === "") - ) { + if (seenUrls.has(url)) { continue } - const isDefaultLocale = locale === DEFAULT_LOCALE + seenUrls.add(url) entries.push({ url, - changeFrequency: isDefaultLocale ? "weekly" : "monthly", - priority: isDefaultLocale ? 0.7 : 0.5, - lastModified: new Date(), + alternates, }) } } diff --git a/src/data-layer/fetchers/developer-tools/utils.ts b/src/data-layer/fetchers/developer-tools/utils.ts index 1468d705a29..c9985f1a8ee 100644 --- a/src/data-layer/fetchers/developer-tools/utils.ts +++ b/src/data-layer/fetchers/developer-tools/utils.ts @@ -1,29 +1,24 @@ import { getDayOfYear, getWeekNumber } from "@/lib/utils/date" +import { + DEV_TOOL_CATEGORY_SLUG_LIST, + DEV_TOOL_CATEGORY_SLUGS, + type DeveloperToolCategorySlug, +} from "@/data/developerTools" + // Import the base DeveloperTool type from tool code (type-only import) // This is acceptable as it's a shared data contract, not a presentation dependency import type { DeveloperTool } from "../../../../app/[locale]/developers/tools/types" // Re-export for convenience export type { DeveloperTool } +export type { DeveloperToolCategorySlug } from "@/data/developerTools" +export { DEV_TOOL_CATEGORY_SLUG_LIST, DEV_TOOL_CATEGORY_SLUGS } // ============================================================================= // Types // ============================================================================= -/** - * Category slug type derived from the category mapping. - * These are URL-friendly identifiers for developer tool categories. - */ -export type DeveloperToolCategorySlug = - | "interoperability" - | "transactions" - | "analytics" - | "education" - | "sdks" - | "contracts" - | "security" - /** * Tools grouped by category slug. */ @@ -62,36 +57,6 @@ export interface DeveloperToolsDataEnvelope { // Constants // ============================================================================= -/** - * Maps human-readable category names to URL-friendly slugs. - * This is the data-layer copy of the constant - no UI dependencies. - */ -export const DEV_TOOL_CATEGORY_SLUGS: Record< - string, - DeveloperToolCategorySlug -> = { - "Cross-Chain & Interoperability": "interoperability", - "Transaction & Wallet Infrastructure": "transactions", - "Data, Analytics & Tracing": "analytics", - "Education & Community Resources": "education", - "Client Libraries & SDKs (Front-End)": "sdks", - "Smart Contract Development & Toolchains": "contracts", - "Security, Testing & Formal Verification": "security", -} - -/** - * List of all category slugs for iteration. - */ -export const DEV_TOOL_CATEGORY_SLUG_LIST: DeveloperToolCategorySlug[] = [ - "interoperability", - "transactions", - "analytics", - "education", - "sdks", - "contracts", - "security", -] - // Number of top tools to show in highlights section const HIGHLIGHTS_PER_CATEGORY = 9 // Number of preview tools to show in category cards diff --git a/src/data/developerTools.ts b/src/data/developerTools.ts new file mode 100644 index 00000000000..e3263fec3ce --- /dev/null +++ b/src/data/developerTools.ts @@ -0,0 +1,22 @@ +export const DEV_TOOL_CATEGORY_SLUG_LIST = [ + "interoperability", + "transactions", + "analytics", + "education", + "sdks", + "contracts", + "security", +] as const + +export type DeveloperToolCategorySlug = + (typeof DEV_TOOL_CATEGORY_SLUG_LIST)[number] + +export const DEV_TOOL_CATEGORY_SLUGS = { + "Cross-Chain & Interoperability": "interoperability", + "Transaction & Wallet Infrastructure": "transactions", + "Data, Analytics & Tracing": "analytics", + "Education & Community Resources": "education", + "Client Libraries & SDKs (Front-End)": "sdks", + "Smart Contract Development & Toolchains": "contracts", + "Security, Testing & Formal Verification": "security", +} satisfies Record diff --git a/src/lib/i18n/translationRegistry.ts b/src/lib/i18n/translationRegistry.ts index fbdd248690e..79721854750 100644 --- a/src/lib/i18n/translationRegistry.ts +++ b/src/lib/i18n/translationRegistry.ts @@ -1,6 +1,8 @@ import { existsSync } from "fs" import { join } from "path" +import { DEV_TOOL_CATEGORY_SLUG_LIST } from "@/data/developerTools" + import { DEFAULT_LOCALE, LOCALES_CODES, @@ -84,13 +86,25 @@ type PageWithTranslations = { type: "md" | "intl" } +function getDynamicIntlPagePaths(): string[] { + // discoverStaticPages() excludes dynamic segments, so add known + // generateStaticParams() routes that should be present in sitemap output. + return DEV_TOOL_CATEGORY_SLUG_LIST.map( + (categorySlug) => `/developers/tools/${categorySlug}/` + ) +} + export async function getAllPagesWithTranslations(): Promise< PageWithTranslations[] > { const pages: PageWithTranslations[] = [] const mdSlugs = await getPostSlugs("/") - const intlPaths = getStaticPagePaths() + const intlPaths = [ + ...getStaticPagePaths(), + ...getDynamicIntlPagePaths(), + ] + const uniqueIntlPaths = Array.from(new Set(intlPaths)) for (const slug of mdSlugs) { const translatedLocales = await getTranslatedLocales(slug) @@ -101,7 +115,7 @@ export async function getAllPagesWithTranslations(): Promise< }) } - for (const path of intlPaths) { + for (const path of uniqueIntlPaths) { const translatedLocales = await getTranslatedLocales(path) pages.push({ slug: path, diff --git a/src/lib/utils/metadata.ts b/src/lib/utils/metadata.ts index 89ebc04740f..116e8f769f3 100644 --- a/src/lib/utils/metadata.ts +++ b/src/lib/utils/metadata.ts @@ -9,7 +9,6 @@ import { import { getTranslatedLocales } from "../i18n/translationRegistry" -import { isLocaleValidISO639_1 } from "./translations" import { getFullUrl } from "./url" import { routing } from "@/i18n/routing" @@ -90,10 +89,7 @@ export const getMetadata = async ({ // Only include hreflang alternates if the current page is translated // Untranslated pages should not have hreflang tags const localesForHreflang = isCurrentPageTranslated - ? routing.locales.filter( - (loc) => - finalTranslatedLocales.includes(loc) && isLocaleValidISO639_1(loc) - ) + ? routing.locales.filter((loc) => finalTranslatedLocales.includes(loc)) : [] const base: Metadata = { diff --git a/src/lib/utils/translations.ts b/src/lib/utils/translations.ts index 7d028379191..d476d55013b 100644 --- a/src/lib/utils/translations.ts +++ b/src/lib/utils/translations.ts @@ -44,6 +44,7 @@ export const PREFIX_PATH_NAMESPACE_MAP: Array<[string, string]> = [ ["/developers/local-environment/", "page-developers-local-environment"], ["/developers/learning-tools/", "page-developers-learning-tools"], ["/developers/tutorials/", "page-developers-tutorials"], + ["/developers/tools/", "page-developers-tools"], ["/developers/", "page-developers-index"], ["/contributing/translation-program/translatathon/", "page-translatathon"], ["/community/events/", "page-community-events"],