VX Development Blog

Delivering Content With Stylish Pelican

Overview

This post is a sequel to The Great CMS Escape. As promised, I’m going to dive deep into the steps we took to customize our Pelican theme and show you just how simple it can (and should) be.

Style Your Pelican

Pelican ships with a default theme: simple. And it is very simple indeed, as we believe everything in our lives—especially our web stack—should be. This brings us to a friendly disclaimer:

Anyway... the simple theme is so simple that we actually struggled to remove anything substantial from the provided HTML templates. Before we show you the minor changes we made, let’s quickly look at the file structure of the simple theme.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
templates.orig/
  ├── archives.html
  ├── article.html
  ├── author.html
  ├── authors.html
  ├── base.html
  ├── categories.html
  ├── category.html
  ├── index.html
  ├── page.html
  ├── pagination.html
  ├── period_archives.html
  ├── tag.html
  ├── tags.html
  └── translations.html

Listing 1. Simple theme file structure

And here's the overview of differed files of our theme against simple one.

1
2
3
Files templates.orig/article.html and templates.vx/article.html differ
Files templates.orig/base.html and templates.vx/base.html differ
Files templates.orig/index.html and templates.vx/index.html differ

Listing 2. Difference between original an VX theme

As you can see we have changed only three files. Let's go file by file.

Changes In Details

In article.html we just moved a author block a couple of lines upper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
43,49d42
<       {% if article.authors %}
<         <address>
<           By {% for author in article.authors %}
<             <a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
<           {% endfor %}
<         </address>
<       {% endif %}
61a55,61
>       {% endif %}
>       {% if article.authors %}
>         <address>
>           By {% for author in article.authors %}
>             <a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
>           {% endfor %}
>         </address>

Listing 3. Changes in article.html file.

In index.html we added more information under each article:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
13a14,26
>   {% if article.category %}
>     <p>
>       Category: <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>
>     </p>
>   {% endif %}
>   {% if article.tags %}
>     <p>
>       Tags:
>       {% for tag in article.tags %}
>         <a href="{{ SITEURL }}/{{ tag.url }}">{{ tag }}</a>
>       {% endfor %}
>     </p>
>   {% endif %}

Listing 3. Changes in index.html file.

And finally in base.html we have added a logo image to the header stripe and included a <script> tag to load our minimal main.js. We won't bore you by pasting the entire diff here. Instead, we have published our customized theme to the vx-theme repository so you can freely check it out, inspect the changes, and use it however you wish.

One specific feature we built into base.html that we think will be incredibly useful for our readers is the light/dark switcher. Crucially, we tried to keep it as simple as possible and completely avoid using JavaScript. We almost succeeded! The only JS-dependent part of the light/dark switcher is the memorization part: saving the user's preference in local storage and restoring it when they return to the site.

To show how minimal this implementation is, here are the HTML, CSS, and JS code snippets we used for it.

1
2
3
4
5
6
<theme-picker aria-label="Theme picker" role="checkbox">
  <label>
      <input type="checkbox" id="theme-switch">
      <span class="slider"></span>
  </label>
</theme-picker>

Listing 4. Theme picker HTML "component" structure

The addition of the theme-picker custom element is mostly for fun and semantic clarity — it’s not like we're developing full-feature JS-native Web Components here (though we are very fond of them, and a future series of blog posts will definitely be dedicated to them!).

Using a custom HTML element simplifies the structural understanding of the HTML and helps us with the encapsulation of the corresponding CSS rules.

In our case, the control is designed to pick between the light and dark themes. But deep down (and we mean two indents down in the code), you can see we simply rely on a native <input> element with the type checkbox. To achieve a cool-looking switcher, we hide the actual checkbox and instead style a slider <span> tag (see Listing 4).

Therefore, the entire functionality of the light/dark switch—the visual change itself—is purely hidden within the CSS rules listed below (Listing 5).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
theme-picker {
  display: flex;
  padding-left: 20px;

  label {
    position: relative;
    display: inline-block;
    width: 48px;
    height: 26px;
  }

  .slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    -webkit-transition: .4s;
    transition: .4s;
    border-radius: 1rem;

    &:before {
      position: absolute;
      content: "";
      height: 22px;
      width: 22px;
      left: 2px;
      bottom: 2px;
      background-color: var(--background);
      -webkit-transition: .4s;
      transition: .4s;
      border-radius: 50%;
    }
  }

  input {
    /* To allow screen reader to still access these. */
    opacity: 0;
    position: absolute;
    pointer-events: none;

    &:checked + .slider:before {
      -webkit-transform: translateX(22px);
      -ms-transform: translateX(22px);
      transform: translateX(22px);
    }
  }

  @media (width <= 550px) {
    padding-left: 0;
  }
}

Listing 5. Theme picker CSS

We've separated the switcher's look CSS rules from its functionality CSS rules across separate files. Below are the rules delivered by the pallete.css file.

Aside from basic link colors, all other styles here utilize the CSS native `light-dark()` function. This function is absolutely amazing and simplifies color mode management enormously. You can read more about it on MDN [1].

Basically, the function returns either its first or second argument depending on the user's current color-scheme value. That value, in turn, is manipulated directly by our hidden checkbox state. (Aha! Now you see why we need that checkbox!).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:root {
    color-scheme: light dark;

    &:has(#theme-switch:checked) {
      color-scheme: light;
    }
    &:has(#theme-switch:not(:checked)) {
      color-scheme: dark;
    }

    --background: light-dark(#f0f0f0, #1c1b22);

    --pre-background: light-dark(#fff, #000);

    --shadow-1: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
    --shadow-2: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.08));

    --link-color: #32c0c4;
    --link-color-hover: #329a9a;
    --link-color-active: #0b6666;

    --text: light-dark(#1c1b22, #f0f0f0);
}

Listing 6. Pallete CSS rules

As I have promised we have also pretty straightforward and simple JS code which is trying to save and load user preferred color-scheme value. Perfect job for JS right?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
document.addEventListener("DOMContentLoaded", (event) => {
  const themeSwitch = document.getElementById('theme-switch');

  themeSwitch.addEventListener("change", (e) => {
    localStorage.setItem('theme', e.target.checked ? 'light' : 'dark');
  }, false);

  const theme = localStorage.getItem('theme');

  themeSwitch.checked = theme == undefined ? window.matchMedia('(prefers-color-scheme: light)').matches : theme === 'light';
});

Listing. 7. Source code for first attempt

Sure this code snippets are not invented by ourselves, you can check these two resources as a references: MDN [2] | Lyra: You no longer need JavaScript.

Conclusion

We successfully customized the simple theme and achieved the look we had in mind by applying only a handful of custom CSS rules—without substantially altering the original theme structure.

You might argue that our ideal theme is too simple, but that’s precisely what we believe a dedicated development blog should look like. It's about the content, not the clutter.

If you need something more complex, there are plenty of way more sophisticated themes in the official theme repository that you can check out and use as your base template.

The takeaway here is simple: if you have a straightforward layout in mind and are committed to keeping things minimal, you can most likely create your custom Pelican theme overnight.