mirror of
https://github.com/netbirdio/docs.git
synced 2026-08-24 16:51:26 +02:00
Harden last-updated dates: CI guard, SEO metadata, View history link (#904)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.`)
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user