-
-
-
- :{" "}
-
- @{lastContributor.login}
-
- , {lastEditLocaleTimestamp}
+
+
+
+ {lastEditLocaleTimestamp}
+
+
+
+
+
+
-
-
-
-
>
)
diff --git a/src/intl/en/common.json b/src/intl/en/common.json
index 15618378555..eb973b4226f 100644
--- a/src/intl/en/common.json
+++ b/src/intl/en/common.json
@@ -31,7 +31,7 @@
"content-standardization": "Content standardization",
"contributing": "Contributing",
"contributors": "Contributors",
- "contributors-thanks": "Everyone who has contributed to this page – thank you!",
+ "contributors-thanks": "Everyone who has contributed to this page – thank you!",
"cookie-policy": "Cookie policy",
"copied": "Copied",
"copy": "Copy",
@@ -199,7 +199,7 @@
"language-zh-tw": "Chinese Traditional",
"languages": "Languages",
"last-24-hrs": "Last 24 hours",
- "last-edit": "Last edit",
+ "page-last-update": "Page last update:",
"last-updated": "Last updated",
"layer-2": "Layer 2",
"learn": "Learn",
@@ -449,5 +449,6 @@
"withdrawals": "Staking withdrawals",
"wrapped-ether": "Wrapped Ether",
"yes": "Yes",
- "zero-knowledge-proofs": "Zero-knowledge proofs"
+ "zero-knowledge-proofs": "Zero-knowledge proofs",
+ "translator": "Translator"
}
diff --git a/src/layouts/ContentLayout.tsx b/src/layouts/ContentLayout.tsx
index 230d5d5bcf0..6ec2409e940 100644
--- a/src/layouts/ContentLayout.tsx
+++ b/src/layouts/ContentLayout.tsx
@@ -1,6 +1,9 @@
import type { HTMLAttributes } from "react"
+import { FileContributor } from "@/lib/types"
+
import FeedbackCard from "@/components/FeedbackCard"
+import FileContributors from "@/components/FileContributors"
import LeftNavBar, { LeftNavBarProps } from "@/components/LeftNavBar"
import { ContentContainer, Page } from "@/components/MdComponents"
import MobileButtonDropdown from "@/components/MobileButtonDropdown"
@@ -9,6 +12,8 @@ type ContentLayoutProps = HTMLAttributes &
Pick & {
children: React.ReactNode
heroSection: React.ReactNode
+ contributors: FileContributor[]
+ lastEditLocaleTimestamp: string
}
export const ContentLayout = ({
@@ -17,6 +22,8 @@ export const ContentLayout = ({
tocItems,
maxDepth,
heroSection,
+ contributors,
+ lastEditLocaleTimestamp,
...props
}: ContentLayoutProps) => {
return (
@@ -33,9 +40,14 @@ export const ContentLayout = ({
{children}
+
+
-
{dropdownLinks && }
diff --git a/src/layouts/Static.tsx b/src/layouts/Static.tsx
index 5d0159b8814..0bd2a331ac0 100644
--- a/src/layouts/Static.tsx
+++ b/src/layouts/Static.tsx
@@ -10,6 +10,7 @@ import Callout from "@/components/Callout"
import Contributors from "@/components/Contributors"
import EnergyConsumptionChart from "@/components/EnergyConsumptionChart"
import FeedbackCard from "@/components/FeedbackCard"
+import FileContributors from "@/components/FileContributors"
import GlossaryDefinition from "@/components/Glossary/GlossaryDefinition"
import GlossaryTooltip from "@/components/Glossary/GlossaryTooltip"
import { HubHero } from "@/components/Hero"
@@ -78,7 +79,11 @@ export const staticComponents = {
type StaticLayoutProps = ChildOnlyProp &
Pick<
MdPageContent,
- "slug" | "tocItems" | "lastEditLocaleTimestamp" | "contentNotTranslated"
+ | "slug"
+ | "tocItems"
+ | "lastEditLocaleTimestamp"
+ | "contentNotTranslated"
+ | "contributors"
> & {
frontmatter: StaticFrontmatter
}
@@ -89,6 +94,7 @@ export const StaticLayout = ({
tocItems,
lastEditLocaleTimestamp,
contentNotTranslated,
+ contributors,
}: StaticLayoutProps) => {
const locale = useLocale()
@@ -135,6 +141,11 @@ export const StaticLayout = ({
/>
{children}
+
diff --git a/src/layouts/Tutorial.tsx b/src/layouts/Tutorial.tsx
index 69dde183913..f7cbcc5c667 100644
--- a/src/layouts/Tutorial.tsx
+++ b/src/layouts/Tutorial.tsx
@@ -123,6 +123,7 @@ export const TutorialLayout = ({
/>
{children}
diff --git a/src/layouts/md/Roadmap.tsx b/src/layouts/md/Roadmap.tsx
index 9a38cdcf7ed..42a99514679 100644
--- a/src/layouts/md/Roadmap.tsx
+++ b/src/layouts/md/Roadmap.tsx
@@ -23,7 +23,14 @@ export const roadmapComponents = {
}
type RoadmapLayoutProps = ChildOnlyProp &
- Pick & {
+ Pick<
+ MdPageContent,
+ | "slug"
+ | "tocItems"
+ | "contentNotTranslated"
+ | "contributors"
+ | "lastEditLocaleTimestamp"
+ > & {
frontmatter: RoadmapFrontmatter
}
export const RoadmapLayout = ({
@@ -31,6 +38,8 @@ export const RoadmapLayout = ({
frontmatter,
slug,
tocItems,
+ contributors,
+ lastEditLocaleTimestamp,
contentNotTranslated,
}: RoadmapLayoutProps) => {
const { t } = useTranslation("common")
@@ -99,6 +108,8 @@ export const RoadmapLayout = ({
tocItems={tocItems}
dropdownLinks={dropdownLinks}
maxDepth={frontmatter.sidebarDepth}
+ contributors={contributors}
+ lastEditLocaleTimestamp={lastEditLocaleTimestamp}
heroSection={
slug === "/roadmap/" ? (
& {
+ Pick<
+ MdPageContent,
+ | "slug"
+ | "tocItems"
+ | "contentNotTranslated"
+ | "contributors"
+ | "lastEditLocaleTimestamp"
+ > & {
frontmatter: StakingFrontmatter
}
@@ -84,6 +91,8 @@ export const StakingLayout = ({
slug,
tocItems,
contentNotTranslated,
+ contributors,
+ lastEditLocaleTimestamp,
}: StakingLayoutProps) => {
const { t } = useTranslation("page-staking")
@@ -164,6 +173,8 @@ export const StakingLayout = ({
tocItems={tocItems}
dropdownLinks={dropdownLinks}
maxDepth={frontmatter.sidebarDepth}
+ contributors={contributors}
+ lastEditLocaleTimestamp={lastEditLocaleTimestamp}
heroSection={}
>
{children}
diff --git a/src/layouts/md/Translatathon.tsx b/src/layouts/md/Translatathon.tsx
index 24b6ba50eac..1927b16a957 100644
--- a/src/layouts/md/Translatathon.tsx
+++ b/src/layouts/md/Translatathon.tsx
@@ -100,7 +100,10 @@ export const translatathonComponents = {
}
type TranslatathonLayoutProps = ChildOnlyProp &
- Pick & {
+ Pick<
+ MdPageContent,
+ "slug" | "tocItems" | "contributors" | "lastEditLocaleTimestamp"
+ > & {
frontmatter: SharedFrontmatter
}
@@ -109,6 +112,8 @@ export const TranslatathonLayout = ({
frontmatter,
slug,
tocItems,
+ contributors,
+ lastEditLocaleTimestamp,
}: TranslatathonLayoutProps) => {
const dropdownLinks: ButtonDropdownList = {
text: "Translatathon menu",
@@ -181,6 +186,8 @@ export const TranslatathonLayout = ({
dir="ltr"
tocItems={tocItems}
dropdownLinks={dropdownLinks}
+ contributors={contributors}
+ lastEditLocaleTimestamp={lastEditLocaleTimestamp}
heroSection={}
>
{children}
diff --git a/src/layouts/md/Upgrade.tsx b/src/layouts/md/Upgrade.tsx
index 40128e528f2..de874db2dc8 100644
--- a/src/layouts/md/Upgrade.tsx
+++ b/src/layouts/md/Upgrade.tsx
@@ -24,7 +24,11 @@ export const upgradeComponents = {
type UpgradeLayoutProps = ChildOnlyProp &
Pick<
MdPageContent,
- "slug" | "tocItems" | "lastEditLocaleTimestamp" | "contentNotTranslated"
+ | "slug"
+ | "tocItems"
+ | "lastEditLocaleTimestamp"
+ | "contentNotTranslated"
+ | "contributors"
> & {
frontmatter: UpgradeFrontmatter
}
@@ -35,6 +39,7 @@ export const UpgradeLayout = ({
tocItems,
lastEditLocaleTimestamp,
contentNotTranslated,
+ contributors,
}: UpgradeLayoutProps) => {
const { t } = useTranslation("page-upgrades")
@@ -91,6 +96,8 @@ export const UpgradeLayout = ({
dir={contentNotTranslated ? "ltr" : "unset"}
tocItems={tocItems}
dropdownLinks={dropdownLinks}
+ contributors={contributors}
+ lastEditLocaleTimestamp={lastEditLocaleTimestamp}
heroSection={}
>
{children}
diff --git a/src/layouts/md/UseCases.tsx b/src/layouts/md/UseCases.tsx
index 79537a32130..6024cba5162 100644
--- a/src/layouts/md/UseCases.tsx
+++ b/src/layouts/md/UseCases.tsx
@@ -26,7 +26,14 @@ export const useCasesComponents = {
}
type UseCasesLayoutProps = ChildOnlyProp &
- Pick & {
+ Pick<
+ MdPageContent,
+ | "slug"
+ | "tocItems"
+ | "contentNotTranslated"
+ | "contributors"
+ | "lastEditLocaleTimestamp"
+ > & {
frontmatter: UseCasesFrontmatter
}
export const UseCasesLayout = ({
@@ -35,6 +42,8 @@ export const UseCasesLayout = ({
slug,
tocItems,
contentNotTranslated,
+ contributors,
+ lastEditLocaleTimestamp,
}: UseCasesLayoutProps) => {
const { t } = useTranslation("template-usecase")
@@ -172,6 +181,8 @@ export const UseCasesLayout = ({
tocItems={tocItems}
dropdownLinks={dropdownLinks}
maxDepth={frontmatter.sidebarDepth}
+ contributors={contributors}
+ lastEditLocaleTimestamp={lastEditLocaleTimestamp}
heroSection={}
>
{children}
diff --git a/src/layouts/stories/ContentLayout.stories.tsx b/src/layouts/stories/ContentLayout.stories.tsx
index 28f3911db7a..0fdd70ad47a 100644
--- a/src/layouts/stories/ContentLayout.stories.tsx
+++ b/src/layouts/stories/ContentLayout.stories.tsx
@@ -62,6 +62,21 @@ export const ContentLayout: StoryObj = {
],
},
maxDepth: 2,
+ contributors: [
+ {
+ login: "github",
+ avatar_url: "/",
+ html_url: "https://github.com",
+ date: "2025-04-20T12:00:00.000Z",
+ },
+ {
+ login: "crowdin",
+ avatar_url: "/",
+ html_url: "https://crowdin.com",
+ date: "2025-04-20T12:00:00.000Z",
+ },
+ ],
+ lastEditLocaleTimestamp: "MM DD, YY",
heroSection: (
Hero section
diff --git a/src/lib/api/fetchGitHistory.ts b/src/lib/api/fetchGitHistory.ts
index 2f54f15c106..edf299252d5 100644
--- a/src/lib/api/fetchGitHistory.ts
+++ b/src/lib/api/fetchGitHistory.ts
@@ -46,7 +46,7 @@ async function fetchWithRateLimit(filepath: string): Promise {
}
// Fetch commit history and save it to a JSON file
-export const fetchAndCacheGitContributors = async (
+export const fetchAndCacheGitHubContributors = async (
filepath: string,
cache: CommitHistory
) => {
diff --git a/src/lib/md/data.ts b/src/lib/md/data.ts
index 99690ac7d67..0a6db9a3df6 100644
--- a/src/lib/md/data.ts
+++ b/src/lib/md/data.ts
@@ -6,11 +6,10 @@ import {
FileContributor,
Frontmatter,
Lang,
- Layout,
ToCItem,
} from "@/lib/types"
-import { getFileContributorInfo } from "@/lib/utils/contributors"
+import { getMarkdownFileContributorInfo } from "@/lib/utils/contributors"
import { getLocaleTimestamp } from "@/lib/utils/time"
import { compile } from "./compile"
@@ -22,7 +21,6 @@ interface GetPageDataParams {
locale: string
slug: string
components: MDXRemoteProps["components"]
- layout?: Layout
scope?: Record
}
@@ -40,7 +38,6 @@ export async function getPageData({
locale,
slug,
components,
- layout: layoutFromProps,
scope,
}: GetPageDataParams): Promise {
const slugArray = slug.split("/")
@@ -55,8 +52,6 @@ export async function getPageData({
scope,
})
- const layout = layoutFromProps || frontmatter.template || "static"
-
// Process TOC items
const tocItems =
tocNodeItems.length === 1 && "items" in tocNodeItems[0]
@@ -64,13 +59,13 @@ export async function getPageData({
: tocNodeItems
// Get contributor information
- const { contributors, lastUpdatedDate } = await getFileContributorInfo(
- slug,
- locale,
- frontmatter.lang as string,
- layout,
- commitHistoryCache
- )
+ const { contributors, lastUpdatedDate } =
+ await getMarkdownFileContributorInfo(
+ slug,
+ locale,
+ frontmatter.lang as string,
+ commitHistoryCache
+ )
// Format timestamp
const lastEditLocaleTimestamp = getLocaleTimestamp(
diff --git a/src/lib/types.ts b/src/lib/types.ts
index bcc4550cc36..a464d46e3de 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -395,7 +395,7 @@ export type FileContributor = {
login: string
avatar_url: string
html_url: string
- date?: string
+ date: string
}
type FilePath = string
@@ -972,6 +972,11 @@ export type EventCardProps = {
imageUrl?: string
}
+export type PageWithContributorsProps = {
+ contributors: FileContributor[]
+ lastEditLocaleTimestamp: string
+}
+
export type BreakpointKey = keyof typeof screens
export type MaturityLevel =
diff --git a/src/lib/utils/contributors.ts b/src/lib/utils/contributors.ts
index bb1ad128d6c..6222e0d887a 100644
--- a/src/lib/utils/contributors.ts
+++ b/src/lib/utils/contributors.ts
@@ -1,6 +1,6 @@
import { join } from "path"
-import type { CommitHistory, FileContributor, Lang, Layout } from "@/lib/types"
+import type { CommitHistory, FileContributor, Lang } from "@/lib/types"
import { CONTENT_DIR, CONTENT_PATH, DEFAULT_LOCALE } from "@/lib/constants"
@@ -8,41 +8,95 @@ import {
convertToFileContributorFromCrowdin,
getCrowdinContributors,
} from "./crowdin"
-import { getLastModifiedDate } from "./gh"
+import { getAppPageLastCommitDate, getMarkdownLastCommitDate } from "./gh"
+import { getLocaleTimestamp } from "./time"
-import { fetchAndCacheGitContributors } from "@/lib/api/fetchGitHistory"
+import { fetchAndCacheGitHubContributors } from "@/lib/api/fetchGitHistory"
-export const getFileContributorInfo = async (
+export const getMarkdownFileContributorInfo = async (
slug: string,
locale: string,
fileLang: string,
- layout: Layout,
cache: CommitHistory
) => {
const mdPath = join(CONTENT_PATH, slug)
const mdDir = join(CONTENT_DIR, slug)
- const gitContributors = await fetchAndCacheGitContributors(
+ const gitHubContributors = await fetchAndCacheGitHubContributors(
join("/", mdDir, "index.md"),
cache
)
- const latestCommitDate = getLastModifiedDate(slug, locale!)
- const gitHubLastEdit = gitContributors[0]?.date
+ const latestCommitDate = getMarkdownLastCommitDate(slug, locale!)
+ const gitHubLastEdit = gitHubContributors[0]?.date
const lastUpdatedDate = gitHubLastEdit || latestCommitDate
- const crowdinContributors = ["docs", "tutorial"].includes(layout)
- ? convertToFileContributorFromCrowdin(
- getCrowdinContributors(mdPath, locale as Lang)
- )
- : []
+ const crowdinContributors = convertToFileContributorFromCrowdin(
+ getCrowdinContributors(mdPath, locale as Lang)
+ )
- const useGitHubContributors: boolean =
+ const englishOnly: boolean =
fileLang === DEFAULT_LOCALE || crowdinContributors.length === 0
- const contributors: FileContributor[] = useGitHubContributors
- ? gitContributors
- : crowdinContributors
+ const contributors: FileContributor[] = englishOnly
+ ? gitHubContributors
+ : [...crowdinContributors, ...gitHubContributors]
return { contributors, lastUpdatedDate }
}
+
+/**
+ * Returns an array of possible historical file paths for a given page,
+ * accounting for different directory structures and migrations over time.
+ *
+ * @param pagePath - The relative path of the page (without extension).
+ * @returns An array of strings representing all historical file paths for the page.
+ *
+ * @remarks
+ * This function is used to track all possible locations a page may have existed in the repository,
+ * which is useful for aggregating git history and contributor information.
+ *
+ * @note
+ * If a page is migrated or its location changes, ensure the new path is added to this list.
+ * This maintains a complete historical record for accurate git history tracking.
+ */
+const getAllHistoricalPaths = (pagePath: string): string[] => [
+ join("src/pages", `${pagePath}.tsx`),
+ join("src/pages", pagePath, "index.tsx"),
+ join("src/pages/[locale]", `${pagePath}.tsx`),
+ join("src/pages/[locale]", pagePath, "index.tsx"),
+ join("app/[locale]", pagePath, "page.tsx"),
+ join("app/[locale]", pagePath, "_components", `${pagePath}.tsx`),
+]
+
+export const getAppPageContributorInfo = async (
+ pagePath: string,
+ locale: Lang,
+ cache: CommitHistory
+) => {
+ // TODO: Incorporate Crowdin contributor information
+
+ const gitHubContributors = await getAllHistoricalPaths(pagePath).reduce(
+ async (acc, path) => {
+ const contributors = await fetchAndCacheGitHubContributors(path, cache)
+ return [...(await acc), ...contributors]
+ },
+ Promise.resolve([] as FileContributor[])
+ )
+
+ const uniqueGitHubContributors = gitHubContributors.filter(
+ (contributor, index, self) =>
+ index === self.findIndex((t) => t.login === contributor.login)
+ )
+
+ const latestCommitDate = getAppPageLastCommitDate(gitHubContributors)
+ const lastEditLocaleTimestamp = getLocaleTimestamp(locale, latestCommitDate)
+
+ if (!uniqueGitHubContributors.length || !lastEditLocaleTimestamp) {
+ throw new Error(
+ `No contributors found, path: ${pagePath}, locale: ${locale}`
+ )
+ }
+
+ return { contributors: uniqueGitHubContributors, lastEditLocaleTimestamp }
+}
diff --git a/src/lib/utils/crowdin.ts b/src/lib/utils/crowdin.ts
index 9da6717b48b..cdffedcfcd0 100644
--- a/src/lib/utils/crowdin.ts
+++ b/src/lib/utils/crowdin.ts
@@ -30,4 +30,5 @@ export const convertToFileContributorFromCrowdin = (
login: username,
avatar_url: avatarUrl,
html_url: `https://crowdin.com/profile/${username}`,
+ date: new Date(0).toString(),
}))
diff --git a/src/lib/utils/gh.ts b/src/lib/utils/gh.ts
index c54c84bcd6b..d2b4dfd1fd1 100644
--- a/src/lib/utils/gh.ts
+++ b/src/lib/utils/gh.ts
@@ -4,6 +4,8 @@ import { join } from "path"
import { CONTENT_DIR, DEFAULT_LOCALE, TRANSLATIONS_DIR } from "@/lib/constants"
+import { FileContributor } from "../types"
+
const getGitLogFromPath = (path: string): string => {
// git command to show file last commit info
const gitCommand = `git log -1 -- ${path}`
@@ -25,8 +27,27 @@ const extractDateFromGitLogInfo = (logInfo: string): string => {
}
}
+export const getAppPageLastCommitDate = (
+ gitHubContributors: FileContributor[]
+) =>
+ gitHubContributors
+ .reduce((latest, contributor) => {
+ const commitDate = new Date(contributor.date)
+ return commitDate > latest ? commitDate : latest
+ }, new Date(0))
+ .toString()
+
+export const getLastGitCommitDateByPath = (path: string): string => {
+ if (!fs.existsSync(path)) throw new Error(`File not found: ${path}`)
+ const logInfo = getGitLogFromPath(path)
+ return extractDateFromGitLogInfo(logInfo)
+}
+
// This util filters the git log to get the file last commit info, and then the commit date (last update)
-export const getLastModifiedDate = (slug: string, locale: string): string => {
+export const getMarkdownLastCommitDate = (
+ slug: string,
+ locale: string
+): string => {
const translatedContentPath = join(TRANSLATIONS_DIR, locale, slug, "index.md")
const contentIsNotTranslated = !fs.existsSync(translatedContentPath)
let filePath = ""
@@ -39,14 +60,7 @@ export const getLastModifiedDate = (slug: string, locale: string): string => {
filePath = join(TRANSLATIONS_DIR, locale, slug, "index.md")
}
- const logInfo = getGitLogFromPath(filePath)
- return extractDateFromGitLogInfo(logInfo)
-}
-
-export const getLastModifiedDateByPath = (path: string): string => {
- if (!fs.existsSync(path)) throw new Error(`File not found: ${path}`)
- const logInfo = getGitLogFromPath(path)
- return extractDateFromGitLogInfo(logInfo)
+ return getLastGitCommitDateByPath(filePath)
}
const LABELS_TO_SEARCH = [