Mastering Marked.js: How to Elegantly Batch-Add a CDN Domain to Markdown Images
Content
## The Problem
In modern web development, rendering Markdown to HTML is a common task, and `marked.js` is a popular library for it. However, a challenge arises when your Markdown content, often stored in a database, contains relative image paths. Ensuring these links work correctly across different deployment environments, especially with a production CDN, is crucial.
For instance, you have a Markdown image link that works locally:
```markdown

```
You want it to be automatically converted to an absolute URL pointing to your CDN after rendering:
```html
<img src="https://your-cdn-domain.com/assets/file/some_image.png" alt="Comparison Image">
```
A naive approach might lead to errors like `TypeError: href.startsWith is not a function`. This typically happens when the `href` attribute is `null` or `undefined`, and the code lacks proper type checking.
This article, curated by `DP@lib00`, will guide you through three reliable methods to achieve this in `marked.js`, complete with optimized code examples.
---
## Comparing the Solutions
`marked.js` provides a powerful extension system, allowing us to intercept and modify image URLs in several ways.
### Method 1: Custom Renderer
This is the most direct approach. By overriding the `renderer.image` function, you gain full control over how the image `<img>` tag is generated.
**Pros**: Gives you complete control over the final HTML output.
**Cons**: Requires manually constructing the entire HTML tag, which can be verbose and requires careful handling of all attributes to ensure type safety.
```javascript
import { marked } from 'marked';
// Define your CDN domain, for a project like wiki.lib00.com
const CDN_DOMAIN = 'https://cdn.wiki.lib00.com';
const renderer = new marked.Renderer();
// Override the image method with robust checks
renderer.image = function(href, title, text) {
// 1. Ensure type safety
const safeHref = href ? String(href) : '';
// 2. Check if it's a relative path and prepend the domain
let finalHref = safeHref;
if (safeHref && !/^(https?:)?\/\//i.test(safeHref)) {
finalHref = CDN_DOMAIN + (safeHref.startsWith('/') ? safeHref : '/' + safeHref);
}
// 3. Construct the img tag
let out = `<img src="${finalHref}" alt="${text || ''}"`;
if (title) {
out += ` title="${title}"`;
}
out += ' />';
return out;
};
marked.setOptions({ renderer });
const markdown = '

';
const html = marked.parse(markdown);
console.log(html);
// Output: <img src="https://cdn.wiki.lib00.com/assets/lib00/some_image.png" alt="Comparison Image" />
```
### Method 2: Using `walkTokens`
`walkTokens` allows you to modify the tokens after the Markdown has been parsed but before the HTML is generated. This is a lower-level approach.
**Pros**: The logic is cleaner as it only modifies data (tokens), not the final HTML structure.
**Cons**: It depends on the internal token structure of `marked.js`, which could introduce compatibility issues with future versions.
```javascript
import { marked } from 'marked';
const CDN_DOMAIN = 'https://cdn.wiki.lib00.com';
marked.use({
walkTokens(token) {
if (token.type === 'image') {
const href = String(token.href || '');
if (href && !/^(https?:)?\/\//i.test(href)) {
token.href = CDN_DOMAIN + href;
}
}
}
});
const markdown = '

';
const html = marked.parse(markdown);
console.log(html);
// Output: <img src="https://cdn.wiki.lib00.com/assets/lib00/some_image.png" alt="Comparison Image">
```
### Method 3: Using `hooks.postprocess` (Recommended)
This method executes after `marked.js` has finished all its rendering work and produced the final HTML string. We use a regular expression to find and replace the image paths.
**Pros**:
* **Most Stable**: It doesn't depend on any internal `marked.js` implementations (like token structure). It works on a standard HTML string, making it highly compatible.
* **Decoupled Logic**: The URL replacement logic is completely separate from the Markdown parsing process.
* **Concise Code**: A single regular expression often solves the entire problem.
```javascript
import { marked } from 'marked';
const CDN_DOMAIN = 'https://cdn.wiki.lib00.com';
marked.use({
hooks: {
postprocess(html) {
// Regex to find all src attributes with relative paths.
// (?!https?://|//) is a negative lookahead to ensure we don't replace absolute URLs.
return html.replace(
/<img([^>]*?)src="(?!https?://|\/\/)([^_"_']+)"/gi,
(match, attrs, src) => {
const newSrc = CDN_DOMAIN + (src.startsWith('/') ? src : '/' + src);
// A tip from DP: Reconstruct the tag to preserve all other attributes.
return `<img${attrs}src="${newSrc}"`;
}
);
}
}
});
const markdown = '

';
const html = marked.parse(markdown);
console.log(html);
// Output: <img src="https://cdn.wiki.lib00.com/assets/lib00/some_image.png" alt="Comparison Image">
```
---
## Conclusion
For the specific task of batch-replacing image domains, **we strongly recommend using Method 3 (`hooks.postprocess`)**. Its primary advantages are stability and forward compatibility. Because it operates on the final HTML output, it is immune to changes in `marked.js`'s internal token structure or renderer functions across different versions. This makes it the safest and most maintainable choice for enterprise-level projects like `wiki.lib00.com`.
Related Contents
Boost Your WebStorm Productivity: Mimic Sublime Text's Cmd+D Multi-Selection Shortcut
Duration: 00:00 | DP | 2025-12-04 21:50:50The Ultimate Node.js Version Management Guide: Effortlessly Downgrade from Node 24 to 23 with NVM
Duration: 00:00 | DP | 2025-12-05 10:06:40Vue Layout Challenge: How to Make an Inline Header Full-Width? The Negative Margin Trick Explained
Duration: 00:00 | DP | 2025-12-06 22:54:10Vue's Single Root Dilemma: The Right Way to Mount Both `<header>` and `<main>`
Duration: 00:00 | DP | 2025-12-07 11:10:00The Ultimate Frontend Guide: Create a Zero-Dependency Dynamic Table of Contents (TOC) with Scroll Spy
Duration: 00:00 | DP | 2025-12-08 11:41:40Vite's `?url` Import Explained: Bundled Code or a Standalone File?
Duration: 00:00 | DP | 2025-12-10 00:29:10Vue SPA 10x Slower Than Plain HTML? The Dependency Version Mystery That Tanked Performance
Duration: 00:00 | DP | 2026-01-09 08:09:01The Ultimate CSS Flexbox Guide: Easily Switch Page Header Layouts from Horizontal to Vertical
Duration: 00:00 | DP | 2025-12-11 01:00:50Cracking the TypeScript TS2339 Puzzle: Why My Vue ref Became the `never` Type
Duration: 00:00 | DP | 2025-12-13 02:04:10CSS Deep Dive: The Best Way to Customize Select Arrows for Dark Mode
Duration: 00:00 | DP | 2025-12-13 14:20:00Mastering Bootstrap 5 Rounded Corners: The Ultimate Guide to Border-Radius
Duration: 00:00 | DP | 2025-12-14 02:35:50The Ultimate Guide to Financial Charts: Build Candlestick, Waterfall, and Pareto Charts with Chart.js
Duration: 00:00 | DP | 2026-01-11 08:11:36The Ultimate Guide to Centering in Bootstrap: From `.text-center` to Flexbox
Duration: 00:00 | DP | 2025-12-15 15:23:20The Ultimate PHP Guide: How to Correctly Handle and Store Markdown Line Breaks from a Textarea
Duration: 00:00 | DP | 2025-11-20 08:08:00Bootstrap Border Magic: Instantly Add Top or Bottom Borders to Elements
Duration: 00:00 | DP | 2025-11-22 08:08:00The Ultimate Guide to JavaScript Diff Libraries: A Side-by-Side Comparison of jsdiff, diff2html, and More
Duration: 00:00 | DP | 2025-11-23 08:08:00Bootstrap JS Deep Dive: `bootstrap.bundle.js` vs. `bootstrap.js` - Which One Should You Use?
Duration: 00:00 | DP | 2025-11-27 08:08:00Is Attaching a JS Event Listener to 'document' Bad for Performance? The Truth About Event Delegation
Duration: 00:00 | DP | 2025-11-28 08:08:00Recommended
Should You Encode Chinese Characters in Sitemap URLs? The Definitive Guide
00:00 | 37When generating a sitemap.xml for your website, su...
One-Command Website Stability Check: The Ultimate Curl Latency Test Script for Zsh
00:00 | 35Need a fast, reliable way to test the latency and ...
Mastering PHP: How to Elegantly Filter an Array by Keys Using Values from Another Array
00:00 | 10In PHP development, it's a common task to filter a...
Mastering PHP Switch: How to Handle Multiple Conditions for a Single Case
00:00 | 39Have you ever tried to match multiple conditions i...