Jekyll에서 Hugo로 이사가기 (Migration log)
jekyll… 제가 예전에는 rubyist(ruby 사용자) 였습니다. 그러다가 2018~19년 쯤에 gopher(golang 사용자)가 되었고 이후부턴 만들고 있는 대다수의 도구는 golang 기반으로 만듭니다.
golang을 사용해도 ruby 자체를 오래 써왔던지라, 그래도 조금이나마 편하려고 jekyll을 선택했었는데.. Post가 1,000개 가까이 다와가니 슬슬 빌드 타임에도 문제가 생기기 시작했습니다. 물론 이게 처음은 아닙니다. 여러가지 방법등으로 15분을 넘기던 빌드 타임을 줄여 현재는 6분 정도로 유지하고 있는 상태인데, 점점 더 길어진다면 다시 10분을 넘길거고 github page 자동 배포에서 분명 걸릴거라는 확신이 들었습니다.
그래서.. 결국 hugo로 이사를 결정했습니다. (아 아까운 내 테마 ㅜㅜ 노가다 많이했는데)
자 오늘은 그냥 버리기 아까운 migration 로그를 글로 남겨봅니다. 아마 hugo로 작성하는 첫글이지 않을까 싶습니다. 그럼 시작할게요!
hugo를 선택한 이유
다른 개발 블로그들의 이야기를 들어보면 속도, Go언어 사용성 등을 이유로 들겠지만, 저에겐 진짜 단순히 빌드 속도가 가장 큰 이유입니다. 물론 jekyll 또한 ruby3에서 절제된 liquid 문법을 사용해서 오버헤드를 최소화한다면 분명히 나쁘지 않은 속도입니다.
다만 github pages는 아마 걸려본적 없으시겠지만, build time이 14분 정도 넘어가면 fail이 발생합니다. 이는 github pages에서 build limit을 15분 언저리로 걸어둬서 발생하는 문제인데, github쪽이랑 이야기도 해봤지만 결국은 build time을 줄여서 맞추는게 최선이라고 합니다.
그리고 github pages 안에서의 동작은 gem 설치에 제한도 있고, 결국 이로인해 일부 사용 불가능한 plugin을 대체하기 위해 직접 코드로 구현해야하는 번거로운 부분들이 있었습니다.
이를 해결하려면 직접 빌드해서 배포하거나 github action 을 통해 github pages가 아닌 자신의 action을 통해 배포하는 방법인데, 어차피 그렇게 배포할꺼라면 jekyll을 사용할 이유가 더더욱 없어지겠지요.. 😱
제가 테스트한바로는 github actino은 12시간 정도까지 러닝이 가능합니다 :D
어쨌던 많은 고민과 사전 테스트를 진행했었고, 결국 jekyll에서 hugo로 이사를 결정하게 된겁니다. 물론 덤으로 Go Tempate을 쓰는건 저에겐 큰 이득이긴 하겠죠. (고퍼니깐z)
Workflow
제가 지금까지 블로그 운영하면서 여러번 플랫폼을 바꿔봤지만, 이 과정은 정말 고되고 힘든 과정입니다. 글이 몇개 없다면 뭐 크게 문제는 아니겠지만 글 갯수와 포맷팅의 차이 등으로 인해서 굉장히 신경 써야할 것들이 많습니다. 우선 저는 github project로 체크하려고 해서 생각나는 이슈들만 만들어뒀습니다.
(아마 작업 후반부에는 총 이슈 갯수가 어마어마하겠죠)
Hugo import jekyll
jekyll의 데이터를 hugo로 migration 하는 방법은 여러가지가 있습니다. 대표적으로 https://github.com/SenjinDarashiva/JekyllToHugo 와 https://github.com/coderzh/ConvertToHugo 같이 스크립트를 이용한 방법이 있겠지만, hugo에선 이제 import 커맨드를 통해 다른 static generator에서 hugo 포맷으로 옮겨올 수 있도록 제공해주고 있습니다.
커맨드는 단순합니다.
hugo import jekyll {jekyll-directory} {hugo-directory}
제 케이스로 본다면 이런 형태입니다.
hugo import jekyll --force --debug ./hahwul.github.io ./hahwul.com
Importing...
Congratulations! 744 post(s) imported!
Now, start Hugo by yourself:
git clone https://github.com/spf13/herring-cove.git ./hahwul.com/themes/herring-cove
cd ./hahwul.com
hugo server --theme=herring-cove
(아 물론 둘다 private repo라 보실 순 없어요 😁)
완벽하진 않지만, 대다수 markdown 문서들이 hugo 포맷으로 변경되어 content 디렉토리에 저장됩니다. (다만 100개 정도 post가 누락됬어요 ㅜㅜ 이제 노가다 작업의 시작)
일단 잘 동작하는지 보기 위해 theme 하나를 임의로 받아서 적용 후 실행해보면..
git clone https://github.com/setsevireon/photophobia.git themes/photophobia
hugo serve --bind 0.0.0.0 --theme=photophobia
Start building sites …
hugo v0.87.0+extended linux/amd64 BuildDate=unknown
| EN
-------------------+------
Pages | 851
Paginator pages | 74
Non-page files | 0
Static files | 94
Processed images | 0
Aliases | 1
Sitemaps | 1
Cleaned | 0
Built in 427 ms
캬 진짜 빠르다… 👍
Theme customize
hugo에서 theme는 git clone 또는 git submodule을 통해 설치/사용이 가능합니다. 보통 git submodule을 추천하는데, 이유는 git clone의 경우 매번 pull 해야 해당 디렉토리의 변경 사항이 반영되지만, submodule은 본 repo가 clone 되는 순간 추가 clone 하기 때문에 최신 상태를 유지할 수 있습니다.
그리고 테마를 수정하기 위해선 2가지의 방법이 있습니다. clone된 repo를 직접 수정하거나, submodule의 경우 theme override를 통해서 커스텀 환경을 적용시킬 수 있습니다. (물론 clone도 override는 가능해요)
theme override는 layout, static 등 상위 프로젝트 구조와 테마 구조 내 같은 파일이 있는 경우 상위 프로젝트의 파일이 우선 적용됨을 의미합니다.
어쨌던 저는 제 스타일대로 꾸밀꺼기 때문에 base가 될 기반 테마를 하나 골라서 clone 후 테마 자체를 수정하는 형태로 진행했습니다. 추가로 경로가 복잡한 파일들을 확실히 override로 처리하는게 편할때가 많더라구요 :D
Utterances 처리하기
jekyll에서 utterances를 쓰고있었는데, 당연히 hugo에서도 사용해야하니 코드를 theme 내부로 이식해줍니다. 다행히 베이스 테마에 이미 기능으로 붙어 있어서 쉽게 붙였습니다. (물론 없다고 해도 그냥 스크립트 코드 한줄만 들어가면 됩니다. 자세한건 아래 링크 참고해주세요.
https://www.hahwul.com/2020/08/08/jekyll-utterances/
Trouble shooting
URL handling
기존 jekyll 블로그는 :year/:month/:day/filename
형태로 filename에서 날짜가 제외된 부분으로 구성되어 있습니다. 처음에는 이 과정 자체가 부담스러워서 URL 패턴을 변경하려고 했다가 결국 지난 blogger to jekyll 이동 시 검색 노출에서 손해본 쓴맛이 기억나 작업을 진행하기로 했습니다. (엎친대 덮친격으로 utterances 또한 pathname 기반으로 핸들링하고 있어서 선택의 여지가 없었죠. 물론 나중에는 해결 방법을 찾았지만요 🤣)
그래서 아래와 같이 permalinks를 세팅하고 기존 포맷에서 날짜 부분을 제거해주는 방식으로 구성했습니다.
permalinks:
post: /:year/:month/:day/:filename/
# ....
위와 같은 경우 jekyll의 filename 포맷이 2021-08-07-owasp-zap-oast.markdown
같은 형태라서 서버를 띄어보면 이렇게 노출되게 됩니다.
- Jekyll:
/2021/08/06/owasp-zap-oast/
- Hugo:
/2021/08/07/2021-08-07-owasp-zap-oast/
날자- 부분까지만 빼주면 우선 포맷은 같아집니다. ruby로 간단하게 스크립트 짜서 처리했습니다.
Dir.entries(".").each do | name |
if name != "check.rb" && name.strip != "." && name != ".."
filename = name[11,999]
# array 11은 0-11까지 즉 12번째 부터 출력하기 때문에 2021-08-07- 다음 글자부터 출력됩니다.
puts "Change #{name} => #{filename}"
begin
File.rename(name, filename.to_s)
rescue
end
end
end
(이렇게 빠르게 작업할 땐 아직 루비가 편하긴 하네요 😭)
자 이제 돌려보면 filename에 날짜 정보가 빠지며 hugo와 동일 포맷으로 구성됬습니다.
-rw-rw-r-- 1 devi devi 4.5K 8월 12 02:01 zaproxy-reissue-request-scripter.html
-rw-rw-r-- 1 devi devi 4.9K 8월 12 02:01 zaproxy-shortcut-key-simple-tip-zap.html
-rw-rw-r-- 1 devi devi 7.2K 8월 12 02:01 zaps-new-report-addon-report-generation.markdown
...
Datetime format issue
여기서 다시 두번쨰 이슈가 나왔는데요.. 빌드 후 페이지를 체크해보니 jekyll의 날짜와 hugo의 날짜가 달랐습니다. 이는 서로의 time 포맷 차이로 인해 발생한 문제입니다. jekyll, hugo 모두 글 작성시에는 제 로컬타임인 GMT+9를 사용하는데, 실제 배포될 땐 jekyll은 GMT+0, hugo는 제 로컬타임인 GMT+9로 계산해서 사용하게 됩니다. 포맷 문제인 것 같긴하나, 결국 Jekyll의 문제였네요… 😡
그래서.. 보통 제가 글을쓰는 00~02시는 9시간 차이로 날짜가 갈리기 때문에 pathname이 달라지는 경우가 생겼습니다. 어쩄던 이 문제도 ruby로 대충 해결해봅시다.
먼저 pathname이 일치하지 않는 URL들을 알아야합니다. 그래서 대충 짜서 돌려봤더니…
require 'uri'
require 'net/http'
require 'uri'
Dir.entries(".").each do | name |
if name != "check.rb" && name.strip != "." && name != ".."
filename = name[11,999]
# filename
path = name[0,10].to_s.gsub('-','/')
testURL = "https://www.hahwul.com/#{path}/#{filename.to_s.split('.')[0]}/"
# puts testURL
uri = URI.parse(testURL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri.request_uri)
res = http.request(request)
puts "[#{res.code}] #{testURL}"
end
end
# ruby 1.rb | tee out
#[200] https://www.hahwul.com/2017/08/17/web-hacking-metadata-payload/
#[200] https://www.hahwul.com/2017/08/07/mad-metasploit-0x40-anti-forensic/
#[404] https://www.hahwul.com/2021/08/01/25-keywords-in-go/
#[200] https://www.hahwul.com/2018/03/01/hacking-kali-linux-following-signatures/
#[200] https://www.hahwul.com/2018/06/03/jruby-ruby-java/
#[404] https://www.hahwul.com/2019/02/07/how-to-re-size-video-in-blogger-posts/
...
cat output | grep "\[200]" | wc -l
368
cat output | grep -v "\[200]" | wc -l
380
아 무슨 운명의 장난인가… 거의 반반에 가까운 값으로 정상 날짜와 밀린 날짜로 나타났습니다. 해결에는 여러가지 방법이 있겠지만 수동으로 수정하기에는 너무 양이 많아서 저는 그냥 각 게시글의 시간값에서 -9h씩 하여 맞춰줬습니다.
자 또 스크립트 갑시다.. 😊
# before 2021-08-01T00:11:56Z
# after 2021-08-12T00:11:56+09:00
require 'time'
Dir.entries(".").each do | name |
if name != "check.rb" && name.strip != "." && name != ".."
filename = name[11,999]
path = name[0,10].to_s.gsub('-','/')
begin
buf = ""
File.readlines(name).each do |line|
if line.match(/^date:/)
d = line.split('"')[1]
a = Time.parse d
b = String(a-(3600*9)).split
c = "#{b[0]}T#{b[1]}+00:00"
#puts "#{d} => #{c}"
buf = buf + "date: \"#{c}\"\n"
else
buf = buf + line
end
end
puts buf
# 여기 주석을 풀면 파일에 반영됩니다.
# File.open(name, "w") do |f|
# f.write(buf)
# end
rescue
end
end
end
date:
로 시작하는 값이 글의 작성 시간이기 때문에 해당 값만 읽어서 Time 객체로 변환한 후 9시간(3600*9 s)를 뺴주어 시간을 맞춰줬습니다.
그래서 다 돌리고 보면 모든 글들의 작성 시간이 조정된 시간(GMT0)+정상적인 포맷으로 되었습니다.
author: HAHWUL
date: "2015-08-18T14:15:00+00:00"
Deploy
Deploy Methods
github pages에 배포하는 방법은 여러가지가 있습니다.
- username.github.io로 repo 생성 후 repo에 데이터 등록
- 일반 repo에 gh-pages 브랜치로 배포
- 임의 경로를 지정하고 pages 에서 디렉토리 지정하여 배포
Deploy with github action
우선은 혹시 몰라 jekyll 블로그 repo도 유지해야하기 때문에 다른 이름으로 repo를 생성했고 2~3번 루트를 통해 배포해야합니다. 다행히 github action에 이미 hugo deploy 만들어진게 있어서 사용하셔도 되고 또는 직접 action 파일 구성해서 돌리셔도 됩니다.
우선 star가 가장 많은 것을 공유드려보면 actions-hugo가 있습니다.
https://github.com/peaceiris/actions-hugo
name: Deploy github pages
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-20.04
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- uses: actions/checkout@v2
with:
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.85.0'
# extended: true
- name: Build
run: hugo --minify
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.ref == 'refs/heads/main' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
해당 action을 사용하면 hugo build 후 결과 파일들을 actions-gh-pages 액션을 통해 배포하게 됩니다.
https://gohugo.io/hosting-and-deployment/hosting-on-github/#build-hugo-with-github-action
Domain 처리
마지막으로 도메인에 매핑될 repo만 교체해주면 됩니다. 제가 이 글을 쓰기 한시간 전 쯤에 마무리한 작업이고.. 아마 단절이 좀 있을거라 부담되긴 했지만, 생각보다 금방 쉽게 마무리되서 기쁘네요.
기존 jekyll repo의 DNS는 hahwul.github.io를 바라보기 때문에 저장소 내 CNAME 파일만 수정해주면 됩니다.
- 먼저 기존 Repo에 CNAME을 제거해줍니다.
- 신규 Repo에 할당할 도메인으로 CNAME 파일을 생성해줍니다.
참고로 CNAME 파일은 repo 최상단에 위치해야합니다. github pages가 repo 최상단 CNAME 파일을 보기 때문에 다른 경로에 있는 경우 www 로 할당하지 못하고 www의 하위 경로나 다른 서브 도메인으로만 할당할 수 있게 됩니다. 이 점만 명시하면 교체는 어렵지 않습니다 😎
Update SEO
자 이게 SEO 관련 서비스에 내가 URL을 업데이트했다 라고 알려만 주면 됩니다. 저는 자동화된 플로우를 사용하고 있는데, 아래 링크 참고해주시면 될 것 같습니다.
https://www.hahwul.com/2021/07/25/automation-seo-with-google-indexing/
Atom+Hugo
저는 jekyll에서 글을 작성, 여러가지 개발에 Atom을 사용합니다. 당연히 현재 환경을 유지하고 싶기 때문에 atom에서 hugo를 사용하는 방법을 찾아봤고, 아래 플러그인을 확인했습니다.
https://atom.io/packages/atom-hugo
다만.. jekyll atom 같이 글 제목을 입력하면 알아서 파일명을 만들어주는게 아닌, 사용자가 파일명을 만들어주는 형태라서 cli로 하는 것과 크게 다를건 없습니다. 편리한 점이라면 atom에서 hugo 서버를 구동할 수 있기 떄문에 live update로 글쓰기엔 조금 편리합니다.
저는 음… 그냥 cli로 쓰렵니다. jekyll은 파일 생성을 직접 해야하지만, hugo는 archetypes에 정의해둔 포맷으로 즉시 생성할 수 있기 때문에 굳이 메리트가 없었습니다. 키보드가 훨씬 빠를텐데 굳이 클릭을..? 😱
hugo new content/post/jekyll-to-hugo.md
간단해요!
Conclusion
드디어 탈 루비의 마지막인 블로그 jekyll 떼어내기를 완료했네요. 물론 hugo를 쓴다고 go를 쓸 일이 있지는 않겠지만(잘해야 contribute) 그래도 간간히 Template 문법 때문에라도 hugo로 옮긴건 잘한 것 같습니다.
어쩄던 빌드 타임은 환상적으로 줄어들었고, 배포도 굉장히 빨라졌습니다. 덤으로 테마 갈아엎은 것도 완성된 결과를 보니 만족스럽네요. 앞으로 귀찮은 작업은 다시 자동화를 붙여야겠어요.