Migracja bloga z Jekyll na Hugo i jego publikacja w GitHub Pages

Spis treści

Prawdopodobnie zauważyliście drobne zmiany w wyglądzie tego bloga oraz pewnie też cześć osób miała w ostatnim czasie problemy z uzyskaniem do niego dostępu. Odpowiedzialne za zaistniałą sytuację są limity narzucone przez GitHub Pages. Zakładają one maksymalny rozmiar repozytorium pod stronę WWW w granicach 1 GiB oraz czas budowania takiego serwisu krótszy niż 10 minut. Limit miejsca na pliki przekroczony został w zasadzie w chwili przeniesienia bloga WordPress na GitHub Pages. Natomiast parę dni temu został przekroczony limit czasu budowania tego projektu. Do momentu transformacji, niniejszy blog działał w oparciu o Jekyll, który generował w zasadzie statyczny kod HTML ze zwykłych plików tekstowych (np. artykuły pisane w MarkDown). GitHub Pages posiada wsparcie dla Jekyll, przez co taką stronę WWW można bardzo prosto wdrożyć. Niemniej jednak, cały ten mechanizm generowania projektu w obrębie infrastruktury GitHub'a zajmuje bardzo dużo czasu. Po rozmowie z supportem okazało się, że gdyby chodziło o miejsce na pliki, to mogli by nagiąć reguły i nie było by problemu ale nie dadzą rady tego zrobić w przypadku przekroczenia czasu generowania projektu. Dlatego też trzeba było zrezygnować z Jekyll'a i poszukać dla niego jakiejś alternatywy. Padło na Hugo, który w odróżnieniu od Jekyll'a nie jest jako tako wspierany przez GitHub i by stronę wygenerowaną przez Hugo podpiąć pod GitHub Pages trzeba się trochę wysilić, bo ten proces nie jest automatyczny i właśnie o tym jak dokonać migracji z Jekyll na Hugo w kontekście GitHub Pages będzie ten poniższy artykuł.

Hugo i Debian

Jako, że projekt Hugo jest otwartoźródłowy, to stosowny pakiet do instalacji w systemie znajduje się w głównym repozytorium Debiana. Możemy go zatem bez najmniejszego problemu zainstalować przy pomocy menadżera pakietów apt-get/aptitude :

# aptitude install hugo

Importowanie projektu Jekyll do Hugo

By ułatwić nieco przejście z Jekyll na Hugo, istnieje możliwość zaimportowania naszego bloga, tak by nie musieć przeprowadzać tego procesu ręcznie. Projekt będzie importowany do osobnego katalogu, także nie ma co się bać ewentualnej utraty danych. By zaimportować nasz serwis WWW do Hugo, wpisujemy w terminalu polecenie hugo import jekyll podając mu dwa argumenty w postaci ścieżki do katalogu z projektem Jekyll oraz ścieżki do katalogu wynikowego:

$ hugo import jekyll ~/morfikov.jekyll/ ~/morfikov.hugo/

Po chwili, nasz serwis powinien znaleźć się w miejscu docelowym. Przechodzimy do tego katalogu:

$ cd ~/morfikov.hugo/

Od tego momentu, wszystkie polecenia w tym wpisie będą wydawane względem tego katalogu roboczego.

Repozytorium git dla bloga

Strony hostowane na GitHub Pages są utrzymywane w systemie kontroli wersji git. W przypadku mojego bloga niestety jego import (i późniejsze dostosowanie plików) wymagał bardzo dużej ilości zmian, przez co pliki repozytorium by się dość znacznie rozrosły i zaczęłyby niepotrzebnie marnować miejsce zarówno na dysku mojego laptopa, jak i w zdalnym repozytorium na GitHub. Dlatego też po przygotowaniu całego projektu zdecydowałem się na zaoranie katalogu .git/ oraz usunięcie projektu GitHub Pages (zdalnego repo) i utworzenie na jego miejsce świeżego repozytorium.

$ git init
$ git add .
$ git commit -S -m "move the blog from jekyll to hugo"
$ git remote add origin git@github.com:morfikov/morfikov.github.io.git
$ git push --set-upstream origin master

Zanim popchniemy zmiany do zdalnego repozytorium, upewnijmy się, że w katalogu projektu Hugo nie ma żadnych wygenerowanych plików serwisu, tj. tych wygenerowanych przez Jekyll/Hugo. To co ma zostać pchnięte do zdalnego zdalnego repozytorium, to tylko i wyłącznie pliki źródłowe bloga, z których Hugo będzie budował w późniejszym czasie nasz serwis.

Motyw dla Hugo

Pierwszą rzeczą, którą będziemy musieli zrobić po imporcie bloga, to poszukać jakiegoś motywu (theme), który będzie odpowiedzialny za wyrenderowanie strony. Bez tego motywu, w przeglądarce przywita nas jedynie biała strona. Skórek dla Hugo jest cała masa i z pewnością znajdziemy tę, która nam będzie odpowiadać. Jeśli jednak będziemy mieli z tym problem, to będziemy musieli napisać własny motyw. Ja zdecydowałem się na Binario, jako że jego kolorystyka bardzo przypomina mi wygląd pulpitu mojego linux'a.

Motywy dla Hugo są przechowywane w systemie kontroli wersji git, podobnie jak nasz projekt GitHub Pages. Wiążą się z tym pewne komplikacje, mianowicie projekt skórki trzeba będzie dołączyć do projektu naszego serwisu WWW w postaci modułu. Zarówno nasz blog jak i jego skórka będą posiadać dwa niezależne repozytoria git i nie mogą ze sobą w żaden sposób kolidować. By dodać moduł, wpisujemy w terminal poniższe polecenie:

$ git submodule add https://github.com/vimux/binario themes/binario

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .gitmodules
        new file:   themes/binario

Jak widać, wydanie tego polecenia wygenerowało plik .gitmodules oraz pojawił się też katalog themes/binario/ . W themes/binario/ jest zawartość motywu Hugo, a w .gitmodules mamy poniższą treść:

[submodule "themes/binario"]
    path = themes/binario
    url = https://github.com/vimux/binario

Dodatkowo w pliku .git/config została dodana taka zwrotka:

[submodule "themes/binario"]
    url = https://github.com/vimux/binario
    active = true

Potwierdzamy dodanie plików modułu (motywu Hugo) do głównego repozytorium:

$ git commit -S -m "add the binario theme submodule"
$ git push origin master

Lokalny test Hugo

Powinniśmy w tej chwili mieć już zaimportowany stary serwis Jekyll'a oraz jeden motyw do wyrenderowania strony WWW. W głównym katalogu repozytorium powinien znajdować się plik config.toml , w którym to określamy szereg opcji konfiguracyjnych dla Hugo. Poniżej znajduje się cały mój config.toml :

baseurl          = "https://morfikov.github.io"
title            = "Morfitronik"
languageCode     = "pl-PL"
paginate         = "10" # Number of elements per page in pagination
theme            = "binario"
disqusShortname  = "morfitronik" # Enable comments by entering your Disqus shortname
googleAnalytics  = "UA-119125303-1" # Enable Google Analytics by entering your tracking id
canonifyURLs     = true
#summarylength   = 100
rssLimit         = 20
defaultContentLanguage = "pl"
#publishDir      = "docs"

[taxonomies]
  category = "categories"
  tag = "tags"

[permalinks]
  post = "/post/:filename/"

[sitemap]
  changefreq = "weekly"
  filename = "sitemap.xml"
  priority = 1.0

[Author] # Used in authorbox
  name   = "Mikhail Morfikov"
  bio    = "Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT,
  Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da
  się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej
  najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania."
  avatar = "img/avatar.png"

[Params]
  description        = "Blog o bezpieczeństwie, prywatności oraz systemach linux (Debian/Ubuntu, OpenWRT/LEDE i Android)" # Site Description. Used in meta description
  copyright          = "Mikhail Morfikov" # Copyright holder, otherwise will use .Site.Title
  opengraph          = true # Enable OpenGraph if true
  schema             = true # Enable Schema
  twitter_cards      = true # Enable Twitter Cards if true
  columns            = 2 # Set the number of cards columns. Possible values: 1, 2, 3
  mainSections       = ["post"] # Set main page sections
  dateFormat         = "02/01/2006" # Change the format of dates
  colorTheme         = "" # dark-green, dark-blue, dark-red, dark-violet
  customCSS          = ["css/custom.css"] # Include custom CSS files
  customJS           = ["js/custom.js"] # Include custom JS files
  mainMenuAlignment  = "right" # Align main menu (desktop version) to the right side
  authorbox          = true # Show authorbox at bottom of single pages if true
  comments           = true # Enable comments for all site pages
  related            = true # Enable Related content for single pages
  relatedMax         = 5 # Set the maximum number of elements that can be displayed in related block. Optional
  mathjax            = true # Enable MathJax for all site pages
  mathjaxPath        = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.6/MathJax.js" # Specify MathJax path. Optional
  mathjaxConfig      = "TeX-AMS-MML_HTMLorMML" # Specify MathJax config. Optional
  hideNoPostsWarning = false # Don't show no posts empty state warning in main page, if true

[Params.Entry]
  meta    = ["date", "categories"] # Enable meta fields in given order
  toc     = true # Enable Table of Contents
  tocOpen = false # Open Table of Contents block. Optional

[Params.Featured]
  previewOnly = false # Show only preview featured image

[Params.Breadcrumb]
  enable   = true # Enable breadcrumb block globally
  homeText = "Home" # Home node text

[Params.Social]
  email         = "morfik@nsa.com"
# facebook      = "username"
  twitter       = "mikhailmorfikov"
  telegram      = "morfikov"
# instagram     = "username"
# pinterest     = "username"
# vk            = "username"
# linkedin      = "username"
  github        = "morfikov"
  gitlab        = "morfikov"
  stackoverflow = "3015317"
# mastodon      = "https://some.instance/@username"
# medium        = "username"

[Params.Share] # Entry Share block
  facebook  = true
  twitter   = true
  reddit    = true
  telegram  = true
  linkedin  = true
  vk        = true
  pocket    = true
  pinterest = true

# Web App Manifest settings
# https://www.w3.org/TR/appmanifest/
# https://developers.google.com/web/fundamentals/web-app-manifest/
[Params.Manifest]
  name            = "Morfitronik"
  shortName       = "Morfitronik"
  display         = "browser"
  startUrl        = "/"
  backgroundColor = "#2a2a2a"
  themeColor      = "#1b1b1b"
  description     = "Blog o bezpieczeństwie, prywatności oraz systemach linux (Debian/Ubuntu, OpenWRT/LEDE i Android)"
  orientation     = "portrait"
  scope           = "/"

[outputFormats]
  [outputFormats.MANIFEST]
    mediaType      = "application/json"
    baseName       = "manifest"
    isPlainText    = true
    notAlternative = true
  [outputFormats.RSS]
    mediatype      = "application/rss"
    baseName       = "feed"

[mediaTypes]
  [mediaTypes."application/rss"]
    suffixes = ["xml"]

[outputs]
  home = ["HTML", "RSS", "MANIFEST"]

[menu]
#  [[menu.main]]
#    identifier = "archives"
#    name       = "Archiwum"
#    url        = "/archives/"
#    weight     = 10

  [[menu.main]]
    identifier = "categories"
    name       = "Kategorie"
    url        = "/categories/"
    weight     = 20

  [[menu.main]]
    identifier = "tags"
    name       = "Tagi"
    url        = "/tags/"
    weight     = 30

[markup]
  defaultMarkdownHandler = "goldmark"
  [markup.asciidocExt]
    backend = "html5"
    docType = "article"
    extensions = []
    failureLevel = "fatal"
    noHeaderOrFooter = true
    safeMode = "unsafe"
    sectionNumbers = false
    trace = false
    verbose = true
    workingFolderCurrent = false
    [markup.asciidocExt.attributes]
  [markup.blackFriday]
    angledQuotes = false
    footnoteAnchorPrefix = ""
    footnoteReturnLinkContents = ""
    fractions = true
    hrefTargetBlank = false
    latexDashes = true
    nofollowLinks = false
    noreferrerLinks = false
    plainIDAnchors = true
    skipHTML = false
    smartDashes = false
    smartypants = false
    smartypantsQuotesNBSP = false
    taskLists = true
  [markup.goldmark]
    [markup.goldmark.extensions]
      definitionList = true
      footnote = true
      linkify = true
      strikethrough = true
      table = true
      taskList = true
      typographer = false
    [markup.goldmark.parser]
      attribute = true
      autoHeadingID = true
      autoHeadingIDType = "github"
    [markup.goldmark.renderer]
      hardWraps = false
      unsafe = false
      xhtml = false
  [markup.highlight]
    codeFences = true
    guessSyntax = false
    hl_Lines = ""
    lineNoStart = 1
    lineNos = false
    lineNumbersInTable = true
    noClasses = true
    style = "monokai"
    tabWidth = 4
  [markup.tableOfContents]
    endLevel = 5
    ordered = true
    startLevel = 2

Trochę tych opcji nazbierałem w drodze konfiguracji swojego bloga ale niekoniecznie potrzebujemy ich wszystkich. Te najważniejsze to baseurl , który ma wskazywać na stronę naszego serwisu (w tym przypadku, blog jest dostępny pod https://morfikov.github.io ). Dalej mamy theme , który określa skórkę użytą przy renderowaniu strony. I to są w zasadzie kluczowe elementy, które musimy skonfigurować, by Hugo nam wygenerował stronę.

Możemy teraz odpalić testowo Hugo, by sprawdzić czy nasz blog się bez problemu zbuduje:

$ hugo server

Polecenie hugo server generuje lokalną wersję strony WWW, tj. blog będzie serwowany z adresu pętli zwrotnej ( 127.0.0.1:1313 ) przez wbudowany w Hugo serwer WWW. To rozwiązanie jest przeznaczone głównie do testów, choć też może być wykorzystywane na produkcji. Niemniej jednak, zaleca się wykorzystywanie pełnowymiarowego serwera WWW do tego celu. W przypadku GitHub Pages, taki serwer WWW będzie zapewniony i by wygenerować w późniejszym czasie stronę, trzeba będzie ominąć server z powyższego polecenia i użyć jedynie hugo . Standardowo też projekt będzie budowany w pamięci RAM. Jeśli mamy mało pamięci operacyjnej albo też chcemy by pliki były zapisywane na dysku, to musimy określić opcję --renderToDisk . Dobrze jest także włączyć tryb rozmowny --verbose oraz ostrzeżenia związane z translacją strony WWW --i18n-warnings . Reasumując, do lokalnych testów bloga Hugo będziemy używać poniższego polecenia:

$ hugo server --i18n-warnings --renderToDisk --verbose

Po wydaniu tego polecenia, nasz serwis powinien zostać zbudowany w katalogu public/ i powinniśmy być w stanie go wyświetlić w przeglądarce odwiedzając adres http://127.0.0.1:1313 :

$ hugo server --i18n-warnings --renderToDisk --verbose
INFO 2020/08/13 21:30:03 Using config file:
Building sites … INFO 2020/08/13 21:30:03 syncing static files to /home/morfik/morfikov.hugo/public/

                   |  PL
-------------------+--------
  Pages            |  1049
  Paginator pages  |   273
  Non-page files   |     1
  Static files     | 10497
  Processed images |     0
  Aliases          |   254
  Sitemaps         |     1
  Cleaned          |     0

Built in 69544 ms
Watching for changes in /home/morfik/morfikov.hugo/{archetypes,content,data,i18n,layouts,static,themes}
Watching for config changes in /home/morfik/morfikov.hugo/config.toml
Environment: "development"
Serving pages from /home/morfik/morfikov.hugo/public
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

Projekt Hugo na GitHub Pages

Migrację bloga z Jekyll na Hugo mamy za sobą. Niemniej jednak, z tych plików projektu nie da rady zbudować strony WWW i udostępnić jej za sprawą GitHub Pages. By strona wygenerowana przez Hugo mogła zostać wyświetlona przez GitHub Pages musimy poczynić kilka dodatkowych kroków.

Podejścia są dwa. Pierwsze z nich zakłada zmianę nazwy budowanego katalogu z public/ na docs/ , jako że GitHub Pages jest w stanie budować projekty z plików w katalogu docs/ . Jeśli interesuje nas to rozwiązanie, to wystarczy w konfiguracji Hugo (plik config.toml głównym katalogu repozytorium git) określić parametr publishDir i podać mu wartość docs . My jednak stworzymy nieco bardziej zaawansowaną konfigurację, która zakłada utworzenie nowej gałęzi dla wygenerowanych plików. W taki sposób zmiany w kodzie źródłowym bloga będą utrzymywane osobno, w stosunku do zmian wygenerowanej strony. Później w ustawieniach GitHub Pages wskażemy, że chcemy by projekt był budowany z tej osobnej gałęzi. Zatem do dzieła.

Na początek tworzymy plik .gitignore w głównym katalogu repozytorium i dodajemy do niego nazwę katalogu, w którym będą przechowywane wygenerowane przez Hugo pliki. W tym przypadku wykorzystywany jest standardowy katalog, tj. public/ :

$ echo "public" >> .gitignore

Potwierdzamy zmiany w git:

$ commit -S -m "add .gitignore"

Następnie musimy przygotować gałąź gh-pages i ją zainicjować:

$ git checkout --orphan gh-pages
$ git reset --hard
$ git commit --allow-empty -m "Initializing gh-pages branch"
$ git push origin gh-pages
$ git checkout master

Po wydaniu polecenia git reset --hard prawdopodobnie nie będzie można skasować katalogu z motywami Hugo. Pod żadnym pozorem nie kasujmy go ręcznie. Pozostawmy go w takiej formie jakiej jest i zwyczajnie wydajmy następne polecenie, tj. git commit ... .

Musimy teraz usunąć cały katalog public/ i utworzyć go za pomocą git-worktree:

$ rm -rf public/
$ git worktree add -B gh-pages public origin/gh-pages

$ git worktree list
/home/morfik/morfikov.hugo/         ebca1ad [master]
/home/morfik/morfikov.hugo/public   52b3630 [gh-pages]

Ma to na celu stworzenie niezależnych drzew roboczych w osobnych katalogach i podpięcie ich pod osobne gałęzie. W taki sposób pliki źródłowe naszego bloga (+ moduł motywu Hugo) będą w gałęzi master , a wygenerowana zawartość strony w gałęzi gh-pages .

Przy pomocy polecenia hugo generujemy nasz blog na produkcję:

$ hugo

                   |  PL
-------------------+--------
  Pages            |  1044
  Paginator pages  |   272
  Non-page files   |     1
  Static files     | 10497
  Processed images |     0
  Aliases          |   252
  Sitemaps         |     1
  Cleaned          |     0

Total in 41934 ms

Po wygenerowaniu, przechodzimy do katalogu public/ , dodajemy wygenerowane pliki i zatwierdzamy zmiany w git:

$ cd public && git add --all && git commit -m "Publishing to gh-pages" && cd ..

Za każdym razem jak będziemy zmieniać zawartość strony, czy to poprawiać jej kod czy dodawać nowe artykuły na blogu, te dwa powyższe polecenia trzeba będzie wydawać, by wygenerować nową stronę do wdrożenia na produkcji.

Teraz już pozostaje nam pchniecie zmian do zdalnego repozytorium na GitHub:

$ git push origin master
$ git push origin gh-pages

Pierwsze z powyższych poleceń przesyła zmiany kodu źródłowego strony do zdalnej gałęzi master , drugie zaś wygenerowany kod strony do zdalnej gałęzi gh-pages . W przypadku wprowadzania zmian w kodzie strony, tylko różnice będą przesyłane, choć rozmiary commit'ów mogą czasem przytłoczyć.

Kwestia opróżniania katalogu public/

Być może w którymś momencie w przyszłości zajdzie potrzeba opróżnienia zawartości katalogu public/ . Hugo w zasadzie nie operuje na tych plikach i wykorzystuje je jedynie do serwowania strony przez wbudowany serwer WWW. Jak plików brakuje, to zostaną na nowo wygenerowane. Problemy mogą się zacząć, gdy w grę wchodzi git. Trzeba pamiętać, że w katalogu public/ utworzyliśmy drzewo robocze, co z kolei skutkuje utworzeniem pliku public/.git . Jest to zwykły i do tego mały plik tekstowy zawierający w zasadzie jedną linijkę:

gitdir: /home/morfik/morfikov.hugo/.git/worktrees/public

Jeśli ten plik zostanie skasowany, to system przestanie widzieć zmiany w katalogu public/ . Dlatego też jeśli będziemy chcieli opróżnić zawartość katalogu public/ , to nie ruszajmy tego pliku -- wszystkie pozostałe pliki z tego katalogu możemy jak najbardziej skasować.

GitHub Pages a gałąź do budowania projektu

Ostatnim krokiem jest wskazanie GitHub Pages gałęzi do budowania projektu. Teoretycznie GitHub Pages powinien automatycznie podebrać gałąź gh-pages i z niej budować stronę WWW. Jeśli jednak tak się nie dzieje, to wchodzimy w ustawienia zdalnego repozytorium:

github-pages-jekyll-hugo-settings

I przechodzimy do sekcji GitHub Pages :

github-pages-jekyll-hugo-settings-branch

Wybieramy gałąź gh-pages i zapisujemy ustawienia. Po paru minutach strona powinna zostać wygenerowana. Proces generowania możemy śledzić przechodząc do gałęzi gh-pages :

github-pages-jekyll-hugo-build-check

github-pages-jekyll-hugo-build-check

I jak widać z powyższych fotek, blog zbudował się bez problemów. Co ciekawe, w porównaniu do 11-12 minut potrzebnych na zbudowanie projektu Jekyll, ten sam projekt Hugo buduje się na GitHub Pages w około 2 minuty. oczywiście lokalnie pierw te pliki zbudowaliśmy ale i tak nawet dodając tę 1 minutę lokalnego generowania strony, to jest około 3 minut. Zatem jeśli zdarzy nam się trafić na podobny problem z uderzeniem w limit czasu generowania strony na GitHub Pages i korzystamy przy tym z Jekyll'a, to pomyślmy nad przejściem na Hugo.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.