Files
netbird-docs/mdx/rehype.mjs
T
Bruno Mercier Costa 6f6ad52072 docs: add common issues to the troubleshooting hub and fill out the area tiles (#1039)
* docs: add common issues to the troubleshooting hub and fill out the area tiles

Lead the Troubleshooting hub with a table of the issues support sees most
often, and give each area tile the sections a reader actually needs,
including a new Reverse Proxy tile.

Add the case where a teammate signs in and gets their own account to the
pending approval page, with sign-in domains as the fix.

List the headings rendered by the troubleshooting components in "On this
page", which previously only showed Markdown headings.

* docs: address review on account-access and support wording

- Personal-address teammate: drop the direct-invite advice, since a user
  who already has an account can't be invited; point to the work address
  or NetBird Support instead.
- Support audience: say NetBird Support is mainly for paying Cloud
  customers and commercial-license deployments, consistently across the
  troubleshooting hub, the support page, and the report-bug page.

* docs: align the troubleshooting hub table and rework Still stuck

- Common issues table: fixed 50/50 columns, with the Start here column
  padded to line up with the right-hand area tiles at any width.
- Still stuck: match the width and side padding of the tiles, center the
  title and buttons, and put a short audience note under each button.
  The report hint moves to its own line above the links, with a new
  Debug bundle link.
2026-10-15 15:35:51 +02:00

221 lines
6.3 KiB
JavaScript

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'
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
// 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',
properties: {
className: ['not-prose', 'text-sm', 'text-slate-400', 'dark:text-zinc-500', 'mt-0', 'mb-8', 'md: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) => {
visit(tree, 'element', (node, _nodeIndex, parentNode) => {
if (node.tagName === 'code' && node.properties.className) {
parentNode.properties.language = node.properties.className[0]?.replace(
/^language-/,
''
)
}
})
}
}
let highlighter
function rehypeShiki() {
return async (tree) => {
highlighter =
highlighter ?? (await shiki.getHighlighter({ theme: 'css-variables' }))
visit(tree, 'element', (node) => {
if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') {
let codeNode = node.children[0]
let textNode = codeNode.children[0]
node.properties.code = textNode.value
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(
textNode.value,
node.properties.language
)
textNode.value = shiki.renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `<span>${children}</span>`,
},
})
}
}
})
}
}
function rehypeSlugify() {
return (tree) => {
let slugify = slugifyWithCounter()
visit(tree, 'element', (node) => {
if ( ['h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
node.properties.id = slugify(toString(node))
}
})
}
}
function rehypeAddMDXExports(getExports) {
return (tree) => {
let exports = Object.entries(getExports(tree))
for (let [name, value] of exports) {
for (let node of tree.children) {
if (
node.type === 'mdxjsEsm' &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
) {
return
}
}
let exportStr = `export const ${name} = ${value}`
tree.children.push({
type: 'mdxjsEsm',
value: exportStr,
data: {
estree: acorn.parse(exportStr, {
sourceType: 'module',
ecmaVersion: 'latest',
}),
},
})
}
}
}
// Components that render their own `h2` from `title` and `id` props. The
// heading is not in the MDX tree, so it is added to the table of contents here.
const sectionComponents = ['TroubleshootingStart', 'TroubleshootingTiles', 'StillStuck']
function jsxStringAttribute(node, name) {
let attribute = node.attributes?.find((a) => a.name === name)
return typeof attribute?.value === 'string' ? attribute.value : undefined
}
function getSections(node) {
let sections = []
for (let child of node.children ?? []) {
if (child.type === 'mdxJsxFlowElement' && sectionComponents.includes(child.name)) {
let title = jsxStringAttribute(child, 'title')
let id = jsxStringAttribute(child, 'id')
if (title && id) {
sections.push(`{
title: ${JSON.stringify(title)},
id: ${JSON.stringify(id)},
tagName: "h2"
}`)
}
} else if (child.type === 'element' && ['h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
sections.push(`{
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
tagName: ${JSON.stringify(child.tagName)},
tag: ${JSON.stringify(child.tag)},
...${child.properties.annotation}
}`)
} else if (child.children) {
sections.push(...getSections(child))
}
}
return sections
}
export const rehypePlugins = [
mdxAnnotations.rehype,
rehypeParseCodeBlocks,
rehypeShiki,
rehypeSlugify,
rehypeMdxTitle,
rehypeInsertLastUpdated,
[
rehypeAddMDXExports,
(tree) => ({
sections: `[${getSections(tree).join()}]`,
}),
],
]