7 minute read

Hello everyone! 👋

A small update for the blog, I’ve moved it from Hugo1 to Jekyll2.

Introduction

Hugo is a static website generator just like Jekyll, it’s written in Go, and it’s pretty fast. It is also distributed as a binary.

However, that didn’t stop me to go back to Jekyll, an older static website generator written in Ruby.

The main reason for the update is that I love the Minimal Mistakes3 theme written by Michael Rose and I like the Ruby ecosystem (bundler, rake) and that’s about it.

Here’s what I did not like about Hugo.

I don’t care about the speed since for my blog Jekyll builds in about 10s and Hugo is 10x faster. Regarding the binary distribution I always had to install a lot of dependencies in order to build hugo-extended version because it most likely wasn’t packaged for my Linux distro.

Sometimes when I updated either Hugo or my theme I got build failures and I had to sync the theme version and Hugo version, and sometimes this would break my changes.

I also didn’t like that I had to specify an image url instead of a file url when using a hero image under the Blowfish[4] theme.

What I loved about Hugo is the performance, the fact that it is written in Go, and it’s internationalization capabilities.

Jekyll is not perfect, but it works well for my use case. I also like having a Rakefile4 that I use it to build my blog under Linux and Windows and upload it to my server via rsync5:

desc "Serve the site locally"
task :serve do
  sh "bundle exec jekyll serve --livereload"
end

desc "Build and deploy the site via rsync"
task :deploy => :build do
  is_windows = Gem.win_platform?
  sync_destination = "[email protected]:/blog"
  if is_windows
    sh "wsl rsync -arv -e 'ssh -i ~/.ssh/nuculabs.dev' ./_site/ #{sync_destination}"
  else
    sh "rsync -arv -e ssh ./_site/ #{sync_destination}"
  end
end

Customizations

I started doing some custom customizations on the blog in order to make it look better. There are small improvements for table of contents, the hero image and the footer.

You can use my custom.sass file:

.footer-flex
  display: flex
  justify-content: space-between

// Page hero modifications
.page__hero
  position: absolute

  .page__hero-image
    opacity: 0.85
    width: 100vw

  &::after
    content: ""
    position: absolute
    top: 0
    left: 0
    width: 100%
    height: 100%
    // Center is transparent (0),
    //  Edges are White (1)
    background: linear-gradient(to bottom, transparent 70%, rgba(255,255,255,1) 100%), radial-gradient(ellipse 50% 100% at center, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.8) 45%, rgba(255,255,255,0) 100%)
    pointer-events: none

// Toc improvement
.toc:not(:has(> ul))
  display: none

.btn--mastodon
  background-color: #6147e6
  color: #fff

  &:visited, &:hover, &:focus, &:active
    background-color: #6147e6
    color: #fff

.btn--reddit
  background-color: #ff4500
  color: #fff

  &:visited, &:hover, &:focus, &:active
    background-color: #ff4500
    color: #fff

Don’t forget to import it in the main scss file.

@import "custom"; // custom styles _sass/custom.sass

Social Share

I’ve also modernized the social share buttons because I like having Mastodon and Bluesky as an option.

Here are the contents of _includes/social-share.html.

<section class="page__share">
    <h4 class="page__share-title">Share on</h4>

    <a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fnuculabs.dev%2Fweb%2520development%2F2026%2F02%2F24%2Ffrom-hugo-to-jekyll.html"
       class="btn btn--facebook" aria-label="Share on Facebook"
       onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
       title="Share on Facebook">
        <i class="fab fa-fw fa-facebook" aria-hidden="true"></i><span> Facebook</span>
    </a>

    <a href="https://www.linkedin.com/shareArticle?mini=true&url=https://nuculabs.dev/web%20development/2026/02/24/from-hugo-to-jekyll.html"
       class="btn btn--linkedin" aria-label="Share on LinkedIn"
       onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
       title="Share on LinkedIn">
        <i class="fab fa-fw fa-linkedin" aria-hidden="true"></i><span> LinkedIn</span>
    </a>

    <a href="https://bsky.app/intent/compose?text=From+Hugo+to+Jekyll%20https%3A%2F%2Fnuculabs.dev%2Fweb%2520development%2F2026%2F02%2F24%2Ffrom-hugo-to-jekyll.html"
       class="btn btn--bluesky"
       onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
       title="Share on Bluesky">
        <i class="fab fa-fw fa-bluesky" aria-hidden="true"></i><span> Bluesky</span>
    </a>

    <a
            href="https://s2f.kytta.dev/?text=From+Hugo+to+Jekyll%20https%3A%2F%2Fnuculabs.dev%2Fweb%2520development%2F2026%2F02%2F24%2Ffrom-hugo-to-jekyll.html"
            class="btn btn--mastodon"
            onclick="
            window.open(
                this.href,
                'window',
                'left=20,top=20,width=500,height=500,toolbar=1,resizable=0',
            );
            return false;
        "
            title="Share on Mastodon"
    >
        <i class="fab fa-fw fa-mastodon" aria-hidden="true"></i
        ><span> Mastodon</span>
    </a>

    <a
            href="https://www.reddit.com/submit/?url=https%3A%2F%2Fnuculabs.dev%2Fweb%2520development%2F2026%2F02%2F24%2Ffrom-hugo-to-jekyll.html&resubmit=true&title=From+Hugo+to+Jekyll"
            class="btn btn--reddit"
            onclick="
            window.open(
                this.href,
                'window',
                'left=20,top=20,width=500,height=500,toolbar=1,resizable=0',
            );
            return false;
        "
            title="Share on Reddit"
    >
        <i class="fab fa-fw fa-reddit" aria-hidden="true"></i
        ><span> Reddit</span>
    </a>
</section>

Having a lightbox feature is useful if you serve pictures on your blog, the following JS and CSS makes so that when you click on an image it resizes it and makes the background darker. I did not write the original script but I have adapted it to work with the Minimal Mistakes theme.

I use plain css:

#lightbox {width: 100%; height: 100%; position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.85); z-index: 9999999; line-height: 0; cursor: pointer; display: none;}
#lightbox .img {
    position: relative;
    top: 50%;
    left: 50%;
    -ms-transform: translateX(-50%) translateY(-50%);
    -webkit-transform: translate(-50%,-50%);
    transform: translate(-50%,-50%);
    max-width: 100%;
    max-height: 100%;
}
#lightbox .img img {opacity: 0; pointer-events: none; width: auto;}
@media screen and (min-width: 1200px) {
    #lightbox .img {
        max-width: 1200px;
    }
}
@media screen and (min-height: 1200px) {
    #lightbox .img {
        max-height: 1200px;
    }
}
#lightbox span {display: block; position: fixed; bottom: 13px; height: 1.5em; line-height: 1.4em; width: 100%; text-align: center; color: white; text-shadow:
        -1px -1px 0 #000,
        1px -1px 0 #000,
        -1px 1px 0 #000,
        1px 1px 0 #000;
}

#lightbox span {display: none;}

#lightbox .videoWrapperContainer {
    position: relative;
    top: 50%;
    left: 50%;
    -ms-transform: translateX(-50%) translateY(-50%);
    -webkit-transform: translate(-50%,-50%);
    transform: translate(-50%,-50%);
    max-width: 900px;
    max-height: 100%;
}
#lightbox .videoWrapperContainer .videoWrapper {
    height: 0;
    line-height: 0;
    margin: 0;
    padding: 0;
    position: relative;
    padding-bottom: 56.333%; /* custom */
    background: black;
}
#lightbox .videoWrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}
#lightbox #prev, #lightbox #next {height: 50px; line-height: 36px; display: none; margin-top: -25px; position: fixed; top: 50%; padding: 0 15px; cursor: pointer; text-decoration: none; z-index: 99; color: white; font-size: 60px;}
#lightbox.gallery #prev, #lightbox.gallery #next {display: block;}
#lightbox #prev {left: 0;}
#lightbox #next {right: 0;}
#lightbox #close {height: 50px; width: 50px; position: fixed; cursor: pointer; text-decoration: none; z-index: 99; right: 0; top: 0;}
#lightbox #close:after, #lightbox #close:before {position: absolute; margin-top: 22px; margin-left: 14px; content: ""; height: 3px; background: white; width: 23px;
    -webkit-transform-origin: 50% 50%;
    -moz-transform-origin: 50% 50%;
    -o-transform-origin: 50% 50%;
    transform-origin: 50% 50%;
    /* Safari */
    -webkit-transform: rotate(-45deg);
    /* Firefox */
    -moz-transform: rotate(-45deg);
    /* IE */
    -ms-transform: rotate(-45deg);
    /* Opera */
    -o-transform: rotate(-45deg);
}
#lightbox #close:after {
    /* Safari */
    -webkit-transform: rotate(45deg);
    /* Firefox */
    -moz-transform: rotate(45deg);
    /* IE */
    -ms-transform: rotate(45deg);
    /* Opera */
    -o-transform: rotate(45deg);
}
#lightbox, #lightbox * {
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

And plain javascript:

function is_youtubelink(url) {
    var p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
    return (url.match(p)) ? RegExp.$1 : false;
}
function is_imagelink(url) {
    var p = /([a-z\-_0-9\/\:\.]*\.(jpg|jpeg|png|gif))/i;
    return (url.match(p)) ? true : false;
}
function is_vimeolink(url,el) {
    var id = false;
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
        if (xmlhttp.readyState == XMLHttpRequest.DONE) {   // XMLHttpRequest.DONE == 4
            if (xmlhttp.status == 200) {
                var response = JSON.parse(xmlhttp.responseText);
                id = response.video_id;
                console.log(id);
                el.classList.add('lightbox-vimeo');
                el.setAttribute('data-id',id);

                el.addEventListener("click", function(event) {
                    event.preventDefault();
                    document.getElementById('lightbox').innerHTML = '<a id="close"></a><a id="next">&rsaquo;</a><a id="prev">&lsaquo;</a><div class="videoWrapperContainer"><div class="videoWrapper"><iframe src="https://player.vimeo.com/video/'+el.getAttribute('data-id')+'/?autoplay=1&byline=0&title=0&portrait=0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></div>';
                    document.getElementById('lightbox').style.display = 'block';

                    setGallery(this);
                });
            }
            else if (xmlhttp.status == 400) {
                alert('There was an error 400');
            }
            else {
                alert('something else other than 200 was returned');
            }
        }
    };
    xmlhttp.open("GET", 'https://vimeo.com/api/oembed.json?url='+url, true);
    xmlhttp.send();
}
function setGallery(el) {
    var elements = document.body.querySelectorAll(".gallery");
    elements.forEach(element => {
        element.classList.remove('gallery');
    });
    if(el.closest('ul, p')) {
        var link_elements = el.closest('ul, p').querySelectorAll("a[class*='lightbox-']");
        link_elements.forEach(link_element => {
            link_element.classList.remove('current');
        });
        link_elements.forEach(link_element => {
            if(el.getAttribute('href') == link_element.getAttribute('href')) {
                link_element.classList.add('current');
            }
        });
        if(link_elements.length>1) {
            document.getElementById('lightbox').classList.add('gallery');
            link_elements.forEach(link_element => {
                link_element.classList.add('gallery');
            });
        }
        var currentkey;
        var gallery_elements = document.querySelectorAll('a.gallery');
        Object.keys(gallery_elements).forEach(function (k) {
            if(gallery_elements[k].classList.contains('current')) currentkey = k;
        });
        if(currentkey==(gallery_elements.length-1)) var nextkey = 0;
        else var nextkey = parseInt(currentkey)+1;
        if(currentkey==0) var prevkey = parseInt(gallery_elements.length-1);
        else var prevkey = parseInt(currentkey)-1;
        document.getElementById('next').addEventListener("click", function() {
            gallery_elements[nextkey].click();
        });
        document.getElementById('prev').addEventListener("click", function() {
            gallery_elements[prevkey].click();
        });
    }
}

document.addEventListener("DOMContentLoaded", function() {

    //create lightbox div in the footer
    var newdiv = document.createElement("div");
    newdiv.setAttribute('id',"lightbox");
    document.body.appendChild(newdiv);

    //add classes to links to be able to initiate lightboxes
    var elements = document.querySelectorAll('.page__content a');
    elements.forEach(element => {
        var url = element.getAttribute('href');
        if(url) {
            if(url.indexOf('vimeo') !== -1 && !element.classList.contains('no-lightbox')) {
                is_vimeolink(url,element);
            }
            if(is_youtubelink(url) && !element.classList.contains('no-lightbox')) {
                element.classList.add('lightbox-youtube');
                element.setAttribute('data-id',is_youtubelink(url));
            }
            if(is_imagelink(url) && !element.classList.contains('no-lightbox')) {
                element.classList.add('lightbox-image');
                var href = element.getAttribute('href');
                var filename = href.split('/').pop();
                var split = filename.split(".");
                var name = split[0];
                element.setAttribute('title',name);
            }
        }

        var imageElements = document.querySelectorAll(".page__content img");
        imageElements.forEach( element => {
            element.classList.add('lightbox-image');
        })
    });

    //remove the clicked lightbox
    document.getElementById('lightbox').addEventListener("click", function(event) {
        if(event.target.id != 'next' && event.target.id != 'prev'){
            this.innerHTML = '';
            document.getElementById('lightbox').style.display = 'none';
        }
    });

    //add the youtube lightbox on click
    var elements = document.querySelectorAll('a.lightbox-youtube');
    elements.forEach(element => {
        element.addEventListener("click", function(event) {
            event.preventDefault();
            document.getElementById('lightbox').innerHTML = '<a id="close"></a><a id="next">&rsaquo;</a><a id="prev">&lsaquo;</a><div class="videoWrapperContainer"><div class="videoWrapper"><iframe src="https://www.youtube.com/embed/'+this.getAttribute('data-id')+'?autoplay=1&showinfo=0&rel=0"></iframe></div>';
            document.getElementById('lightbox').style.display = 'block';

            setGallery(this);
        });
    });

    //add the image lightbox on click
    var elements = document.querySelectorAll('a.lightbox-image');
    elements.forEach(element => {
        element.addEventListener("click", function(event) {
            event.preventDefault();
            document.getElementById('lightbox').innerHTML = '<a id="close"></a><a id="next">&rsaquo;</a><a id="prev">&lsaquo;</a><div class="img" style="background: url(\''+this.getAttribute('href')+'\') center center / contain no-repeat;" title="'+this.getAttribute('title')+'" ><img src="'+this.getAttribute('href')+'" alt="'+this.getAttribute('title')+'" /></div><span>'+this.getAttribute('title')+'</span>';
            document.getElementById('lightbox').style.display = 'block';

            setGallery(this);
        });
    });
    var elements = document.querySelectorAll('img.lightbox-image');
    elements.forEach(element => {
        element.addEventListener("click", function(event) {
            event.preventDefault();
            document.getElementById('lightbox').innerHTML = '<a id="close"></a><a id="next">&rsaquo;</a><a id="prev">&lsaquo;</a><div class="img" style="background: url(\''+this.getAttribute('src')+'\') center center / contain no-repeat;" title="'+this.getAttribute('title')+'" ><img src="'+this.getAttribute('src')+'" alt="'+this.getAttribute('title')+'" /></div><span>'+this.getAttribute('title')+'</span>';
            document.getElementById('lightbox').style.display = 'block';

            setGallery(this);
        });
    });

});

Conclusion

Hugo is faster and newer than Jekyll but if you like the Ruby ecosystem you can give Jekyll a try. The Minimal Mistakes theme by Michael Rose still rocks!

  1. https://gohugo.io/ 

  2. https://jekyllrb.com/ 

  3. https://mmistakes.github.io/minimal-mistakes/ 

  4. https://ruby.github.io/rake/ 

  5. https://linux.die.net/man/1/rsyncÂ