ID: I202602211303
Status: idea
Tags: Plugins, quartz

WikiLinkValidator

NOTE

This plugin is a custom plugin made by me. It is not an official plugin (yet?). I might make a PR in the future

This plugin validates internal wikilinks and handles broken links (links to pages that don’t exist) by converting them to plain text, bold text, or leaving them as-is based on your configuration.

This plugin accepts the following configuration options:

  • brokenLinkFormat: How to display broken wikilinks. Can be one of:
    • "text" (default): Convert broken links to plain text
    • "bold": Convert broken links to bold text
    • "ignore": Leave broken links unchanged

API

  • Category: Transformer
  • Function name: Plugin.WikiLinkValidator().

Added files:

  • quartz/plugins/transformers/wikiLinkValidator.ts:
import { QuartzTransformerPlugin } from "../types"
import { Root as HastRoot } from "hast"
import { Root as MdastRoot } from "mdast"
import { visit } from "unist-util-visit"
import {
    FullSlug,
    RelativeURL,
    TransformOptions,
    simplifySlug,
    transformLink,
    stripSlashes,
} from "../../util/path"
import isAbsoluteUrl from "is-absolute-url"
// import { FrontMatter } from "./frontmatter"
 
interface Options {
    /** How to display broken wikilinks: as plain text, bold text, or leave as-is */
    brokenLinkFormat: "text" | "bold" | "ignore"
}
 
const defaultOptions: Options = {
    brokenLinkFormat: "text",
}
 
interface FileMetadata {
    frontmatter: any
    slug: string
}
 
/**
 * Validates internal wikilinks and converts broken/private links to plain text, bold text,
 * or leaves them as-is based on configuration.
 */
export const WikiLinkValidator: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
    const opts = { ...defaultOptions, ...userOpts }
 
    // Shared cache across markdown and html phases
    const fileMetadataCache = new Map<string, FileMetadata>()
 
 
    return {
        name: "WikiLinkValidator",
        markdownPlugins(_ctx) {
            return [
                () => {
                    return (_tree: MdastRoot, file) => {
                        // Cache frontmatter from markdown phase so it's available in html phase
                        const slug = file.data.slug as string
                        if (slug && file.data.frontmatter) {
                            fileMetadataCache.set(slug, {
                                frontmatter: file.data.frontmatter,
                                slug,
                            })
                        }
                    }
                },
            ]
        },
        htmlPlugins(ctx) {
            return [
                () => {
                    return (tree: HastRoot, file) => {
                        const nodesToReplace: Array<{
                            node: any
                            parent: any
                            index: number
                            text: string
                        }> = []
 
                        visit(tree, "element", (node, index, parent) => {
                            // Process all links
                            if (
                                node.tagName === "a" &&
                                node.properties &&
                                typeof node.properties.href === "string"
                            ) {
                                const href = node.properties.href as RelativeURL
                                const isExternal = isAbsoluteUrl(href)
                                const isAnchor = href.startsWith("#")
 
                                // Only check internal links (not external URLs or anchors)
                                if (!isExternal && !isAnchor) {
                                    const transformOptions: TransformOptions = {
                                        strategy: "absolute",
                                        allSlugs: ctx.allSlugs,
                                    }
 
                                    // Transform the link to get the canonical destination
                                    const dest = transformLink(
                                        file.data.slug! as FullSlug,
                                        href,
                                        transformOptions,
                                    )
                                    const url = new URL(
                                        dest,
                                        "https://base.com/" +
                                        stripSlashes(simplifySlug(file.data.slug! as FullSlug), true),
                                    )
                                    let destCanonical = url.pathname
 
                                    if (destCanonical.endsWith("/")) {
                                        destCanonical += "index"
                                    }
 
                                    const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug
 
                                    // Check if target page exists in the list of all slugs
                                    const targetExists = ctx.allSlugs.includes(full)
 
                                    if (!targetExists) {
                                        // Handle broken link
                                        if (opts.brokenLinkFormat === "ignore") {
                                            return
                                        }
 
                                        // Extract the link display text
                                        const linkText = node.children
                                            .filter((child): child is any => child.type === "text")
                                            .map((child) => child.value)
                                            .join("")
 
                                        if (opts.brokenLinkFormat === "bold") {
                                            node.tagName = "strong"
                                            node.properties = {}
                                        } else {
                                            if (parent && index !== undefined) {
                                                nodesToReplace.push({
                                                    node,
                                                    parent,
                                                    index,
                                                    text: linkText,
                                                })
                                            }
                                        }
                                    }
                                }
                            }
                        })
 
                        // Replace marked nodes in reverse order to avoid index shifting
                        for (let i = nodesToReplace.length - 1; i >= 0; i--) {
                            const { parent, index, text } = nodesToReplace[i]
                            if (parent && "children" in parent && parent.children[index]) {
                                parent.children[index] = {
                                    type: "text",
                                    value: text,
                                }
                            }
                        }
                    }
                },
            ]
        },
    }
}

Changed Files:

  • quartz/plugins/transformers/index.ts
  • quartz.config.ts

quartz/plugins/transformers/index.ts: Just add the following line:

export { WikiLinkValidator } from "./wikiLinkValidator"

quartz.config.ts: Add the following in the transformer plugins list:

Plugin.WikiLinkValidator({
	brokenLinkFormat: "bold"
}),

References

I was annoyed at how many 404 pages were being indexed by Google. Cause this would ruin my SEO. So I decided to make my own quartz plugin to fix this issue.