Mastering Marked.js: How to Elegantly Batch-Add a CDN Domain to Markdown Images

Published: 2025-11-27
Author: DP
Views: 9
Category: Markdown
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 ![Comparison Image](/assets/file/some_image.png) ``` 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 = ' ![Comparison Image](/assets/lib00/some_image.png) '; 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 = ' ![Comparison Image](/assets/lib00/some_image.png) '; 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 = ' ![Comparison Image](/assets/lib00/some_image.png) '; 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`.
Recommended