Harden last-updated dates: CI guard, SEO metadata, View history link (#904)

This commit is contained in:
Brandon Hopkins
2026-08-05 13:06:30 -07:00
committed by GitHub
parent 8aa22ac4ce
commit a51653a93d
6 changed files with 95 additions and 34 deletions

View File

@@ -1,16 +1,7 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
import coreWebVitals from "eslint-config-next/core-web-vitals";
const eslintConfig = [
...compat.extends("next/core-web-vitals"),
...coreWebVitals,
];
export default eslintConfig;

View File

@@ -36,6 +36,20 @@ function rehypeInsertLastUpdated() {
const formatted = formatLastUpdated(iso)
if (!formatted) return
// Exposed to pageProps via recma-nextjs-static-props so _app.jsx can emit
// per-page SEO metadata without shipping the full route→date map client-side.
const exportStr = `export const lastUpdated = ${JSON.stringify(iso)}`
tree.children.push({
type: 'mdxjsEsm',
value: exportStr,
data: {
estree: acorn.parse(exportStr, {
sourceType: 'module',
ecmaVersion: 'latest',
}),
},
})
const node = {
type: 'element',
tagName: 'p',

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env node
/**
* Generates src/lib/edit-on-github-routes.js by scanning src/pages for index.mdx.
* Used by Layout.jsx so "Edit on GitHub" links point to .../index.mdx for those routes.
* Generates src/lib/edit-on-github-routes.js by scanning src/pages for .mdx files.
* Used by Layout.jsx so "Edit on GitHub" links point to .../index.mdx for index
* routes, and so GitHub links render only on routes backed by a real MDX file
* (e.g. not on the prerendered 404/500 pages, where pathname is /_error).
* Run automatically with dev and build.
*/
@@ -14,29 +16,37 @@ const ROOT = path.join(__dirname, '..')
const PAGES_DIR = path.join(ROOT, 'src/pages')
const OUT_PATH = path.join(ROOT, 'src/lib/edit-on-github-routes.js')
function findIndexMdxRoutes(dir, basePath = '') {
function findMdxRoutes(dir, basePath = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const routes = []
const indexRoutes = []
const allRoutes = []
for (const e of entries) {
const rel = basePath ? `${basePath}/${e.name}` : e.name
if (e.isDirectory()) {
routes.push(...findIndexMdxRoutes(path.join(dir, e.name), rel))
const nested = findMdxRoutes(path.join(dir, e.name), rel)
indexRoutes.push(...nested.indexRoutes)
allRoutes.push(...nested.allRoutes)
} else if (e.name === 'index.mdx') {
routes.push('/' + basePath)
indexRoutes.push('/' + basePath)
allRoutes.push('/' + basePath)
} else if (e.name.endsWith('.mdx')) {
allRoutes.push('/' + rel.replace(/\.mdx$/, ''))
}
}
return routes
return { indexRoutes, allRoutes }
}
const routes = findIndexMdxRoutes(PAGES_DIR)
.filter((r) => r !== '') // ignore root index if any
.sort()
const { indexRoutes, allRoutes } = findMdxRoutes(PAGES_DIR)
const routes = indexRoutes.filter((r) => r !== '/').sort() // ignore root index if any
const mdxRoutes = allRoutes.filter((r) => r !== '/').sort()
const content = `// Auto-generated by scripts/generate-github-routes.mjs do not edit
/** Pathnames served by src/pages/.../index.mdx (Edit on GitHub must link to /index.mdx). */
export const EDIT_ON_GITHUB_INDEX_ROUTES = new Set(${JSON.stringify(routes)});
/** Every pathname backed by an .mdx file under src/pages. GitHub links are hidden elsewhere. */
export const MDX_PAGE_ROUTES = new Set(${JSON.stringify(mdxRoutes)});
`
fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true })
fs.writeFileSync(OUT_PATH, content, 'utf8')
console.log('Generated', OUT_PATH, 'with', routes.length, 'index routes')
console.log('Generated', OUT_PATH, 'with', routes.length, 'index routes,', mdxRoutes.length, 'mdx routes')

View File

@@ -60,3 +60,19 @@ 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')
if (Object.keys(map).length === 0) {
const msg =
'[last-updated] no routes received a date, git history is unavailable or the clone is shallow'
// Hard-fail only where full history is guaranteed (the GitHub workflow
// checks out with fetch-depth: 0). Other CI environments like Vercel
// shallow-clone by design, so there a missing map is expected: warn and
// ship pages without "Updated" dates instead of breaking the deploy.
if (process.env.GITHUB_ACTIONS) {
console.error(
`${msg}. Failing the build: the workflow checks out with fetch-depth: 0, so an empty map means something is broken.`
)
process.exit(1)
}
console.warn(`${msg}. Pages will render without an "Updated" date.`)
}

View File

@@ -12,12 +12,12 @@ import {NavigationAPI} from "@/components/NavigationAPI";
import {motion} from "framer-motion";
import {Footer} from "@/components/Footer";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import { faPaperclip } from '@fortawesome/free-solid-svg-icons';
import { faPaperclip, faClockRotateLeft } from '@fortawesome/free-solid-svg-icons';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import {toast} from "react-toastify";
import {AnnouncementBanner} from "@/components/announcement-banner/AnnouncementBanner";
import {useAnnouncements} from "@/components/announcement-banner/AnnouncementBannerProvider";
import { EDIT_ON_GITHUB_INDEX_ROUTES } from '@/lib/edit-on-github-routes'
import { EDIT_ON_GITHUB_INDEX_ROUTES, MDX_PAGE_ROUTES } from '@/lib/edit-on-github-routes'
const navigation = [
{
@@ -227,16 +227,30 @@ export function Layout({ children, title, tableOfContents }) {
<span>Copy link</span>
</button>
</li>
<li key="edit-on-github">
<Link
href={"https://github.com/netbirdio/docs/tree/main/src/pages" + (EDIT_ON_GITHUB_INDEX_ROUTES.has(router.pathname) ? router.pathname + "/index.mdx" : router.pathname + ".mdx")}
className="dark:hover:text-slate-300 dark:text-slate-400 text-slate-500 hover:text-slate-700 font-normal'"
style={{display: "flex", alignItems: 'center'}}
>
<FontAwesomeIcon icon={faGithub} style={iconStyle} className="icon pr-1" />
<span>Edit on Github</span>
</Link>
</li>
{MDX_PAGE_ROUTES.has(router.pathname) && (
<>
<li key="edit-on-github">
<Link
href={"https://github.com/netbirdio/docs/tree/main/src/pages" + (EDIT_ON_GITHUB_INDEX_ROUTES.has(router.pathname) ? router.pathname + "/index.mdx" : router.pathname + ".mdx")}
className="dark:hover:text-slate-300 dark:text-slate-400 text-slate-500 hover:text-slate-700 font-normal'"
style={{display: "flex", alignItems: 'center'}}
>
<FontAwesomeIcon icon={faGithub} style={iconStyle} className="icon pr-1" />
<span>Edit on GitHub</span>
</Link>
</li>
<li key="view-history">
<Link
href={"https://github.com/netbirdio/docs/commits/main/src/pages" + (EDIT_ON_GITHUB_INDEX_ROUTES.has(router.pathname) ? router.pathname + "/index.mdx" : router.pathname + ".mdx")}
className="dark:hover:text-slate-300 dark:text-slate-400 text-slate-500 hover:text-slate-700 font-normal"
style={{display: "flex", alignItems: 'center'}}
>
<FontAwesomeIcon icon={faClockRotateLeft} style={iconStyle} className="icon pr-1" />
<span>View history</span>
</Link>
</li>
</>
)}
</ol>
<nav aria-labelledby="on-this-page-title" className="w-80">
{tableOfContents.length > 0 && (

View File

@@ -30,6 +30,7 @@ function AppInner({ Component, pageProps }) {
let router = useRouter()
let tableOfContents = collectHeadings(pageProps.sections)
const { isAccepted } = useCookieConsent()
const lastUpdated = pageProps.lastUpdated
return (
<>
@@ -41,6 +42,21 @@ function AppInner({ Component, pageProps }) {
<title>{`${pageProps.title} - NetBird API`}</title> : <title>{`${pageProps.title} - NetBird Docs`}</title>
}
<meta name="description" content={pageProps.description} />
{lastUpdated && <meta property="article:modified_time" content={lastUpdated} />}
{lastUpdated && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: pageProps.title,
description: pageProps.description,
dateModified: lastUpdated,
}),
}}
/>
)}
</Head>
<AnnouncementBannerProvider>
<MDXProvider components={mdxComponents}>