
Tôi đang rebuild documentation site dùng Jekyll. Vì documentation pages khá lớn, chúng ta phải có subnavigation ngoài top-level navigation.
Bài này cover cách làm simple Jekyll plugin generate subnav links từ headings trong posts/pages.
Tổng quan
Tôi chia project thành tasks sau:
- Tạo Jekyll generator chạy trên mọi page của site.
- Dạy generator pre-render pages để extract heading info.
- Dùng nokogiri parse HTML page extract headings và content liên quan.
- Render subnavigation.
Trong ví dụ dưới, tất cả subnav links là anchor links. Cho approach này chạy, bạn cần đảm bảo markdown processor tạo IDs cho headings. RedCarpet với option with_toc_data chạy ngon.
Basic Jekyll Generator
Có vài loại plugin khác nhau cho Jekyll. Chúng ta tạo generator.
Generators đơn giản là class kế thừa Jekyll::Generator và cung cấp method generate.
Generators chạy sau Jekyll load tất cả markdown files nhưng trước khi convert sang HTML. Object site truyền vào generate method. Và bạn dùng site object truy cập tất cả pages, posts và resources khác của site.
Ví dụ dưới, tạo generator loop qua tất cả pages và in title.
class MySubnavGenerator < Jekyll::Generator def generate(site) site.pages.each do |page| puts page.data["title"] end end end
Chúng ta cũng có thể modify page và site data bên trong generator -- data load từ front-matter và site config file.
page.data["title"] += " - modified!" site.data["tagline"]
Pre-Rendering Markdown sang HTML
Chúng ta muốn extract headings từ markdown documents. Cách đơn giản nhất là convert markdown sang HTML rồi parse HTML dùng tool như nokogiri.
Điều này làm tôi hơi dirty? Có. Sẽ hơi chậm? Hoàn toàn. Nhưng vì Jekyll là static site generator, tôi không lo real-time performance. Vì vậy tôi chỉ nín hơi và làm.
Ở đây, dùng Jekyll built-in markdown wrapper convert mọi markdown page sang 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']) # Làm gì đó với html ở đây end end end end
Extract Headings
Cho documentation site mới, tôi quyết định mỗi H2 tag nên có subnavigation link tương ứng. Vì vậy dùng nokogiri parse HTML mỗi page, sau đó pluck mỗi H2 tag từ page.
Hiện tại, chỉ in content và ID của H2 ra màn hình:
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
Tạo Subnavigation menu
Bây giờ có heading text và ID, tạo list subnavigation links.
Chúng ta lưu list này làm data attribute của page. Cách đó, truy cập links từ page template.
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
Bây giờ, trong template, bạn loop qua subnav và hiển thị mỗi link:
{% for item in page.subnav %} <a href="{{ item.url }}">{{ item.title }}</a> {% endfor %}
Troubleshooting
Như nói trước, tất cả phụ thuộc markdown processor tạo unique IDs cho mỗi heading. Đây là markdown settings từ _config.yml:
# Dùng RedCarpet Markdown renderer markdown: redcarpet redcarpet: extensions: [ 'no_intra_emphasis', 'fenced_code_blocks', 'autolink', 'strikethrough', 'superscript', 'with_toc_data', 'tables', 'hardwrap' ]
Next Up...
Approach trên chạy tốt nếu chỉ cần một level subnavigation. Nhưng nếu cần hơn? i.e. muốn tất cả H3 tags "bên trong" H2 tạo sub-subnavigation links trong menu?
Chúng ta sẽ cover đó và hơn nữa trong blog post tương lai. Stay tuned!