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
49 changes: 24 additions & 25 deletions app/[locale]/developers/tools/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
28 changes: 18 additions & 10 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,37 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const pages = await getAllPagesWithTranslations()

const entries: MetadataRoute.Sitemap = []
const seenUrls = new Set<string>()

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,
})
}
}
Expand Down
51 changes: 8 additions & 43 deletions src/data-layer/fetchers/developer-tools/utils.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/data/developerTools.ts
Original file line number Diff line number Diff line change
@@ -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<string, DeveloperToolCategorySlug>
18 changes: 16 additions & 2 deletions src/lib/i18n/translationRegistry.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
6 changes: 1 addition & 5 deletions src/lib/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {

import { getTranslatedLocales } from "../i18n/translationRegistry"

import { isLocaleValidISO639_1 } from "./translations"
import { getFullUrl } from "./url"

import { routing } from "@/i18n/routing"
Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down