fix(blog): resolve relative URLs in Markdown images and links with a fixed base (#4287)

These changes add a layered hook to the `markdown-it` renderer rules to
resolve `<img>` element `src` attributes and `<a>` element `href`
attributes to a path-absolute relative URL, to ensure that such URLs
always point to the same resource URL even when the URL the current
resource is being viewed from changes.

This fixes an issue with relative links and image source URLs being
broken when a blog post was watched from a URL that lacked a trailing
slash, as web browsers adjust the path relative URLs are resolved from
depending on whether such character is present, and we didn't account
for that.

While at it, I've rebuilt all the blog posts and their associated RSS
feed.
This commit is contained in:
Alejandro González
2025-08-29 15:44:03 +02:00
committed by GitHub
parent 7afe35a6cd
commit 8b98087936
25 changed files with 80 additions and 51 deletions

View File

@@ -3,6 +3,9 @@ import { promises as fs } from 'fs'
import { glob } from 'glob'
import matter from 'gray-matter'
import { minify } from 'html-minifier-terser'
import type { Options } from 'markdown-it'
import type Renderer from 'markdown-it/lib/renderer.mjs'
import type Token from 'markdown-it/lib/token.mjs'
import * as path from 'path'
import RSS from 'rss'
import { parseStringPromise } from 'xml2js'
@@ -65,15 +68,41 @@ async function compileArticles() {
process.exit(1)
}
const html = md().render(content)
const minifiedHtml = await minify(html, {
const mdIt = md()
const slug = frontSlug || path.basename(file, '.md')
// Normalizes relative URL resolution to occur in the context of the article's directory.
// This prevents user agents from resolving relative URLs differently based on whether
// the current document URL has a trailing slash or not.
function normalizeRendererHtmlUriAttribute(ruleName: string, attrName: string) {
const defaultRenderer =
mdIt.renderer.rules[ruleName] ||
function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options)
}
return (tokens: Token[], idx: number, options: Options, env: object, self: Renderer) => {
const attrUrlValue = tokens[idx].attrGet(attrName)
if (attrUrlValue) {
tokens[idx].attrSet(
attrName,
new URL(attrUrlValue, `${SITE_URL}/news/article/${slug}/`).href.replace(SITE_URL, ''),
)
}
return defaultRenderer(tokens, idx, options, env, self)
}
}
mdIt.renderer.rules.image = normalizeRendererHtmlUriAttribute('image', 'src')
mdIt.renderer.rules.link_open = normalizeRendererHtmlUriAttribute('link_open', 'href')
const minifiedHtml = await minify(mdIt.render(content), {
collapseWhitespace: true,
removeComments: true,
})
const authors = authorsData ? authorsData : []
const slug = frontSlug || path.basename(file, '.md')
const varName = toVarName(slug)
const exportFile = path.posix.join(COMPILED_DIR, `${varName}.ts`)
const contentFile = path.posix.join(COMPILED_DIR, `${varName}.content.ts`)