Skip to content

About

한걸 티내기 위해 만드는 수제 블로그

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

나의 옵시디언 블로그 퍼블리셔

나의 워크플로우(옵시디언 + neovim)에 맞춘 블로그

Next.js 기반

기본 설정

  1. env 설정 프로젝트 내부의 .env 파일에 추가

    NOTES_SOURCE_DIR=/home/lazydino/vaults/notes
    IGNORED_DIRS=_templates
    • NOTES_SOURCE_DIR: 노트 저장소 주소 (절대주소)
    • IGNORED_DIRS: 무시하고 싶은 폴더 (상대주소)
  2. github actions 설정 .github/workflows/blog.yml 파일에 추가

    name: Deploy to GitHub Pages
    
    on:
      push:
        branches:
          - blog # 배포할 브랜치 (예: main 또는 master)
    
    jobs:
      build-and-deploy:
        runs-on: ubuntu-latest
        permissions:
          contents: write # GitHub Pages 배포를 위해 쓰기 권한 부여t
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
          - uses: oven-sh/setup-bun@v2
    
          - name: Install Dependencies
            run: bun install
    
          - name: Build Project
            run: bun run build # out 폴더 생성
    
          - name: Deploy to GitHub Pages
            uses: JamesIves/github-pages-deploy-action@v4
            with:
              branch: gh-pages # 배포할 브랜치
              folder: out # 배포할 폴더
              clean: true
              token: ${{ secrets.GITHUB_TOKEN }}

    github 사이트에서 블로그 레포지토리 설정 변경하기

    • settings->actions->general#Workflow permissions을 Read and write permissions으로 설정
  3. 의존 프로그램 설치

    • Ubuntu/Debian

      sudo apt update && sudo apt install -y rsync python
      
      pip install pyyaml
    • Fedora

      sudo dnf install -y rsync python
      pip install pyyaml
    • MacOS

      brew install rsync
      pip install pyyaml
  4. neovim 에 배포용 플러그인 생성

    lua/plugins/blog_deploy/core.lua 파일 생성

     local M = {}
    
     function M.deploy_to_blog()
       -- 기존 deploy_to_blog 함수의 내용을 여기에 복사합니다
       -- 예:
       if not vim.g.blog_config then
         vim.notify("❌ opts가 설정되지 않았습니다!", vim.log.levels.ERROR)
         return
       end
    
       local blog_config = vim.g.blog_config or {}
       local blog_path = blog_config.path and vim.fn.expand(blog_config.path) or vim.fn.expand("~/my-blog")
       local branch = blog_config.branch or "blog"
       local vault_path = blog_config.vault_path and vim.fn.expand(blog_config.vault_path) or vim.fn.expand("~/vaults/notes")
    
       local current_branch = vim.fn.system("git -C " .. blog_path .. " rev-parse --abbrev-ref HEAD"):gsub("%s+", "")
    
       if current_branch ~= branch then
         vim.fn.system("git -C " .. blog_path .. " checkout " .. branch)
       end
       -- 블로그 경로에서 bun run sync 실행
       local sync_cmd = "cd " .. vim.fn.shellescape(blog_path) .. " && bun run sync"
       local sync_result = vim.fn.system(sync_cmd)
    
       -- 동기화 작업이 실패한 경우
       if vim.fn.stridx(sync_result, "error") >= 0 then
         vim.notify("❌ 동기화 작업에 실패했습니다: " .. sync_result, vim.log.levels.ERROR)
         return
       end
    
       local has_changes = vim.fn.system("git -C " .. blog_path .. " status --porcelain") ~= ""
    
       if not has_changes then
         vim.notify("변경된 파일이 없습니다. 배포를 중지합니다.", vim.log.levels.INFO)
         return
       end
    
       -- 디버깅용 출력
       print("📌 blog_path:", blog_path)
       print("📌 branch:", branch)
       print("📌 vault_path:", vault_path)
    
       -- 명령어에 동적 값 적용
       local cmd = string.format(
         "cd %s && git pull && git add . && git commit -m 'update post' && git push",
         vim.fn.shellescape(blog_path)
       )
    
       -- -- 디버깅용 로그
       -- vim.notify("📋 실행 명령어: " .. cmd, vim.log.levels.DEBUG)
    
       -- 로딩 애니메이션을 위한 변수
       -- 더 부드러운 애니메이션을 위한 프레임 증가
       local spinner_frames = { "⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾" }
       local current_frame = 1
       local timer
       local notify_title = "블로그 배포"
    
       -- 알림 모듈 직접 사용
       local notify = require("notify")
    
     -- 데이터 수집용 변수
     local stdout_data = {}
     local stderr_data = {}
    
     -- 기존 알림 모두 제거 (배포 관련 알림만 제거하려면 filter 옵션 추가)
     notify.dismiss({
       title = notify_title,
       pending = true,
       silent = true,
     }) -- 블로그 배포 제목을 가진 알림만 제거
    
     -- 알림 ID 저장용 변수
     local notification_id
    
     -- 초기 알림 생성
     notification_id = notify("블로그 배포 준비 중...", vim.log.levels.INFO, {
       title = notify_title,
       icon = "🚀",
       timeout = false,
       hide_from_history = false,
     })
    
     -- 로딩 애니메이션 시작 - 타이머 간격을 150ms로 줄여 더 부드럽게
     timer = vim.loop.new_timer()
    
     if timer ~= nil then
       timer:start(
         100,
         50,
         vim.schedule_wrap(function()
           current_frame = (current_frame % #spinner_frames) + 1
           notification_id = notify("배포 진행 중... " .. spinner_frames[current_frame], vim.log.levels.INFO, {
             title = notify_title,
             icon = "🔄",
             timeout = false,
             replace = notification_id, -- 이전 알림 ID로 대체
           })
         end)
       )
     end
    
    -- 작업 실행
    vim.fn.jobstart(cmd, {
     on_stdout = function(_, data)
       if data and #data > 0 then
         for _, line in ipairs(data) do
           if line and line ~= "" then
             table.insert(stdout_data, line)
           end
         end
       end
     end,
     on_stderr = function(_, data)
       if data and #data > 0 then
         for _, line in ipairs(data) do
           if line and line ~= "" then
             table.insert(stderr_data, line)
           end
         end
       end
     end,
     on_exit = function(_, code)
       -- 타이머 정리
       if timer ~= nil then
         timer:stop()
         timer:close()
       end
    
       if code == 0 then
         -- 성공 알림
         notify("블로그 배포가 완료되었습니다!", vim.log.levels.INFO, {
           title = notify_title,
           icon = "✅",
           timeout = 3000,
           replace = notification_id, -- 이전 알림 ID로 대체
         })
    
         -- 성공 로그 (필요시)
         if #stdout_data > 0 then
           vim.defer_fn(function()
             notify("📄 실행 로그:\n" .. table.concat(stdout_data, "\n"):sub(1, 1000), vim.log.levels.DEBUG, {
               title = "배포 로그",
               timeout = 5000,
             })
           end, 1000)
         end
       else
         -- 실패 알림
         notify("블로그 배포에 실패했습니다! (코드: " .. code .. ")", vim.log.levels.ERROR, {
           title = notify_title,
           icon = "❌",
           timeout = 7000,
           replace = notification_id, -- 이전 알림 ID로 대체
         })
    
         -- 에러 로그
         if #stderr_data > 0 then
           vim.defer_fn(function()
             notify("🚨 에러 로그:\n" .. table.concat(stderr_data, "\n"):sub(1, 1000), vim.log.levels.ERROR, {
               title = "에러 상세",
               timeout = 10000,
             })
           end, 1000)
         end
    
         -- 출력 로그 (디버깅용)
         if #stdout_data > 0 then
           vim.defer_fn(function()
             notify("📄 출력 로그:\n" .. table.concat(stdout_data, "\n"):sub(1, 1000), vim.log.levels.DEBUG, {
               title = "실행 로그",
               timeout = 5000,
             })
           end, 2000)
         end
       end
     end,
     stdout_buffered = false,
     stderr_buffered = false,
    })
    end
     return M

    배포 플러그인을 위한 옵션 설정 lua/config/options.lua 에 블로그 설정추가

    -- 블로그 배포 관련 설정을 전역 변수로 저장
    vim.g.blog_config = {
      path = "~/Development/my-blog", -- 블로그 저장소 경로
      branch = "blog", -- 배포용 브랜치
      vault_path = "~/vaults/notes", -- 옵시디언 볼트 경로
    }

    [!info] 사용 방법

    local map = vim.keymap.set
    local map_opts = { noremap = true, silent = true }
    
    -- 블로그 배포 커맨드
    local blog_deploy = require("plugins.blog_deploy.core")
    vim.api.nvim_create_user_command("BlogDeploy", blog_deploy.deploy_to_blog, {})
    
    -- 키맵 설정
    map("n", "<localleader>oP", ":BlogDeploy<CR>", map_opts)
  5. obsidian.nvim 설정 (중요!) 해당 블로그는 파일 이름과 파일 내부의 프론트메터를 통해 블로그에 포스팅 되기 때문에 필수적으로 해야하는 설정이 있다

    • 파일명과 프론트메터의 분리 opts.note_path_func 수정 파일 명을 기반으로 블로그에서 주소를 만들게됨

       note_path_func = function(spec)
         local path = spec.dir / tostring(spec.title or "untitled")
         return path:with_suffix(".md")
       end,

      opts.note_id_func 수정(선택) 선택사항이긴 하나 id에 최초 이름이 들어가기 때문에 지저분해보임 그냥 랜덤 생성으로 변경

       note_id_func = function()
         local suffix = ""
         for _ = 1, 4 do
           suffix = suffix .. string.char(math.random(65, 90))
         end
         return tostring(os.time()) .. "-" .. suffix
       end,
    • 필수 프론트 메터 설정 opts.note_frontmatter_func 수정

       note_frontmatter_func = function(note)
         local out = {
           id = note.id,
           tags = note.tags,
           publish = "", -- 블로그의 폴더구조(카테고리) 설정 빈값이면 포스팅에서 제외
           series = "", -- 연관된 노트들을 그룹화 하기 위한 값
           createdAt = os.date("%Y-%m-%d %H:%M:%S"), -- 생성 시간 추가
           modifiedAt = os.date("%Y-%m-%d %H:%M:%S"), -- 생성 시간 추가
         }
         if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then
           for k, v in pairs(note.metadata) do
             out[k] = v
           end
         end
         return out
       end,
    • 편집 설정

      opts.wiki_link_func 수정 기본 설정의 경우 [[아이디|표시이름]] 의형태로 링크가 삽입되는데 블로그는 아이디 값이 아니라 파일 명만 검색함 [[파일명|표시이름]] 으로 링크를 삽입해야 정삭 작동

      wiki_link_func = "use_alias_only",

      opts.callbacks.pre_write_note 함수 추가(선택) 파일의 헤딩을 수정할시 해당 파일의 이름도 변경되며 동시에 백링크도 수정하는 기능 필수는 아니지만 파일의 이름을 바꾸고 타이틀을 변경하는 경우 서로의 연결이 끊길수 있기 때문에 사용하면 좋음

         pre_write_note = function(_, note)
           local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
           local content = table.concat(lines, "\n")
           -- 첫 번째 헤딩 찾기 (여러 패턴 시도)
           -- 패턴 1: 문서 시작 부분의 # 헤딩
           local first_heading = content:match("^#%s+(.-)[\r\n]")
      
           -- 패턴 2: 문서 중간의 # 헤딩
           if not first_heading then
             first_heading = content:match("\n#%s+(.-)[\r\n]")
           end
      
           -- 패턴 3: 더 관대한 패턴 (줄 끝이 아닌 어떤 문자든 허용)
           if not first_heading then
             first_heading = content:match("#%s+([^\r\n]+)")
           end
           if first_heading and #first_heading > 0 then
             first_heading = first_heading:gsub('[/\\:*?"<>|]', "_"):gsub("^%s*(.-)%s*$", "%1")
             if #first_heading > 0 then
               local full_path = vim.fn.expand("%:p")
               local current_dir = vim.fn.fnamemodify(full_path, ":h")
               local current_filename = vim.fn.fnamemodify(full_path, ":t")
               local new_filename = first_heading .. ".md"
               local new_path = current_dir .. "/" .. new_filename
               if current_filename ~= new_filename then
                 if vim.fn.filereadable(new_path) == 0 then
                   vim.cmd("silent! write")
                   local old_filename_no_ext = current_filename:gsub("%.md$", "")
                   local new_filename_no_ext = new_filename:gsub("%.md$", "")
                   local ok, err = pcall(function()
                     vim.cmd("silent! saveas! " .. vim.fn.fnameescape(new_path))
                     vim.cmd("bdelete! " .. vim.fn.fnameescape(full_path))
                     vim.cmd("silent! !rm " .. vim.fn.fnameescape(full_path))
      
                     local function get_workspace_root()
                       local full_path = vim.fn.expand("%:p")
                       local current_dir = vim.fn.fnamemodify(full_path, ":h")
      
                       -- Git 루트 찾기
                       local git_root = vim.fn.systemlist("git rev-parse --show-toplevel")[1]
                       if vim.fn.isdirectory(git_root) == 1 then
                         return git_root
                       end
      
                       -- `.obsidian` 폴더가 있으면 그걸 기준으로 루트 찾기
                       local obsidian_root = vim.fn.finddir(".obsidian", current_dir .. ";")
                       if obsidian_root and #obsidian_root > 0 then
                         return vim.fn.fnamemodify(obsidian_root, ":h")
                       end
      
                       -- 기본적으로 현재 파일이 있는 최상위 디렉터리 사용
                       return current_dir
                     end
      
                     local vault_path = get_workspace_root()
                     local Path = require("plenary.path")
                     local scan = require("plenary.scandir")
      
                     -- 볼트 내 모든 마크다운 파일 찾기
                     local function find_all_markdown_files(dir)
                       return scan.scan_dir(dir, { search_pattern = "%.md$", hidden = false, depth = 10 })
                     end
      
                     -- 파일 내용에서 백링크 업데이트
                     local function update_links_in_file(file_path, old_name, new_name)
                       local path = Path:new(file_path)
                       if not path:exists() then
                         return
                       end
      
                       local content = path:read()
                       if not content then
                         return
                       end
      
                       local updated = false
      
                       -- 1. 별칭 없는 위키 링크 처리
                       local wiki_pattern_simple = "%[%[%s*" .. vim.pesc(old_name) .. "%s*%]%]"
                       local wiki_replace_simple = "[[" .. new_name .. "]]"
                       if content:match(wiki_pattern_simple) then
                         content = content:gsub(wiki_pattern_simple, wiki_replace_simple)
                         updated = true
                       end
      
                       -- 2. 별칭 있는 위키 링크 처리
                       local wiki_pattern_alias = "%[%[%s*" .. vim.pesc(old_name) .. "%s*|%s*(.-)%s*%]%]"
                       local wiki_replace_alias = "[[" .. new_name .. "|%1]]"
                       if content:match(wiki_pattern_alias) then
                         content = content:gsub(wiki_pattern_alias, wiki_replace_alias)
                         updated = true
                       end
      
                       -- 마크다운 링크 형식 [텍스트](old_name.md) -> [텍스트](new_name.md) 업데이트
                       local md_pattern = "(%[.-%])%(" .. vim.pesc(old_name) .. "%.md%)"
                       local md_replace = "%1(" .. new_name .. ".md)"
                       if content:match(md_pattern) then
                         content = content:gsub(md_pattern, md_replace)
                         updated = true
                       end
      
                       -- 파일이 변경된 경우에만 저장
                       if updated then
                         path:write(content, "w")
                         print("✓ 백링크 업데이트 완료: " .. file_path)
                       end
                     end
      
                     -- 모든 마크다운 파일에서 백링크 업데이트 수행
                     local all_md_files = find_all_markdown_files(vault_path)
                     for _, file_path in ipairs(all_md_files) do
                       update_links_in_file(file_path, old_filename_no_ext, new_filename_no_ext)
                     end
      
                     -- 노트 경로 업데이트
                     note.path = Path:new(new_path)
                   end)
                   if ok then
                     print("✓ 파일 이름 변경 성공:", new_filename)
                   else
                     print("✗ 파일 이름 변경 실패:", tostring(err))
                   end
                 else
                   print("✗ 같은 이름의 파일이 이미 존재함:", new_path)
                 end
               else
                 print("현재 파일명이 이미 헤딩과 동일함")
               end
             end
           else
             print("헤딩을 찾을 수 없음")
           end
           -- 메타데이터 업데이트
           local frontmatter = note.metadata or {}
           frontmatter.modifiedAt = os.date("%Y-%m-%d %H:%M:%S")
         end,

기본 사용법

기본 워크플로우는 ./scripts/sync-notes.py 를 실행시켜 옵시디언 노트 내부의 폴더의 노트들을 복사해온 뒤 gh-page cli를 통해 gh-page 에 퍼블리시 한다.

Important

모든 md 문서중 프론트메터 값에 publish 값에 문자열값이(빈문자열 제외) 존재하는 문서들만 가져온다.

./scripts/sync-notes.py 의 역할은 작성 중인 노트 폴더를 탐색하여 옳바른 구조로 정리해 블로그 배포용 프로젝트에 복사해오는 역할을 담당한다.

가져온 노트는 구조에 맞춰 content/posts폴더 안에 복사해오며 문서에 연결된 이미지도 마찬가지로 public/postImg폴더 안에 복사해오게 된다

가져온 노트와 이미지 그리고 노트 끼리의 연결은 public/link-map.json 과 public/meta-data.json 을 생성하고 읽어 블로그를 생성한다.

문서의 프론트메터에 대한 설명

---
id: 1740877282-AGHK
tags:
  - blog
createdAt: 2025-03-02 10:01:22
modifiedAt: 2025-03-05 13:04:17
publish: blog
series: 나의 맞춤 블로그 만들기
---

보통 노트 작성을 하는 폴더 구조를 블로그에 그대로 사용하는것은 좋은 생각이 아니다. 하나의 계층으로 블로그주소/포스트이름 처럼 폴더 없이 하나의 계층으로 묶어두거나 다른 원하는 카테고리로 묶는 방법도 있다.

이 방식을 사용하기 위해 publish라는 값을 이용한다.

publish의 값이 비어있거나 키 자체가 존재하지 않는다면 스크립트의 탐색 대상에서 제외되며 블로그에 올라가지 않는다.

publish의 값은 문자열이어야 하며 할당된 값이 블로그 내의 주소 구조로 활용되며 카테고리를 만드는 역할을 한다. 즉 블로그주소/posts/publish 값/파일이름 의 주소를 갖게 된다.

프로젝트 포트폴리오 구조

포트폴리오 페이지의 프로젝트들은 각 프로젝트 폴더에 개별 project.json 파일로 관리됩니다.

프로젝트 파일 구조

public/projects/
├── 프로젝트명/
│   ├── project.json    # 프로젝트 정보
│   └── (이미지/영상 파일들)

project.json 스키마

interface Project {
  projectType: "project" | "study"; // 프로젝트 타입
  id: string; // 고유 ID
  title: string; // 프로젝트 제목
  description: string; // 간단한 설명
  thumbnail: string; // 썸네일 경로 (상대경로 가능: "./이미지.png")
  thumbnailOptions?: {
    // 썸네일 옵션 (비디오인 경우)
    playbackRate?: number; // 재생 속도
  };
  tags: string[]; // 태그 목록
  technologies: string[]; // 사용 기술 목록
  githubUrl?: string; // GitHub 저장소 URL
  serviceUrl?: string; // 서비스 URL
  featured: boolean; // 주요 프로젝트 여부
  createdAt: string; // 생성일 (YYYY-MM-DD)
  overview: string; // 프로젝트 개요
  features: string[]; // 주요 기능 목록
  lessons: string; // 배운 점
  challenges: string[]; // 도전 과제 목록
  demoType: "images" | "video"; // 데모 타입
  demoImages?: {
    // 데모 이미지/영상 목록
    url: string; // 파일 경로 (상대경로 가능: "./파일.mp4")
    description: string; // 설명
    showControls?: boolean; // 비디오 컨트롤 표시 여부
    playbackRate?: number; // 재생 속도
  }[];
  demoVideo?: string; // 데모 비디오 URL (demoType이 "video"인 경우)
  inDevelopment?: boolean; // 개발 중 여부
  futurePlans?: string[]; // 향후 계획
  publishPath: string; // 블로그 포스트 경로
}

프로젝트 추가 방법

  1. /public/projects/새프로젝트명/ 폴더 생성
  2. 해당 폴더에 project.json 파일 생성
  3. 프로젝트 에셋(이미지, 영상) 추가
  4. npm run build-projects 실행 (또는 bun run build 시 자동 실행)

경로 작성 방법

  • 상대 경로 사용: "./파일명.png" - 빌드 시 자동으로 절대 경로로 변환됨
  • 절대 경로 사용: "/projects/프로젝트명/파일명.png"

정렬 순서

프로젝트는 createdAt 날짜 기준으로 최신순으로 정렬됩니다.

추가해야할 기능혹은 페이지

  • contents layer 로 대체
  • 소개 패이지
  • 포트폴리오 페이지
  • 댓글 기능
  • 옵시디언 스타일의 콜아웃 적용
  • 비디오 임베드
  • D3.js를 홀용한 그래프 뷰
  • 연결된 관련 노트 리스트 추가

고칠것

  • 사이드바 스크롤 카테고리의 길이가 너무 늘어나면 전체 페이지 하단에 도달할때까지 스크롤이 안됨 카테고리의 최대 높이를 정하고 스크롤로 표현해야함

About

한걸 티내기 위해 만드는 수제 블로그

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages