81 Commits

Author SHA1 Message Date
renovate[bot]
baf2bce206 Update dependency stylelint-prettier to v5.0.3 2025-08-05 01:35:32 +00:00
Dave Gallant
223b7a9113 Delete themes/custom-theme/layouts/robots.txt 2025-08-04 19:25:34 -04:00
Dave Gallant
c200552255 Update truenas post about using magicdns 2025-06-22 21:39:04 -04:00
Dave Gallant
8a27d7284f Test using obsidian 2025-04-26 18:06:19 -04:00
Dave Gallant
9be886267d Add link to blog in front page 2025-04-21 22:08:54 -04:00
Dave Gallant
8f1892115f Add 'Using a Realtek NIC with OPNsense' 2025-04-21 21:47:03 -04:00
Dave Gallant
d4971f0138 Add content/blog/replicating-truenas-datasets-to-sftpgo-over-tailscale/index.md 2025-04-17 23:31:09 -04:00
renovate[bot]
00875c94e4 Update dependency postcss-custom-media to v10.0.8 (#45)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 22:56:08 -05:00
renovate[bot]
43e110ee5b Update dependency postcss to v8.5.2 (#50)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-16 23:21:15 -05:00
Dave Gallant
dff6b8921a Add opting-out-of-haveibeenpwned 2025-02-16 23:14:56 -05:00
Dave Gallant
8c3d1bdd5b Update email 2025-02-16 11:04:05 -05:00
Dave Gallant
1536ce8ed1 Update shell.nix 2025-02-05 20:32:08 -05:00
Dave Gallant
af833d173c Update socks article now that dns over socks works with dbeaver. 2025-01-16 19:34:52 -05:00
Dave Gallant
3272b67283 Replace google analytics with umami analytics 2025-01-12 10:27:21 -05:00
Dave Gallant
4275a6adc4 Add note about the aws-efs-csi-driver 2024-07-27 07:43:08 -04:00
Dave Gallant
1ff146f4aa Update youtube post to use Redirector rather than requestly 2024-07-20 08:14:28 -04:00
Dave Gallant
54eb3fc904 Add Cal.com to the list of connection options 2024-07-12 09:02:25 -04:00
Dave Gallant
842a7ef80a Update renovate.json 2024-06-28 12:02:30 -04:00
Dave Gallant
a2d6ebac07 Remove environment variable in ebs post 2024-04-08 08:57:33 -04:00
Dave Gallant
c2fe588fe8 Adjust theme 2024-04-07 22:30:13 -04:00
Dave Gallant
3bf6014537 Condense post list 2024-04-07 22:25:50 -04:00
Dave Gallant
8d898eb69a Tweak theme 2024-04-07 21:55:42 -04:00
Dave Gallant
2475d2d67e Update theme in gitea post 2024-04-07 18:54:55 -04:00
Dave Gallant
9367f85f5f Tweak contrast of theme a bit 2024-04-07 18:52:07 -04:00
Dave Gallant
d839024d95 Fix grammar for ebs csi driver post 2024-04-07 18:39:29 -04:00
Dave Gallant
f9ee17986d Add additional breaks before comments 2024-04-07 17:33:55 -04:00
Dave Gallant
953cf64989 Increase brightness of fg1 2024-04-07 16:50:01 -04:00
Dave Gallant
c59415d6b3 Add new post aws-ebs-csi-driver-terraform 2024-04-07 16:36:36 -04:00
Dave Gallant
f8d313309a Force dark theme 2024-04-07 15:48:13 -04:00
Dave Gallant
190e0b2835 Fix wording in aks blogpost 2024-04-06 23:25:57 -04:00
Dave Gallant
8b4902e3e2 Update coffee button colour 2024-04-06 23:15:40 -04:00
Dave Gallant
824f145e2c Update gitea themes 2024-04-06 23:12:01 -04:00
Dave Gallant
7ea7031521 Update prism themes 2024-04-06 23:08:23 -04:00
Dave Gallant
391d164ae9 Disable theme toggle 2024-04-06 23:08:23 -04:00
Dave Gallant
1aeb6d20b6 Add tokyonight-inspired theme 2024-04-06 23:08:23 -04:00
renovate[bot]
0ee904d34f Update dependency postcss-custom-media to v10.0.4 (#33)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 13:32:33 -04:00
renovate[bot]
a24bf002ab Update dependency postcss to v8.4.38 (#32)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 13:26:31 -04:00
Dave Gallant
9da895cd8a Revert "Update dependency flexsearch to v0.7.43 (#28)" (#34)
This reverts commit dc75012739.
2024-03-31 13:26:19 -04:00
renovate[bot]
dc75012739 Update dependency flexsearch to v0.7.43 (#28)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 13:22:53 -04:00
renovate[bot]
adf37e736e Update dependency eslint-plugin-prettier to v5.1.3 (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 13:22:39 -04:00
Dave Gallant
f76666c70d Set paginate to 20 2024-03-31 12:48:05 -04:00
renovate[bot]
a49699a5d6 Update dependency lint-staged to v15.2.2 (#30)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 12:43:13 -04:00
Dave Gallant
5d87003de0 Simplify posts list 2024-03-31 12:40:33 -04:00
Dave Gallant
37a27723f9 Fix shell.nix pin to 23.11 2024-03-31 12:29:46 -04:00
Dave Gallant
7202c2c37e Update publish.yml 2024-03-31 12:17:06 -04:00
renovate[bot]
ac454d75dc Add renovate.json (#26)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-31 11:46:54 -04:00
Dave Gallant
2902f5735b Migrate from Makefile to justfile 2024-03-18 21:52:42 -04:00
Dave Gallant
54de6a38c8 Add gitea.snake-cloud.ts.net 2024-03-18 20:37:46 -04:00
Dave Gallant
e89222a64e Fix GA 2024-02-24 11:53:51 -05:00
Dave Gallant
caac52cc74 Add link to nix config. 2024-02-24 10:27:45 -05:00
Dave Gallant
f7af260779 Add shell.nix with hugo 2024-02-20 07:22:54 -05:00
Dave Gallant
eb1136bf90 Update gitea blog post to use Tailscale Serve and Funnel 2024-02-10 10:55:20 -05:00
Dave Gallant
379e9ce5ff Fix postcss 2024-02-10 09:44:18 -05:00
Dave Gallant
ba0f6170af Add bytespider to robots.txt 2024-02-03 09:11:05 -05:00
Dave Gallant
5880c0d9da Add miniflux to homelab post 2024-01-27 14:57:54 -05:00
Dave Gallant
8f0cec9739 Fix utterances theme check 2024-01-22 19:36:25 -05:00
Dave Gallant
a56eeeb528 Update old posts 2024-01-22 19:09:46 -05:00
Dave Gallant
b913e9b4a3 Add theme toggle back and default to 'auto' 2024-01-13 15:27:44 -05:00
Dave Gallant
24ad44a9ec Add static avatars 2024-01-11 13:02:22 -05:00
Dave Gallant
f4806ab344 Fix website description 2024-01-11 12:27:50 -05:00
Dave Gallant
fad71f3265 Redesign home page 2024-01-08 06:16:54 -05:00
Dave Gallant
6a405662e9 Fix favicon and menu css 2024-01-07 21:20:18 -05:00
Dave Gallant
c9bd65f2b9 Update publish.yml 2024-01-07 19:25:50 -05:00
Dave Gallant
d90d71b9c0 Add dark visitors to robots.txt 2024-01-07 19:06:09 -05:00
Dave Gallant
4b1cccc156 Fix typo 2024-01-07 17:31:11 -05:00
Dave Gallant
c12f2173db Switch prism and comments theme on changes 2024-01-07 17:17:46 -05:00
Dave Gallant
29a621a4c3 Remove theme toggle and cleanup menu 2024-01-07 15:12:35 -05:00
Dave Gallant
fdc27aad3a Cleanup 2024-01-07 14:50:58 -05:00
Dave Gallant
bb5e1f688b Expand about section 2024-01-07 12:07:45 -05:00
Dave Gallant
e2883bc1e8 Remove image optimization to webp and fix gitea images 2024-01-07 11:04:42 -05:00
Dave Gallant
413b06bb8a Fix utterances auto theme 2024-01-07 10:26:45 -05:00
Dave Gallant
398cde5481 Fix credits 2024-01-07 10:22:29 -05:00
Dave Gallant
5e3b2d2dce Only save theme when toggle is done 2024-01-07 10:20:05 -05:00
Dave Gallant
51bd556992 Rename theme path 2024-01-07 10:19:25 -05:00
Dave Gallant
b9926ba634 Update menu 2024-01-07 09:55:06 -05:00
Dave Gallant
c7a2b2f4d2 Get rid of side panel to minimize noise in layout 2024-01-07 09:45:36 -05:00
Dave Gallant
604f3aaa4c Add themes in-tree 2024-01-07 08:49:07 -05:00
Dave Gallant
a5a09666f5 Add author to posts 2024-01-06 11:35:27 -05:00
Dave Gallant
21f72ae736 Bulletize credits 2024-01-06 11:31:31 -05:00
Dave Gallant
37c803f5c7 Add git.davegallant.ca 2024-01-05 00:29:28 -05:00
Dave Gallant
2cd866adfa Give credit to utterances. 2024-01-04 17:49:37 -05:00
179 changed files with 16008 additions and 311 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use_nix

View File

@@ -31,7 +31,7 @@ jobs:
- name: Setup Hugo - name: Setup Hugo
uses: peaceiris/actions-hugo@v2 uses: peaceiris/actions-hugo@v2
with: with:
hugo-version: "0.121.1" hugo-version: "0.120.3"
extended: true extended: true
- run: npm ci - run: npm ci
@@ -41,3 +41,4 @@ jobs:
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: generated

2
.gitignore vendored
View File

@@ -119,3 +119,5 @@ dist
# End of https://www.toptal.com/developers/gitignore/api/hugo,node # End of https://www.toptal.com/developers/gitignore/api/hugo,node
public/ public/
.obsidian/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Dave Gallant
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.

View File

@@ -1,31 +0,0 @@
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
ifeq ($(origin .RECIPEPREFIX), undefined)
$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif
.RECIPEPREFIX = >
build: clean
> npm ci
> hugo --minify
clean:
> rm -rf public/
## server: run server locally on port 1313 and open in a browser
server:
> hugo server --buildDrafts
## help: Print this help message
help:
> @echo
> @echo "Usage:"
> @echo
> @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | sort
> @echo
.PHONY: help

View File

@@ -4,6 +4,7 @@ date: "{{ .Date }}"
draft: true draft: true
comments: true comments: true
toc: false toc: false
author: "Dave Gallant"
--- ---
<!--more--> <!--more-->

View File

@@ -1,15 +1,18 @@
baseURL: / baseURL: /
googleAnalytics: G-V8WJDERTX9
copyright: Dave Gallant copyright: Dave Gallant
title: davegallant.ca title: davegallant.ca
enableGitInfo: true enableGitInfo: true
enableRobotsTXT: true enableRobotsTXT: true
noJSConfigInAssets: true paginate: 20
build:
noJSConfigInAssets: true
writeStats: true
params: params:
author: Dave Gallant author: Dave Gallant
subtitle: blog subtitle:
favicon: https://davegallant.ca/favicon.ico description: "Dave Gallant is a software developer and a computer enthusiast."
logo: logo:
text: davegallant.ca text: davegallant.ca
@@ -23,7 +26,7 @@ params:
issueTerm: "pathname" issueTerm: "pathname"
github: github:
username: davegallant username: davegallant
repository: davegallant.github.io repository: site
prism: prism:
languages: languages:
@@ -48,11 +51,11 @@ params:
menu: menu:
main: main:
- name: About - name: Home
url: /about url: /
weight: 1 weight: 1
- name: RSS - name: Blog
url: /index.xml url: /blog
weight: 2 weight: 2
permalinks: permalinks:
@@ -65,15 +68,7 @@ markup:
module: module:
imports: imports:
- path: github.com/davegallant/hugo-theme-gruvbox - path: custom-theme
- path: github.com/schnerring/hugo-mod-json-resume
mounts:
- source: data
target: data
- source: layouts
target: layouts
- source: assets/css/json-resume.css
target: assets/css/critical/44-json-resume.css
mounts: mounts:
- source: node_modules/simple-icons/icons - source: node_modules/simple-icons/icons
target: assets/simple-icons target: assets/simple-icons

View File

@@ -1,5 +1,27 @@
--- # Hello
title: Welcome
---
This is a space where I share notes about problems and solutions I've been exploring. Please do not hesitate to reach out by email, social media, or by commenting on the posts below. Continuous improvement is what motivates me to keep learning. 👋 I'm a software tinkerer with a passion for infra, security, privacy, and self-hosting.
This is a space where I document my learnings and share them with others. I hope you find something useful here. Continuous improvement is what motivates me to keep learning.
I choose to host this site, alongside other tools, rather than relying exclusively on larger platforms because I believe in the open web. Interoperability is often not a consideration for popular platforms today and I find that concerning.
My blog can be found [here](./blog).
## Connect
If you would like to connect with me:
- [Email](mailto:davegallant@proton.me)
- [LinkedIn](https://www.linkedin.com/in/dave-gallant)
- [Cal.com](https://cal.com/davegallant)
- [Mastodon](https://mastodon.social/@davegallant)
- [GitHub](https://github.com/davegallant)
- [RSS Feed](https://davegallant.ca/index.xml)
- [gitea.snake-cloud.ts.net](https://gitea.snake-cloud.ts.net/explore/repos)
## Credits
- The site is generated with [hugo](https://gohugo.io/)
- The theme is a modified version of [hugo-theme-gruvbox](https://github.com/schnerring/hugo-theme-gruvbox)
- The comments system is powered by [utterances](https://github.com/utterance/utterances)

View File

@@ -1,16 +0,0 @@
---
title: "About"
draft: false
---
Connect with me using any the following methods:
- Email: <mailto:me@davegallant.ca>
- LinkedIn: <https://www.linkedin.com/in/dave-gallant>
- Mastodon: <https://mastodon.social/@davegallant>
- GitHub: <https://github.com/davegallant>
- RSS Feed: <https://davegallant.ca/index.xml>
## Credits
This website is generated using [hugo](https://gohugo.io/) and the theme is a modified version of [hugo-theme-gruvbox](https://github.com/schnerring/hugo-theme-gruvbox). My general rule of thumb is to look for a gruvbox theme for most of the software I use since it delivers such a pleasant experience for the eyes.

5
content/blog/_index.md Normal file
View File

@@ -0,0 +1,5 @@
---
title: Blog
---
Subscribe via [RSS](https://davegallant.ca/index.xml).

View File

@@ -0,0 +1,95 @@
---
title: "Amazon EBS CSI driver with terraform"
date: "2024-04-07T15:20:23-04:00"
draft: false
comments: true
toc: false
author: "Dave Gallant"
tags: ['aws', 'eks', 'ebs', 'aws-ebs-csi-driver', 'oidc', 'efs', 'aws-efs-csi-driver']
---
I recently configured the Amazon EBS CSI driver and found the setup with terraform to be more effort than expected. I wanted to avoid third-party modules and keep it as simple as possible, while remaining least privilege.
> UPDATE: This approach can also be used for the aws-efs-csi-driver
<!--more-->
The [Amazon EBS CSI driver docs](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html) mention that the following are needed:
- an existing EKS cluster
- IAM role (that allows communication to the EC2 API)
- EKS add-on (aws-ebs-csi-driver)
- OIDC provider
This sounded simple enough but I was unable to find a "grab-and-go" terraform example that followed the recommendations in the docs. I saw some suggestions about attaching an `AmazonEBSCSIDriverPolicy` policy to the node groups but did not think this was the best idea since this would allow many pods to potentially have access to the EC2 API.
After a few minutes of LLM prompting, I was unimpressed with the results. I began to piece together the config myself, and after some trial and error, this is the terraform that I came up with:
```hcl
# TLS needed for the thumbprint
provider "tls" {}
data "tls_certificate" "oidc" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
# EKS addon
resource "aws_eks_addon" "ebs_csi_driver" {
cluster_name = aws_eks_cluster.main.name
addon_name = "aws-ebs-csi-driver"
addon_version = "v1.29.1-eksbuild.1"
service_account_role_arn = aws_iam_role.ebs_csi_driver.arn
}
# AWS Identity and Access Management (IAM) OpenID Connect (OIDC) provider
resource "aws_iam_openid_connect_provider" "eks" {
url = aws_eks_cluster.main.identity.0.oidc.0.issuer
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.oidc.certificates[0].sha1_fingerprint]
}
# IAM
resource "aws_iam_role" "ebs_csi_driver" {
name = "ebs-csi-driver"
assume_role_policy = data.aws_iam_policy_document.ebs_csi_driver_assume_role.json
}
data "aws_iam_policy_document" "ebs_csi_driver_assume_role" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.eks.arn]
}
actions = [
"sts:AssumeRoleWithWebIdentity",
]
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.eks.url}:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.eks.url}:sub"
values = ["system:serviceaccount:kube-system:ebs-csi-controller-sa"]
}
}
}
resource "aws_iam_role_policy_attachment" "AmazonEBSCSIDriverPolicy" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
role = aws_iam_role.ebs_csi_driver.name
}
```
The above configuration follows the docs, binding an IAM role to the service account `kube-system/ebs-csi-controller-sa` using an OpenID connect provider.
After applying the changes above, I deployed [the sample application](https://docs.aws.amazon.com/eks/latest/userguide/ebs-sample-app.html) and noticed that the persistent volume claims were bound to EBS volumes.

View File

@@ -5,6 +5,7 @@ lastmod: 2021-09-17T12:48:33-04:00
draft: false draft: false
comments: true comments: true
tags: ['aws', 'python', 'security', 'aws-vault'] tags: ['aws', 'python', 'security', 'aws-vault']
author: "Dave Gallant"
--- ---
Rotating credentials is a security best practice. This morning, I read a question about automatically rotating AWS Access Keys without having to go through the hassle of navigating the AWS console. There are some existing solutions already, but I decided to write a [script](https://gist.github.com/davegallant/2c042686a78684a657fe99e20fa7a924#file-aws_access_key_rotator-py) since it was incredibly simple. The script could be packed up as a systemd/launchd service to continually rotate access keys in the background. Rotating credentials is a security best practice. This morning, I read a question about automatically rotating AWS Access Keys without having to go through the hassle of navigating the AWS console. There are some existing solutions already, but I decided to write a [script](https://gist.github.com/davegallant/2c042686a78684a657fe99e20fa7a924#file-aws_access_key_rotator-py) since it was incredibly simple. The script could be packed up as a systemd/launchd service to continually rotate access keys in the background.

View File

@@ -5,6 +5,7 @@ lastmod: 2022-03-13T18:49:10-04:00
comments: true comments: true
draft: false draft: false
tags: ["synology", "gmail", "backup", "ransomware"] tags: ["synology", "gmail", "backup", "ransomware"]
author: "Dave Gallant"
--- ---
I've used gmail since the beta launched touting a whopping 1GB of storage. I thought this was a massive leap in email technology at the time. I was lucky enough to get an invite fairly quickly. Not suprisingly, I have many years of emails, attachments, and photos. I certainly do not want to lose the content of many of these emails. Despite the redundancy of the data that Google secures, I still feel better retaining a copy of this data on my own physical machines. I've used gmail since the beta launched touting a whopping 1GB of storage. I thought this was a massive leap in email technology at the time. I was lucky enough to get an invite fairly quickly. Not suprisingly, I have many years of emails, attachments, and photos. I certainly do not want to lose the content of many of these emails. Despite the redundancy of the data that Google secures, I still feel better retaining a copy of this data on my own physical machines.

View File

@@ -0,0 +1,43 @@
---
title: "Opting out of haveibeenpwned"
date: "2025-02-16T21:15:07-05:00"
draft: false
comments: true
toc: false
author: "Dave Gallant"
tags:
[
"breach",
"darkweb",
"haveibeenpwned",
"hibp",
"passwords",
"privacy",
]
author: "Dave Gallant"
---
Data breaches are a concern for anyone trying to live a life of relative privacy. Last month, PowerSchool informed its customers that [hackers stole data of 62 million students](https://www.bleepingcomputer.com/news/security/powerschool-hacker-claims-they-stole-data-of-62-million-students/). This may not have impacted you, but unless you have been practicing [Extreme Privacy](https://inteltechniques.com/book7.html) techniques for decades, you likely have been impacted by a data breach in the past.
<!--more-->
## Understanding Data Breaches
Data breaches occur when unauthorized individuals gain access to sensitive information (names, addresses, emails, phone numbers among other details). If the breach is substantial enough, the raw data is likely to make it into the hands of data brokers that will collect, aggregate, and sell the information on the [dark web](https://en.wikipedia.org/wiki/Dark_web).
## Check if you have been impacted
There are a number of services that can be used to check if you have been impacted by a data breach, including [Mozilla monitor](https://monitor.mozilla.org), [Google Dark web report](https://myactivity.google.com/dark-web-report/dashboard), and [haveibeenpwned.com](https://haveibeenpwned.com/). Some password managers offer features that compare your credentials against known breaches. These services can also be configured to send you notifications when a breach occurs. It is a good idea to become aware of these breaches as soon as you can, so that you can protect yourself from malicious behaviour such as phishing.
If you have been an email or phone number for any length of time, there is a high probability that some of your data has been exposed. You can easily check by querying [haveibeenpwned.com](https://haveibeenpwned.com/). Many of the tools that offer breach detection, query the haveibeenpwned database. Although I believe this is service is a public good, it also opens the door for anyone who may be looking to gain more information about your present and past usages of various websites and services.
## Opting out
If you have an identity that you'd like to protect, I'd suggest [opting out of public searchability](https://haveibeenpwned.com/OptOut/). This of course does not undo the data breach that happened, but does it make it more challenging for someone to quickly search for an impacted email address. Even after opting out, you can still [subscribe to breach notifications](https://haveibeenpwned.com/NotifyMe), as long as you can validate that you have access to the email in question.
There are other websites that offer similar style lookups, but many of them are either paywalled or require account registration.
## Email aliases
A more proactive method of reducing the likelihood of future exposures is to use an email aliasing service such as [Firefox Relay](https://relay.firefox.com), [DuckDuckGo Email Protection](https://duckduckgo.com/email/), or if you use Proton Mail, [hide-my-email aliases](https://proton.me/support/addresses-and-aliases#hide). This will allow you sign up for services using an alias instead of revealing your email address. The service then forwards all emails to your real address that you configure when setting up the alias.

View File

@@ -5,6 +5,7 @@ lastmod: 2021-10-11T10:43:35-04:00
draft: false draft: false
comments: true comments: true
tags: ["docker", "podman", "containers"] tags: ["docker", "podman", "containers"]
author: "Dave Gallant"
--- ---
There are a number of reasons why you might want to replace docker, especially on macOS. The following feature bundled in Docker Desktop might have motivated you enough to consider replacing docker: There are a number of reasons why you might want to replace docker, especially on macOS. The following feature bundled in Docker Desktop might have motivated you enough to consider replacing docker:

View File

@@ -0,0 +1,30 @@
---
title: "Replicating TrueNAS datasets to sftpgo over Tailscale"
date: "2025-04-17T22:03:33-04:00"
draft: false
comments: true
toc: false
author: "Dave Gallant"
tags:
[
"tailscale",
"truenas",
"sftpgo",
]
---
I've recently spun up an instance of TrueNAS SCALE after salvaging a couple hard drives from a past computer build and decided I could use additional network storage for various backups such as Proxmox VMs and home directory backups.
<!--more-->
The only app I've needed to install has been Tailscale which has enabled me to access the TrueNAS Web UI from anywhere. I've setup a few datasets and NFS shares to store various backups and the rest of the periodic backups have routinely been working without a hitch. Since my homelab is becoming more of a vital piece of infrastructure for my daily needs, I wanted to ensure that these datasets had [Cloud Sync Tasks](https://www.truenas.com/docs/scale/scaletutorials/dataprotection/cloudsynctasks/) setup for offsite backups. These encrypted backups are mostly being stored in places such as Google Drive and other blob storage providers.
More recently, to reduce cloud costs, I've setup some a small node at another physical location and installed both Tailscale and [sftpgo](https://github.com/drakkan/sftpgo) on it to facilitate offsite backups. After setting up the infrastructure and adding a Cloud Sync Task in TrueNAS SCALE to replicate these backups offsite to sftpgo, I noticed that Tailscale's Magic DNS was not working, nor was the Tailscale IPv4 address.
After reading the [Tailscale docs](https://tailscale.com/kb/1483/truenas#route-non-tailnet-traffic-through-truenas) , it became clear that the **Userspace** box had to be unchecked in the Tailscale app settings. This is because the Tailscale app is running within a docker container on the TrueNAS SCALE VM. After unchecking the **Userspace** box, I was able to verify that the Backup Credentials created for sftpgo worked when specifying the host as a Tailscale IPv4 address. This was probably good enough since the IP won't change unless the node is re-registered.
~~To get MagicDNS working, I went to Network > Global Configuration and set "Nameserver 1" to **100.100.100.100**. After this, I was able to specify the FQDN in the Backup Credentials and the Cloud Sync Tasks started.~~
This method of adding MagicDNS can lead to issues with DNS when updating the tailscale application in TrueNAS, so I ended using the Tailscale IP directly.

View File

@@ -5,6 +5,7 @@ lastmod: 2021-11-14T10:07:03-05:00
draft: false draft: false
comments: true comments: true
tags: ["k3s", "proxmox", "lxc", "self-hosted"] tags: ["k3s", "proxmox", "lxc", "self-hosted"]
author: "Dave Gallant"
--- ---
It has been a while since I've actively used Kubernetes and wanted to explore the evolution of tools such as [Helm](https://helm.sh) and [Tekton](https://tekton.dev). I decided to deploy [K3s](https://k3s.io), since I've had success with deploying it on resource-contrained Raspberry Pis in the past. I thought that this time it'd be convenient to have K3s running in a LXC container on Proxmox. This would allow for easy snapshotting of the entire Kubernetes deployment. LXC containers also provide an efficient way to use a machine's resources. It has been a while since I've actively used Kubernetes and wanted to explore the evolution of tools such as [Helm](https://helm.sh) and [Tekton](https://tekton.dev). I decided to deploy [K3s](https://k3s.io), since I've had success with deploying it on resource-contrained Raspberry Pis in the past. I thought that this time it'd be convenient to have K3s running in a LXC container on Proxmox. This would allow for easy snapshotting of the entire Kubernetes deployment. LXC containers also provide an efficient way to use a machine's resources.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -6,6 +6,7 @@ lastmod: 2023-12-10T17:22:11-05:00
draft: false draft: false
description: "" description: ""
tags: ["gitea", "gitea actions", "github actions", "tailscale", "self-hosted"] tags: ["gitea", "gitea actions", "github actions", "tailscale", "self-hosted"]
author: "Dave Gallant"
--- ---
In this post I'll go through the process of setting up Gitea Actions and [Tailscale](https://tailscale.com/), unlocking a simple and secure way to automate workflows. In this post I'll go through the process of setting up Gitea Actions and [Tailscale](https://tailscale.com/), unlocking a simple and secure way to automate workflows.
@@ -28,17 +29,18 @@ Actions (gitea's implementation) has me excited because it makes spinning up a n
## Integration with Tailscale ## Integration with Tailscale
So how does Tailscale help here? Well, more recently I've been exposing my self-hosted services through a combination of traefik and the tailscale (through the tailscale-traefik proxy integration described [here](https://traefik.io/blog/exploring-the-tailscale-traefik-proxy-integration/)). This allows for a nice looking dns name (i.e. gitea.my-tailnet-name.ts.net) and automatic tls certificate management. I can also share this tailscale node securely with other tailscale users without configuring any firewall rules on my router. > **2024-02-10**: I had originally written this post to include [Tailscale-Traefik Proxy Integration](https://traefik.io/blog/exploring-the-tailscale-traefik-proxy-integration/), but have since removed it in favour of Tailscale Serve after learning from this [example](https://github.com/tailscale-dev/docker-guide-code-examples). This simplifies the setup and reduces the number of moving parts.
So how does Tailscale help here? Well, more recently I've been exposing my self-hosted services using Tailscale [Serve](https://tailscale.com/kb/1312/serve). This allows for a nice looking dns name (i.e. gitea.my-tailnet-name.ts.net), automatic tls certificate management, and optionally allowing the address to be publically accessible (by using [Funnel](https://tailscale.com/kb/1223/funnel)).
## Deploying Gitea, Traefik, and Tailscale ## Deploying Gitea, Traefik, and Tailscale
In my case, the following is already set up: In my case, the following is already set up:
- [docker-compose is installed](https://docs.docker.com/compose/install/linux/) - [docker-compose is installed](https://docs.docker.com/compose/install/linux/)
- [tailscale is installed on the gitea host](https://tailscale.com/kb/1017/install/)
- [tailscale magic dns is enabled](https://tailscale.com/kb/1081/magicdns/) - [tailscale magic dns is enabled](https://tailscale.com/kb/1081/magicdns/)
My preferred approach to deploying code in a homelab environment is with docker compose. I have deployed this in a [proxmox lxc container](https://pve.proxmox.com/wiki/Linux_Container) based on debian with a hostname `gitea`. This could be deployed in any environment and with any hostname (as long you updated the tailscale machine name to your preferred subdomain for magic dns). My preferred approach to deploying code in a homelab environment is with docker compose. I have deployed this in a LXC on Proxmox. You could run this on a virtual machine or a physical host as well.
The `docker-compose.yaml` file looks like: The `docker-compose.yaml` file looks like:
@@ -48,6 +50,7 @@ services:
gitea: gitea:
image: gitea/gitea:1.21.1 image: gitea/gitea:1.21.1
container_name: gitea container_name: gitea
network_mode: service:ts-gitea
environment: environment:
- USER_UID=1000 - USER_UID=1000
- USER_GID=1000 - USER_GID=1000
@@ -61,67 +64,62 @@ services:
- ./data:/data - ./data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
traefik: ts-gitea:
image: traefik:v3.0.0-beta4 image: tailscale/tailscale:v1.58
container_name: traefik container_name: ts-gitea
security_opt: hostname: gitea
- no-new-privileges:true environment:
restart: unless-stopped - TS_AUTHKEY=<FILL THIS IN>
ports: - TS_SERVE_CONFIG=/config/gitea.json
- 80:80 - TS_STATE_DIR=/var/lib/tailscale
- 443:443
volumes: volumes:
- ./traefik/data/traefik.yaml:/traefik.yaml:ro - ${PWD}/state:/var/lib/tailscale
- ./traefik/data/dynamic.yaml:/dynamic.yaml:ro - ${PWD}/config:/config
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock - /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
``` ```
`traefik/data/traefik.yaml`: Note that you must specify a `TS_AUTHKEY` in the `ts-gitea` service. You can generate an auth key [here](https://login.tailscale.com/admin/settings/keys).
`config/gitea.json`:
```yaml ```yaml
entryPoints: {
https: "TCP": { "443": { "HTTPS": true } },
address: ":443" "Web":
providers: {
file: "${TS_CERT_DOMAIN}:443":
filename: dynamic.yaml { "Handlers": { "/": { "Proxy": "http://127.0.0.1:3000" } } },
certificatesResolvers: },
myresolver: "AllowFunnel": { "${TS_CERT_DOMAIN}:443": false }
tailscale: {} }
log:
level: INFO
``` ```
and finally `traefik/data/dynamic/dynamic.yaml`:
```yaml
http:
routers:
gitea:
rule: Host(`gitea.my-tailnet-name.ts.net`)
entrypoints:
- "https"
service: gitea
tls:
certResolver: myresolver
services:
gitea:
loadBalancer:
servers:
- url: "http://gitea:3000"
```
Something to consider is whether or not you want to use ssh with git. One method to get this to work with containers is to use [ssh container passthrough](https://docs.gitea.com/installation/install-with-docker#ssh-container-passthrough). I decided to keep it simple and not use ssh, since communicating over https is perfectly fine for my use case.
After adding the above configuration, running `docker compose up -d` should be enough to get an instance up and running. It will be accessible at [https://gitea.my-tailnet-name.ts.net](https://gitea.my-tailnet-name.ts.net) from within the tailnet. After adding the above configuration, running `docker compose up -d` should be enough to get an instance up and running. It will be accessible at [https://gitea.my-tailnet-name.ts.net](https://gitea.my-tailnet-name.ts.net) from within the tailnet.
## Connecting a Runner Something to consider is whether or not you want to use ssh with git. One method to get this to work with containers is to use [ssh container passthrough](https://docs.gitea.com/installation/install-with-docker#ssh-container-passthrough). I decided to keep it simple and not use ssh, since communicating over https is perfectly fine for my use case.
I installed the runner by [following the docs](https://docs.gitea.com/usage/actions/quickstart#set-up-runner). I opted for installing it on a separate host (another lxc container) as recommended in the docs. I used the systemd unit file to ensure that the runner comes back online after system reboots. I installed tailscale on this gitea runner as well, so that it can have the same "networking privileges" as the main instance. ## Theming
After registering this runner and starting the daemon, it appeared in `/admin/actions/runners`: I discovered some themes for gitea [here](https://git.sainnhe.dev/sainnhe/gitea-themes).
![image](gitea-runners.png) I added the theme by copying [theme-palenight.css](https://git.sainnhe.dev/sainnhe/gitea-themes/raw/branch/master/dist/theme-palenight.css) into `./data/gitea/public/assets/css`. I then added the following to `environment` in `docker-compose.yml`:
```yaml
- GITEA__ui__DEFAULT_THEME=palenight
- GITEA__ui__THEMES=palenight
```
After restarting the gitea instance, the default theme was applied.
## Connecting runners
I installed the runner by [following the docs](https://docs.gitea.com/usage/actions/quickstart#set-up-runner). I opted for installing it on a separate host as recommended in the docs. I used the systemd unit file to ensure that the runner comes back online after system reboots. I installed tailscale on the gitea runner as well, so that it can be part of the same tailnet as the main instance.
After registering this runner and starting the daemon, the runner appeared in `/admin/actions/runners`. I added two other runners to help with parallelization.
## Running a workflow ## Running a workflow
@@ -141,19 +139,6 @@ on:
jobs: jobs:
run-ansible-playbook: run-ansible-playbook:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
host:
- changedetection
- homelab
- invidious
- jackett
- ladder
- miniflux
- plex
- qbittorrent
- tailscale-exit-node
- uptime-kuma
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -167,7 +152,6 @@ jobs:
requirements: requirements.yml requirements: requirements.yml
options: | options: |
--inventory inventory --inventory inventory
--limit ${{ matrix.host }}
- name: Send failure notification - name: Send failure notification
uses: dawidd6/action-send-mail@v3 uses: dawidd6/action-send-mail@v3
if: always() && failure() if: always() && failure()
@@ -177,7 +161,7 @@ jobs:
secure: true secure: true
username: myuser username: myuser
password: ${{ secrets.MAIL_PASSWORD }} password: ${{ secrets.MAIL_PASSWORD }}
subject: ansible runbook '${{ matrix.host }}' failed subject: ansible runbook failed
to: me@davegallant.ca to: me@davegallant.ca
from: RFD Notify from: RFD Notify
body: | body: |
@@ -186,7 +170,7 @@ jobs:
And voilà: And voilà:
{{< video poster="gitea-workflow" >}} ![image](gitea-workflow.png)
You may be wondering how the gitea runner is allowed to connect to the other hosts using ansible? Well, the nodes are in the same tailnet and have [tailscale ssh](https://tailscale.com/tailscale-ssh) enabled. You may be wondering how the gitea runner is allowed to connect to the other hosts using ansible? Well, the nodes are in the same tailnet and have [tailscale ssh](https://tailscale.com/tailscale-ssh) enabled.
@@ -199,3 +183,5 @@ One enhancement that I would like to see is the ability to send notifications on
Gitea Actions are fast and the resource footprint is minimal. My gitea instance is currently using around 250mb of memory and a small fraction of a single cpu core (and the runner is using a similar amount of resources). This is impressive since many alternatives tend to require substantially more resources. It likely helps that the codebase is largely written in go. Gitea Actions are fast and the resource footprint is minimal. My gitea instance is currently using around 250mb of memory and a small fraction of a single cpu core (and the runner is using a similar amount of resources). This is impressive since many alternatives tend to require substantially more resources. It likely helps that the codebase is largely written in go.
By combining gitea with the networking marvel that is tailscale, running workflows becomes simple and fun. Whether you are working on a team or working alone, this setup ensures that your workflows are securely accessible from anywhere with an internet connection. By combining gitea with the networking marvel that is tailscale, running workflows becomes simple and fun. Whether you are working on a team or working alone, this setup ensures that your workflows are securely accessible from anywhere with an internet connection.
Check out my gitea instance exposed via Funnel [here](https://gitea.snake-cloud.ts.net).

View File

@@ -0,0 +1,83 @@
---
title: "Using a Realtek NIC with OPNsense"
date: "2025-04-21T17:17:46-04:00"
draft: false
comments: true
toc: false
author: "Dave Gallant"
tags:
[
linux,
freebsd,
opnsense,
pfsense,
proxmox,
realtek,
nic,
]
---
For the past few years, I've been running pfSense (and more recently OPNsense) in a virtual machine within Proxmox. This has been running fine with a single onboard Intel NIC. A few months ago, I upgraded to a machine that has a CPU that supports hardware-accelerated transcoding, has more SATA ports, and has more PCI slots for future expansion. With the goal of having a dedicated NIC for WAN, I bought an inexpensive 1Gbps PCIe NIC (TG-3468) despite reading about some of the concerns around Realtek NICs (sluggish performance, driver instability, and in some cases system crashes).
I've been running a Realtek NICs reliably on Linux and Windows desktops, so I figured I could make it work without too much effort, but it turns out Realtek NICs really can be problematic when it comes to FreeBSD-based routers, and commonly documented workarounds did not solve my problems.
<!--more-->
## Environment
My environment consists of:
- Proxmox 8.4
- OPNsense 25.1 (QEMU VM)
- Ethernet controller: Intel Corporation Ethernet Connection (5) I219-LM
- Ethernet controller: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller (rev 15)
# Goal
The goal is to upgrade the OPNsense router from a single NIC to two NICs. The NICs are responsible for:
1. **LAN**: the internal network for computers, phones, cameras, printers, etc (NIC 1)
2. **WAN**: the connection from the ISP (NIC 2)
Having two separate physical interfaces for LAN and WAN creates clear, physical separation between the trusted internal network and the untrusted external network at the hardware level. This also should improve performance and throughput since the same physical connection is no longer shared between LAN and WAN.
## Device Passthrough
For maximum performance and reduced hypervisor overhead, passing through a physical NIC for WAN directly to the VM seemed to make the most sense, so I passed it through to the OPNsense VM.
![passthrough](./opnsense-device-passthrough.png)
![passthrough-add-pci](./opnsense-device-passthrough-add-pci.png)
I added the PCI device and restarted the OPNsense VM and re-configured the WAN in OPNsense to use this device.
I received the WAN IP and everything appeared to be working. I ran a few speed tests and noticed that the download speeds were much lower than normal from all of my devices. I checked my instance of [speedtest-tracker](https://docs.speedtest-tracker.dev) noticed that the download speeds were significantly slower than historical records:
![speedtest-tracker](./speedtest-tracker.png)
These speeds tests were going through Mullvad, which occasionally is inconsistent, but the results remained consistently lower than the previous configuration.
I reverted the WAN back to the original NIC, and the download speeds returned to more average results immediately so it became obvious that something was not right with this setup.
### Realtek drivers
I did some web searching / LLM prompting and discovered that some people have had improved results after installing the OPNsense plugin **os-realtek-re**.
After installing the plugin and ensuring the kernel module was loaded at boot by following the post-install instructions, the throughput was still signicantly slower than before adding a second NIC.
I was starting to think that there might be a problem with the hardware and began the process to return it to the vendor.
## Virtualized NIC with a Linux bridge
As one last shot, I created Linux Bridge in the Proxmox GUI with the Realtek NIC and passed it through to the OPNsense VM:
![linux-bridge](./linux-bridge.png)
![linux-bridge-add-network-device](./linux-bridge-add-network-device.png)
I re-configured the WAN interface in OPNsense to use the newly added network device, and the download and upload speeds returned to the typical speeds. Another added benefit to this setup is that it bypasses the need for installing Realtek FreeBSD drivers on the OPNsense VM, since the network device is virtual and managed on the Proxmox host (debian-based).
## Conclusion
Although I am not sure why passing through a Realtek NIC to an OPNsense VM causes so much degradation in throughput, I am glad that there is a workaround. If I get ahold of another NIC, I would be interested in trying to reproduce the issue.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -19,13 +19,13 @@ tags:
"socat", "socat",
"socks", "socks",
] ]
author: "Dave Gallant"
--- ---
I ran into a roadblock recently where I wanted to be able to conveniently connect to a managed postgres database within Azure that was not running on public subnets. And by conveniently, I mean that I'd rather not have to spin up an ephemeral virtual machine running in the same network and proxy the connection, and I'd like to use a local client (preferably with a GUI). After several web searches, it became evident that Azure does not readily provide much tooling to support this. I ran into a roadblock recently where I wanted to conveniently connect to a managed postgres database within Azure that was not running on public subnets. And by conveniently, I mean that I'd rather not have to spin up an ephemeral virtual machine running in the same network and proxy the connection, and I'd like to use a local client (preferably with a GUI). After several web searches, it became evident that Azure does not readily provide much tooling to support this.
<!--more--> <!--more-->
## Go Public? ## Go Public?
Should the database be migrated to public subnets? Ideally not, since it is good practice to host internal infrastructure in restricted subnets. Should the database be migrated to public subnets? Ideally not, since it is good practice to host internal infrastructure in restricted subnets.
@@ -81,4 +81,6 @@ If these stars align, than this solution might work as a stopgap for accessing a
It would be nice if Azure provided tooling similar to cloud-sql-proxy, so that using private databases would be more of a convenient experience. It would be nice if Azure provided tooling similar to cloud-sql-proxy, so that using private databases would be more of a convenient experience.
One other thing to note is that some clients (such as [dbeaver](https://dbeaver.io/)) [do not provide DNS resolution over SOCKS](https://github.com/dbeaver/dbeaver/issues/872). So in this case, you won't be able to use DNS as if you were inside the cluster, but instead have to rely on knowing private ip addresses. ~~One other thing to note is that some clients (such as [dbeaver](https://dbeaver.io/)) [do not provide DNS resolution over SOCKS](https://github.com/dbeaver/dbeaver/issues/872). So in this case, you won't be able to use DNS as if you were inside the cluster, but instead have to rely on knowing private ip addresses.~~
> **2025-01-16:**: DNS over SOCKS now works with the latest dbeaver client.

View File

@@ -14,6 +14,7 @@ tags:
"vlan", "vlan",
"self-hosted", "self-hosted",
] ]
author: "Dave Gallant"
--- ---
My aging router has been running [OpenWrt](https://en.wikipedia.org/wiki/OpenWrt) for years and for the most part has been quite reliable. OpenWrt is an open-source project used on embedded devices to route network traffic. It supports many different configurations and there exists a [large index of packages](https://openwrt.org/packages/index/start). Ever since I've connected some standalone wireless access points, I've had less of a need for an off-the-shelf all-in-one wireless router combo. I've also recently been experiencing instability with my router (likely the result of a combination of configuration tweaking and firmware updating). OpenWrt has served me well, but it is time to move on! My aging router has been running [OpenWrt](https://en.wikipedia.org/wiki/OpenWrt) for years and for the most part has been quite reliable. OpenWrt is an open-source project used on embedded devices to route network traffic. It supports many different configurations and there exists a [large index of packages](https://openwrt.org/packages/index/start). Ever since I've connected some standalone wireless access points, I've had less of a need for an off-the-shelf all-in-one wireless router combo. I've also recently been experiencing instability with my router (likely the result of a combination of configuration tweaking and firmware updating). OpenWrt has served me well, but it is time to move on!

View File

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 935 KiB

View File

@@ -13,6 +13,7 @@ tags:
"privacy", "privacy",
"self-hosted", "self-hosted",
] ]
author: "Dave Gallant"
--- ---
I recently stumbled upon [yewtu.be](https://yewtu.be) and found it intriguing. It not only allows you to watch YouTube without _being on YouTube_, but it also allows you to create an account and subscribe to channels without a Google account. What sort of wizardry is going on under the hood? It turns out that it's a hosted instance of [invidious](https://invidious.io/). I recently stumbled upon [yewtu.be](https://yewtu.be) and found it intriguing. It not only allows you to watch YouTube without _being on YouTube_, but it also allows you to create an account and subscribe to channels without a Google account. What sort of wizardry is going on under the hood? It turns out that it's a hosted instance of [invidious](https://invidious.io/).
@@ -82,10 +83,32 @@ After invidious was up and running, I installed [Tailscale](https://tailscale.co
I figured it would be nice to redirect existing YouTube links that others send me, so that I could seamlessly watch the videos using invidious. I figured it would be nice to redirect existing YouTube links that others send me, so that I could seamlessly watch the videos using invidious.
I went looking for a way to redirect paths at the browser level. I found the lightweight proxy [requestly](https://requestly.io/), which can be used to modify http requests in my browser. I created the following rules: I went looking for a way to redirect paths at the browser level. I found [Redirector](https://github.com/einaregilsson/Redirector), which can be used to modify http requests in the browser. I created the following redirect (exported as json):
![requestly](requestly-rules.png) ```json
{
"redirects": [
{
"description": "youtube to invidious",
"exampleUrl": "https://www.youtube.com/watch?v=-lz30by8-sU",
"exampleResult": "http://invidious:3000/watch?v=-lz30by8-sU",
"error": null,
"includePattern": "https://*youtube.com/*",
"excludePattern": "",
"patternDesc": "Any youtube video should redirect to invidious",
"redirectUrl": "http://invidious:3000/$2",
"patternType": "W",
"processMatches": "noProcessing",
"disabled": false,
"grouped": false,
"appliesTo": [
"main_frame"
]
}
]
}
```
Now the link https://www.youtube.com/watch?v=-lz30by8-sU will redirect to [http://invidious:3000/watch?v=-lz30by8-sU](http://invidious:3000/watch?v=-lz30by8-sU) Now the link <https://www.youtube.com/watch?v=-lz30by8-sU> will redirect to [http://invidious:3000/watch?v=-lz30by8-sU](http://invidious:3000/watch?v=-lz30by8-sU)
I'm still looking for ways to improve this invidious setup. There doesn't appear to be a way to stream in 4K yet. I'm still looking for ways to improve this invidious setup. There doesn't appear to be a way to stream in 4K yet.

View File

@@ -12,9 +12,9 @@ A homelab can be an inexpensive way to host a multitude of internal/external ser
<!--more--> <!--more-->
Do you want host your own Media server? Ad blocker? Web server? Do you want host your own media server? ad blocker? reverse proxy?
Are you interested in learning more about Linux? Virtualization? Networking? Security? Are you interested in learning more about Linux? Virtualization? Networking? Security?
Building a homelab can be an entertaining playground to enhance your computer skills. A homelab can be a playground to enhance your computer skills, without worrying about breaking anything important.
One of the best parts about building a homelab is that it doesn't have to be a large investment in terms of hardware. One of the simplest ways to build a homelab is out of a [refurbished computer](https://ca.refurb.io/products/hp-800-g1-usff-intel-core-i5-4570s-16gb-ram-512gb-ssd-wifi-windows-10-pro?variant=33049503825943). One of the best parts about building a homelab is that it doesn't have to be a large investment in terms of hardware. One of the simplest ways to build a homelab is out of a [refurbished computer](https://ca.refurb.io/products/hp-800-g1-usff-intel-core-i5-4570s-16gb-ram-512gb-ssd-wifi-windows-10-pro?variant=33049503825943).
Having multiple machines/nodes provides the advantage of increased redundancy, but starting out with a single node is enough to reap many of the benefits of having a homelab. Having multiple machines/nodes provides the advantage of increased redundancy, but starting out with a single node is enough to reap many of the benefits of having a homelab.
@@ -30,20 +30,23 @@ A hypervisor such as [Proxmox](https://www.proxmox.com/en/proxmox-ve/get-started
## Services ## Services
So what are some useful services to deploy? Here is a list of some useful services to consider:
- [Jellyfin](https://jellyfin.org/) or [Plex](https://www.plex.tv/) - basically a self-hosted Netflix that can be used to stream from multiple devices, and the best part is that you manage the content! Unlike Plex, Jellyfin is open source and can be found [here](https://github.com/jellyfin/jellyfin). - [Jellyfin](https://jellyfin.org/) or [Plex](https://www.plex.tv/) - a common gateway to self-hosting that enables a "self-hosted Netflix" experience that puts you in control of the content (guaranteed to make your partner and kids happy)
- [changedetection](https://github.com/dgtlmoon/changedetection.io) - is a self-hosted equivalent to something like [visualping.io](https://visualping.io/) that will notify you when a webpage changes and keep track of the diffs - [changedetection](https://github.com/dgtlmoon/changedetection.io) - is a self-hosted equivalent to something like [visualping.io](https://visualping.io/) that can notify you when a webpage changes and keep track of the diffs
- [Adguard](https://github.com/AdguardTeam/AdGuardHome) or [Pihole](https://pi-hole.net/) - can block a list of known trackers for all clients on your local network. I've used pihole for a long time, but have recently switched to Adguard since the UI is more modern and it has the ability to toggle on/off a pre-defined list of services, including Netflix (this is useful if you have stealthy young kids). Either of these will speed up your internet experience, simply because you won't need to download all of the extra tracking bloat. - [Adguard](https://github.com/AdguardTeam/AdGuardHome) or [Pihole](https://pi-hole.net/) - can block a list of known trackers for all clients on your local network with the added benefit of speeding up web page load times
- [Gitea](https://gitea.io/) - A lightweight git server. I use this to mirror git repos from GitHub, GitLab, etc. - [gitea](https://gitea.io/) - A lightweight git server that can be used to mirror git repos and host private content
- [Homer](https://github.com/bastienwirtz/homer) - A customizable landing page for services you need to access (including the ability to quickly search). - [miniflux](https://github.com/miniflux/v2) - a minimalist RSS reader
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) - A fancy tool for monitoring the uptime of services. - [gethomepage](https://github.com/gethomepage/homepage) - A customizable landing page for quick access to services with many supported widgets that can query APIs and display information
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) - A tool for monitoring the uptime of services, with notification support
- [Speedtest Tracker](https://github.com/alexjustesen/speedtest-tracker) - a way to monitor the performance of your internet connection and/or vpn connection
- [Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF) - a self-hosted PDF manipulation tool that will keep your data private
There is a large number of services you can self-host, including your own applications that you might be developing. [awesome-self-hosted](https://github.com/awesome-selfhosted/awesome-selfhosted) provides a curated list of services that might be of interest to you. There is a large number of services you can self-host, including your own applications that you might be developing. Homelabbing allows you to have control over your data and services, and gives you the opportunity to be a software, network, and infrastructure engineer all at once.
## VPN ## VPN
You could certainly setup and manage your own VPN by using something like [OpenVPN](https://openvpn.net/community-downloads/), but there is also something else you can try: [tailscale](https://tailscale.com/). It is a very quick way to create fully-encrypted connections between clients. With its [MagicDNS](https://tailscale.com/kb/1081/magicdns/), your can reference the names of machines like `homer` rather than using an IP address. By using this mesh-like VPN, you can easily create a secure tunnel to your homelab from anywhere. [Tailscale](https://tailscale.com/) is a quick way to create a flat network for all of your services. With its [MagicDNS](https://tailscale.com/kb/1081/magicdns/), your can reference the names of machines like `changedetection` rather than using an IP address, or managing DNS yourself. By using this mesh-like VPN, you can easily create a secure tunnel to your homelab from anywhere.
## Monitoring ## Monitoring
@@ -60,3 +63,5 @@ As mentioned above, [Uptime Kuma](https://github.com/louislam/uptime-kuma) is a
## In Summary ## In Summary
Building out a homelab can be a rewarding experience and it doesn't require buying a rack full of expensive servers to get a significant amount of utility. There are many services that you can run that require very minimal setup, making it possible to get a server up and running in a short period of time, with monitoring, and that can be securely connected to remotely. Building out a homelab can be a rewarding experience and it doesn't require buying a rack full of expensive servers to get a significant amount of utility. There are many services that you can run that require very minimal setup, making it possible to get a server up and running in a short period of time, with monitoring, and that can be securely connected to remotely.
If you're looking for a steady stream of ideas for your homelab, check out [selfhosted.show](https://selfhosted.show/).

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -5,6 +5,7 @@ lastmod: 2021-09-08T00:42:33-04:00
draft: false draft: false
comments: true comments: true
tags: ['nix', 'dotfiles', 'home-manager'] tags: ['nix', 'dotfiles', 'home-manager']
author: "Dave Gallant"
--- ---
Over the years I have collected a number of dotfiles that I have shared across both Linux and macOS machines (`~/.zshrc`, `~/.config/git/config`, `~/.config/tmux/tmux.conf`, etc). I have tried several different ways to manage them, including [bare git repos](https://www.atlassian.com/git/tutorials/dotfiles) and utilities such as [GNU Stow](https://www.gnu.org/software/stow/). These solutions work well enough, but I have since found what I would consider a much better solution for organizing user configuration: [home-manager](https://github.com/nix-community/home-manager). Over the years I have collected a number of dotfiles that I have shared across both Linux and macOS machines (`~/.zshrc`, `~/.config/git/config`, `~/.config/tmux/tmux.conf`, etc). I have tried several different ways to manage them, including [bare git repos](https://www.atlassian.com/git/tutorials/dotfiles) and utilities such as [GNU Stow](https://www.gnu.org/software/stow/). These solutions work well enough, but I have since found what I would consider a much better solution for organizing user configuration: [home-manager](https://github.com/nix-community/home-manager).
@@ -181,3 +182,5 @@ In ways, home-manager can be seen as a gateway to the nix ecosystem. If you have
## Wrapping up ## Wrapping up
The title of this post is slightly misleading, since it's possible to retain some of your dotfiles and have them intermingle with home-manager by including them alongside nix. The idea of defining user configuration using nix can provide a clean way to maintain your configuration, and allow it to be portable across platforms. Is it worth the effort to migrate away from shell scripts and dotfiles? I'd say so. The title of this post is slightly misleading, since it's possible to retain some of your dotfiles and have them intermingle with home-manager by including them alongside nix. The idea of defining user configuration using nix can provide a clean way to maintain your configuration, and allow it to be portable across platforms. Is it worth the effort to migrate away from shell scripts and dotfiles? I'd say so.
You can find my nix config [here](https://github.com/davegallant/nix-config).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,29 +0,0 @@
{
"basics": {
"name": "Dave Gallant",
"image": "",
"email": "me@davegallant.ca",
"summary": "👋 I'm a software tinkerer with a passion for infra, security and self-hosting.",
"location": {
"region": "From 🇨🇦"
},
"profiles": [
{
"network": "LinkTree",
"url": "https://linktr.ee/davegallant"
},
{
"network": "GitHub",
"url": "https://github.com/davegallant"
},
{
"network": "Mastodon",
"url": "https://mastodon.social/@davegallant"
},
{
"network": "LinkedIn",
"url": "https://www.linkedin.com/in/dave-gallant"
}
]
}
}

8
go.mod
View File

@@ -1,8 +0,0 @@
module davegallant.github.io
go 1.21
require (
github.com/davegallant/hugo-theme-gruvbox v0.0.0-20240102231105-90b486f98b82 // indirect
github.com/schnerring/hugo-mod-json-resume v0.0.0-20231224014047-e651a547c19a // indirect
)

4
go.sum
View File

@@ -1,4 +0,0 @@
github.com/davegallant/hugo-theme-gruvbox v0.0.0-20240102231105-90b486f98b82 h1:X9jUG5D4OyxLjT3OTsL5TN3jFdpTXyLJJcRpLbgCsSc=
github.com/davegallant/hugo-theme-gruvbox v0.0.0-20240102231105-90b486f98b82/go.mod h1:BQehNdf/SB/+bCc031OVsLECIgB9ZaN1dfUFKTeOIuo=
github.com/schnerring/hugo-mod-json-resume v0.0.0-20231224014047-e651a547c19a h1:EZRiOf0iW5k9lycVv3LngzSsGUxDRszYS4U7ea2r8RY=
github.com/schnerring/hugo-mod-json-resume v0.0.0-20231224014047-e651a547c19a/go.mod h1:5dixHC0WHu0w2Aqb8hsOCrIU1OBYr1w5Q6HZAmTub7Q=

10
justfile Normal file
View File

@@ -0,0 +1,10 @@
build: clean
npm ci
hugo --minify
clean:
rm -rf public/
server: clean
npm ci
hugo server --buildDrafts

View File

@@ -3,7 +3,7 @@
src="https://storage.ko-fi.com/cdn/widget/Widget_2.js" src="https://storage.ko-fi.com/cdn/widget/Widget_2.js"
></script> ></script>
<script type="text/javascript"> <script type="text/javascript">
kofiwidget2.init("Buy me a coffee", "#458588", "F1F2S4LWI"); kofiwidget2.init("Buy me a coffee", "#32344a", "F1F2S4LWI");
kofiwidget2.draw(); kofiwidget2.draw();
</script> </script>
@@ -12,6 +12,8 @@
{{- $utterancesEnabled := $config.utterances.enable -}} {{- $utterancesEnabled := $config.utterances.enable -}}
{{- if $utterancesEnabled -}} {{- if $utterancesEnabled -}}
<br>
<br>
<section id='comments' class='comments'> <section id='comments' class='comments'>
<div class='container sep-before'> <div class='container sep-before'>
<div class='comments'> <div class='comments'>

View File

@@ -1,23 +1,19 @@
{{- $scriptSrc := "https://utteranc.es/client.js" -}} {{- $scriptSrc := "https://utteranc.es/client.js" -}} {{- $issueTerm :=
.Page.Site.Params.comments.utterances.issueTerm -}} {{- $label :=
{{- $issueTerm := .Page.Site.Params.comments.utterances.issueTerm -}} .Page.Site.Params.comments.utterances.label -}} {{- $username :=
{{- $label := .Page.Site.Params.comments.utterances.label -}} .Page.Site.Params.comments.utterances.github.username -}} {{- $repository :=
{{- $username := .Page.Site.Params.comments.utterances.github.username -}} .Page.Site.Params.comments.utterances.github.repository -}}
{{- $repository := .Page.Site.Params.comments.utterances.github.repository -}}
<script> <script>
// load utteranc comment // load comments
var getTheme = window.localStorage && window.localStorage.getItem("theme"); let theme = "dark-blue";
getTheme = getTheme == null ? 'dark' : getTheme; let script = document.createElement("script");
script.src = "https://utteranc.es/client.js";
let theme = getTheme === 'dark' ? 'gruvbox-dark' : 'github-light'; script.setAttribute("repo", '{{ print $username "/" $repository }}');
let s = document.createElement('script'); script.setAttribute("issue-term", "{{ $issueTerm }}");
s.src = 'https://utteranc.es/client.js'; script.setAttribute("theme", theme);
s.setAttribute('repo', '{{ print $username "/" $repository }}'); script.setAttribute("crossorigin", "anonymous");
s.setAttribute('issue-term', '{{ $issueTerm }}'); script.setAttribute("async", "");
s.setAttribute('theme', theme); document.querySelector("div.comments").innerHTML = "";
s.setAttribute('crossorigin', 'anonymous'); document.querySelector("div.comments").appendChild(script);
s.setAttribute('async', '');
document.querySelector('div.comments').innerHTML = '';
document.querySelector('div.comments').appendChild(s);
</script> </script>

View File

@@ -5,3 +5,5 @@
data-cf-beacon='{"token": "b96799f53f9940dca6f660e6052ba009"}' data-cf-beacon='{"token": "b96799f53f9940dca6f660e6052ba009"}'
></script> ></script>
<!-- End Cloudflare Web Analytics --> <!-- End Cloudflare Web Analytics -->
{{ template "_internal/google_analytics.html" . }}

View File

@@ -0,0 +1,2 @@
<!-- Umami Analytics -->
<script defer src="https://umami.snake-cloud.ts.net/script.js" data-website-id="e8adafba-b892-4dad-a139-2bd61fe5fab9"></script>

125
package-lock.json generated
View File

@@ -227,9 +227,9 @@
} }
}, },
"node_modules/@csstools/cascade-layer-name-parser": { "node_modules/@csstools/cascade-layer-name-parser": {
"version": "1.0.7", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz",
"integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -241,12 +241,13 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
}, },
"peerDependencies": { "peerDependencies": {
"@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-parser-algorithms": "^2.7.1",
"@csstools/css-tokenizer": "^2.2.3" "@csstools/css-tokenizer": "^2.4.1"
} }
}, },
"node_modules/@csstools/color-helpers": { "node_modules/@csstools/color-helpers": {
@@ -319,9 +320,9 @@
} }
}, },
"node_modules/@csstools/css-parser-algorithms": { "node_modules/@csstools/css-parser-algorithms": {
"version": "2.5.0", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz",
"integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -333,17 +334,18 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
}, },
"peerDependencies": { "peerDependencies": {
"@csstools/css-tokenizer": "^2.2.3" "@csstools/css-tokenizer": "^2.4.1"
} }
}, },
"node_modules/@csstools/css-tokenizer": { "node_modules/@csstools/css-tokenizer": {
"version": "2.2.3", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz",
"integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -355,14 +357,15 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
} }
}, },
"node_modules/@csstools/media-query-list-parser": { "node_modules/@csstools/media-query-list-parser": {
"version": "2.1.7", "version": "2.1.13",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz",
"integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==", "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -374,12 +377,13 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
}, },
"peerDependencies": { "peerDependencies": {
"@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-parser-algorithms": "^2.7.1",
"@csstools/css-tokenizer": "^2.2.3" "@csstools/css-tokenizer": "^2.4.1"
} }
}, },
"node_modules/@csstools/postcss-cascade-layers": { "node_modules/@csstools/postcss-cascade-layers": {
@@ -1672,9 +1676,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001572", "version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==", "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1689,7 +1693,8 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
] ],
"license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
@@ -2469,9 +2474,9 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.1.2", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
"integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==", "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0", "prettier-linter-helpers": "^1.0.0",
@@ -3397,9 +3402,9 @@
} }
}, },
"node_modules/lint-staged": { "node_modules/lint-staged": {
"version": "15.2.0", "version": "15.2.2",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.0.tgz", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz",
"integrity": "sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==", "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "5.3.0", "chalk": "5.3.0",
@@ -3407,7 +3412,7 @@
"debug": "4.3.4", "debug": "4.3.4",
"execa": "8.0.1", "execa": "8.0.1",
"lilconfig": "3.0.0", "lilconfig": "3.0.0",
"listr2": "8.0.0", "listr2": "8.0.1",
"micromatch": "4.0.5", "micromatch": "4.0.5",
"pidtree": "0.6.0", "pidtree": "0.6.0",
"string-argv": "0.3.2", "string-argv": "0.3.2",
@@ -3436,9 +3441,9 @@
} }
}, },
"node_modules/listr2": { "node_modules/listr2": {
"version": "8.0.0", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.0.tgz", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz",
"integrity": "sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==", "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
@@ -3815,9 +3820,9 @@
"dev": true "dev": true
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3825,6 +3830,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@@ -4066,10 +4072,11 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -4105,9 +4112,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.32", "version": "8.5.2",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4123,10 +4130,11 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.8",
"picocolors": "^1.0.0", "picocolors": "^1.1.1",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -4324,9 +4332,9 @@
} }
}, },
"node_modules/postcss-custom-media": { "node_modules/postcss-custom-media": {
"version": "10.0.2", "version": "10.0.8",
"resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.8.tgz",
"integrity": "sha512-zcEFNRmDm2fZvTPdI1pIW3W//UruMcLosmMiCdpQnrCsTRzWlKQPYMa1ud9auL0BmrryKK1+JjIGn19K0UjO/w==", "integrity": "sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4338,11 +4346,12 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"@csstools/cascade-layer-name-parser": "^1.0.5", "@csstools/cascade-layer-name-parser": "^1.0.13",
"@csstools/css-parser-algorithms": "^2.3.2", "@csstools/css-parser-algorithms": "^2.7.1",
"@csstools/css-tokenizer": "^2.2.1", "@csstools/css-tokenizer": "^2.4.1",
"@csstools/media-query-list-parser": "^2.1.5" "@csstools/media-query-list-parser": "^2.1.13"
}, },
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
@@ -5832,10 +5841,11 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -6045,10 +6055,11 @@
} }
}, },
"node_modules/stylelint-prettier": { "node_modules/stylelint-prettier": {
"version": "5.0.0", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-5.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-5.0.3.tgz",
"integrity": "sha512-RHfSlRJIsaVg5Br94gZVdWlz/rBTyQzZflNE6dXvSxt/GthWMY3gEHsWZEBaVGg7GM+XrtVSp4RznFlB7i0oyw==", "integrity": "sha512-B6V0oa35ekRrKZlf+6+jA+i50C4GXJ7X1PPmoCqSUoXN6BrNF6NhqqhanvkLjqw2qgvrS0wjdpeC+Tn06KN3jw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0" "prettier-linter-helpers": "^1.0.0"
}, },

View File

@@ -1,4 +0,0 @@
{
"name": "davegallant.github.io",
"version": "0.1.0"
}

View File

@@ -1,37 +1,4 @@
{ {
"comments": {
"dependencies": {
"@tabler/icons": "github.com/davegallant/hugo-theme-gruvbox",
"flexsearch": "github.com/davegallant/hugo-theme-gruvbox",
"normalize.css": "github.com/davegallant/hugo-theme-gruvbox",
"prism-themes": "github.com/davegallant/hugo-theme-gruvbox",
"prismjs": "github.com/davegallant/hugo-theme-gruvbox",
"simple-icons": "github.com/schnerring/hugo-mod-json-resume",
"typeface-fira-code": "github.com/davegallant/hugo-theme-gruvbox",
"typeface-roboto-slab": "github.com/davegallant/hugo-theme-gruvbox"
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "github.com/davegallant/hugo-theme-gruvbox",
"cssnano": "github.com/davegallant/hugo-theme-gruvbox",
"eslint": "github.com/davegallant/hugo-theme-gruvbox",
"eslint-config-prettier": "github.com/davegallant/hugo-theme-gruvbox",
"eslint-plugin-prettier": "github.com/davegallant/hugo-theme-gruvbox",
"husky": "github.com/davegallant/hugo-theme-gruvbox",
"lint-staged": "github.com/davegallant/hugo-theme-gruvbox",
"markdownlint-cli": "github.com/davegallant/hugo-theme-gruvbox",
"postcss": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-cli": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-custom-media": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-import": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-nesting": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-preset-env": "github.com/davegallant/hugo-theme-gruvbox",
"postcss-url": "github.com/davegallant/hugo-theme-gruvbox",
"prettier": "github.com/davegallant/hugo-theme-gruvbox",
"prettier-plugin-go-template": "github.com/davegallant/hugo-theme-gruvbox",
"stylelint": "github.com/davegallant/hugo-theme-gruvbox",
"stylelint-prettier": "github.com/davegallant/hugo-theme-gruvbox"
}
},
"dependencies": { "dependencies": {
"@tabler/icons": "^2.44.0", "@tabler/icons": "^2.44.0",
"flexsearch": "^0.7.31", "flexsearch": "^0.7.31",

9
renovate.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"schedule": [
"every weekend"
],
"extends": [
"config:recommended"
]
}

11
shell.nix Normal file
View File

@@ -0,0 +1,11 @@
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.11";
pkgs = import nixpkgs { config = { }; overlays = [ ]; };
in
pkgs.mkShellNoCC {
packages = with pkgs; [
hugo
];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
static/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,7 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space

View File

@@ -0,0 +1,5 @@
assets/js/flexsearch.js
assets/js/prism.js
public
resources

View File

@@ -0,0 +1,14 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"sourceType": "module"
}
}

View File

@@ -0,0 +1,43 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"rebaseWhen": "behind-base-branch",
"npm": {
"fileMatch": ["(^|/)package\\.hugo\\.json$"],
"rangeStrategy": "bump",
"updateLockFiles": false,
"ignoreTests": true,
"ignorePaths": [
"package.json",
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/examples/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**"
]
},
"gomod": {
"fileMatch": ["(^|/)go\\.mod$"],
"rangeStrategy": "bump"
},
"packageRules": [
{
"matchManagers": ["gomod"],
"matchDepTypes": ["indirect"],
"enabled": true,
"groupName": "Hugo Modules"
},
{
"extends": "packages:linters",
"groupName": "linters"
},
{
"extends": "packages:postcss",
"groupName": "postcss packages"
}
]
}

View File

@@ -0,0 +1,84 @@
name: Publish Hugo Site
on:
push:
branches:
- main
jobs:
update_hugo_npm_dependencies:
name: Update Hugo npm Dependencies
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
commit_hash: ${{ steps.commit_changes.outputs.commit_hash }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Install Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.111.3"
extended: true
- name: Write composite package.json
run: hugo mod npm pack
- name: Install npm Packages
run: npm install
- name: Display Changes
run: git status
- name: Commit Changes
id: commit_changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Update Hugo npm Dependencies
publish:
name: Publish Hugo Site
needs: update_hugo_npm_dependencies
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
ref: ${{ needs.update_hugo_npm_dependencies.outputs.commit_hash }}
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Install Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.111.3"
extended: true
- name: Install npm Packages
run: npm ci
- name: Build Hugo
run: hugo --minify
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: hugo-theme-gruvbox
directory: ./public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,35 @@
name: Renovate Hugo Modules
on: pull_request
jobs:
renovate_hugo_modules:
name: Renovate Hugo Modules
if: startsWith(github.head_ref, 'renovate/hugo-modules')
runs-on: ubuntu-latest
steps:
- name: Checkout Pull Request HEAD Commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.111.3"
extended: true
- name: Update All Hugo Modules
run: hugo mod get -u
- name: Tidy Hugo Modules
run: hugo mod tidy
- name: Display Changes
run: git status
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Renovate Hugo Modules
commit_author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

159
themes/custom-theme/.gitignore vendored Normal file
View File

@@ -0,0 +1,159 @@
# Created by https://www.toptal.com/developers/gitignore/api/hugo,node
# Edit at https://www.toptal.com/developers/gitignore?templates=hugo,node
### Hugo ###
# Generated files by hugo
/public/
/resources/_gen/
/assets/jsconfig.json
hugo_stats.json
# Executable may be added to repository
hugo.exe
hugo.darwin
hugo.linux
# Temporary lock file while building
/.hugo_build.lock
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/hugo,node

1
themes/custom-theme/.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -0,0 +1,11 @@
node_modules
public
resources
# Leave content from gohugoio/hugoBasicExample as is
content/blog/emoji-support.md
content/blog/markdown-syntax.md
content/blog/math-typesetting.md
content/blog/placeholder-text.md
content/blog/rich-content.md

View File

@@ -0,0 +1,4 @@
assets/css/critical/15-colors.css
assets/css/non-critical/00-vendor.css
assets/js/flexsearch.js
assets/js/prism.js

View File

@@ -0,0 +1,12 @@
{
"plugins": ["prettier-plugin-go-template"],
"proseWrap": "always",
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "go-template"
}
}
]
}

View File

@@ -0,0 +1,5 @@
assets/css/critical/15-colors.css
assets/css/non-critical/00-vendor.css
public
resources

View File

@@ -0,0 +1,3 @@
{
"extends": ["stylelint-prettier/recommended"]
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Michael Schnerring
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.

View File

@@ -0,0 +1,573 @@
# Gruvbox Hugo Theme
A retro-looking [Hugo](https://gohugo.io/) theme inspired by
[gruvbox](https://github.com/morhetz/gruvbox) to build secure, fast, and
SEO-ready websites.
This theme is easily customizable with features that any coder loves.
I took a lot of inspiration from the
[Hello Friend](https://github.com/panr/hugo-theme-hello-friend) and
[Doks](https://github.com/h-enk/doks) Hugo themes.
## DEMO [https://hugo-theme-gruvbox.schnerring.net/](https://hugo-theme-gruvbox.schnerring.net/)
![Screenshot of the theme in dark and light colors](https://raw.githubusercontent.com/schnerring/hugo-theme-gruvbox/main/images/tn.png)
## DISCLAIMER: Project Status
This theme is still in early development.
[Check out the issues](https://github.com/schnerring/hugo-theme-gruvbox/issues)
to see what's still missing.
## Highlights
- [Code highlighting with Prism](#prism)
- Full-text search with [Flex Search](https://github.com/nextapps-de/flexsearch)
- Display your CV using structured [JSON Resume](https://jsonresume.org/) data
- [Integrated image optimization with next-gen image formats and lazy loading](#image-optimization)
- Dark mode that also changes Prism themes
- [Dynamic color choices from the Gruvbox color palette](#colors)
- [Extensible to make it suit your needs](#extensibility)
- Responsive, mobile-first design
- Beautiful SVG icons with [Tabler Icons](https://tabler-icons.io/)
A big thank you to the authors of the software that make this theme possible! ❤️
## Quickstart
The theme requires _extended_ Hugo because it uses Sass/SCSS. You'll also have
to install Go because the theme uses Go modules.
1. `git clone` the repository and `cd` into it
2. Run `npm ci` to install the dependencies
3. Run `hugo server`
## Install The Theme
Create a new Hugo website:
```shell
hugo new site example.com
cd example.com/
```
Initialize the site as Hugo module
```shell
hugo mod init example.com
```
Add the following to the `config.toml` file:
```toml
[markup]
# (Optional) To be able to use all Prism plugins, the theme enables unsafe
# rendering by default
#_merge = "deep"
[build]
# The theme enables writeStats which is required for PurgeCSS
_merge = "deep"
# This hopefully will be simpler in the future.
# See: https://github.com/schnerring/hugo-theme-gruvbox/issues/16
[module]
[[module.imports]]
path = "github.com/schnerring/hugo-theme-gruvbox"
[[module.imports]]
path = "github.com/schnerring/hugo-mod-json-resume"
[[module.imports.mounts]]
source = "data"
target = "data"
[[module.imports.mounts]]
source = "layouts"
target = "layouts"
[[module.imports.mounts]]
source = "assets/css/json-resume.css"
target = "assets/css/critical/44-json-resume.css"
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "layouts"
target = "layouts"
[[module.mounts]]
source = "static"
target = "static"
[[module.mounts]]
source = "node_modules/prismjs"
target = "assets/prismjs"
[[module.mounts]]
source = "node_modules/prism-themes/themes"
target = "assets/prism-themes"
[[module.mounts]]
source = "node_modules/typeface-fira-code/files"
target = "static/fonts"
[[module.mounts]]
source = "node_modules/typeface-roboto-slab/files"
target = "static/fonts"
[[module.mounts]]
source = "node_modules/@tabler/icons/icons"
target = "assets/tabler-icons"
```
Install the theme:
```shell
hugo mod get
```
Initialize the NPM `package.json` and install the dependencies:
```shell
hugo mod npm pack
npm install
```
Run Hugo:
```shell
hugo server
```
## Update The Theme
Update the Hugo modules:
```shell
hugo mod get -u
hugo mod tidy
```
Update the NPM dependencies:
```shell
hugo mod npm pack
npm install
```
## Colors
Two options are available to configure the theme colors:
- `defaultTheme`: `dark` or `light` (defaults to `light`)
Default theme color for when a user visits the site for the first time. OS or
user preference override this setting.
[See this comment for more details.](https://github.com/schnerring/hugo-theme-gruvbox/issues/34#issuecomment-1235870375)
- `themeColor`: `gray`, `red`, `green`, `yellow`, `blue`, `purple`, `aqua`, or
`orange` (defaults to `blue`)
Theme color for things such as links, headings etc.
- `themeContrast`: `soft`, `medium`, or `hard` (defaults to `medium`)
Theme background color
## Prism
The theme allows customization of [Prism](https://prismjs.com/) via
`config.toml` parameters:
```toml
[params]
[params.prism]
languages = [
"markup",
"css",
"clike",
"javascript"
]
plugins = [
"normalize-whitespace",
"toolbar",
"copy-to-clipboard"
]
```
In my opinion, this is the coolest feature of the theme. Other Hugo themes
usually include a pre-configured version of Prism, which complicates updates and
change tracking, and clutters the theme's code base with third-party JavaScript.
The Prism theme is not configurable because of the integration with the dark
mode functionality. Toggling between color modes swaps the Prism theme between
[`gruvbox-dark`](https://github.com/PrismJS/prism-themes/blob/master/themes/prism-gruvbox-dark.css)
and
[`gruvbox-light`](https://github.com/PrismJS/prism-themes/blob/master/themes/prism-gruvbox-light.css)
from [github.com/PrismJS/prism-themes](https://github.com/PrismJS/prism-themes).
Check out the
[Prism showcase on the Demo site for examples](https://hugo-theme-gruvbox.schnerring.net/blog/prism-code-highlighting-showcase/)
### Explore Prism Features
After running `npm install`, explore Prism features like this:
```shell
# Languages
ls node_modules/prismjs/components
# Plugins
ls node_modules/prismjs/plugins
```
## Image Optimization
Images are optimized by default without requiring
[shortcodes](https://gohugo.io/content-management/shortcodes/). A
[custom render hook](https://gohugo.io/getting-started/configuration-markup#markdown-render-hooks)
does all the heavy lifting (see
[render-image.html](./layouts/_default/_markup/render-image.html)).
By default, the theme creates resized versions of images ranging from 300 to 700
pixels wide in increments of 100 pixels.
If the image format is not [WebP](https://en.wikipedia.org/wiki/WebP), the image
is converted. The original file format will serve as a fallback for browsers
that don't support the WebP format.
Note that only images that are part of the
[page bundle](https://gohugo.io/content-management/page-bundles/) are processed.
If served from the `static/` directory or external sources, the image will be
displayed but not be processed.
Additionally, all images are lazily loaded to save the bandwidth of your users.
### Configuration
The default quality is 75%. See the
[official Image Processing Config Hugo docs](https://gohugo.io/content-management/image-processing/#image-processing-config).
Change it by adding the following to the `config.toml` file:
```toml
[imaging]
quality = 75
```
Change the resize behavior:
```toml
[params]
[params.imageResize]
min = 300
max = 700
increment = 100
```
### Captions
```markdown
![Alt text](image-url.jpg "Caption with **markdown support**")
```
[The demo site features examples you can look at](https://hugo-theme-gruvbox.schnerring.net/blog/image-optimization/).
I also use the theme for [my website](https://schnerring.net).
### Blog Post Covers
Add blog post covers by defining them in the
[front matter](https://gohugo.io/content-management/front-matter/) of your
posts:
```markdown
---
cover:
src: my-blog-cover.jpg
alt: A beautiful image containing interesting things
caption: [Source](https://www.flickr.com/)
---
```
## Embed Video Files
Use the
[video shortcode](https://github.com/schnerring/hugo-theme-gruvbox/blob/main/layouts/shortcodes/video.html)
to embed your video files from
[Page Resources](https://gohugo.io/content-management/page-resources/).
With a page bundle looking like the following:
```text
embed-videos/
|-- index.md
|-- my-video.jpg
|-- my-video.mp4
|-- my-video.webm
```
You can embed `my-video` like this:
```markdown
{{< video src="my-video" autoplay="true" controls="false" loop="true" >}}
```
The shortcode looks for media files matching the filename `my-video*`. For each
`video` MIME type file, a `<source>` element is added. The first `image` MIME
type file is used as `poster` (thumbnail). It will render the following HTML:
```html
<video
autoplay
loop
poster="/blog/embed-videos/my-video.jpg"
width="100%"
playsinline
>
<source src="/blog/embed-videos/my-video.mp4" type="video/mp4" />
<source src="/blog/embed-videos/my-video.webm" type="video/webm" />
</video>
```
You can set a Markdown `caption`, wrapping the `<video>` inside a `<figure`>.
Additionally, the shortcode allows you to set the following attributes:
| Attribute | Default |
| ----------- | ------- |
| autoplay | `false` |
| controls | `true` |
| height | |
| loop | `false` |
| muted | `true` |
| preload | |
| width | `100%` |
| playsinline | `true` |
[Learn more about the `<video>` attributes here.](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attributes)
## SEO
Due to the
[European Copyright Directive](https://wayback.archive-it.org/12090/20210304045117/https://ec.europa.eu/digital-single-market/en/modernisation-eu-copyright-rules)
it is required to opt into displaying
[snippets](https://developers.google.com/search/docs/advanced/appearance/title-link?hl=en)
in search engine results.
By default, every page (except 404) includes the
`index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1`
robots meta value, opting into all snippet features.
You can override the robots meta value in the front matter of your pages:
```markdown
---
robots: noindex, nofollow
---
```
## Social Share Links
Configure social share links in the Hugo config like this:
```toml
[params]
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "facebook"
formatString = "https://www.facebook.com/sharer.php?u={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "reddit"
formatString = "https://reddit.com/submit?url={url}&title={title}"
[[params.socialShare]]
iconSuite = "tabler-icon"
iconName = "mail"
formatString = "mailto:?subject={title}&body={url}"
```
Use the `iconSuite` option to specify the icon suite used for the social share
link: `simple-icon` or `tabler-icon`. Select an icon from the suite with the
`iconName` option.
The `formatString` supports the following placeholders:
- `{url}` is replaced with the `.Permalink` of the post
- `{title}` is replaced with the `.Title` of the post
To enable social share links, set the following in the post's front matter:
```markdown
---
socialShare: true
---
```
Check out the
[Social Share URLs repo on GitHub](https://github.com/bradvin/social-share-urls)
for more format strings.
## Favicon
The favicons and [corresponding markup](./layouts/partials/head/favicons.html)
were generated with the free
[RealFaviconGenerator.net](https://realfavicongenerator.net/).
The easiest way to replace the default favicons is to generate them using
RealFaviconGenerator.net and put the generated files into the `static/`
directory.
## Extensibility
You can extend the theme by overriding the following partials in the
`layouts/partials` directory which by default are empty placeholder files:
- [`head/head_start.html`](./layouts/partials/head_start.html)
Custom HTML at the start of `<head>`
- [`head/head_end.html`](./layouts/partials/head_end.html)
Custom HTML at the end of `<head>`
- [`footer_end.html`](./layouts/partials/footer_end.html)
Custom HTML at the end of `<body>`
- [`comments.html`](./layouts/partials/comments.html)
Comments at the end of posts
### Example: Adding KaTeX Support to the Theme
[KaTeX](https://katex.org/) is a fast, easy-to-use JavaScript library for TeX
math rendering on the web. Let's add it to the theme via `npm`. First, add the
following to the `package.hugo.json` file:
```json
"dependencies": {
"katex": "^0.16.8"
}
```
Then run `hugo mod npm pack` to sync the `package.hugo.json` dependencies with
`package.json`. Run `npm install` after. We then need to mount the
`node_modules/katex` folder into Hugo's virtual filesystem by adding the
following to the `config/_default/module.toml` file:
```toml
[[mounts]]
source = "node_modules/katex"
target = "assets/katex"
```
We can then add the following to `layouts/partials/head/head_end.html`:
<!-- prettier-ignore-start -->
```html
{{ if .Params.katex }}
{{ $katexCSS := resources.Get "katex/dist/katex.min.css" }}
<link
rel="stylesheet"
href="{{ $katexCSS }}"
{{ if hugo.IsProduction }}
integrity="{{ $katexCSS.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
/>
{{ $katexJS := resources.Get "katex/dist/katex.min.js" }}
<script
defer
src="{{ $katexJS.RelPermalink }}"
{{ if hugo.IsProduction }}
integrity="{{ $katexJS.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
></script>
{{ $autoRender := resources.Get "katex/dist/contrib/auto-render.min.js" }}
<script
defer
src="{{ $autoRender.RelPermalink }}"
{{ if hugo.IsProduction }}
integrity="{{ $autoRender.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
onload="renderMathInElement(document.body);"
></script>
{{ end }}
```
<!-- prettier-ignore-end -->
The only thing left is enabling KaTeX in the front matter of our content:
```markdown
---
title: "Hello World"
description: "The first post of this blog"
date: 2021-03-14T15:00:21+01:00
draft: false
katex: true
---
I'm a .NET developer by trade, so let's say hello in C#!
```
## Configure the Tag Cloud
The theme comes with a tag cloud partial. It is included in the sidebar, but it
is disabled by default. If you wish to configure it, add the following to the
`[params]` section in the `config.toml` file:
```toml
[params.tagCloud]
enable = true
minFontSizeRem = 0.8
maxFontSizeRem = 2.0
```
## Remove the Sidebar
If you want to get rid of the sidebar, add an empty `data/en.json` file with the
following content:
```json
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
"basics": {},
"work": [],
"volunteer": [],
"education": [],
"awards": [],
"certificates": [],
"publications": [],
"skills": [],
"languages": [],
"interests": [],
"references": [],
"projects": [],
"meta": {
"canonical": "https://raw.githubusercontent.com/jsonresume/resume-schema/master/resume.json",
"version": "v1.0.0",
"lastModified": "2017-12-24T15:53:00"
}
}
```
## Extend CSS
The theme uses PostCSS with following plugins:
- [postcss-import](https://github.com/postcss/postcss-import)
- [postcss-url](https://github.com/postcss/postcss-url)
- [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting)
- [postcss-custom-media](https://github.com/postcss/postcss-custom-media)
Additionally the following plugins are used if building the site with
`hugo -e production`:
- [postcss-preset-env](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env)
- [cssnano](https://github.com/cssnano/cssnano) for minification
- [@fullhuman/postcss-purgecss](https://github.com/FullHuman/purgecss)
Inside the `assets/css` two folders exist, `critical` and `non-critical`. Files
inside `critical` are concatenated during build time and inlined into the
`<head>` element. The styles target mostly
[above the fold content](https://en.wikipedia.org/wiki/Above_the_fold#In_web_design).
Try to keep inline CSS to a minimum because it can't be cached and will be
inlined into every single page. Files inside `non-critical` are concatenated
into a single file and included as `<style>`. Most of the styles are in there.
Files are concatenated in lexicographic order of their file names. File names
start with two digits and a hyphen: `NN-`. The order of files might differ
between Linux and Windows, so using this convention improves cross-platform
compatibility.
[You might know this approach if you're familiar with Xorg](https://wiki.archlinux.org/title/Xorg#Using_.conf_files).
You can add new CSS files to the PostCSS pipeline like this:
- `critical/50-foo.css`
- `non-critical/05-bar.css`
- `non-critical/99-last.css`

View File

@@ -0,0 +1,10 @@
---
title: "{{ humanize .Name | title }}"
date: "{{ .Date }}"
draft: true
comments: false
socialShare: true
toc: false
cover:
src: cover.png
---

View File

@@ -0,0 +1,28 @@
/*
Critical CSS for above-the-fold content, delivered inline to increase first
paint performance
*/
/*! purgecss start ignore */
/* Typefaces */
@import "typeface-roboto-slab";
@import "typeface-fira-code";
/* Normalize */
@import "normalize.css/normalize.css";
/*! purgecss end ignore */
/*! CC BY-SA 3.0 License | https://stackoverflow.com/a/36118384/1154965 */
@keyframes blink {
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Default license header for non-vendor CSS source code that follows */
/*! MIT License | github.com/schnerring/hugo-theme-gruvbox */

View File

@@ -0,0 +1,9 @@
/*
Bootstrap 5 breakpoints
See: https://getbootstrap.com/docs/5.0/layout/breakpoints/#available-breakpoints
*/
@custom-media --sm (min-width: 576px);
@custom-media --md (min-width: 768px);
@custom-media --lg (min-width: 992px);
@custom-media --xl (min-width: 1200px);
@custom-media --xxl (min-width: 1400px);

View File

@@ -0,0 +1,49 @@
{{ $themeContrast := .Param "themeContrast" | default "medium" }}
{{ $backgroundColor := "bg0" }}
{{ if eq $themeContrast "soft" }}
{{ $backgroundColor = "bg0_s" }}
{{ else if eq $themeContrast "hard" }}
{{ $backgroundColor = "bg0_h" }}
{{ end }}
:root[data-theme="dark"] {
--bg: var(--{{ $backgroundColor }});
--bg0: #1a1b26;
--bg0_h: #1d2021;
--bg0_s: #32302f;
--bg1: #181922;
--bg2: #32344a;
--bg3: #665c54;
--bg4: #32344a;
--fg: var(--fg1);
--fg0: #a1a1a1;
--fg1: #dddfeb;
--fg2: #7da6ff;
--fg3: #6a6c67;
--fg4: #32344a;
--gray1: var(--fg4);
--gray2: #444b6a;
--red1: #f7768e;
--red2: #ff7a93;
--green1: #9ece6a;
--green2: #b9f27c;
--yellow1: #e0af68;
--yellow2: #ff9e64;
--blue1: #63A8D3;
--blue2: #63A8D3;
--purple1: #ad8ee6;
--purple2: #bb9af7;
--aqua1: #449dab;
--aqua2: #0db9d7;
--orange1: #d65d0e;
--orange2: #fe8019;
& .dark--hidden {
display: none;
}
}
:root {
--primary: var(--blue1);
--primary-alt: var(--blue2);
}

View File

@@ -0,0 +1,238 @@
:root {
--font-monospace: "Fira Code", "Lucida Console", Monaco, monospace;
--font-sans-serif: Verdana, Helvetica, sans-serif;
--font-serif: "Roboto Slab", Georgia, serif;
}
html {
font-family: var(--font-serif);
font-size: 1rem;
scroll-behavior: smooth;
}
body {
background: var(--bg);
color: var(--fg);
line-height: 1.675;
word-wrap: break-word;
}
strong {
letter-spacing: 0.35px;
}
a {
color: inherit;
text-decoration: none;
}
a.link--external::after {
/* 2009 = Thin Space */
content: "\2009↗";
}
img,
video {
border: 2px solid var(--bg1);
height: auto;
max-width: 100%;
}
figure {
display: inline-block;
}
figcaption {
color: var(--fg3);
font-family: var(--font-serif);
font-size: 0.9rem;
}
*::selection {
color: var(--fg0);
background: var(--bg4);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--fg0);
font-family: var(--font-monospace);
font-weight: 300;
line-height: 1.4;
& code {
font-size: 1em;
}
}
h2,
h3,
h4,
h5,
h6 {
border-bottom: 1px solid var(--bg1);
}
h1,
h2 {
font-weight: 400;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.625rem;
}
@media (--md) {
h1 {
font-size: 2.375rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: 1.5rem;
}
h5 {
font-size: 1.375rem;
}
h6 {
font-size: 1.25rem;
}
table {
table-layout: fixed;
border-collapse: collapse;
width: 100%;
margin: 2rem 0;
}
table,
th,
td {
border: 1px solid var(--bg1);
padding: 0.5rem;
}
hr {
border: none;
background: var(--bg1);
height: 1px;
margin: 3rem auto;
width: 80%;
}
mark {
background: var(--yellow1);
color: var(--bg0);
}
abbr {
text-underline-offset: 0.2rem;
}
blockquote,
code,
kbd,
mark,
pre {
border-radius: 0.2rem;
padding: 0 0.2em;
}
pre code {
padding: 0;
}
blockquote,
code,
kbd,
pre,
th {
background: var(--bg1);
}
code,
kbd,
pre,
th {
font-family: var(--font-monospace);
}
code,
kbd {
& code,
& kbd {
background: var(--bg2);
}
}
blockquote,
pre {
padding: 1rem;
}
pre {
/* TODO is !important really needed because of Prism? */
background: var(--bg1) !important;
overflow: auto;
& code {
background: none;
}
}
blockquote,
blockquote.twitter-tweet {
border-left: var(--primary-alt) 5px solid;
margin: 0.5rem 0;
& code {
background: var(--bg2);
}
& p:first-of-type {
margin-top: 0;
}
& p:last-of-type {
margin-bottom: 0;
}
}
blockquote.twitter-tweet {
border-color: var(--blue2);
color: inherit;
font: inherit;
font-size: inherit;
line-height: inherit;
& a {
color: var(--blue2);
}
& a:hover {
color: var(--blue1);
text-decoration: none !important;
}
}

View File

@@ -0,0 +1,14 @@
pre::-webkit-scrollbar {
height: 0.5rem;
scrollbar-width: auto;
}
pre::-webkit-scrollbar-track {
background: var(--bg2);
border-radius: 0.2rem;
}
pre::-webkit-scrollbar-thumb {
background: var(--bg4);
border-radius: 0.2rem;
}

View File

@@ -0,0 +1,48 @@
.layout {
display: grid;
grid-template-areas:
"header"
"main"
"footer";
grid-template-rows: auto 1fr auto;
height: 100vh;
}
main {
align-items: start;
display: grid;
grid-area: main;
grid-template-areas: "empty content sidebar";
grid-template-columns: 2fr minmax(0, 860px) 2fr;
}
header {
background: var(--bg1);
grid-area: header;
}
footer {
grid-area: footer;
}
main,
footer {
margin: 0.5em 1.1em;
}
.content {
grid-area: content;
}
.sidebar {
display: none;
flex-direction: column;
grid-area: sidebar;
margin-top: 3rem;
position: sticky;
top: 2rem;
@media (--lg) {
display: flex;
}
}

View File

@@ -0,0 +1,285 @@
header {
display: grid;
font-family: var(--font-monospace);
font-size: 1.125rem;
grid-template-columns: auto auto 1fr auto;
grid-template-areas: "heading search nav theme-toggle";
padding: 0.75rem;
}
.logo {
color: var(--fg0);
display: flex;
font-weight: 700;
grid-area: heading;
&:hover .logo__cursor {
animation: 1s blink infinite;
opacity: 1;
}
}
.logo__chevron,
.logo__cursor {
margin-left: 0.5rem;
}
.logo__cursor {
opacity: 0;
}
.logo__text {
display: none;
}
@media (--md) {
.logo__text {
display: block;
}
}
/*! purgecss start ignore */
.search {
display: flex;
grid-area: search;
margin: 0 1rem;
}
#search__text {
border: 1px solid var(--bg2);
border-radius: 0.2rem;
background: var(--bg2);
caret-color: var(--fg);
color: var(--fg);
outline: none;
padding: 0 0.5rem;
width: 100%;
&:hover {
border-color: var(--bg3);
}
&:focus {
border-color: var(--bg4);
}
&::placeholder {
color: var(--fg1);
}
&[type="search"]::-webkit-search-cancel-button {
appearance: none;
}
}
#search__suggestions {
background: var(--bg);
border-radius: 0.2rem;
box-shadow: 0 0.5rem 1rem var(--bg1);
font-family: var(--font-serif);
left: 0;
margin-top: 2rem;
position: absolute;
width: 95vw;
z-index: 1000;
}
@media (--md) {
.search {
position: relative;
}
#search__suggestions {
width: 60vw;
}
}
.search__suggestions--hidden {
display: none;
}
.search__suggestion-item {
border-bottom: 1px dashed var(--bg2);
display: grid;
grid-template-columns: 1fr 2fr;
&:focus,
&:focus-visible,
&:hover {
background: var(--bg1);
cursor: pointer;
outline: none;
}
&:last-child {
border: none;
}
}
.search__suggestion-title,
.search__suggestion-description {
padding: 0 1rem;
margin: 1rem 0;
}
.search__suggestion-title {
font-weight: 700;
}
.search__suggestion-description {
border-left: 1px solid var(--bg2);
}
.search__no-results {
padding: 0.75rem;
}
/*! purgecss end ignore */
.theme__toggle {
align-items: center;
background: none;
border: none;
color: var(--yellow1);
cursor: pointer;
display: flex;
grid-area: theme-toggle;
margin: 0 1rem;
&:hover {
color: var(--yellow2);
}
& svg {
height: 28px;
width: 28px;
}
}
/* TODO: simplify deep nesting */
nav#menu {
align-items: center;
display: flex;
grid-area: nav;
justify-content: flex-end;
& .menu__item {
color: var(--fg);
&:hover {
color: var(--fg3);
cursor: pointer;
}
}
& ul {
list-style: none;
margin: 0;
padding: 0;
}
& ul.menu--horizontal {
align-items: center;
display: none;
& li {
display: inline-block;
margin: 0 0.75rem;
}
@media (--md) {
display: flex;
}
}
& ul.menu--vertical {
background: var(--bg1);
bottom: 0;
margin: 0;
padding: 3rem;
position: fixed;
right: 0;
top: 0;
transform: translate(100%, 0);
transition: transform 0.5s cubic-bezier(0.9, 0, 0.1, 1);
width: 50%;
z-index: 10;
& .menu__item {
color: var(--fg1);
&:hover {
color: var(--fg3);
}
}
}
& .menu__burger {
display: flex;
height: 24px;
width: 24px;
& > * {
position: absolute;
}
& svg {
width: inherit;
z-index: 20;
height: inherit;
& line {
transition-duration: 0.5s;
transition-property: stroke, opacity, transform;
transition-timing-function: cubic-bezier(0.9, 0, 0.1, 1);
}
& line:nth-of-type(1) {
transform-origin: center 6px;
}
& line:nth-of-type(2) {
transform-origin: center 12px;
}
& line:nth-of-type(3) {
transform-origin: center 18px;
}
}
& input {
height: inherit;
opacity: 0;
width: inherit;
z-index: 30;
&:checked {
& ~ ul.menu--vertical {
transform: none;
}
& ~ svg {
stroke: var(--fg1);
& line:nth-of-type(1) {
transform: translate(0, 6px) rotate(45deg);
}
& line:nth-of-type(2) {
opacity: 0;
transform: scale(0.2);
}
& line:nth-of-type(3) {
transform: translate(0, -6px) rotate(-45deg);
}
}
}
}
@media (--md) {
display: none;
}
}
}

View File

@@ -0,0 +1,70 @@
.sidebar {
font-family: var(--font-monospace);
max-width: 350px;
margin-left: auto;
margin-right: auto;
padding-left: 2.5rem;
& hr {
margin: 1.5rem auto;
}
& svg {
fill: var(--fg);
}
}
.sidebar__heading {
font-size: 1.3rem;
}
aside.toc {
& a {
color: var(--primary-alt);
}
& a:hover {
color: var(--primary);
}
& ul {
list-style: none;
margin: 0;
padding: 0;
& ul {
font-size: 0.9rem;
margin-left: 0.5rem;
}
& li {
line-height: 1.1;
& a {
display: block;
padding: 0.2rem 0;
}
}
}
}
.jr-basics__image {
background: var(--bg1);
border: 2px solid var(--bg2);
}
.jr-basics__summary {
color: var(--fg3);
font-family: var(--font-serif);
margin: 0.75rem 0;
}
.jr-basics__profile {
& a:hover {
color: var(--fg3);
& svg {
fill: var(--fg3);
}
}
}

View File

@@ -0,0 +1,76 @@
.post,
.content-section {
border-bottom: 2px dotted var(--bg1);
padding: 0rem 0;
}
.post {
& figure,
& img:not(figure img),
& video:not(figure video) {
margin: 0.5rem 0;
box-sizing: border-box;
}
}
.post-header,
.post-content__read-more {
font-family: var(--font-monospace);
}
.post-meta__author {
font-weight: 700;
}
.post-content {
margin: 1.3rem 0;
}
.post-content__read-more {
color: var(--primary-alt);
margin-top: 1.3rem;
}
.content-section,
.post-header,
.post-content {
& a {
color: var(--primary-alt);
}
& a:hover {
color: var(--primary);
}
}
.post-tags {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.9rem;
margin: 1rem 0;
}
.post-tag {
font-size: 0.9rem;
line-height: 1;
&::before {
content: "#";
}
}
.post-heading__anchor {
display: none;
}
h1:hover,
h2:hover,
h3:hover,
h4:hover,
h5:hover,
h6:hover {
& .post-heading__anchor {
display: inline-block;
}
}

View File

@@ -0,0 +1,24 @@
/*! purgecss start ignore */
{{ range $.Site.Params.prism.plugins }}
{{ $path := printf "prismjs/plugins/%s/prism-%s.css" . . }}
{{ $pluginCSS := resources.Get $path }}
{{ if $pluginCSS }}
{{ $importPath := trim $pluginCSS.RelPermalink "/" }}
{{ printf "/* Import stylesheet for Prism plugin '%s' from '%s' */" . $importPath }}
@import "{{ $importPath }}";
{{ end }}
{{ end }}
/* Prism Font */
code,
kbd,
code[class*="language-"],
pre[class*="language-"] {
font-family: var(--font-monospace);
}
/*! purgecss end ignore */
/* Default license header for non-vendor CSS source code that follows */
/*! MIT License | github.com/schnerring/hugo-theme-gruvbox */

View File

@@ -0,0 +1,9 @@
/*
Bootstrap 5 breakpoints
See: https://getbootstrap.com/docs/5.0/layout/breakpoints/#available-breakpoints
*/
@custom-media --sm (min-width: 576px);
@custom-media --md (min-width: 768px);
@custom-media --lg (min-width: 992px);
@custom-media --xl (min-width: 1200px);
@custom-media --xxl (min-width: 1400px);

View File

@@ -0,0 +1,11 @@
footer {
align-items: center;
color: var(--fg3);
display: flex;
font-family: var(--font-monospace);
font-size: 0.8rem;
justify-content: center;
padding-bottom: 0.5rem;
padding-top: 2rem;
text-align: center;
}

View File

@@ -0,0 +1,18 @@
.pagination {
display: flex;
margin-top: 2rem;
}
.pagination__button {
color: var(--primary-alt);
font-family: var(--font-monospace);
font-size: 1.125rem;
}
.pagination__button:hover {
color: var(--primary);
}
.pagination__button--next {
margin-left: auto;
}

View File

@@ -0,0 +1,46 @@
function getTheme() {
if (localStorage && localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
return "auto";
}
function saveTheme(theme) {
localStorage.setItem("theme", theme);
}
function setPrismTheme(theme) {
const prismDark = document.getElementById("prism-dark");
const prismLight = document.getElementById("prism-light");
prismDark.toggleAttribute("disabled", theme === "light");
prismLight.toggleAttribute("disabled", theme === "dark");
}
function setCommentsTheme(theme) {
if (document.querySelector(".utterances-frame")) {
const iframe = document.querySelector(".utterances-frame");
var message = {
type: "set-theme",
theme: theme == "dark" ? "gruvbox-dark" : "github-light",
};
iframe.contentWindow.postMessage(message, "https://utteranc.es");
}
}
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
setPrismTheme(theme);
setCommentsTheme(theme);
}
setTheme("dark");
// This script is inlined in the <head> of the document, so we have to wait
// for the DOM content before can add event listeners to the toggle buttons
document.addEventListener("DOMContentLoaded", function () {
const toggleButtons = document.querySelectorAll(".theme__toggle");
toggleButtons.forEach((btn) => {
btn.addEventListener("click", toggleTheme);
});
});

View File

@@ -0,0 +1,137 @@
//! Source: https://github.com/h-enk/doks/blob/master/assets/js/index.js
import { Document } from "flexsearch";
const search = document.getElementById("search__text");
const suggestions = document.getElementById("search__suggestions");
if (search !== null) {
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.key === "/") {
// Focus search bar with CTRL + /
e.preventDefault();
search.focus();
} else if (e.key === "Escape") {
// Unfocus search bar with ESC
search.blur();
suggestions.classList.add("search__suggestions--hidden");
}
});
}
document.addEventListener("click", (e) => {
const clickInsideSuggestions = suggestions.contains(e.target);
if (!clickInsideSuggestions) {
// Hide search suggestions if clicking elsewhere
suggestions.classList.add("search__suggestions--hidden");
}
});
/*! Source: https://dev.to/shubhamprakash/trap-focus-using-javascript-6a3 */
document.addEventListener("keydown", (e) => {
const suggestionsHidden = suggestions.classList.contains(
"search__suggestions--hidden"
);
if (suggestionsHidden) return;
const focusableSuggestions = [...suggestions.querySelectorAll("a")];
if (focusableSuggestions.length === 0) return;
const currentIndex = focusableSuggestions.indexOf(document.activeElement);
if (e.key === "ArrowDown") {
// Focus next suggestion
e.preventDefault();
const nextIndex =
currentIndex + 1 < focusableSuggestions.length
? currentIndex + 1
: currentIndex;
focusableSuggestions[nextIndex].focus();
} else if (e.key === "ArrowUp") {
// Focus previous suggestion
e.preventDefault();
nextIndex = currentIndex > 0 ? currentIndex - 1 : 0;
focusableSuggestions[nextIndex].focus();
}
});
(function () {
const index = new Document({
tokenize: "forward",
cache: 100,
document: {
id: "id",
store: ["href", "title", "description"],
index: ["title", "description", "content"],
},
});
//! Source: https://discourse.gohugo.io/t/range-length-or-last-element/3803/2
{{ $list := (where .Site.RegularPages "Type" "in" .Site.Params.mainSections) }}
{{ $len := (len $list) }}
index.add(
{{ range $index, $element := $list }}
{
id: {{ $index }},
href: "{{ .RelPermalink }}",
title: {{ .Title | jsonify }},
{{ with .Description }}
description: {{ . | jsonify }},
{{ else }}
description: {{ .Summary | plainify | jsonify }},
{{ end }}
content: {{ .Plain | jsonify }}
})
{{ if ne (add $index 1) $len }}
.add(
{{ end }}
{{ end }}
{{ if eq 0 $len }}
)
{{ end }}
;
search.addEventListener("input", function () {
const maxResultsCount = {{ $.Site.Params.flexsearch.maxResultsCount | default 5 }};
const searchText = this.value;
const searchResults = index.search(searchText, maxResultsCount, { enrich: true });
const searchResultsMap = new Map();
// Deduplicate search results by href
for (const searchResult of searchResults.flatMap((r) => r.result)) {
if (searchResultsMap.has(searchResult.href)) continue;
searchResultsMap.set(searchResult.doc.href, searchResult.doc);
}
suggestions.innerHTML = "";
suggestions.classList.remove("search__suggestions--hidden");
if (searchResultsMap.size === 0 && searchText) {
const noResultsMessage = document.createElement("div")
noResultsMessage.innerHTML = `No results for "<strong>${searchText}</strong>"`
noResultsMessage.classList.add("search__no-results");
suggestions.appendChild(noResultsMessage);
return;
}
for (const [href, searchResult] of searchResultsMap) {
const suggestion = document.createElement("a");
suggestion.href = href;
suggestion.classList.add("search__suggestion-item");
suggestions.appendChild(suggestion);
const title = document.createElement("div");
title.textContent = searchResult.title;
title.classList.add("search__suggestion-title");
suggestion.appendChild(title);
const description = document.createElement("div");
description.textContent = searchResult.description;
description.classList.add("search__suggestion-description");
suggestion.appendChild(description);
if (suggestions.childElementCount === maxResultsCount) break;
}
});
})();

View File

@@ -0,0 +1,11 @@
import Prism from "prismjs";
{{ range $.Site.Params.prism.languages }}
import "prismjs/components/prism-{{ . }}";
{{ end }}
{{ range $.Site.Params.prism.plugins }}
import "prismjs/plugins/{{ . }}/prism-{{ . }}";
{{ end }}
Prism.highlightAll();

View File

@@ -0,0 +1,2 @@
noJSConfigInAssets = true
writeStats = true

View File

@@ -0,0 +1,150 @@
baseURL = "http://localhost"
copyright = "Copyright © 2021"
title = "hugo-theme-gruvbox"
#paginate = 10
enableRobotsTXT = true
# Enable to calculate the last modified date from Git history and show it in the post header
#enableGitInfo = true
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
# This setting allows inlining <script> and <style> tags in markdown,
# which is useful and required to use Prism plugins, but may be dangerous
# if the content isn't trustworthy.
unsafe = true
[imaging]
# JPEG and WebP image processing quality, defaults to 75%
#quality = 75
[params]
# dark or light, defaults to light. Local storage and OS preference override this param. For more details see:
# https://github.com/schnerring/hugo-theme-gruvbox/issues/34#issuecomment-1235870375
#defaultTheme = "light"
# gray red green yellow blue purple aqua orange, defaults to blue
#themeColor = "blue"
# soft medium hard, defaults to medium
#themeContrast = "medium"
author = "Michael Schnerring"
subtitle = "Theme Demo"
description = "A retro-looking Hugo theme inspired by gruvbox. The pastel colors are high contrast, easily distinguishable, pleasing to the eye, and feature light and dark color palettes."
[params.logo]
text = "gruvbox"
url = "/"
[params.prism]
languages = [
"markup",
"css",
"clike",
"javascript",
"bash",
"diff",
"toml"
]
plugins = [
"normalize-whitespace",
"toolbar",
"copy-to-clipboard",
"line-numbers",
"command-line",
"diff-highlight"
]
# By default, the theme creates resized versions of images ranging from 300 to
# 700 pixels wide in increments of 200 pixels
#[params.imageResize]
# min = 300
# max = 700
# increment = 200
[params.tagCloud]
enable = false
minFontSizeRem = 0.8
maxFontSizeRem = 2.0
# Social share links for posts:
# - iconSuite: "simple-icon" or "tabler-icon"
# - iconName: name of the icon from the "iconSuite"
# - {url} placeholder for post .Permalink
# - {title} placeholder for post .Title
# See https://github.com/bradvin/social-share-urls for more format strings
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "facebook"
formatString = "https://www.facebook.com/sharer.php?u={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "reddit"
formatString = "https://reddit.com/submit?url={url}&title={title}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "twitter"
formatString = "https://twitter.com/intent/tweet?url={url}&text={title}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "linkedin"
formatString = "https://www.linkedin.com/sharing/share-offsite/?url={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "whatsapp"
formatString = "whatsapp://send/?text={title}%20{url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "tumblr"
formatString = "https://www.tumblr.com/widgets/share/tool?canonicalUrl={url}&title={title}&caption={title}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "pinterest"
formatString = "http://pinterest.com/pin/create/button/?url={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "ycombinator"
formatString = "https://news.ycombinator.com/submitlink?u={url}&t={title}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "vk"
formatString = "http://vk.com/share.php?url={url}&title={title}&comment={title}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "xing"
formatString = "https://www.xing.com/spi/shares/new?url={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "telegram"
formatString = "https://telegram.me/share/url?url={url}&text={title}"
[[params.socialShare]]
iconSuite = "tabler-icon"
iconName = "mail"
formatString = "mailto:?subject={title}&body={url}"
[menu]
[[menu.main]]
identifier = "blog"
name = "Blog"
url = "/blog"
weight = 10
[[menu.main]]
identifier = "cv"
name = "CV"
url = "/cv"
weight = 20
[[menu.main]]
identifier = "about"
name = "About"
url = "/about"
weight = 30
[languages]
[languages.en]
languageName = "English"
weight = 10
[languages.en.params.jsonResume]
present = "present"
[languages.de]
languageName = "Deutsch"
weight = 20
[languages.de.params.jsonResume]
present = "heute"

Some files were not shown because too many files have changed in this diff Show More