
Jekyll로 문서 사이트를 다시 구축하면서 한 가지 고민에 부딪혔습니다. 문서 페이지가 워낙 방대하다 보니, 최상위 내비게이션만으로는 부족하고 별도의 하위 내비게이션(subnavigation)이 반드시 필요했습니다.
이 글에서는 포스트나 페이지의 헤딩(heading)을 기반으로 하위 내비게이션 링크를 자동 생성해 주는 간단한 Jekyll 플러그인을 만드는 방법을 단계별로 살펴보겠습니다.
개요
이 프로젝트는 다음과 같은 작업들로 나누어 진행합니다.
- 사이트의 모든 페이지에 대해 실행되는 Jekyll 제너레이터(Generator)를 만듭니다.
- 제너레이터가 페이지를 미리 렌더링(pre-render)하여 헤딩 정보를 추출할 수 있도록 합니다.
nokogiri를 사용해 페이지의 HTML을 파싱하고, 필요한 헤딩과 콘텐츠를 가져옵니다.- 추출한 데이터로 하위 내비게이션을 렌더링합니다.
아래 예제에서 모든 하위 내비게이션 링크는 앵커(anchor) 링크 방식입니다. 이 방식이 동작하려면 마크다운 처리기가 각 헤딩에 ID를 자동으로 생성해야 합니다. with_toc_data 옵션을 활성화한 RedCarpet이 이 용도에 딱 맞습니다.
기본적인 Jekyll 제너레이터 만들기
Jekyll용 플러그인에는 여러 종류가 있지만, 여기서는 그중 제너레이터(generator)를 사용합니다.
제너레이터는 Jekyll::Generator를 상속받고 generate 메서드를 제공하는 클래스일 뿐입니다. 아주 단순한 구조죠.
제너레이터는 Jekyll이 모든 마크다운 파일을 로드한 후, 해당 파일들이 HTML로 변환되기 전에 실행됩니다. 이때 site 객체가 generate 메서드로 전달되며, 이 객체를 통해 사이트의 모든 페이지, 포스트 및 기타 리소스에 접근할 수 있습니다.
아래 예제는 모든 페이지를 순회하면서 제목을 출력하는 간단한 제너레이터입니다.
class MySubnavGenerator < Jekyll::Generator
def generate(site)
site.pages.each do |page|
puts page.data["title"]
end
end
end
또한 제너레이터 안에서 페이지와 사이트의 데이터를 수정할 수도 있습니다. 여기에는 front-matter에서 로드된 데이터와 사이트 설정 파일의 데이터가 모두 포함됩니다.
page.data["title"] += " - modified!"
site.data["tagline"]
마크다운을 HTML로 사전 렌더링하기
우리의 목표는 마크다운 문서에서 헤딩을 추출하는 것입니다. 가장 간단한 방법은 마크다운을 먼저 HTML로 변환한 뒤, nokogiri 같은 도구로 해당 HTML을 파싱하는 것입니다.
솔직히 말하면 이 방식이 조금 지저분하게 느껴지나요? 네. 느리기도 할까요? 물론입니다. 하지만 Jekyll은 정적 사이트 생성기이기 때문에 실시간 성능까지 걱정할 필요가 없습니다. 그래서 저는 코를 살짝 잡고 일단 완성하기로 했습니다.
아래 코드에서는 Jekyll에 내장된 마크다운 변환기를 사용해 모든 마크다운 페이지를 HTML로 변환합니다.
class MySubnavGenerator < Jekyll::Generator
def generate(site)
parser = Jekyll::Converters::Markdown.new(site.config)
site.pages.each do |page|
if page.ext == ".md"
html = parser.convert(page['content'])
# 여기서 html로 원하는 작업을 수행합니다
end
end
end
end
헤딩 추출하기
새로운 문서 사이트에서는 모든 H2 태그에 대응하는 하위 내비게이션 링크가 있기를 원했습니다. 따라서 nokogiri로 각 페이지의 HTML을 파싱한 후, 페이지에서 H2 태그만 골라내겠습니다.
일단은 H2의 텍스트 내용과 ID를 화면에 출력해 보겠습니다.
require "nokogiri"
class MySubnavGenerator < Jekyll::Generator
def generate(site)
parser = Jekyll::Converters::Markdown.new(site.config)
site.pages.each do |page|
if page.ext == ".md"
doc = Nokogiri::HTML(parser.convert(page['content']))
doc.css('h2').each do |heading|
puts "#{ heading.text }: #{ heading['id'] }"
end
end
end
end
end
하위 내비게이션 메뉴 만들기
이제 헤딩의 텍스트와 ID를 확보했으니, 하위 내비게이션 링크 목록을 만들 수 있습니다.
이 링크 목록은 페이지 자체의 데이터 속성(data attribute)으로 저장합니다. 그러면 페이지 템플릿 안에서 손쉽게 접근할 수 있습니다.
require "nokogiri"
class MySubnavGenerator < Jekyll::Generator
def generate(site)
parser = Jekyll::Converters::Markdown.new(site.config)
site.pages.each do |page|
if page.ext == ".md"
doc = Nokogiri::HTML(parser.convert(page['content']))
page.data["subnav"] = []
doc.css('h2').each do |heading|
page.data["subnav"] << { "title" => heading.text, "url" => [page.url, heading['id']].join("#") }
end
end
end
end
end
이제 템플릿에서 subnav를 순회하면서 각 링크를 표시할 수 있습니다.
{% for item in page.subnav %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
문제 해결
앞서 언급했듯이, 이 방식 전체가 마크다운 처리기가 각 헤딩에 고유한 ID를 생성한다는 전제에 의존합니다. 참고로 제 _config.yml의 마크다운 설정은 다음과 같습니다.
# redcarpet 마크다운 렌더러 사용
markdown: redcarpet
redcarpet:
extensions: [
'no_intra_emphasis',
'fenced_code_blocks',
'autolink',
'strikethrough',
'superscript',
'with_toc_data',
'tables',
'hardwrap'
]
다음 편 예고
위에서 소개한 방식은 하위 내비게이션이 한 단계만 필요한 경우에 잘 동작합니다. 하지만 두 단계 이상이 필요하다면 어떨까요? 예를 들어 특정 H2 "내부"에 있는 H3 태그들을 하위-하위 내비게이션 링크로 만들고 싶다면요?
이 내용과 더 많은 팁은 다음 블로그 포스트에서 다룰 예정이니 기대해 주세요!