docs: fix broken links utility + uncaught broken links (#12637)
* fix broken links * update broken links utility * add missing payment evens * generate llms * fix segment link
This commit is contained in:
1 parent
9866baa852
commit
40e73c6ea2
64 files changed
+151813
-150840
No files matched your search
@@ -18,28 +18,99 @@ import { MD_LINK_REGEX } from "./constants.js"
|
||||
function getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
additionalMessage = "",
|
||||
}: {
|
||||
link: string
|
||||
file: VFile
|
||||
additionalMessage?: string
|
||||
}): string {
|
||||
return `Broken link found! ${link} linked in ${file.history[0]}`
|
||||
return `Broken link found! ${link} linked in ${file.history[0]}${
|
||||
additionalMessage ? `: ${additionalMessage}` : ""
|
||||
}`
|
||||
}
|
||||
|
||||
function checkLocalLinkExists({
|
||||
link,
|
||||
file,
|
||||
currentPageFilePath,
|
||||
rootBasePath = { default: "app" },
|
||||
slugChanges = [],
|
||||
}: {
|
||||
link: string
|
||||
file: VFile
|
||||
currentPageFilePath: string
|
||||
rootBasePath?: BrokenLinkCheckerOptions["rootBasePath"]
|
||||
slugChanges?: SlugChange[]
|
||||
}) {
|
||||
let fileToCheck = link
|
||||
let basePath = currentPageFilePath
|
||||
if (!fileToCheck.endsWith("page.mdx")) {
|
||||
// if the link doesn't end with page.mdx, add it
|
||||
// find route base path
|
||||
let routeBasePath = rootBasePath.overrides
|
||||
? Object.entries(rootBasePath.overrides).find(([key]) =>
|
||||
fileToCheck.startsWith(key)
|
||||
)?.[1]
|
||||
: rootBasePath.default
|
||||
if (routeBasePath === undefined) {
|
||||
routeBasePath = rootBasePath.default
|
||||
}
|
||||
fileToCheck = `${routeBasePath}${fileToCheck}/page.mdx`.replace(
|
||||
/\/{2,}/g,
|
||||
"/"
|
||||
)
|
||||
basePath = file.cwd
|
||||
}
|
||||
// remove leading slash if present
|
||||
fileToCheck = fileToCheck.replace(/^\//, "")
|
||||
// get absolute path of the URL
|
||||
const linkedFilePath = path
|
||||
.resolve(currentPageFilePath, link)
|
||||
.replace(/#.*$/, "")
|
||||
const linkedFilePath = path.resolve(basePath, fileToCheck).replace(/#.*$/, "")
|
||||
// console.log(link, currentPageFilePath, basePath, fileToCheck, linkedFilePath)
|
||||
// check if the file exists
|
||||
if (!existsSync(linkedFilePath)) {
|
||||
if (existsSync(linkedFilePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// maybe check in slugs
|
||||
if (!slugChanges.length) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
checkSlugExists({
|
||||
slugChanges,
|
||||
pathToCheck: link,
|
||||
link,
|
||||
file,
|
||||
baseProjectPath: file.cwd,
|
||||
})
|
||||
}
|
||||
|
||||
function checkSlugExists({
|
||||
slugChanges,
|
||||
pathToCheck,
|
||||
link,
|
||||
file,
|
||||
baseProjectPath,
|
||||
}: {
|
||||
slugChanges: SlugChange[]
|
||||
pathToCheck: string
|
||||
baseProjectPath: string
|
||||
link: string
|
||||
file: VFile
|
||||
}) {
|
||||
const slugChange = slugChanges.find(
|
||||
(change) => change.newSlug === pathToCheck
|
||||
)
|
||||
|
||||
if (
|
||||
!slugChange ||
|
||||
!mdxPageExists(path.join(baseProjectPath, slugChange.origSlug))
|
||||
) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
@@ -156,18 +227,20 @@ function checkLink({
|
||||
// try to remove hash
|
||||
const hashIndex = link.lastIndexOf("#")
|
||||
const likeWithoutHash = hashIndex !== -1 ? link.substring(0, hashIndex) : link
|
||||
if (likeWithoutHash.match(/page\.mdx?$/)) {
|
||||
if (likeWithoutHash.match(/page\.mdx?$/) || likeWithoutHash.startsWith("/")) {
|
||||
checkLocalLinkExists({
|
||||
link: likeWithoutHash,
|
||||
file,
|
||||
currentPageFilePath,
|
||||
slugChanges: options.generatedSlugs,
|
||||
rootBasePath: options.rootBasePath,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const parsedLink = parseCrossProjectLink(likeWithoutHash)
|
||||
|
||||
if (!parsedLink || !Object.hasOwn(options.crossProjects, parsedLink.area)) {
|
||||
if (!parsedLink) {
|
||||
if (MD_LINK_REGEX.test(link)) {
|
||||
// try fixing MDX links
|
||||
let linkMatches
|
||||
@@ -192,10 +265,33 @@ function checkLink({
|
||||
}
|
||||
}
|
||||
return
|
||||
} else if (!Object.hasOwn(options.crossProjects, parsedLink.area)) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
additionalMessage: `Unknown project area: ${parsedLink.area}`,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const projectOptions = options.crossProjects[parsedLink.area]
|
||||
|
||||
if (parsedLink.path.length && !parsedLink.path.startsWith("/")) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
additionalMessage: `Cross project paths must start with a slash.`,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (projectOptions.skipSlugValidation) {
|
||||
// if slug validation is skipped, just check if the file exists
|
||||
return
|
||||
}
|
||||
|
||||
const isReferenceLink =
|
||||
parsedLink.path.startsWith("/references") &&
|
||||
parsedLink.path !== "/references-overview"
|
||||
@@ -210,41 +306,40 @@ function checkLink({
|
||||
return
|
||||
}
|
||||
|
||||
// file doesn't exist, check if slugs are enabled and generated
|
||||
if (!projectOptions.generatedSlugs) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
checkSlugExists({
|
||||
slugChanges: projectOptions.generatedSlugs,
|
||||
pathToCheck: parsedLink.path,
|
||||
link,
|
||||
file,
|
||||
baseProjectPath: projectOptions.projectPath,
|
||||
})
|
||||
}
|
||||
|
||||
function loadSlugs(projectPath: string): SlugChange[] {
|
||||
// file doesn't exist, check if slugs are enabled and generated
|
||||
const generatedSlugsPath = path.join(
|
||||
projectOptions.projectPath,
|
||||
projectPath,
|
||||
"generated",
|
||||
"slug-changes.mjs"
|
||||
)
|
||||
if (!projectOptions.hasGeneratedSlugs || !existsSync(generatedSlugsPath)) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
})
|
||||
)
|
||||
if (!existsSync(generatedSlugsPath)) {
|
||||
throw new Error(`Cross project ${projectPath} has no generated slugs file.`)
|
||||
}
|
||||
|
||||
// get slugs from file
|
||||
const generatedSlugContent = readFileSync(generatedSlugsPath, "utf-8")
|
||||
const slugChanges: SlugChange[] = JSON.parse(
|
||||
return JSON.parse(
|
||||
generatedSlugContent.substring(generatedSlugContent.indexOf("["))
|
||||
)
|
||||
const slugChange = slugChanges.find(
|
||||
(change) => change.newSlug === parsedLink.path
|
||||
)
|
||||
|
||||
if (
|
||||
!slugChange ||
|
||||
!mdxPageExists(path.join(projectOptions.projectPath, slugChange.origSlug))
|
||||
) {
|
||||
throw new Error(
|
||||
getErrorMessage({
|
||||
link,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
) as SlugChange[]
|
||||
}
|
||||
|
||||
const allowedComponentNames = [
|
||||
@@ -261,6 +356,22 @@ export function brokenLinkCheckerPlugin(
|
||||
return async (tree, file) => {
|
||||
const { visit } = await import("unist-util-visit")
|
||||
|
||||
// load slug changes for the current project
|
||||
if (options.hasGeneratedSlugs) {
|
||||
options.generatedSlugs = loadSlugs(file.cwd)
|
||||
}
|
||||
|
||||
// load slug changes for cross projects
|
||||
if (options.crossProjects) {
|
||||
for (const project of Object.values(options.crossProjects)) {
|
||||
if (!project.hasGeneratedSlugs) {
|
||||
continue
|
||||
}
|
||||
|
||||
project.generatedSlugs = loadSlugs(project.projectPath)
|
||||
}
|
||||
}
|
||||
|
||||
const currentPageFilePath = file.history[0].replace(
|
||||
`/${path.basename(file.history[0])}`,
|
||||
""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Node } from "unist-builder"
|
||||
import { SlugChange } from "./build-scripts.js"
|
||||
|
||||
export interface UnistNode extends Node {
|
||||
type: string
|
||||
@@ -128,11 +129,21 @@ export declare type CrossProjectLinksOptions = {
|
||||
}
|
||||
|
||||
export declare type BrokenLinkCheckerOptions = {
|
||||
rootBasePath?: {
|
||||
default: string
|
||||
overrides?: {
|
||||
[k: string]: string
|
||||
}
|
||||
}
|
||||
hasGeneratedSlugs?: boolean
|
||||
generatedSlugs?: SlugChange[]
|
||||
crossProjects: {
|
||||
[k: string]: {
|
||||
projectPath: string
|
||||
contentPath?: string
|
||||
hasGeneratedSlugs?: boolean
|
||||
generatedSlugs?: SlugChange[]
|
||||
skipSlugValidation?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user