From 28b7c13bd3db088780c587d12f311d675f582648 Mon Sep 17 00:00:00 2001 From: Brandon Hopkins <76761586+TechHutTV@users.noreply.github.com> Date: Mon, 4 May 2026 09:37:57 -0700 Subject: [PATCH] Added auto-generated "Updated..." line under H1 (#719) --- .gitignore | 3 ++ mdx/rehype.mjs | 59 ++++++++++++++++++++++++++++++ package.json | 5 ++- scripts/generate-last-updated.mjs | 61 +++++++++++++++++++++++++++++++ scripts/git-dates.mjs | 18 +++++++++ src/pages/get-started/index.mdx | 2 - 6 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 scripts/generate-last-updated.mjs create mode 100644 scripts/git-dates.mjs diff --git a/.gitignore b/.gitignore index 2e1c4af6..fbf190be 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ package-lock.json # Edit on GitHub index routes (generated by scripts/generate-github-routes.mjs) /src/lib/edit-on-github-routes.js + +# Last updated dates per route (generated by scripts/generate-last-updated.mjs) +/src/lib/last-updated-routes.mjs diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs index 9c93cab1..593f8b36 100644 --- a/mdx/rehype.mjs +++ b/mdx/rehype.mjs @@ -1,3 +1,5 @@ +import path from 'path' +import { fileURLToPath } from 'url' import { mdxAnnotations } from 'mdx-annotations' import { visit } from 'unist-util-visit' import rehypeMdxTitle from 'rehype-mdx-title' @@ -5,6 +7,62 @@ import shiki from 'shiki' import { toString } from 'mdast-util-to-string' import * as acorn from 'acorn' import { slugifyWithCounter } from '@sindresorhus/slugify' +import { LAST_UPDATED_BY_ROUTE } from '../src/lib/last-updated-routes.mjs' + +const PAGES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/pages') + +function routeFromFilePath(filePath) { + if (!filePath) return null + const rel = path.relative(PAGES_DIR, filePath) + if (rel.startsWith('..')) return null + const noExt = rel.replace(/\.mdx$/, '') + const route = noExt === 'index' ? '/' : '/' + noExt.replace(/\/index$/, '') + return route +} + +function formatLastUpdated(iso) { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso || '') + if (!m) return null + const months = ['January','February','March','April','May','June','July','August','September','October','November','December'] + return `${months[Number(m[2]) - 1]} ${Number(m[3])}, ${m[1]}` +} + +function rehypeInsertLastUpdated() { + return (tree, file) => { + const filePath = file?.path || file?.history?.[file.history.length - 1] + const route = routeFromFilePath(filePath) + if (!route) return + const iso = LAST_UPDATED_BY_ROUTE[route] + const formatted = formatLastUpdated(iso) + if (!formatted) return + + const node = { + type: 'element', + tagName: 'p', + properties: { + className: ['not-prose', 'text-sm', 'text-slate-400', 'dark:text-zinc-500', 'mt-0', 'mb-8', 'ml-2.5'], + }, + children: [ + { type: 'text', value: 'Updated ' }, + { + type: 'element', + tagName: 'time', + properties: { dateTime: iso }, + children: [{ type: 'text', value: formatted }], + }, + ], + } + + const children = tree.children + for (let i = 0; i < children.length; i++) { + const c = children[i] + if (c.type === 'element' && c.tagName === 'h1') { + children.splice(i + 1, 0, node) + return + } + } + } +} function rehypeParseCodeBlocks() { return (tree) => { @@ -119,6 +177,7 @@ export const rehypePlugins = [ rehypeShiki, rehypeSlugify, rehypeMdxTitle, + rehypeInsertLastUpdated, [ rehypeAddMDXExports, (tree) => ({ diff --git a/package.json b/package.json index 2e0de0e2..ff9c06e8 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "npm run gen:llm && npm run gen:edit-routes && next dev --webpack", - "build": "npm run gen:llm && npm run gen:edit-routes && next build --webpack", + "dev": "npm run gen:llm && npm run gen:edit-routes && npm run gen:last-updated && next dev --webpack", + "build": "npm run gen:llm && npm run gen:edit-routes && npm run gen:last-updated && next build --webpack", "gen:edit-routes": "node scripts/generate-github-routes.mjs", + "gen:last-updated": "node scripts/generate-last-updated.mjs", "gen": "swagger-codegen generate -i https://raw.githubusercontent.com/netbirdio/netbird/main/management/server/http/api/openapi.yml -l openapi -o generator/openapi && npx ts-node generator/index.ts gen --input generator/openapi/openapi.json --output src/pages/ipa/resources", "gen:llm": "node scripts/generate-llm-docs.mjs", "start": "next start", diff --git a/scripts/generate-last-updated.mjs b/scripts/generate-last-updated.mjs new file mode 100644 index 00000000..4962a6d3 --- /dev/null +++ b/scripts/generate-last-updated.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * Generates src/lib/last-updated-routes.js by scanning src/pages for .mdx files + * and reading the last commit date for each from git. Used by Layout.jsx to + * render an "Updated " line in the right rail. + * + * Skips src/pages/ipa/resources/ — those are auto-generated from the OpenAPI + * spec, so their git date reflects generator runs, not real content edits. + * + * Run automatically with dev and build. + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { getGitLastModified } from './git-dates.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.join(__dirname, '..') +const PAGES_DIR = path.join(ROOT, 'src/pages') +const OUT_PATH = path.join(ROOT, 'src/lib/last-updated-routes.mjs') +const SKIP_PREFIX = 'ipa/resources' + +function findMdxRoutes(dir, basePath = '') { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + const results = [] + for (const e of entries) { + const rel = basePath ? `${basePath}/${e.name}` : e.name + if (e.isDirectory()) { + if (rel === SKIP_PREFIX || rel.startsWith(`${SKIP_PREFIX}/`)) continue + results.push(...findMdxRoutes(path.join(dir, e.name), rel)) + } else if (e.name.endsWith('.mdx')) { + const filePath = path.join(dir, e.name) + const route = + e.name === 'index.mdx' + ? '/' + basePath + : '/' + rel.replace(/\.mdx$/, '') + results.push({ route, filePath }) + } + } + return results +} + +const entries = findMdxRoutes(PAGES_DIR) + .filter((r) => r.route !== '/' && r.route !== '') + .sort((a, b) => a.route.localeCompare(b.route)) + +const map = {} +for (const { route, filePath } of entries) { + const date = getGitLastModified(filePath) + if (date) map[route] = date +} + +const content = `// Auto-generated by scripts/generate-last-updated.mjs – do not edit +/** Last commit date (YYYY-MM-DD) keyed by Next.js router.pathname. */ +export const LAST_UPDATED_BY_ROUTE = ${JSON.stringify(map, null, 2)}; +` + +fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }) +fs.writeFileSync(OUT_PATH, content, 'utf8') +console.log('Generated', OUT_PATH, 'with', Object.keys(map).length, 'dated routes') diff --git a/scripts/git-dates.mjs b/scripts/git-dates.mjs new file mode 100644 index 00000000..42396738 --- /dev/null +++ b/scripts/git-dates.mjs @@ -0,0 +1,18 @@ +import { execSync } from 'child_process' + +/** + * Get the last modified date for a file from git history. + * Returns YYYY-MM-DD or null if the file is not tracked / git is unavailable. + */ +export function getGitLastModified(filePath) { + try { + const date = execSync(`git log -1 --format=%cI -- "${filePath}"`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'ignore'], + }).trim() + + return date ? date.split('T')[0] : null + } catch { + return null + } +} diff --git a/src/pages/get-started/index.mdx b/src/pages/get-started/index.mdx index 0523917b..0ef66ef8 100644 --- a/src/pages/get-started/index.mdx +++ b/src/pages/get-started/index.mdx @@ -2,8 +2,6 @@ import {Note} from "@/components/mdx"; # Getting Started -## Quickstart Guide - Welcome to NetBird! This guide will walk you through our new onboarding process to create your account, connect your first devices, and build a secure peer-to-peer overlay network in less than ten minutes.