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:10Recommended
MySQL Primary Key Inversion: Swap 1 to 110 with Just Two Lines of SQL
00:00 | 8In database management, you might face the unique ...
Crontab Logs Missing Dates? 4 Practical Ways to Easily Add Timestamps
00:00 | 18Crontab is a powerful tool for task automation, bu...
Composer Script Not Running? Unveiling the `post-install-cmd` Trap and the Ultimate Solution
00:00 | 0Have you ever run `composer install` only to find ...
Cracking the TypeScript TS2339 Puzzle: Why My Vue ref Became the `never` Type
00:00 | 7Ever encountered the tricky `Property '...' does n...