Please note:
-
This is not a theme for Hugo. It's a complete website. There are not many variables or easy options to configure. You might need to edit the code directly.
-
This repository contains lots of media files and the total size goes beyond 400 MiB. Please take that into consideration before cloning or downloading the repository as it is.
Blogfolio
Repository of my personal website (portfolio + blog), made in Hugo, hosted on Netlify. Check it out here.
TOC
What is this repository?
After struggling to make my personal portfolio website in Gatsby for months by trying different starters and themes, it was time I either looked for alternative ways or give up on the plan. I finally decided to give Hugo a shot and being a newcomer, I checked out the availability of templates first. I found a few interesting ones and so, decided to start developing. I watched Mike Dane's Hugo tutorial series on YouTube and was intrigued by the fact that Hugo was mostly dependent on pure HTML. So, instead of using a theme, I switched to a custom layout and design. This repository is the end result of a lot of Google searches, YouTube videos, forums posts, etc.
Getting started
If someone wants to get a hold of this code, this section contains some trivial getting started information. It is assumed that Hugo (v0.84.0) is installed and configured on the system.
Some features would also need Node.js
, Python
, FFmpeg
, etc. installed, but it's optional. It's recommended to run npm i
before proceeding to install the dependencies listed in package.json
.
File structure
Here's a vital file structure to help you understand it better. Only the vital files for the functioning of this website are listed, the content files are excluded. Once you understand this organization structure, it would be easy for you to follow the rest of the guide.
./
├── animations/
│ ├── assets.cdr
│ ├── error404.saola
│ ├── home.saola
│ └── offline.saola
├── assets/
│ ├── logic.js
│ └── threadtalk.scss
├── content/
│ ├── blog/
│ │ └── _index.md
│ ├── projects/
│ │ └── _index.md
│ ├── tags/
│ │ └── _index.md
│ ├── 404.md
│ ├── about.md
│ ├── contact.md
│ ├── offline.md
│ └── search.md
├── layouts/
│ ├── _default/
│ │ └── _markdown/
│ │ │ ├── render-image.html
│ │ │ └── render-link.html
│ │ ├── baseof.html
│ │ ├── section.html
│ │ ├── single.html
│ │ ├── taxonomy.html
│ │ └── term.html
│ ├── page/
│ │ ├── 404.html
│ │ ├── contact.html
│ │ ├── offline.html
│ │ └── search.html
│ ├── partials/
│ │ ├── breadcrumbs.html
│ │ ├── header.html
│ │ ├── nav-btn.html
│ │ ├── navigation.html
│ │ ├── pagination.html
│ │ └── toc.html
│ ├── shortcodes/
│ │ ├── audio.html
│ │ ├── gallery.html
│ │ └── video.html
│ ├── index.html
│ ├── index.json
│ ├── index.manifest.json
│ ├── index.sprites.svg
│ ├── index.styles.css
│ ├── index.sw.js
│ ├── robots.txt
│ └── sitemap.xml
├── static/
│ ├── animations/
│ │ ├── error404.js
│ │ ├── home.js
│ │ └── offline.js
│ ├── css/
│ │ ├── glide-v3.4.1.css
│ │ └── uikit-v3.7.2.css
│ ├── images/
│ │ ├── chrome-icon-192.png
│ │ ├── chrome-icon-512.png
│ │ ├── explorer-square-70.png
│ │ ├── explorer-square-150.png
│ │ ├── explorer-square-310.png
│ │ ├── explorer-wide.png
│ │ ├── og.png
│ │ ├── safari-home.png
│ │ ├── safari-pinned.svg
│ │ └── twitter.png
│ ├── js/
│ │ ├── dplayer-v1.26.0.js
│ │ ├── flexsearch-v0.7.2-light.js
│ │ ├── glide-v3.4.1.js
│ │ ├── hls-v1.0.10-light.js
│ │ ├── saola-animate-v3.0.0.js
│ │ ├── turbo-v7.0.0-rc.3.js
│ │ ├── uikit-v3.7.2.js
│ │ └── wavesurfer-v5.2.0.js
│ ├── browserconfig.xml
│ └── favicon.ico
├── config.toml
└── netlify.toml
./
directory
In the root directory, the 2 most important files are config.toml
and netlify.toml
. You probably would not need to edit netlify.toml
. However, in config.toml
, you need to edit the baseURL
. Don't keep it /
as Hugo uses this parameter to generate links in sitemap.xml
. Also, you need to end the URL with a /
to match all other configurations in the code.
./animations/
directory
This folder contains the HTML5 animations I made in Saola Animate v3.0.0. So, unless you want to edit the animations, this folder can be safely deleted.
./assets/
directory
This folder contains the files used by Hugo to process as CSS and JS. It wasn't needed till I had everything as static CSS/JS files. But I had to integrate ThreadTalk.JS which doesn't have a UMD build. To import that, npm
is needed.
./content/
directory
The content
directory is supposed to have all the content files (Markdown, page assets, etc.) that you'd need. You can organise it in sections or create single pages. I have also created the tags
directory to add all my tags to my menu as there's no other way to do it automatically while maintaining its order in the menu.
./layouts/
directory
This is one of the most important directories as it contains all the templates and page creation logic. It consists of:
-
_default
directory: This one contains the default pages, that is the base page consisting of the top-level skeleton of the page and also other templates likesection
,single
,taxonomy
andterm
. It also consists of_markdown
directory which contains the templates to manipulate the rendering of certain Markdown syntax. -
page
directory: This one contains the single pages for the name mentioned in thefrontmatter
of the Markdown files. -
partials
directory: This one contains some re-usable components that I have used in the files in the_default
directory. -
shortcodes
directory: This one contains some re-usable components that are required by the Markdown files. -
index.html
: Home page. -
index.json
: Search index. -
index.manifest.json
: Manifest for PWA. -
index.sprites.svg
: SVG icon sprites. -
index.styles.css
: Custom CSS. -
index.sw.js
: Service Worker. -
robots.txt
: Search Engine rules. -
sitemap.xml
: All Search Engine indexable pages.
./static/
directory
Static folder is directly mapped to the root of the website. All the required CSS and JS libraries are stored in the ./css/
and ./js/
folder. This is because they're pre-minified and I didn't want Hugo to process them. I have directly referenced them in my HTML files. This folder also consists contains images needed by the entire website, etc. The generated files of the animations exist in ./animations/
folder. So, if you're not planning to use the animations, you can delete that folder too.
Features and customization
With my limited skill set, I have tried to add the most essential features for the website to 'just work'. Nothing fancy, but after a lot of trial and error, everything that's set-up seems to work fine. While I'm not a UI/UX designer myself, I have tried to think of best practices according to the users' point of view.
UIkit-based
I have chosen UIkit as a front-end library for this website. It gives a clean, minimalistic and responsive layout almost out of the box. It also has a wide range of components to use. Check it out here.
Light/Dark mode
The website comes with support for light/dark mode out of the box. By default, it's configured to adapt to system settings. If the user's system (and browser) is set to use light mode, the website will load in light mode. The same goes for dark mode. However, the end-user has an option to override the mode settings. When the theme button is clicked, the theme is toggled. It's done using appending a class
to <html>
, either light-theme
or dark-theme
and changing colours using CSS variables
. Once a user manually toggles the mode, the preference is stored in localstorage
. So, on subsequent visits, the manually set theme takes preference. I have set the light mode as the default. This is used when the user's system doesn't report any preferred theme and if the user has not set a preferred theme.
Frontmatter
All available frontmatter variables:
---
title: "string"
date: YYYY-MM-DD
description: "string"
tags: ["string", "string"]
draft: boolean
static: boolean
layout: "string"
menu:
main:
parent: "string"
_build:
render: boolean
---
title
(mandatory): It contains the title of the post. It's shown on the list pages. It's also used in SEO tags, the title of the webpage and shown on the post's page itself.
date
(mandatory): The date of the post. It must be in the format YYYY-MM-DD. It is used to sort posts. The latest ones are shown first.
description
(mandatory): It has the same functionality as the title
, except it won't be shown in the title of the webpage.
tags
(recommended): It contains the list of tags the post is supposed to be categorized in.
draft
(optional): It accepts only boolean values, that is, true or false. When not specifying, it defaults to false. For all pages with the value of this variable = true, the page won't be rendered unless the command-line command has the argument -D
.
static
(optional): It accepts only boolean values. When not specifying, it defaults to false. For all pages with the value of this variable = true, the previous/next navigation links and the comment box won't be rendered.
layout
(optional): It can contain any string value. The value that you provide here should match the name of the file in layouts/page/*.html
, where * is the value of the string. When this parameter is specified, the page will be generated using the contents of the HTML file and not using the default single page template.
menu:
main:
pre: "string"
weight: integer
parent: "string"
(recommended) It's used to configure the positioning of a page in the menu. pre
contains the name of the icon. It should exist in ./layouts/index.sprites.svg
with the prefix mi-
. weight
defines the position of the page in the menu. parent
defines the parent of the menu item.
_build:
render:
(optional): It accepts only boolean values. Not specifying it, defaults to true. When this parameter is specified as false, the page won't be rendered.
Taxonomy
The website is configured to use Tags as its taxonomy. A page is automatically generated for each tag that's added in the frontmatter. A class is automatically generated for each tag with a unique name and it follows the convention .tag-<name>
. To customise the colours, edit ./layouts/index.styles.css
.
Markdown links
Markdown is configured to render 7 types of links:
-
Anchor links (
[Link text](anc:#anchor-on-page)
): Anchor links are configured to scroll smoothly to the destination. -
Download links (
[Link text](dwn:/path-to-file/)
): This prompts to download the file at the link without opening it in browser. -
Relative internal links 1 (
[Link text](rel:/relative-path/)
): Internal links are loaded directly using the internal navigation of the website. -
E-mail links (
[Link text](mail:username@example.com)
):mailto
links open in a new tab (or open the default mail client) and show an icon-indication. -
Phone links: (
[Link text](tel:+<country-code><phone-number>)
):tel
links prompt the browser to open the default Phone application to make a call. -
Relative internal links 2 (
[Link text](noturbo:/relative-path/)
): This will exclude the link from the internal navigation of the website and cause a full-page reload. -
External links: (
[Link text](ext:https://www.domain.tld/page/)
): External links are configured to open in new tab with therel = "nofollow noopener noreferrer"
attribute and also show an icon-indication.
Media
The code is configured to handle image, audio and video files. However, some special treatment is needed for it to work perfectly.
Image
There are three types of images: Cover images, post images and social images.
Cover images are not optional and need to exist at ./content/<section>/<slug>/assets/cover.png
.
Social images for posts are also not optional and need to exist at ./content/<section>/<slug>/assets/og.png
and ./content/<section>/<slug>/assets/twitter.png
. More info below.
Post images on the other hand are totally optional. They must exist inside the ./content/<section>/<slug>/assets/
directory. The standard Markdown image syntax can be used to add those images to the page.
Cover and post images are responsive by default. That means, they will adapt to the screen size to prevent overflows, etc. However, no optimised sizes of the images would be generated. You need to handle the compression, optimization, etc. yourself.
For each of the cover and post image, you'd also have to generate a low-quality placeholder (width ≈ 64px) of the same ratio as the original image. These images should exist at the same location as that of the original images and should be suffixed by -low
. For example, a high-quality cover would be cover.png
and the low quality one would be cover-low.png
.
Even though the examples mention .png
extension, you're free to use .jpg
images too.
Gallery
Alongside the standard images of Markdown, you can also create image galleries/slideshows. It's made possible with the {{< gallery >}}
shortcode. It accepts any number of parameters, however, none of it should be named. The image paths should be relative to the post and the alt text for the images should be separated with a :
. The images used in the gallery also need to have a low-quality placeholder as mentioned above. All images need to be of the same size. It will work with different sized images, but there would be inconsistencies in the overall layout. Here's an example usage of the shortcode:
{{< gallery "assets/img1.png:Alt text 1" "assets/img2.png:Alt text 2" >}}
The gallery is made possible using Glide.js.
Audio
Audio files can be added to Markdown files using the {{< audio >}}
shortcode. It accepts only one parameter src
. Add the source URL of the audio file there. Only .mp3
, .ogg
and .wav
files are supported. For example:
{{< audio src = "assets/audio.mp3" >}}
The audio player is generated using wavesurfer.js. For this to fully work, you'd also need to generate a JSON file and save it along with the audio file with the same name. For example, if the audio file exists at ./content/<section>/<slug>/assets/audio.mp3
, the JSON file must exist at ./content/<section>/<slug>/assets/audio.json
. This JSON needs to be generated manually. To do this, you'd need Python installed and configured on your system.
To generate the JSON, use audiowaveform. Once you have it installed and configured on your system, use the following command in your shell:
audiowaveform -i audio.mp3 -o audio.json --pixels-per-second 20 --bits 8
Once you get a JSON file, create a file called scaleJSON.py
with the following code:
import json
import sys
if len(sys.argv) < 2:
print("Usage: python scale-json.py file.json")
exit()
filename = sys.argv[1]
with open(filename, "r") as f:
file_content = f.read()
json_content = json.loads(file_content)
data = json_content["data"]
digits = 2
max_val = float(max(data))
new_data = []
for x in data:
new_data.append(round(x / max_val, digits))
json_content["data"] = new_data
file_content = json.dumps(json_content, separators=(',', ':'))
with open(filename, "w") as f:
f.write(file_content)
Then, at the location of this file, run the following command:
python scaleJSON.py audio.json
Use this generated JSON in the location mentioned before.
Video
Videos can be inserted in Markdown using {{< video >}}
shortcode. It accepts 4 parameters: src
, poster
, thumbnails
and subtitles
. Only the subtitles
parameter is optional. The video supports only HLS (m3u8)
stream which is specified by the src
parameter. It must have at least 5 qualities: 256px × 144p, 426px × 240p, 640px × 360p, 854px × 480p, 1280px × 720p, 1920px × 1080px and in the same order. Higher qualities can be included too. By default, 1080p quality is selected. poster
and thumbnails
parameters can contain any image file, however, the thumbnails need to be generated in a special way (more on that below). Subtitles only support .vtt
files and only 1 subtitle file per video is accepted. It can be used like:
{{< video src = "assets/video.m3u8" poster = "assets/poster.png" thumbnails = "assets/thumbnails.jpg" subtitles = "assets/subtitles.vtt" >}}
The video player is made possible using DPlayer and hls.js.
The required .m3u8
file can be generated with the following command (needs ffmpeg installed and configured):
ffmpeg ^
-i vid1-2160p.mp4 ^
-sc_threshold 0 ^
-keyint_min 60 ^
-c:v libx264 ^
-c:a aac ^
-r 30 ^
-g 60 ^
-map v:0 -s:0 3840x2160 -maxrate:0 3M -bufsize:0 6M ^
-map v:0 -s:1 2160x1440 -maxrate:1 2.7M -bufsize:1 5.4M ^
-map v:0 -s:2 1920x1080 -maxrate:2 2.4M -bufsize:2 4.8M ^
-map v:0 -s:3 1280x720 -maxrate:3 2.1M -bufsize:3 4.2M ^
-map v:0 -s:4 854x480 -maxrate:4 1.8M -bufsize:4 3.6M ^
-map v:0 -s:5 640x360 -maxrate:5 1.5M -bufsize:5 3M ^
-map v:0 -s:6 426x240 -maxrate:6 1.2M -bufsize:6 2.4M ^
-map v:0 -s:7 256x144 -maxrate:7 0.9M -bufsize:7 1.8M ^
-map a:0 -b:a:0 128k ^
-map a:0 -b:a:1 128k ^
-map a:0 -b:a:2 128k ^
-map a:0 -b:a:3 96k ^
-map a:0 -b:a:4 80k ^
-map a:0 -b:a:5 64k ^
-map a:0 -b:a:6 48k ^
-map a:0 -b:a:7 32k ^
-var_stream_map "v:0,a:0,name:2160p v:1,a:1,name:1440p v:2,a:2,name:1080p v:3,a:3,name:720p v:4,a:4,name:480p v:5,a:5,name:360p v:6,a:6,name:240p v:7,a:7,name:144p" ^
-master_pl_name vid1.m3u8 ^
-f hls ^
-hls_time 2 ^
-hls_playlist_type vod ^
-hls_segment_filename vid1-%v/segment-%03d.ts ^
vid1-%v/index.m3u8
The above command can be used as-is for 4K (2160p) 30FPS videos. For videos with other resolutions, you might have to reduce the number of map
statements. For a different frame rate, change the value of -keyint_min
, -r
and -g
. -r
= FPS. -keyint_min
= -g
= 2 × FPS. Also, kindly note, the above command is beautified for easy editing. When actually using the command (at least on Windows), it should all go in one single line.
To generate the video thumbnails, run the following command (needs Node.js installed and configured): npm install -g dplayer-thumbnails
. Once installed, thumbnails can be generated using the following command dplayer-thumbnails -o ./thumbnails.jpg -q 100 video.mp4
Media optimization
Cover and post images are lazy-loaded but are not optimised by the code. That is, no responsive sizes etc. are generated, the compression and optimization are to be done by the developer.
Since audio and video files are known to be heavy on bandwidth, there are some special features added to handle them:
-
Only 1 instance of a media player can be played on one page. As soon as any other player is played, all others are paused. The players continue to remain paused even when the new player is paused.
-
Media players are paused when any part of them is scrolled out of the viewport, or the tab is changed or the device gets disconnected from the internet. Once any of these conditions are reverted back to the normal state, the paused state persists.
Single Page Application (SPA)
The internal navigation of the website is managed using Turbo. It gives the feel of a SPA by not reloading the entire page to navigate. The body content is replaced with the new body requested using AJAX.
Contact form
There's a contact form set up to work with Netlify forms + AJAX submission. It shows a toast notification for submitting, success and error.
Comments
A comment system has been setup to show comments on all content pages not marked as static
in the frontmatter. This is made possible using ThreadTalk.JS. You'd have to change it with your config in ./assets/logic.js
. You'd have to update the following:
new ThreadTalkJS({
// config
})
There's a lot more config required. So, do check out the docs of the library.
Search
A client-side search has been implemented which should work out of the box without any changes. It is made possible by FlexSearch.js. The search features 'search-as-you-type' and query parameter support. While 'search-as-you-type' is self-explanatory, by query parameter support, I mean, it responds to search queries directly from URL, for example: https://www.domain.tld/search/?q=hello
would search the website for hello
. This helps to also enable the Potential Action in Structured Data.
Table Of Contents
A sticky TOC is generated for all content pages for the <h2-4>
headings. Everything is pre-configured and automatic, including the numbering of headings.
Pagination
There's support for pagination of posts on list pages (except taxonomy page). The pagination shows the following buttons (from left to right): First page, previous page, current page, next page and last page. Only the available buttons are generated, that is when on the second page, a button for the first page won't be generated because previous page = first page.
Off-canvas menu
The website's internal navigation is managed by an off-canvas menu. The generation of the menu is automatic to some extent, that is, all the posts are automatically added to the menu. However, any taxonomy or single page should be added/removed manually. The icon for each should also be added to ./layouts/index.sprites.svg
.
PWA
The website is configured to be installable as a Progressive Web Application. You'd have to customise the website name, etc. The title from ./config.toml
is used. Each section also generates a shortcut, icons for which need to be generated manually.
Offline support
By default, the service worker is caching all files needed by the website from the ./static/
directory, along with all the custom CSS and JS that's generated. Additionally, it's also caching the 404 and the offline page. The service worker is configured to show the home page when offline and any other page (except 404) would show the offline page. If any of the content organization is changed, make sure to edit the service worker at ./layouts/index.sw.js
. The service worker also responds to all the fetch events to all the GET
requests, while the POST
requests are ignored. So, any cached file is later served by the service worker. Moreover, it's easy to update an already installed service worker is users' browsers by just updating the service worker version number.
Users are also shown a toast notification whenever they get disconnected from the internet. It can be dismissed by clicking on it, but otherwise, it stays permanently. As soon as the internet is back, another notification is fired to let them know. Any audio/video player on the page or contact form is disabled whenever the user goes offline and is enabled whenever they're back online.
SEO
The website has been configured with some of the best SEO practices.
<meta>
tags
All the <meta>
tags are set up in ./layouts/_default/baseof.html
. The title
and the description
of the pages are conditionally generated according to their type. Paginated pages also get a bit of a different treatment. The title from the ./config.toml
is automatically appended to the page's title. The keywords are also added to the same file. They're the same on each page. Other meta
tags like the viewport tag, etc. are also included.
Structured data
The website also makes use of JSON-LD structured data. They are also conditionally generated like the meta
tags. The Potential Action is enabled for all of the pages. For eligible pages, Breadcrumbs are also enabled.
Sitemap
The sitemap is set up to add the home page and all content pages, list pages, taxonomy pages and paginated pages. However, any single page should be added/removed manually.
404 and Offline pages are blocked by the noindex
tag.
Social sharing
The most common social media meta
tags, OG and Twitter (Card) ones are already included. For all content pages, there must exist a ./contents/<section>/<slug>/assets/og.png
and ./contents/<section>/<slug>/assets/twitter.png
of size 1200px × 638px and 1200px × 600px respectively. For all other pages, the images from ./static/images/
directory are used.
Using this code
While I've manually run a lost of tests to check every aspect of this website, there can always be scenarios in which I've missed something. Moreover, I don't have any Apple device to test Safari browsers. Sometimes, some external libraries break certain stuff. Basically, while there are no known issues currently, the same cannot be said for the future.
Also, my skillset in programming is fairly limited. Thus, there might be instances in which you might find a lot of unoptimized, sub-optimal pieces of code. In such cases, kindly start a new discussion.
Since this code is MIT licensed, you're free to use it, modify it or redistribute it as you like, with or without credits.
Support
I'd be happy to help anyone regarding anything about this website. In case of a how-to, suggestion, security concern or a feature request, kindly open an issue. I usually respond in a few hours. However, I cannot make any promises for feature requests as I said, my skills in programming are fairly limited. However, kindly note, I speak English, Hindi and Marathi. So, if you write in any other language, I might not be able to respond.
However, please make sure to ask for help related to issues only with my part of the code as I can't fix issues caused by other libraries. I do keep libraries updated to the latest versions as fast as possible, so such problems should be less, but unavoidable.
License
MIT License
Copyright (c) 2021 Hrishikesh Kokate
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.