Hexo 插件:自动转换 Markdown 相对路径链接


在本地用 Markdown 写文章时,我们常用相对路径引用其他文章,例如:

[Hexo永久链接最佳实践:终极方案与优化指南](./Hexo永久链接最佳实践:终极方案与优化指南.md)

但 Hexo 在生成静态页面时,会将上述语法直接转换为:

<a href="./Hexo永久链接最佳实践:终极方案与优化指南.md" target="_blank"></a>

由于浏览器无法解析 .md 文件路径,导致超链接失效。

解决方案

在 Hexo 项目根目录下创建 scripts/fix-relative-links.js 文件,并添加以下代码:


// scripts/fix-relative-links.js
const path = require('path');
const fs = require('fs');
const glob = require('glob');

// 存储所有文章的数据
let allPostsCache = null;
let postsCache = [];

// 获取所有文章的函数
function getAllPosts(hexoInstance) {
  const posts = hexoInstance.locals.get('posts');
  if (posts && posts.length > 0) {
    return posts.toArray().map(post => ({
      source: post.source,
      path: post.path,
      permalink: post.permalink
    }));
  }
  return null;
}

// 处理链接的核心函数
function replaceLinks(content, currentSource, config, posts) {
  if (!content || !posts || posts.length === 0) return null;

  const normalize = p => p.replace(/\\/g, '/');

  // 匹配 HTML 中的 <a> 标签,href 包含 .md
  const linkRegex = /<a\s+(?:[^>]*?\s+)?href="([^"]*\.md)"([^>]*)>([\s\S]*?)<\/a>/gi;

  let hasChanged = false;
  let newContent = content.replace(linkRegex, (match, href, attrs, text) => {
    // 跳过外部链接
    if (href.includes('://')) return match;

    // 解析相对路径
    const currentDir = path.dirname(currentSource);
    const decodedPath = decodeURIComponent(href);
    const relPath = path.normalize(path.join(currentDir, decodedPath));
    const targetSourcePath = path.join(config.source_dir, relPath);

    // 在缓存的文章中查找目标
    const targetPost = posts.find(p => {
      const postSourcePath = path.join(config.source_dir, p.source);
      return normalize(postSourcePath) === normalize(targetSourcePath);
    });

    if (targetPost) {
      hasChanged = true;
      // 优先使用自定义 permalink,否则使用 Hexo 生成的默认链接
      let finalUrl = targetPost.permalink || targetPost.path;
      if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
        const siteRoot = config.root || '/';
        finalUrl = siteRoot + finalUrl;
      }

      // 确保是绝对 URL
      if (!finalUrl.startsWith('http')) {
        finalUrl = config.url.replace(/\/$/, '') + finalUrl;
      }

      // 保留原有属性,只替换 href
      return `<a href="${finalUrl}"${attrs}>${text}</a>`;
    }

    return match; // 未找到则保留原链接
  });

  return hasChanged ? newContent : null;
}

// 在 before_generate 时尝试预加载(适用于 hexo server)
hexo.extend.filter.register('before_generate', function () {
  const posts = getAllPosts(this);
  if (posts && posts.length > 0) {
    allPostsCache = posts;
    hexo.log.info('fix-relative-links: loaded ' + allPostsCache.length + ' posts in before_generate');
  }
});

// 在 after_post_render 中处理链接
hexo.extend.filter.register('after_post_render', function (data) {
  const { config } = this;

  // 收集文章信息(用于 after_generate 处理文件)
  postsCache.push({
    source: data.source,
    path: data.path,
    content: data.content,
    permalink: data.permalink
  });

  // 获取最新的文章列表
  const posts = getAllPosts(this);
  if (posts && posts.length > 0) {
    allPostsCache = posts;
  }

  // 尝试处理内容(hexo server 模式下可以实时处理)
  const newContent = replaceLinks(data.content, data.source, config, allPostsCache || postsCache);
  if (newContent) {
    data.content = newContent;
    hexo.log.info('Updated links in after_post_render: ' + data.source);
  }

  return data;
});

// 在 before_exit 中统一处理文件(适用于 hexo generate)
// before_exit 在 hexo generate 完成且文件写入磁盘后触发
hexo.extend.filter.register('before_exit', function () {
  const { config } = this;

  // 获取完整的文章列表 - 总是尝试从 locals 获取最新的
  let posts = getAllPosts(this);

  // 如果 locals 没有或数量不足,尝试从 hexo.locals 获取
  if (!posts || posts.length < 10) {
    try {
      const hexoPosts = hexo.locals.get('posts');
      if (hexoPosts && hexoPosts.length > 0) {
        posts = hexoPosts.toArray().map(post => ({
          source: post.source,
          path: post.path,
          permalink: post.permalink
        }));
        hexo.log.info('fix-relative-links: loaded ' + posts.length + ' posts from hexo.locals');
      }
    } catch (e) {
      // 忽略错误
    }
  }

  // 如果还是没有,使用 postsCache 中的文章
  if (!posts || posts.length === 0) {
    if (postsCache.length > 0) {
      posts = postsCache.map(p => ({ source: p.source, path: p.path, permalink: p.permalink }));
      hexo.log.info('fix-relative-links: using ' + posts.length + ' cached posts from postsCache');
    } else {
      // 没有文章时静默返回,避免在 hexo server 启动时显示警告
      return;
    }
  }

  allPostsCache = posts;
  hexo.log.info('fix-relative-links: processing with ' + posts.length + ' posts, postsCache has ' + postsCache.length + ' items');

  // 处理所有 HTML 文件
  const htmlFiles = glob.sync('**/*.html', { cwd: this.public_dir });
  let updatedCount = 0;

  htmlFiles.forEach(file => {
    const publicPath = path.join(this.public_dir, file);
    if (!fs.existsSync(publicPath)) return;

    let fileContent = fs.readFileSync(publicPath, 'utf-8');
    // 只处理包含 .md 链接的文件
    if (!fileContent.includes('.md')) return;

    // 找到对应的 source 文件
    const post = posts.find(p => p.path === file || file.endsWith(p.path)) ||
                 postsCache.find(p => p.path === file || file.endsWith(p.path));

    const sourcePath = post ? post.source : ('_posts/' + path.basename(file, '.html') + '.md');

    const newContent = replaceLinks(fileContent, sourcePath, config, posts);

    if (newContent) {
      fs.writeFileSync(publicPath, newContent, 'utf-8');
      hexo.log.info('Updated links in: ' + file);
      updatedCount++;
    }
  });

  if (updatedCount > 0) {
    hexo.log.info('fix-relative-links: updated ' + updatedCount + ' files');
  }

  // 清空缓存
  postsCache = [];
});

在hexo根据markdown文章生成网页之后,匹配<a>标签中href是以 .md 结尾的文件,将原本的href属性替换为对应文件真正的跳转路径。

替换逻辑

  1. 正则匹配 <a href="... .md"> 标签
  2. 解码 URL 编码的路径
  3. 解析相对路径为绝对路径
  4. 在文章列表中查找目标文章
  5. 使用 permalink || path 构建最终 URL
  6. 确保为绝对 URL(拼接 config.url)

文章作者: gloamfox
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gloamfox !
 上一篇
uniCloud加速GitHub Pages博客访问
本文介绍了如何将Hexo博客从GitHub Pages迁移到uniCloud前端网页托管,以解决国内访问速度慢的问题。通过详细的步骤指导,包括创建uniCloud服务空间、配置GitHub仓库密钥、修改部署脚本以及验证访问,帮助读者实现博客的国内加速访问。同时,文章还提供了域名绑定的相关建议和备案码申请方法。
2026-03-25
下一篇 
Hexo 搜索跳转失效?一招解决链接域名丢失问题
本文详细介绍了Hexo博客搜索功能中出现的链接域名丢失问题,分析了根本原因是Hexo搜索插件只记录文章相对路径,提供了通过修改主题JavaScript文件,将相对路径拼接为绝对链接的解决方案,并给出了具体的代码修改步骤。
2026-03-24
  目录