My experience building this website with Jekyll and GitHub Pages

2 August 2025

25 mins

My experience building this website with Jekyll and GitHub Pages

I started this as a test blog post so I have some content to play around with while figuring out how to build this site. However, as this is the first time I ever build a website or use Jekyll, I thought my process of figuring stuff out would be a good opportunity to document what I’m learning as I go.

Using GitHub Pages as a website platform

One of the main driving factors in choosing Jekyll is that it is very easy to publish the website on GitHub pages.
One should note that it is better to use the Jekyll workflow action rather than the one provided by github as it builds the site in a docker image and is not restricted by the default github pages limitations (use of plugins, old version of Jekyll etc.).

Markdown content

I’ve never been a huge fan of Markdown, probably because I’ve been spoiled by emacs org-mode, and I often get the syntax confused. In the future, I will probably figure out how to automatically publish Jekyll-buildable html directly from org-mode, but for now, markdown will have to do - and it sure beats writing directly with html.

Tweaking the theme

To get started with building the actual site, I thought I’d best find a decent theme I like and use it as a base for tweaking. This of course has its ups and downs, but I have no aspiration of becoming a web developer, so I think it is the fastest path to getting the desired outcome, which is this site. I choose Awesome Jekyll Theme for this purpose, with clear intent to modify it.
This leads to some caveats: everything from here on is based on modifying “this” theme, which comes with a pre-built Tailwind css present from the Gem install, and makes the assumption that all the Tailwind features used are present. Replicating everything below might not work exactly the same way with a different theme, so please keep that in mind.

I decided to go down this path mainly because it is really convenient being able to add only the html and css files I want to change and having them override the default ones installed (not without some difficulties as you will read about later).

To get started, I made just a few small tweaks. First, I changed the navbar to be sticky for simpler navigation. For this, I ended up needing some additional custom css to overwrite the default behaviour.

nav.sticky,
.sticky-nav {
  position: -webkit-sticky !important;
  position: sticky !important;
  top: 0 !important;
  z-index: 999 !important;
  background-color: inherit !important;
}

I wrote this to assets/css/custom.css and loaded this as an additional stylesheet in _layouts/default.html , then added the class to nav object in _includes/navbar.html.

<nav class="sticky top-0 left-0 right-0 z-50 flex w-full flex-wrap items-center justify-between py-4 bg-white dark:bg-neutral-900 shadow-md">

Some other changes I made were to remove the ‘pages’ from the footbar (as I preder to navigate from the navbar, which is now sticky), and ensure the titles in the _layouts/page.html changed colour when switching between dark and light themes. This last issue actually seems like a bug, so maybe I should open a PR with this suggested fix. It was only changing

<h1 class="text-left text-2xl font-bold md:text-5xl text-black">{{page.title}}</h1>

to

<h1 class="text-left text-2xl font-bold md:text-5xl text-neutral-900 dark:text-neutral-200">{{page.title}}</h1>

I then went ahead and changed the home page to remove the side-scroll buttons and made it more fluid to match my design intension which was pretty straight forward all things considered. It was a bit of a pain to teak to spacing, but eventually I got there.

Adding contact_channels

I wanted to add some more channels I am on which are not part of the theme. Compared to other things, this was quite straight forward, and I got to learn about simpleicons.org which I never knew about before!
All it took was to copy the one of the existing blocks in _includes/contact_channels.html and replace the label and img. For example, for ORCID I added this code block, with the .svg saved in the corresponding directory.

{% endif %} {% if contact_channels.orcid %}
<a
  class="transition hover:scale-110 duration-300"
  href="{{ contact_channels.orcid }}"
  target="_blank"
  rel="noopener noreferrer"
  aria-label="ORCID"
>
  <img
    class="h-6 w-6 dark:invert"
    src="{{ '/assets/images/contact_channels/orcid.svg' | relative_url }}"
    alt="ORCID Icon"
  />
</a>

I ended up adding Google Scholar, ORCID and ResearchGate to the list.

Robots and maps

I asked a web developer friend of mine for some feedback and he suggested I should add robots.txt and a sitemap. I had no idea these things even exist! Luckily, with Jekyll this was quite easy.
I added the sitemap with a Jekyll plugin, jekyll-sitemap, in the _config.yml and Gemfile. That automatically generates the sitemap.
Next is robots.txt, which suggests to crawlers what to index in the site. From what I understood, I only need a very basic one, so I just added

User-agent: *
Disallow:
Sitemap: {{ site.url }}/sitemap.xml

and saved it in the root directory. As I need the liquid tag (i.e. the things in {{ }}) to get built by liquid, I added this line to the _config.yml

include:
  - robots.txt

These two things should now ensure that the sitemap is built and that it accessible by crawlers.

Meta-title and favicon

Again, new concepts I never heard of before. These too were pretty straight forward to add.
The meta-title is the title you see in the tabs. I set it with some unicode to fit the style I’m going for in this site. I just added the following like to the <head> of the default layout

<title>mdg &#x276f My experience building this website with Jekyll and GitHub Pages</title>

This again uses liquid to find the title, and I just added a permanent “mdg ❯” before it as I think it makes more sense than just a title like “about”.

The favicon is the icon in the tab / bookmark etc. which can be quite useful! I experimented with using the logo as a favicon but did not like it, so for now I do not have one. However, to add it is as simple as adding a line again in the head of the default layout

<link rel="icon" href="/assets/favicon.ico" type="image/x-icon"

More fighting with Tailwind

In general, the thing I found the hardest in building this site is that a lot of what I read online about how to change things in html, Flexbox and Liquid did not seem to work. The theme ships with a pre-built Tailwind dist-style.css, and if I understood correctly (which I very well might not have!), as I am not building the Tailwind styles, the Tailwind JIT compiler purged any unused styles from the pre-built css. Hence, when I tried to use styles that were not there at build time, their css could not be found and they could not be applied by the browser.
The really issue was that I never really knew which styles were actually available to me and would work when used in the html, and which would not.
To overcome this limitation, I ended up having to place most changes in the custom css file in a similar way to the sticky navbar. This included a bunch of issues with controlling the footer and the navbar in particular.

As an example, to get the a page to fill the screen and to keep the footer at the bottom of the screen when it under filled, I added a Flexbox to the body with min-h-screen to the body will fill the viewport, then wrapped the content with a <main class="flex-grow">

<body class="flex flex-col min-h-screen bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-200">
  {% include navbar.html %}
  <main class="flex-grow">
    {{ content }}
  </main>
  {% include footer.html %}
</body>

This should use Flexbox to make the main area grow and take up all the space not occupied by the footer in the viewport, so it pushes the footer to the bottom when there is extra vertical space.
What was needed next is to add margin-top: auto to the footer in assets/css/custom.css. At first that did not seem to work, so I had to also add the following

body > footer {
  margin-top: auto !important;
}

Notice the body > footer bit, which makes sure the css treats the footer as a direct child of body when applying the top margin. From my understanding, it already should do that if I built the styles with Tailwind myself — but as I could not, it had to be added this way instead. Finding out such limitations really took me some time and a lot of trial and error.

Similarly, to gain finer control over other layouts, like the top padding and the width in the page layout, I could not simply modify the class style in the layout’s html by changing the values in pt-20 lg:pt-[90px], but instead had to add it to the page layout class without Tailwind commands through the assets/css/custom.css like this

#page {
  padding-top: 25px;
  max-width: 90ch;
}

Things got much harder when I tried to reduce the footer size. I decided I wanted all elements of the footer aligned at the bottom, with the contact channels to the left, copyright notice in the centre, and rss subscription in the right.

This should be simple enough, but figuring out how to untangle it from the current pre-built styles was not so easy. I ended up using a Flexbox container which finally did the trick, as adapting the existing style with Tailwind just did not work. I also wanted to make sure that in mobile view the footer gets stacked and changes the order so that the rss subscription is in between the others. To do that, I added some classes to the custom.css file:

.mobile-only {
  display: block;
}
.desktop-only {
  display: none;
}

@media (min-width: 768px) {
  .mobile-only {
    display: none !important;
  }
  .desktop-only {
    display: block !important;
  }
}

@media (min-width: 768px) {
  .footer-copyright {
    align-self: flex-end; /* Desktop only */
  }
}

These classes are then used in the rss subscription block and change its display property based on the width of the window. The footer-copyright is added as an additional class with align-self: flex-end to ensure the copyright is aligned to the bottom better, and also ensures that it only applies on Desktop so that it doesn’t break the alignment on mobile.

So I am no designer, but I did want to add some flare to the site. I thought of the simplest logo possible that would match the site - a terminal prompt with my initials.
Finding a prompt icon and adding letters was easy enough, but to get it to work with the theme properly, I needed to get it in a simple .svg format that I can embed in the html so that the theme can affect it directly. I used Inkscape to trace the logo, then edited the XML of the .svg to remove fill and size information as these will be controlled properly by css using the theme’s currentColor so it adapts with the theme switcher for dark mode, as well as browser extensions like Dark Reader.

.logo-image {
  color: inherit;
  display: inline-block;
  overflow: visible;
  max-width: 100%;
}

.logo-image svg path {
  fill: currentColor;
}

.logo-image svg {
  height: 1.6rem;
  width: auto;
  display: block;
  padding-top: 0.22rem;
  overflow: visible;
  shape-rendering: geometricPrecision;
}

Of course, I still had to inline the icon itself. One option was to add it to the _include directory and use liquid to inline it, but I really didn’t want to have a .svg there. Instead, I wrote a simple ruby plugin to read and inline it that works perfectly with Jekyll.

# _plugins/inline_svg.rb
module Jekyll
  module InlineSVG
    def inline_svg(path)
      file_path = File.join(@context.registers[:site].source, path)
      File.read(file_path)
    end
  end
end

Liquid::Template.register_filter(Jekyll::InlineSVG)

Custom plugins can be added to the _plugins directory and will automatically be added to the project, so that’s really cool! With this plugin, I could inline the logo with in _includes/navbar.html

<div class="logo-image">
  {{ '/assets/images/logo.svg' | inline_svg }}
</div>

I also wanted to change how the breadcrumbs look, so I added this to the navbar as well

<p class="pl-4 hidden md:block text-lg font-bold">
  <span class="text-mulish text-lg font-normal">~/posts/first-steps-with-jekyll/</span>
</p>

Now that looks like a prompt navigation in the navbar - lovely!

Better responsive design

The theme, as lovely as it is, does not have a burger menu for mobile mode, so that had to be added as well.

As before, I added some css to custom.css

@media (min-width: 768px) {
  .mobile-burger {
    display: none;
  }

  .desktop-nav {
    display: flex !important;
    width: auto !important;
  }

  .nav-links-container {
    flex-direction: row;
    padding-top: 0;
  }
}

With this in place, I changed the navbar with the following. First, I added the button itself with the mobile-burger class that hides the button unless in mobile view.

<button id="mobile-menu-toggle" class="mobile-burger" aria-label="Toggle mobile menu">
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
  </svg>
</button>

Then, I added a container that is hidden in mobile view and a Flexbox in Desktop view for the navigation menu itself.

<div id="mobile-menu" class="desktop-nav hidden w-full items-center">
  <div class="nav-links-container flex flex-col pt-4">
    {% for page in site.pages %}
    {% if page.nav %}
    <div class="py-2 md:py-0"><a class="px-4 hover:underline {% if page.url == current_page_url %}font-bold{% endif %}"
            href="{{ page.url | relative_url }}">{{ page.title | default: page.name }}</a></div>
    {% endif%}
    {% endfor %}

    <!-- theme mode switcher from original code -->

  </div>
</div>

The switching ‘on’ of the Flexbox is controlled by the desktop-nav class, and the mobile view with the nav-links-container class.

Bibtex with jekyll-scholar, ruby plugins and more custom css

The final thing I wanted to get working in the site in a nice publications page.
Browsing around, the best example I could find was from Prof. David Moxey (or Dave), my PI during my last postdoc position. I saw he used jekyll-scholar, but try as I might, I could not figure out how to control the bibtex entries, not to mention the badges to show the BibTeX entry and abstracts, features I absolutely loved and wanted on my site!
So, I reached out to Dave, and he was kind enough to give me some pointers to what he has done and granted me access to his site’s repo so I can learn from the code itself.

With this as a reference, I set out three goals for this section:

  1. Highlight my name in the reference;
  2. DOI and URLs should actually be hyperlinks;
  3. The cool badges for the BibTeX and Abstract windows!

The first thing to do is configure jekyll-scholar with the regular parameters to format the bibliography entries as html. Going over the docs, it needed the following added to _config.yml

scholar:
 style: _bibliography/ieee.csl
 locale: en
 source: ./_bibliography
 bibliography: mashy.bib

Going over Dave’s site and reading up some more on jekyll-scholar, I realised that it uses CiteProc-Ruby to parse the BibTeX and returns formatted html, so it is very hard to interact and adapt it after it is formated to html. Hence, the way to modify the entries is by using custom ruby plugins. I shamelessly copied the plugins from Dave and placed them in _plugins. I only needed two of the plugins: the first is a simple one that strips the html <div> from the jekyll-scholar reference which I will discuss later. The other and far more important plugin is highlight_name.rb, which I stripped down to just do what I wanted

module Jekyll
  module HighlightName
    def highlightname(input)
      # Highlight name
      input = input.gsub(/(M\. D\. Green)/, '<strong style="border-bottom: 1px solid #000">\1</strong>')

      # Add DOI link if tagged
      if input.include?("#doi#")
        input = input.gsub(/#doi#(.*?)#edoi#/, '<a href="https://doi.org/\1" target="_blank" rel="noopener noreferrer">\1</a>')
      end

      # Add URL link if found
      if input =~ /Available at:\s+(https?:\/\/[^\s<]+)/
        input = input.gsub(/Available at:\s+(https?:\/\/[^\s<]+)/) do
          url = Regexp.last_match(1)
          %Q{Available at: <a href="#{url}" target="_blank" rel="noopener noreferrer">#{url}</a>}
        end
      end
    end
  end
end

Liquid::Template.register_filter(Jekyll::HighlightName)

This plugin dose 3 things. The first is to gobble the text, find my name and change the text to bold with an underline for better visibility. The second is to look for custom doi tags #doi# and #edoi#, then create a hyperlink from the text between them. For that, I had to modify the BibTeX style to add these tags with

<text variable="DOI" prefix=". #doi#" suffix="#edoi#" />

The last relies on the specific formatting in the ieee format for URLs to match “Available at:”, gobble the url text and turn it into a hyperlink.

That takes care of goals 1 and 2. Goal 3 was rather more fun. The way to adapt the formatting of each reference is using a specific html layout for jekyll-scholar. To do this, I created a new layout in _layouts/bib.html and added it to the config

scholar:
 bibliography_template: bib

Once again, I shameless copied Dave’s code and started adapted it to my needs. A major point of difference is that Dave was using jQuery, whcih I did not want to install in the site, to create the animated boxes with slideToggle, so instead I ended up writing some javascript to replicate that same behaviour., as well as some more custom css to control the animations.

<div id="{{ key }}">
  {{ reference | strip_div }}

  {% if entry.bibtex and entry.type != 'speech' %}
    <span class="badge badge-bibtex" onclick="slideToggle('bibtex-{{ entry.key }}')">BibTeX</span>
  {% endif %}

  {% if entry.abstract %}
    <span class="badge badge-abstract" onclick="slideToggle('abstract-{{ entry.key }}')">Abstract</span>
  {% endif %}

  {% if entry.bibtex and entry.type != 'speech' %}
    <pre id="bibtex-{{ entry.key }}" class="bibtex-block">{{ entry.bibtex }}</pre>
  {% endif %}

  {% if entry.abstract %}
    <div id="abstract-{{ entry.key }}" class="abstract-block">{{ entry.abstract }}</div>
  {% endif %}
</div>

<script>
function slideToggle(id) {
  const el = document.getElementById(id);
  if (!el) return;

  if (el.style.height && el.style.height !== '0px') {
    // Collapse
    el.style.height = el.scrollHeight + 'px'; // set current height to trigger transition
    el.classList.remove('expanded');         // remove gap
    requestAnimationFrame(() => {
      el.style.height = '0px';
    });
  } else {
    // Expand
    el.style.height = '0px';                 // first reset to zero
    el.classList.add('expanded');            // then add gap
    requestAnimationFrame(() => {
      el.style.height = el.scrollHeight + 'px';
    });
  }
}
</script>

This layout is where that first ruby plugin I mentioned earlier to strip the <div> from the html is used

module Jekyll
  module StripDiv
    def strip_div(input)
      input.gsub(/^<div id=\".*?\">(.*?)<\/div>$/m, '\1')
    end
  end
end

Liquid::Template.register_filter(Jekyll::StripDiv)

To make sense of the layout, I should also include the css

.badge {
  display: inline-block;
  padding: 2px 6px;
  color: var(--polar-night-0);
  font-size: 0.75rem;
  font-weight: 600;
  border-radius: 4px;
  text-decoration: none;
  margin-left: 8px;
  cursor: pointer;
}

pre.bibtex-block,
div.abstract-block {
  background: var(--color-gray-200);
  border-left: 4px solid var(--frost-blue);
  padding: 0px;
  margin: 0;
  font-size: 0.9rem;
  overflow-x: auto;
  color: var(--color-gray-900);

  height: 0;
  overflow: hidden;
  transition: height 0.3s ease, margin-top 0.3s ease;
}

pre.bibtex-block.expanded,
div.abstract-block.expanded {
  margin-top: 8px;
}

.badge-bibtex {
  background-color: var(--aurora-green);
}
.badge-bibtex:hover {
  background-color: color-mix(in srgb, var(--aurora-green) 80%, white);
}

.badge-abstract {
  background-color: var(--frost-cyan);
}
.badge-abstract:hover {
  background-color: color-mix(in srgb, var(--frost-cyan) 80%, white);
}

Hopefully, after the explanations in the previous sections, this should be clear enough, and as this blogpost is already far too long, I will leave the details of how this all works out. If anyone is interested, feel free to contact me and I can either provide more details in private or try and write a shorter and more comprehensive dedicated post on this specifically.

Final thoughts

I learned a lot in the week I’ve been working on this website and I’m mostly happy with the result. I found starting from a theme was in fact very limiting and resulted in me applying ‘fixes’ and ‘patches’ over things rather than actually designing them that way from the start. I ended up with some probably over-complicated code. The biggest pain was dealing with Tailwind and Flexbox - I really only gained a vague idea what they are doing as the theme used a pre-built Tailwind styles file that provided css for only some class styles under the hood in a way that was not transparent to me. This really made granular control of some things I wanted to do much more difficult than it had to be.
It is entirely possible that if I built everything from the ground-up, these tools would not have been so difficult to work with.

Saying that, I certainly got a better result within this single week, having no prior web-related development experience at all (not even plain html or css, and certainly nothing with Jekyll, Tailwind or Flexbox), than I would had if I started from scratch. The theme threw me straight into the deep end and I was exposed to a lot more than I would have been otherwise, so I probably also gained quite a lot going with this approach.

So my final conclusion is that as I already spent a full week of my annual leave on this site and it is functioning well enough, I will leave it as it is for now. Next summer, I will use everything I learned this time and design a similar Jekyll-based site from scratch without using any pre-configured themes and hopefully be able to keep things much simpler and leaner.

Until that time, hope you found some things here useful - I certainly did!