From 13773abd638e47624ca6052f4479e8a5016f6e65 Mon Sep 17 00:00:00 2001 From: Jagadisha V Date: Mon, 6 Apr 2026 20:02:12 +0530 Subject: [PATCH 1/2] Added the three columns --- plugins/recentDocsPlugin/index.js | 203 ++++++++++++++++++++++ src/components/MostViewedArticles.js | 64 +++++++ src/components/RecentlyCreatedArticles.js | 64 +++++++ src/components/RecentlyUpdatedArticles.js | 64 +++++++ src/css/sumo.scss | 11 ++ src/pages/index.tsx | 21 ++- 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 plugins/recentDocsPlugin/index.js create mode 100644 src/components/MostViewedArticles.js create mode 100644 src/components/RecentlyCreatedArticles.js create mode 100644 src/components/RecentlyUpdatedArticles.js diff --git a/plugins/recentDocsPlugin/index.js b/plugins/recentDocsPlugin/index.js new file mode 100644 index 0000000000..e92db1fe62 --- /dev/null +++ b/plugins/recentDocsPlugin/index.js @@ -0,0 +1,203 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const matter = require('gray-matter'); + +async function fetchGA4PageViews(propertyId) { + const viewMap = new Map(); + + if (!propertyId) { + console.warn('[recentDocsPlugin] GA4_PROPERTY_ID not set — skipping most-viewed.'); + return viewMap; + } + + const keyPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; + if (!keyPath || !fs.existsSync(keyPath)) { + console.warn('[recentDocsPlugin] GOOGLE_APPLICATION_CREDENTIALS not set or file missing — skipping most-viewed.'); + return viewMap; + } + + try { + const { BetaAnalyticsDataClient } = require('@google-analytics/data'); + const client = new BetaAnalyticsDataClient({ + keyFilename: keyPath, + }); + + const [response] = await client.runReport({ + property: `properties/${propertyId}`, + dateRanges: [{ startDate: '90daysAgo', endDate: 'today' }], + dimensions: [{ name: 'pagePath' }], + metrics: [{ name: 'screenPageViews' }], + orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }], + limit: 50, // fetch top 50, we'll filter down to docs pages only + }); + + for (const row of response.rows ?? []) { + const pagePath = row.dimensionValues[0].value; + const views = parseInt(row.metricValues[0].value, 10); + viewMap.set(pagePath, views); + } + } catch (err) { + console.warn('[recentDocsPlugin] GA4 fetch failed:', err.message); + } + + return viewMap; +} + +function getGitLastUpdated(fullPath) { + try { + const result = execSync( + `git log -1 --format=%cI -- "${fullPath}"`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] } + ).trim(); + return result || null; + } catch { + return null; + } +} + + +function getGitFirstCommit(fullPath) { + try { + const result = execSync( + `git log --follow --format=%cI -- "${fullPath}" | tail -1`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] } + ).trim(); + return result || null; + } catch { + return null; + } +} + + +module.exports = function recentDocsPlugin(context) { + return { + name: 'recent-docs-plugin', + + async loadContent() { + const docsDir = path.join(context.siteDir, 'docs'); + const docs = []; + + function scanDir(dir) { + let files; + try { + files = fs.readdirSync(dir); + } catch { + return; + } + + for (const file of files) { + const fullPath = path.join(dir, file); + let stat; + try { + stat = fs.statSync(fullPath); + } catch { + continue; + } + + if (stat.isDirectory()) { + scanDir(fullPath); + continue; + } + + if (!file.endsWith('.md') && !file.endsWith('.mdx')) continue; + + let fileContent; + try { + fileContent = fs.readFileSync(fullPath, 'utf-8'); + } catch { + continue; + } + + const { data } = matter(fileContent); + if (!data.title) continue; // skip partials and untitled files + + const relativePath = path + .relative(docsDir, fullPath) + .replace(/\\/g, '/') + .replace(/\.mdx?$/, ''); + + + const gitUpdated = getGitLastUpdated(fullPath); + const updated = gitUpdated + ? new Date(gitUpdated).toISOString() + : stat.mtime.toISOString(); // local dev fallback only + + const createdRaw = data.created_at ?? null; + const gitCreated = createdRaw ? null : getGitFirstCommit(fullPath); + const created = createdRaw + ? new Date(createdRaw).toISOString() + : gitCreated + ? new Date(gitCreated).toISOString() + : null; + + docs.push({ + id: relativePath, + title: data.title, + path: `${context.baseUrl}docs/${relativePath}`, + created, + updated, + }); + } + } + + scanDir(docsDir); + return docs; + }, + + async contentLoaded({ content, actions }) { + const { createData } = actions; + + // ── Recently Created ───────────────────────────────────────────────── + const recentCreated = [...content] + .filter((d) => d.created !== null) + .sort((a, b) => new Date(b.created) - new Date(a.created)) + .slice(0, 10) + .map(({ id, title, path, created }) => ({ id, title, path, created })); + + // ── Recently Updated ───────────────────────────────────────────────── + const recentUpdated = [...content] + .sort((a, b) => new Date(b.updated) - new Date(a.updated)) + .slice(0, 10) + .map(({ id, title, path, updated }) => ({ id, title, path, updated })); + + // ── Most Viewed (GA4) ──────────────────────────────────────────────── + // Set these two env vars to enable: + // GA4_PROPERTY_ID — numeric GA4 property ID (no "properties/" prefix) + // GOOGLE_APPLICATION_CREDENTIALS — path to the service account JSON key file + const propertyId = process.env.GA4_PROPERTY_ID; + const viewMap = await fetchGA4PageViews(propertyId); + + let mostViewed = []; + + if (viewMap.size > 0) { + mostViewed = content + .map((doc) => { + const normalised = doc.path.replace(/\/$/, '').toLowerCase(); + const views = + viewMap.get(normalised + '/') ?? + viewMap.get(normalised) ?? + 0; + return { ...doc, views }; + }) + .filter((d) => d.views > 0) + .sort((a, b) => b.views - a.views) + .slice(0, 10) + .map(({ id, title, path, views }) => ({ id, title, path, views })); + } + + await createData( + 'recent-created.json', + JSON.stringify(recentCreated, null, 2), + ); + await createData( + 'recent-updated.json', + JSON.stringify(recentUpdated, null, 2), + ); + await createData( + 'most-viewed.json', + JSON.stringify(mostViewed, null, 2), + ); + }, + }; +}; \ No newline at end of file diff --git a/src/components/MostViewedArticles.js b/src/components/MostViewedArticles.js new file mode 100644 index 0000000000..c6ebc3c059 --- /dev/null +++ b/src/components/MostViewedArticles.js @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { Box, Typography } from '@mui/material'; +import mostViewed from '@generated/recent-docs-plugin/default/most-viewed.json'; + +export default function MostViewedArticles() { + return ( + + + Most Viewed Articles + + + {!mostViewed?.length ? ( + + Analytics data not yet available. + + ) : ( + + {mostViewed.map((doc) => ( + + + {doc.title} + + + ))} + + )} + + ); +} \ No newline at end of file diff --git a/src/components/RecentlyCreatedArticles.js b/src/components/RecentlyCreatedArticles.js new file mode 100644 index 0000000000..72992ddb5e --- /dev/null +++ b/src/components/RecentlyCreatedArticles.js @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { Box, Typography } from '@mui/material'; +import recentCreated from '@generated/recent-docs-plugin/default/recent-created.json'; + +export default function RecentlyCreatedArticles() { + return ( + + + Recently Created Articles + + + {!recentCreated?.length ? ( + + No recently created articles found. + + ) : ( + + {recentCreated.map((doc) => ( + + + {doc.title} + + + ))} + + )} + + ); +} \ No newline at end of file diff --git a/src/components/RecentlyUpdatedArticles.js b/src/components/RecentlyUpdatedArticles.js new file mode 100644 index 0000000000..c99c3251df --- /dev/null +++ b/src/components/RecentlyUpdatedArticles.js @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { Box, Typography } from '@mui/material'; +import recentUpdated from '@generated/recent-docs-plugin/default/recent-updated.json'; + +export default function RecentlyUpdatedArticles() { + return ( + + + Recently Updated Articles + + + {!recentUpdated?.length ? ( + + No recently updated articles found. + + ) : ( + + {recentUpdated.map((doc) => ( + + + {doc.title} + + + ))} + + )} + + ); +} diff --git a/src/css/sumo.scss b/src/css/sumo.scss index 1efcf2bf1d..61a7cbffd1 100644 --- a/src/css/sumo.scss +++ b/src/css/sumo.scss @@ -1646,3 +1646,14 @@ article header h2[class*='title_'] { white-space: nowrap; border: 0; } + +/* Styles for recently created and recently updated articles section */ + +.recent-articles-row { + display: flex; + gap: 40px; +} + +.recent-articles-col { + flex: 1; +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 774e273137..468ec4c299 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -282,7 +282,26 @@ export const Home = () => { ))} - + + + + + + + + + + + + + From 64f194efe705e6267cd7223271f478cd8926c2a5 Mon Sep 17 00:00:00 2001 From: Jagadisha V Date: Mon, 6 Apr 2026 20:05:15 +0530 Subject: [PATCH 2/2] added the import files --- src/pages/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 468ec4c299..d1aa4053d8 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -10,6 +10,9 @@ import { Feature } from '../components/Feature'; import { features } from '../helper/features'; import ErrorBoundary from '../components/ErrorBoundary'; import GoogleTranslateNavbarItem from '../theme/NavbarItem/GoogleTranslateNavbarItem'; +import RecentlyCreatedArticles from '../components/RecentlyCreatedArticles'; +import RecentlyUpdatedArticles from '../components/RecentlyUpdatedArticles'; +import MostViewedArticles from '../components/MostViewedArticles'; export const Home = () => { const [tab, setTab] = useState('0');