Обратные ссылки в Hugo

Можно создавать обратные ссылки и в самом Hugo.

Содержание

1 Источники обратных ссылок

  • У меня обратные ссылки получаются из заметок в Org-roam, которые я стараюсь вести по методике Zettelkasten (см. Метод Zettelkasten).

2 Создание обратных ссылок из emacs

  • Обратные ссылки можно создавать напрямую при экспорте из emacs.
  • Для экспорта обратных ссылок я использовал следующий скрипт на elisp:
     1;;; -*- mode: emacs-lisp; lexical-binding: t; coding: utf-8-unix; -*-
     2;;; Export Backlinks
     3
     4(defun ecf/org-roam--backlinks-list (file)
     5  (with-temp-buffer
     6    (if-let* ((backlinks (org-roam--get-backlinks file))
     7              (grouped-backlinks (--group-by (nth 0 it) backlinks)))
     8        (progn
     9          (dolist (group grouped-backlinks)
    10            (let ((file-from (car group))
    11                  (bls (cdr group)))
    12              (insert (format "- [[file:%s][%s]]\n"
    13                              file-from
    14                              (org-roam-db--get-title file-from)))
    15              (dolist (backlink bls)
    16                (pcase-let ((`(,file-from _ ,props) backlink))
    17                  (s-trim (s-replace "\n" " " (prin1-to-string (plist-get props :content))))
    18                  (insert "\n\n")))))))
    19    (buffer-string)))
    20
    21(defun ecf/org-export-preprocessor (backend)
    22  (let (
    23        (links (ecf/org-roam--backlinks-list (buffer-file-name))))
    24    (unless (string= links "")
    25      (save-excursion
    26        (goto-char (point-max))
    27        (insert (concat "\n* Backlinks\n") links)))))
    28
    29;;; Export backlinks only for ox-hugo
    30(defun ecf/org-export-preprocessor-hugo (backend)
    31  (when (org-export-derived-backend-p backend 'hugo)
    32    (ecf/org-export-preprocessor 'hugo)))
    33
    34(add-hook 'org-export-before-processing-hook 'ecf/org-export-preprocessor-hugo)
    
  • Недостатки:
    • Использовался вызов внутреннего API org-roam. При этом он постоянно менялся.
    • Расположение обратных ссылок жёстко кодируется.
    • Обратные ссылки получались статичными: при изменении связей необходимо повторно экспортировать все связанные ноды.

3 Создание обратных ссылок с помощью генератора Hugo

  • Но, возможно, оптимальнее создавать их с помощью самого Hugo.

3.1 Общая информация

3.2 Каталоги для новой структуры

  • Добавим каталог layouts в корень нашего сайта для создания собственных шаблонов:
    1layouts
    2├── _default
    3│   └── single.html
    4└── partials
    5    └── backlinks.html
    

3.3 Шаблон генерации обратных ссылок

  • Зададим частичный шаблон layouts/partials/backlinks.html, создающий список обратных ссылок и отображающий его:
     1{{ $re := $.File.BaseFileName }}
     2{{ $backlinks := slice }}
     3{{ range .Site.AllPages }}
     4   {{ if and (findRE $re .RawContent) (not (eq $re .File.BaseFileName)) }}
     5      {{ $backlinks = $backlinks | append . }}
     6   {{ end }}
     7{{ end }}
     8
     9<hr>
    10{{ if gt (len $backlinks) 0 }}
    11  <div class="bl-section">
    12    <h4>Links to this note</h4>
    13    <div class="backlinks">
    14      <ul>
    15       {{ range $backlinks }}
    16          <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
    17       {{ end }}
    18     </ul>
    19    </div>
    20  </div>
    21{{ else  }}
    22  <div class="bl-section">
    23    <h4>No notes link to this note</h4>
    24  </div>
    25{{ end }}
    

3.4 Шаблон страницы

  • Шаблон страницы лучше делать на основе используемой темы.
  • Я использую шаблон Academic (https://github.com/wowchemy/starter-hugo-academic), основанную на модуле wowchemy (https://github.com/wowchemy/wowchemy-hugo-modules.git).
    • Скачиваю модуль:
      1git clone https://github.com/wowchemy/wowchemy-hugo-modules.git
      
    • Копирую файл wowchemy-hugo-modules/wowchemy/layouts/_default/single.html в локальный каталог. Он имеет следующий вид (зависит от темы):
       1{{- define "main" -}}
       2
       3<article class="article">
       4
       5  {{ partial "page_header" . }}
       6
       7  <div class="article-container">
       8
       9    <div class="article-style">
      10      {{ .Content }}
      11    </div>
      12
      13    {{ partial "page_footer" . }}
      14
      15  </div>
      16</article>
      17
      18{{- end -}}
      
    • Добавляю использование частичного шаблона обратных ссылок:
       1--- orig/single.html	2021-06-02 18:12:17.337530907 +0300
       2+++ single.html	2021-06-02 16:15:08.178764024 +0300
       3@@ -8,6 +8,7 @@
       4
       5     <div class="article-style">
       6       {{ .Content }}
       7​+      {{ partial "backlinks" . }}
       8     </div>
       9
      10     {{ partial "page_footer" . }}
      
    • Получаю следующий файл layouts/_default/single.html:
       1{{- define "main" -}}
       2
       3<article class="article">
       4
       5  {{ partial "page_header" . }}
       6
       7  <div class="article-container">
       8
       9    <div class="article-style">
      10      {{ .Content }}
      11      {{ partial "backlinks" . }}
      12    </div>
      13
      14    {{ partial "page_footer" . }}
      15
      16  </div>
      17</article>
      18
      19{{- end -}}
      

4 Исправление обратных ссылок

  • При экспорте ox-hugo экспортирует ссылки как указатели на имя файла.
  • Имя файла в каталоге org-roam может отличаться от имени, которое имеет файл в каталоге для сайта.
  • Имя для сайта задаётся полем #+EXPORT_FILE_NAME:.
  • Предлагается после экспорта заменять ссылки на файлы на внутренние ссылки Hugo типа {{< relref имя_файла >}} (см. Синтаксис Markdown для генератора сайтов Hugo).
  • Имя файла предполагается брать из поля #+EXPORT_FILE_NAME:.
  • Для этого был написан скрипт:
     1#!/usr/bin/env python3
     2# -*- coding: utf-8 -*-
     3# org-roam-links
     4
     5import sys
     6import re
     7import os
     8
     9orgroamdir = "~/work/org/notes/"
    10
    11def find_md_links(md):
    12    """Returns dict of links in markdown:
    13    'regular': [foo](some.url)
    14    'footnotes': [foo][3]
    15
    16    [3]: some.url
    17    """
    18    # https://stackoverflow.com/a/30738268/2755116
    19
    20    INLINE_LINK_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
    21    FOOTNOTE_LINK_TEXT_RE = re.compile(r'\[([^\]]+)\]\[(\d+)\]')
    22    FOOTNOTE_LINK_URL_RE = re.compile(r'\[(\d+)\]:\s+(\S+)')
    23
    24    links = list(INLINE_LINK_RE.findall(md))
    25    footnote_links = dict(FOOTNOTE_LINK_TEXT_RE.findall(md))
    26    footnote_urls = dict(FOOTNOTE_LINK_URL_RE.findall(md))
    27
    28    footnotes_linking = []
    29
    30    for key in footnote_links.keys():
    31        footnotes_linking.append((footnote_links[key], footnote_urls[footnote_links[key]]))
    32
    33    return {'regular': links, 'footnotes': footnotes_linking}
    34
    35
    36def replace_md_links(md, f):
    37    """Replace links url to f(url)"""
    38
    39    links = find_md_links(md)
    40    newmd = md
    41
    42    for r in links['regular']:
    43        newmd = newmd.replace(r[1], f(r[1]))
    44
    45    for r in links['footnotes']:
    46        newmd = newmd.replace(r[1], f(r[1]))
    47
    48    return newmd
    49
    50if __name__ == "__main__":
    51    filename = sys.argv[1]
    52    print(filename)
    53    fin = open(filename, "rt")
    54    filetext = fin.read()
    55
    56    export_file_name_re = re.compile(r'#\+EXPORT_FILE_NAME:\s+\S+', re.IGNORECASE)
    57    relref_re = re.compile(r"{{< relref \"(.+)\" >}}")
    58
    59
    60    links = find_md_links(filetext)
    61    for mdlink in links['regular']:
    62        fulllink = mdlink[1]
    63        isrelref = relref_re.match(fulllink)
    64        if isrelref:
    65            linkpath = isrelref.group(1)
    66        else:
    67            # drop extention
    68            linkpath = os.path.splitext(fulllink)[0]
    69        # add extention
    70        md2org = linkpath + '.org'
    71        orgfile = os.path.expanduser(orgroamdir + md2org)
    72        if os.path.exists(orgfile):
    73            orgfiletext =  open(orgfile).read()
    74            export_file_name_text = export_file_name_re.findall(orgfiletext)
    75            export_file_name_link = export_file_name_text[0].split()
    76            hugolink = "{{< relref \"" + export_file_name_link[1] + "\" >}}"
    77            if isrelref:
    78                filetext = filetext.replace("{{< relref \"" + linkpath + "\" >}}",hugolink)
    79            else:
    80                filetext = filetext.replace(linkpath + '.md',hugolink)
    81
    82    fin.close()
    83    fin = open(filename, "wt")
    84    fin.write(filetext)
    85    fin.close()
    
  • Запуск скрипта осуществляется через Makefile:
    1all:
    2
    3fixlinks:
    4        find . -name "*.md" -newer .fixlinks -exec ./org-roam-links '{}' \;
    5        touch .fixlinks
    6
    7clean:
    8        find . -name "*~" -delete
    
  • Запуск осуществляется следующим образом:
    1make fixlinks clean
    
  • Скрипты помещаются в каталог content/.

Дмитрий Сергеевич Кулябов
Дмитрий Сергеевич Кулябов
Профессор кафедры теории вероятностей и кибербезопасности

Мои научные интересы включают физику, администрирование Unix и сетей.

Похожие