↓ 跳过正文
  1. 文章/

Obsidian 联动 Hugo 叶页面导出实现记录

Hugo笔记 Obsidian笔记 Blog

目录
BlogBuild - 这篇文章属于一个选集。
§ 3: 本文

Hugo 叶页面(Leaf Bundle)导出实现记录
#

本文记录在 Obsidian 中使用 obsidian-enhancing-export + Pandoc,将每篇笔记导出为 Hugo 叶页面结构的过程:目录命名为笔记名,文章改名为 index.md,图片与文章放在同一目录。

目标结构:

<笔记名>/
  index.md
  image1.png
  image2.jpg

背景
#

  • 使用插件:obsidian-enhancing-export(下称 OEE),内部通过 Pandoc 与 Lua filter 转换。
  • 需求:每篇笔记导出时,形成 Hugo 的 leaf bundle(index.md + 同目录资源)。
  • Vault 中图片可能位于自定义附件目录(由 obsidian-custom-attachment-location 决定),导出时希望自动落到与 index.md 同一目录。

尝试与问题
#

1) 命令参数转义与 -lua-filter 误写
#

  • 现象:Pandoc 报 Unknown option -l/-u/-a。
  • 根因:将 --lua-filter 写成了 -lua-filter 或引号转义混乱,导致被拆分为 -l -u -a。
  • 放弃:统一使用 --lua-filter="..." 或 --lua-filter=...,且在 Windows 下避免多重 \\\" 转义。

2) 过滤器链:先 Hugo 再"打包/改路径"
#

  • 最初思路:
    • 先运行 markdown+hugo.lua 以适配 Hugo,再运行自写 bundle.lua 或 index.lua 去复制图片并改写链接。
  • 问题:
    • bundle.lua 复制逻辑可行,但与路径/模板(bundle_target_dir、resource-path)搭配较繁琐。
    • 用户已有图片在同名目录,实际只需"保持同目录 + 改名 index.md",无需单独复制。
  • 调整:
    • 放弃额外的 bundle.lua;改为在内置的 markdown.lua 中直接处理 Hugo 模式的资源落地策略,减少链路复杂度。

最终方案(核心处理)
#

将obsidian-enhancing-export的fork下来,用nodejs编译自己的版本 修改时可以在obsidian安装hot-reload插件,方便查看更改是否编译成功

# 在obsidian-enhancing-export的目录下

# 安装依赖
npm install

# 开启开发环境并更改
npm dev

将输出的

  • main.js
  • manifest.json
  • styles.css 导入YOUR_Obsidan_VAULT.obsidian\plugins的安装,这样就不会因为插件更新导致导出失败了

A. 修改 lua/markdown.lua(仅对 Hugo 模式)
#

  • 目标:将媒体文件复制到与 index.md 同一目录,并在文内改写为相对文件名。

  • 将 Hugo 模式下的 media_dir 改为 doc_dir。

  • get_output_file() 在 Hugo 模式使用原始文件名,复制到 media_dir。

  • 取消对 Hugo 模式 ../ 前缀的拼接。

  • 关键点:

    1. 媒体目录选择:
      • 原逻辑:若无变量 media_dir,使用 <output_basename>-media(非 Hugo),或为 Hugo 情况构造相对 ../index/... 路径。
      • 现逻辑:若 Mode == 'hugo',则 media_dir = doc_dir(即输出 index.md 的目录)。 ~lua if PANDOC_STATE.output_file then local output_file = PANDOC_STATE.output_file doc_dir = PATH.directory(output_file) if PANDOC_WRITER_OPTIONS.variables\["media_dir"\] then media_dir = tostring(PANDOC_WRITER_OPTIONS.variables\["media_dir"\]) else – 默认策略: – - Hugo 模式:将媒体文件放在与输出 index.md 相同的目录(叶子 Bundle) – - 其他模式:使用 <output_basename>-media 目录 if Mode == ‘hugo’ then media_dir = doc_dir else media_dir = PATH.split_extension(output_file) .. ‘-media’ end end end ~
    2. 文件名策略:
      • 原逻辑:使用 sha1(file_path) 生成哈希文件名。
      • 现逻辑:Hugo 模式保留原始文件名(basename)。
    3. 链接改写:
      • 原逻辑:Hugo 模式常加 ../ 前缀以指向上级 index 子目录。
      • 现逻辑:直接输出复制后的相对路径(通常就是文件名),确保 ![](image.png)。

B. 导出模板(OEE data.json)
#

  • 「Markdown (Hugo)」arguments(关键项):
-f ${fromFormat} --resource-path="${vaultDir}/你的附件位置/${(currentPath.substring(vaultDir.length+1).replace(/\\\\/g,'/').replace(/\\.[^/.]+$/,''))}" --lua-filter="${luaDir}/markdown+hugo.lua" -s -o "${outputDir}/${outputFileName}/index.md" -t commonmark_x-attributes

验证步骤
#

  1. 在 OEE 中选择「Markdown (Hugo)」导出。
  2. 观察输出:<导出根>/<笔记名>/index.md 与图片位于同一目录。
  3. 打开 index.md,确认图片引用为 ![](文件名) 或相对无上级前缀的短路径。

附录:markdown.lua完整代码
#

package.path=debug.getinfo(1).source:gsub('@',''):sub(0):match('(.*[/\\])'):sub(0) .. '?.lua' .. ';' .. package.path

require("polyfill")
local url = require('url')

local pandoc=pandoc
local PANDOC_STATE=PANDOC_STATE

PANDOC_VERSION:must_be_at_least '3.1.7'

os.text = pandoc.text

local PATH = pandoc.path
local doc_dir = nil
local media_dir = nil

if Mode == nil then
  Mode = 'default'
end

-- print("Mode: "..Mode)

if PANDOC_STATE.output_file then
  local output_file = PANDOC_STATE.output_file
  doc_dir = PATH.directory(output_file)
  if PANDOC_WRITER_OPTIONS.variables["media_dir"] then
    media_dir = tostring(PANDOC_WRITER_OPTIONS.variables["media_dir"])
  else
    -- 默认策略:
    -- - Hugo 模式:将媒体文件放在与输出 index.md 相同的目录(叶子 Bundle)
    -- - 其他模式:使用 <output_basename>-media 目录
    if Mode == 'hugo' then
      media_dir = doc_dir
    else
      media_dir = PATH.split_extension(output_file) .. '-media'
    end
  end
end
assert(doc_dir, "doc_dir is nil")
assert(media_dir, "media_dir is nil")


local function get_absolute_path(file_path)
  if PATH.is_absolute(file_path) then
    return file_path
  end
  for _, dir in pairs(PANDOC_STATE.resource_path) do
    local full_path = PATH.join({dir, file_path})
    if os.exists(full_path) then
      return full_path
    end
  end
  for _, file in pairs(PANDOC_STATE.input_files) do
    if not PATH.is_absolute(file) then
      file = PATH.join({pandoc.system.get_working_directory(), file_path})
    end
    local dir = PATH.directory(file)
    local full_path = PATH.join({dir, file_path})
    if os.exists(full_path) then
      return full_path
    end
  end
  return nil
end

local function basename(p)
  local parts = PATH.split(p)
  return parts[#parts]
end

local function get_output_file(file_path)
  if not media_dir then return nil end
  if Mode == 'hugo' then
    -- 在 Hugo 模式下,图片与 index.md 放在同一目录且保留原文件名
    local name = basename(file_path)
    return PATH.join({media_dir, name})
  else
    -- 其他模式维持原逻辑,使用哈希文件名并放入 media 目录
    local new_file_name = pandoc.utils.sha1(file_path)
    local _, new_file_ext = PATH.split_extension(file_path)
    local name = new_file_name .. new_file_ext
    return PATH.join({media_dir, name})
  end
end

local function extract_media(file_path)
  os.mkdir(media_dir)
  file_path = url.decode(file_path)
  local abs_path = get_absolute_path(file_path)
  local file = get_output_file(file_path)
  if abs_path and file then
    if not os.exists(file) then
      os.copy(abs_path, file)
    end
    local rel_path = PATH.make_relative(file, doc_dir, false)
    local parts = PATH.split(rel_path)
    for i,v in ipairs(parts) do
      parts[i] = url.encode(v)
    end
    local encoded_rel_path = table.concat(parts, "/")
    -- Hugo 模式下,文件已复制至与 index.md 同目录,直接使用相对文件名/路径
    return encoded_rel_path
  end
end

local function raw(s)
  return pandoc.RawInline('markdown', s)
end

function Image(el)
  local src = extract_media(el.src)
  if src then
    el.src = src
  end
  return el
end

function Space()
  return raw(' ')
end

function SoftBreak()
  return raw('\n')
end

function RawInline(el)
  if el.format == "html" then
    el.format = 'markdown'
    el.text = string.gsub(el.text, '<img[^>]+>', function(img)
      return string.gsub(img, 'src="([^"]+)"', function(url)
        if string.find(url, '^[Hh][Tt][Tt][Pp][Ss]?://') == nil  then
          local extract_media_url = extract_media(url)
          if extract_media_url then
            return 'src="' .. extract_media_url .. '"'
          end
          return '123'
        end
        return 'src="' .. url .. '"'
      end)
    end)
  end
  return el
end

function RawBlock(el)
  if el.format == "html" then
    el.format = 'markdown'
  end
  return el
end

function Math(el)
  if Mode == 'hugo' then
    if el.mathtype == 'DisplayMath' then
      return raw('
  \n$$'  .. el.text .. '$$\n')
    else
      el.text = string.gsub(el.text, '\\[\\{\\}]', function (v)
        return '\\' .. v
      end)
      el.text = string.gsub(el.text, '_', function (v)
        return '\\' .. v
      end)
    end
  end
  return el
end

local function headerLink(input)
  -- github style section link
  return "#"..input:gsub(' ', '-')
end


local function insertLink(content, linkDescription)
  local descriptionText = table.concat(linkDescription, "")

  if string.find(descriptionText, '|') then
    local target, desc = descriptionText:match("(.*)|(.*)")
    table.insert(content, pandoc.Link(desc, headerLink(target)))
  else
    table.insert(content, pandoc.Link(descriptionText, headerLink(descriptionText)))
  end
end

function Para(el)
  local content = el.content
  content = ProcessMath(content)
  content = ProcessInternalLinks(content)
  el.content = content
  return el
end

function ProcessMath(elements)
  local content = {}
  local in_display_math = false
  for _, item in pairs(elements) do
    if item.t == 'Str'and item.text == "$$" then
      in_display_math = not in_display_math
    else
      if in_display_math then
        if item.t == 'RawInline' and item.format == 'tex' then
          local n = pandoc.Math('DisplayMath', '\n' .. item.text .. '\n')
          table.insert(content, Math(n))
        else
          table.insert(content, item)
        end
      else
        table.insert(content, item)
      end
    end
  end
  return content
end

function ProcessInternalLinks(elements)
  local content = {}
  local in_section_link = false
  local linkDescription = {}

  for _, item in pairs(elements) do
    if item.t == 'Str' and string.starts_with(item.text, '[[#') then
      in_section_link = true
      table.insert(linkDescription, string.sub(item.text, 4))
    elseif in_section_link then
      if string.ends_with(item.text, ']]') then
        table.insert(linkDescription, string.sub(item.text, 1, -3))
        insertLink(content, linkDescription)
        in_section_link = false
        linkDescription = {}
      else
        table.insert(linkDescription, item.text)
      end
    else
      table.insert(content, item)
    end
  end
  return content
end

function Plain(el)
  el.content = ProcessInternalLinks(el.content)
  return el
end

function Pandoc(el)
  return el
end
BlogBuild - 这篇文章属于一个选集。
§ 3: 本文

相关文章