congo theme

master
Jeff Clement 6 months ago
parent 4f2ee5f0e6
commit 1a09ddba60
Signed by: jclement
GPG Key ID: 3BCB43A3F0E1D7DA
  1. 8
      .drone.yml
  2. 1
      .gitignore
  3. 0
      .hugo_build.lock
  4. 12
      Dockerfile
  5. 87
      Gruntfile.js
  6. 1
      README.md
  7. 7
      assets/css/custom.css
  8. 0
      assets/img/author.png
  9. 141
      config.toml
  10. 13
      config/_default/config.toml
  11. 62
      config/_default/languages.en.toml
  12. 13
      config/_default/markup.toml
  13. 61
      config/_default/menus.en.toml
  14. 2
      config/_default/module.toml
  15. 62
      config/_default/params.toml
  16. 2
      config/_default/taxonomies.toml
  17. 14
      content/_index.md
  18. 8
      content/about/index.md
  19. 4
      content/categories/_index.md
  20. 5
      content/categories/tutorial/_index.md
  21. 61
      content/post/2015/gpg-smartcard/index.md
  22. 9
      content/post/2016/kub-kar-timer/index.md
  23. 1
      content/post/2017/qubes-os-presentation/index.md
  24. 151
      content/post/2018/docker-ghost/index.md
  25. BIN
      content/post/2018/docker-ghost/ssl.png
  26. 5
      content/post/2018/gpg-yubikey5/index.md
  27. 1
      content/post/2018/nginx-semiprivate/index.md
  28. 6
      content/post/2019/blog-hugo-gitlab/index.md
  29. 1
      content/post/2019/ubuntu-18-04-encrypted-disks-with-usb-boot/index.md
  30. 1
      content/post/2019/wireguard-access-server/index.md
  31. 1
      content/post/2019/yubikey-setup/index.md
  32. 11
      content/post/2020/nginx_golang_react_sockets/index.md
  33. 4
      content/post/2020/tor-relay/index.md
  34. 20
      content/post/2022/syncthing/index.md
  35. 9
      content/post/_index.md
  36. 18
      content/post/templates/2015-01-04-first-post.md
  37. 49
      content/post/templates/index.md
  38. 10
      content/projects/_index.md
  39. 7
      content/projects/werdz/index.md
  40. 18
      content/services/_index.md
  41. 4
      content/tags/_index.md
  42. 5
      content/tags/privacy/_index.md
  43. 5
      go.mod
  44. 2
      go.sum
  45. 8
      layouts/partials/home/custom.html
  46. 6
      layouts/shortcodes/info.html
  47. 14
      layouts/shortcodes/note.html
  48. 14
      layouts/shortcodes/swatches.html
  49. 6
      layouts/shortcodes/tip.html
  50. 6
      layouts/shortcodes/warning.html
  51. BIN
      static/android-chrome-192x192.png
  52. BIN
      static/android-chrome-512x512.png
  53. BIN
      static/apple-touch-icon.png
  54. 881
      static/css/main.css
  55. 59
      static/css/syntax.css
  56. BIN
      static/favicon-16x16.png
  57. BIN
      static/favicon-32x32.png
  58. BIN
      static/favicon.ico
  59. BIN
      static/img/favicon.ico
  60. 1
      static/site.webmanifest
  61. 1
      themes/hugo-coder
  62. 17
      themes/jeff/.gitattributes
  63. 50
      themes/jeff/.gitignore
  64. 22
      themes/jeff/LICENSE
  65. 9
      themes/jeff/archetypes/default.md
  66. 137
      themes/jeff/data/beautifulhugo/social.toml
  67. 109
      themes/jeff/exampleSite/config.toml
  68. 2
      themes/jeff/exampleSite/content/_index.md
  69. 16
      themes/jeff/exampleSite/content/page/about.md
  70. 6
      themes/jeff/exampleSite/content/post/2015-01-04-first-post.md
  71. 6
      themes/jeff/exampleSite/content/post/2015-01-15-pirates.md
  72. 11
      themes/jeff/exampleSite/content/post/2015-01-19-soccer.md
  73. 6
      themes/jeff/exampleSite/content/post/2015-01-27-dear-diary.md
  74. 41
      themes/jeff/exampleSite/content/post/2015-02-13-hamlet-monologue.md
  75. 35
      themes/jeff/exampleSite/content/post/2015-02-20-test-markdown.md
  76. 14
      themes/jeff/exampleSite/content/post/2015-02-26-flake-it-till-you-make-it.md
  77. 42
      themes/jeff/exampleSite/content/post/2016-03-08-code-sample.md
  78. 49
      themes/jeff/exampleSite/content/post/2017-03-05-math-sample.md
  79. 40
      themes/jeff/exampleSite/content/post/2017-03-07-bigimg-sample.md
  80. 37
      themes/jeff/exampleSite/content/post/2017-03-20-photoswipe-gallery-sample.md
  81. 7
      themes/jeff/exampleSite/layouts/partials/footer_custom.html
  82. 18
      themes/jeff/exampleSite/layouts/partials/head_custom.html
  83. 0
      themes/jeff/exampleSite/static/.gitkeep
  84. 74
      themes/jeff/i18n/br.yaml
  85. 74
      themes/jeff/i18n/de.yaml
  86. 74
      themes/jeff/i18n/en.yaml
  87. 74
      themes/jeff/i18n/eo.yaml
  88. 74
      themes/jeff/i18n/es.yaml
  89. 74
      themes/jeff/i18n/fr.yaml
  90. 74
      themes/jeff/i18n/it.yaml
  91. 74
      themes/jeff/i18n/ja.yaml
  92. 74
      themes/jeff/i18n/nb.yaml
  93. 74
      themes/jeff/i18n/nl.yaml
  94. 74
      themes/jeff/i18n/pl.yaml
  95. 74
      themes/jeff/i18n/ru.yaml
  96. 74
      themes/jeff/i18n/zh-CN.yaml
  97. 74
      themes/jeff/i18n/zh-TW.yaml
  98. BIN
      themes/jeff/images/blue.png
  99. BIN
      themes/jeff/images/screenshot.png
  100. BIN
      themes/jeff/images/tn.png
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,14 +13,6 @@ steps:
- echo "Checking Hugo version."
- hugo version
- name: Build Index
image: node:lts-alpine
commands:
- npm install --location=global grunt
- npm install grunt string yamljs
- grunt lunr-index
- pwd
- name: Build
image: jguyomard/hugo-builder
commands:

1
.gitignore vendored

@ -1,4 +1,5 @@
public/
resources/
node_modules/
package-lock.json
.DS_Store

@ -1,12 +0,0 @@
FROM node:lts-alpine as build-step
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY . ./
RUN npm install grunt string yamljs
RUN grunt lunr-index
RUN apk add hugo
RUN hugo
FROM caddy:alpine
EXPOSE 80
COPY --from=build-step /app/public /usr/share/caddy

@ -1,87 +0,0 @@
var yaml = require("yamljs");
var S = require("string");
var CONTENT_PATH_PREFIX = "content";
module.exports = function(grunt) {
grunt.registerTask("lunr-index", function() {
grunt.log.writeln("Build pages index");
var indexPages = function() {
var pagesIndex = [];
grunt.file.recurse(CONTENT_PATH_PREFIX, function(abspath, rootdir, subdir, filename) {
grunt.verbose.writeln("Parse file:",abspath);
var processedFile = processFile(abspath, filename);
if (processedFile) {
pagesIndex.push(processedFile);
}
});
return pagesIndex;
};
var processFile = function(abspath, filename) {
var pageIndex;
if (S(filename).endsWith(".html")) {
pageIndex = processHTMLFile(abspath, filename);
} else if (S(filename).endsWith(".md")) {
pageIndex = processMDFile(abspath, filename);
}
return pageIndex;
};
var processHTMLFile = function(abspath, filename) {
var content = grunt.file.read(abspath);
var pageName = S(filename).chompRight(".html").s;
var href = S(abspath)
.chompLeft(CONTENT_PATH_PREFIX).s;
return {
title: pageName,
href: href,
content: S(content).trim().stripTags().stripPunctuation().s
};
};
var processMDFile = function(abspath, filename) {
var content = grunt.file.read(abspath);
var pageIndex;
// First separate the Front Matter from the content and parse it
content = content.split("---");
var frontMatter;
try {
frontMatter = yaml.parse(content[1].trim());
} catch (e) {
grunt.log.writeln(filename + " - " + e.message);
return;
}
var href = S(abspath).chompLeft(CONTENT_PATH_PREFIX).chompRight(".md").s;
// href for index.md files stops at the folder name
if (filename === "index.md") {
href = S(abspath).chompLeft(CONTENT_PATH_PREFIX).chompRight(filename).s;
}
// Skip drafts
if (frontMatter.draft) {
return;
}
// Build Lunr index for this page
pageIndex = {
title: frontMatter.title,
tags: frontMatter.tags,
href: href,
content: S(content[2]).trim().stripTags().stripPunctuation().s
};
return pageIndex;
};
grunt.file.write("public/lunr.json", JSON.stringify(indexPages()));
grunt.log.ok("Index built");
});
};

@ -1 +0,0 @@
........

@ -0,0 +1,7 @@
.max-w-prose, .prose {
max-width: 100%;
}
#TableOfContents {
min-width: 300px;
}

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

@ -1,141 +0,0 @@
baseurl = "https://blog-dev.erraticbits.ca"
contentdir = "content"
layoutdir = "layouts"
publishdir = "public"
title = "erraticbits"
canonifyurls = true
DefaultContentLanguage = "en"
theme = "jeff"
metaDataFormat = "yaml"
#disqusShortname = "zeos-ca"
#googleAnalytics = "XXX"
pygmentsUseClasses = true
pygmentsCodeFences = true
pygmentsCodefencesGuessSyntax = false
[BlackFriday]
smartypants = false
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
[markup.tableOfContents]
endLevel = 3
startLevel = 1
[Params]
subtitle = "Code. Privacy. Wood. Unicycles. Radio."
readingTime = true
hideAuthor = true
logo = "img/avatar-icon.png"
favicon = "img/favicon.ico"
dateFormat = "January 2, 2006"
commit = false
selfHosted = true
rss = true
comments = true
#gcse = "002888195400749182309:cakccbp7nyf"
[[Params.bigimg]]
src = "img/blue.png"
[[Params.bigimg]]
src = "img/green.png"
[[Params.bigimg]]
src = "img/red.png"
[[Params.bigimg]]
src = "img/yellow.png"
#[[Params.bigimg]]
# src = "img/triangle.jpg"
#desc = "Triangle"
#[[Params.bigimg]]
# src = "img/sphere.jpg"
# desc = "Sphere"
#[[Params.bigimg]]
# src = "img/hexagon.jpg"
# desc = "Hexagon"
[Author]
name = "OneWheelGeek"
#email = "NA"
#facebook = "username"
#googleplus = "+username" # or xxxxxxxxxxxxxxxxxxxxx
#gitlab = "jeff"
github = "jclement"
twitter = "OneWheelGeek"
#reddit = "OneWheelGeek"
#linkedin = "username"
#xing = "username"
#stackoverflow = "users/XXXXXXX/username"
#snapchat = "username"
#instagram = "username"
#youtube = "user/username" # or channel/channelname
#soundcloud = "username"
#spotify = "username"
#bandcamp = "username"
#itchio = "username"
mastodon = "onewheelgeek@mastodon.social"
keybase = "jsc"
[[menu.main]]
name = "Blog"
url = ""
weight = 1
# [[menu.main]]
# identifier = "samples"
# name = "Samples"
# weight = 2
# [[menu.main]]
# parent = "samples"
# name = "Big Image Sample"
# url = "post/2017-03-07-bigimg-sample"
# weight = 1
# [[menu.main]]
# parent = "samples"
# name = "Math Sample"
# url = "post/2017-03-05-math-sample"
# weight = 2
# [[menu.main]]
# parent = "samples"
# name = "Code Sample"
# url = "post/2016-03-08-code-sample"
# weight = 3
#[[menu.main]]
# identifier = "projects"
# name = "Projects"
# weight = 2
[[menu.main]]
identifier = "projects"
name = "Projects"
weight = 4
[[menu.main]]
parent = "projects"
name = "Werdz"
url = "https://werdz.ca"
weight = 1
[[menu.main]]
name = "Github"
url = "https://github.com/jclement"
weight = 3
[[menu.main]]
name = "About"
url = "/about/"
weight = 80
#[[menu.main]]
# name = "Tags"
# url = "tags"
# weight = 90

@ -0,0 +1,13 @@
# -- Site Configuration --
# Refer to the theme docs for more details about each of these parameters.
# https://jpanther.github.io/congo/docs/getting-started/
baseURL = "https://blog-dev.erraticbits.ca/"
defaultContentLanguage = "en"
enableRobotsTXT = true
paginate = 10
summaryLength = 0
[outputs]
home = ["HTML", "RSS", "JSON"]

@ -0,0 +1,62 @@
languageCode = "en"
languageName = "English"
displayName = "EN"
isoCode = "en"
weight = 1
rtl = false
title = "Erraticbits"
# logo = "img/avatar-icon.png"
# description = "My awesome website"
copyright = ""
dateFormat = "2 January 2006"
[author]
name = "Jeff Clement"
image = "img/author.png"
#headline = "Code, Wood, Unicycles and Radio"
#bio = "A little bit about you"
links = [
{ email = "mailto:jeff@erraticbits.ca" },
# { link = "https://link-to-some-website.com/" },
# { amazon = "https://www.amazon.com/hz/wishlist/ls/wishlist-id" },
# { apple = "https://www.apple.com" },
# { blogger = "https://username.blogspot.com/" },
# { codepen = "https://codepen.io/username" },
# { dev = "https://dev.to/username" },
# { discord = "https://discord.gg/invitecode" },
# { dribbble = "https://dribbble.com/username" },
# { facebook = "https://facebook.com/username" },
# { flickr = "https://www.flickr.com/photos/username/" },
# { foursquare = "https://foursquare.com/username" },
# { github = "https://github.com/jclement" },
# { gitlab = "https://gitlab.com/username" },
# { google = "https://www.google.com/" },
# { hashnode = "https://username.hashnode.dev" },
# { instagram = "https://instagram.com/username" },
{ keybase = "https://keybase.io/jsc" },
# { kickstarter = "https://www.kickstarter.com/profile/username" },
# { lastfm = "https://lastfm.com/user/username" },
{ linkedin = "https://www.linkedin.com/profile/view?id=6394933" },
{ mastodon = "https://mastodon.social/@OneWheelGeek" },
# { medium = "https://medium.com/username" },
# { microsoft = "https://www.microsoft.com/" },
# { orcid = "https://orcid.org/userid" },
# { patreon = "https://www.patreon.com/username" },
# { pinterest = "https://pinterest.com/username" },
# { reddit = "https://reddit.com/user/username" },
# { researchgate = "https://www.researchgate.net/profile/username" },
# { slack = "https://workspace.url/team/userid" },
# { snapchat = "https://snapchat.com/add/username" },
# { soundcloud = "https://soundcloud.com/username" },
# { stack-overflow = "https://stackoverflow.com/users/userid/username" },
# { steam = "https://steamcommunity.com/profiles/userid" },
{ telegram = "https://t.me/OneWheelGeek" },
# { tiktok = "https://tiktok.com/@username" },
# { tumblr = "https://username.tumblr.com" },
# { twitch = "https://twitch.tv/username" },
{ twitter = "https://twitter.com/OneWheelGeek" },
# { whatsapp = "https://wa.me/phone-number" },
# { youtube = "https://youtube.com/username" },
]

@ -0,0 +1,13 @@
# -- Markup --
# These settings are required for the theme to function.
[goldmark]
[goldmark.renderer]
unsafe = true
[highlight]
noClasses = false
[tableOfContents]
startLevel = 2
endLevel = 4

@ -0,0 +1,61 @@
# -- Main Menu --
# The main menu is displayed in the header at the top of the page.
# Acceptable parameters are name, pageRef, page, url, title, weight.
#
# The simplest menu configuration is to provide:
# name = The name to be displayed for this menu link
# pageRef = The identifier of the page or section to link to
#
# By default the menu is ordered alphabetically. This can be
# overridden by providing a weight value. The menu will then be
# ordered by weight from lowest to highest.
[[main]]
name = "About"
pageRef = "about"
weight = 5
[[main]]
name = "Writings"
pageRef = "post"
weight = 10
[[main]]
name = "Projects"
pageRef = "projects"
weight = 40
[[main]]
name = "Code"
url = "https://git.erraticbits.ca/jclement"
weight = 50
[[main]]
name = "Photos"
url = "https://photos.erraticbits.ca/"
weight = 55
#[[main]]
# name = "Categories"
# pageRef = "categories"
# weight = 20
#[[main]]
# name = "Tags"
# pageRef = "tags"
# weight = 30
# -- Footer Menu --
# The footer menu is displayed at the bottom of the page, just before
# the copyright notice. Configure as per the main menu above.
[[footer]]
name = "Tags"
pageRef = "tags"
weight = 10
[[footer]]
name = "Categories"
pageRef = "categories"
weight = 20

@ -0,0 +1,2 @@
[[imports]]
path = "github.com/jpanther/congo/v2"

@ -0,0 +1,62 @@
# -- Theme Options --
# These options control how the theme functions and allow you to
# customise the display of your website.
#
# Refer to the theme docs for more details about each of these parameters.
# https://jpanther.github.io/congo/docs/configuration/#theme-parameters
colorScheme = "fire"
defaultAppearance = "light" # valid options: light or dark
autoSwitchAppearance = true
showAppearanceSwitcher = true
enableSearch = true
enableCodeCopy = true
mainSections = ["post"]
# robots = ""
showScrollToTop = true
[homepage]
layout = "profile" # valid options: page, profile, custom
showRecent = true
[article]
showDate = true
showDateUpdated = false
showAuthor = false
showBreadcrumbs = false
showDraftLabel = true
showEdit = false
# editURL = "https://github.com/username/repo/"
editAppendPath = true
showHeadingAnchors = true
showPagination = true
invertPagination = false
showReadingTime = true
showTableOfContents = false
showTaxonomies = true
showWordCount = false
# sharingLinks = ["facebook", "twitter", "pinterest", "reddit", "linkedin", "email"]
[list]
showBreadcrumbs = false
showSummary = false
showTableOfContents = false
groupByYear = true
[sitemap]
excludedKinds = ["taxonomy", "term"]
[taxonomy]
showTermCount = true
[fathomAnalytics]
# site = "ABC12345"
# domain = "llama.yoursite.com"
[verification]
# google = ""
# bing = ""
# pinterest = ""
# yandex = ""

@ -0,0 +1,2 @@
tags = "tags"
categories = "categories"

@ -0,0 +1,14 @@
---
title: "Welcome to a collection of Erraticbits"
description: ""
---
{{< lead >}}
Product Manager/Software Developer based in Calgary, AB, Canada.
{{< /lead >}}
Hello, my name is Jeff Clement. I'm a Product Manager/Developer based in Calgary, AB, Canada.
I love writing code and building software in various languages. I'm ~~obsessed with~~ fascinated by privacy and security tech. Lately, I've been having fun moving more of my online life into self-hosted services. When time permits, I love to get out into the mountains where I enjoy hiking, camping, and riding unicycles.
This website is a collection of stuff on all of the above :)

@ -1,9 +1,11 @@
---
title: About me
comments: false
showReadingTime: false
showDate: false
---
{{< gallery dir="/gallery/me" caption-effect="fade" />}}
![Me](img/author.png)
My name is Jeff Clement.
@ -33,17 +35,15 @@ My name is Jeff Clement.
<a href="/gpg/transition_20150323.txt">transition statement 2015-03-23</a></td>
</tr>
<!--
<tr>
<th>LinkedIn:</th>
<td><a href="http://www.linkedin.com/profile/view?id=6394933">Jeff Clement</a></td>
<td><a href="https://www.linkedin.com/profile/view?id=6394933">Jeff Clement</a></td>
</tr>
<tr>
<th>Github:</th>
<td><a href="https://github.com/jclement">jclement</a></td>
</tr>
-->
<tr>
<th>Mastodon:</th>

@ -0,0 +1,4 @@
---
title: Categories
---

@ -0,0 +1,5 @@
---
title: Tutorials
---
Tutorials on various technical things.

@ -2,8 +2,9 @@
title: Using GPG with Smart Cards
date: 2015-04-14
tags: [yubikey]
categories: ["tutorial"]
comments: true
showToc: true
showTableOfContents: true
---
I use SSH daily (with SSH keys) and would like to use GPG routinely (if only people I conversed with would use it) but key management is always a problem. I don't like leaving secret keys on my work computer, work laptop, various home computers, etc. To mitigate this problem I used a strong password on each of these keys which makes actually using them annoying.
@ -14,8 +15,10 @@ Smart cards let you store the private key on a tamper resistant piece of hardwar
Unfortunately, despite existing for over a decade, it's been difficult to find comprehensive information about setting up and using smart cards, for use with GPG and SSH, under Linux, Windows and OSX.
<div class="note">This article is heavily based on "[Offline GnuPG Master Key and Subkeys on YubiKey NEO Smartcard](http://blog.josefsson.org/2014/06/23/offline-gnupg-master-key-and-subkeys-on-yubikey-neo-smartcard/)" by
Simon Josefsson. Much like the reason Simon wrote his post, this article was primarily created to document my setup for my future reference.</div>
{{<note>}}
This article is heavily based on "[Offline GnuPG Master Key and Subkeys on YubiKey NEO Smartcard](http://blog.josefsson.org/2014/06/23/offline-gnupg-master-key-and-subkeys-on-yubikey-neo-smartcard/)" by
Simon Josefsson. Much like the reason Simon wrote his post, this article was primarily created to document my setup for my future reference.
{{</note>}}
Roughly:
@ -45,7 +48,9 @@ Use the [Yubikey Neo Manager](https://developers.yubico.com/yubikey-neo-manager/
I did this on Windows because it was convenient but there are packages for OSX and Linux too. There is also the *ykpersonalize* CLI tool that can do this.
<div class="danger">Use the Yubikey Neo Manager to verify that you have the OpenPGP applet &gt;= 1.0.10 due to a [bug in previous versions of the app on the Yubikey Neo](https://developers.yubico.com/ykneo-openpgp/SecurityAdvisory%202015-04-14.html) that allows a bypass of the PIN when performing cryptographic operations.</div>
{{<alert>}}
Use the Yubikey Neo Manager to verify that you have the OpenPGP applet &gt;= 1.0.10 due to a [bug in previous versions of the app on the Yubikey Neo](https://developers.yubico.com/ykneo-openpgp/SecurityAdvisory%202015-04-14.html) that allows a bypass of the PIN when performing cryptographic operations.
{{</alert>}}
![Yubikey Neo Manager](yubikey-mode.png)
@ -64,15 +69,17 @@ Install additional dependencies on the machine:
$ sudo apt-get install haveged gnupg2 gnupg-agent libpth20 pinentry-curses libccid pcscd scdaemon libksba8 paperkey opensc
```
<div class="warning"><p>To work with the Yubikey you must have gnupg2 &gt;= 2.0.22 and scdaemon &gt;= 2.0.22</p><p> If you are using Debian Wheezy you can install updated version of gnupg2 and scdaemon from backports with: <pre>
{{<alert>}}
To work with the Yubikey you must have gnupg2 &gt;= 2.0.22 and scdaemon &gt;= 2.0.22</p><p> If you are using Debian Wheezy you can install updated version of gnupg2 and scdaemon from backports with: <pre>
$ echo "deb http://http.debian.net/debian wheezy-backports main" >> /etc/apt/sources.list
$ apt-get update
$ apt-get -t wheezy-backports install gnugp2 scdaemon
</pre></div>
</pre>
{{</alert>}}
<div class="note">haveged is an entropy harvesting daemon that is installed to help improve the entropy in the entropy pool and speed up key generation.</div>
**haveged** is an entropy harvesting daemon that is installed to help improve the entropy in the entropy pool and speed up key generation.
<div class="note">paperkey is a package for exporting private key material to a text file for paper backup.</div>
**paperkey** is a package for exporting private key material to a text file for paper backup.
Configure GnuPG with safer defaults and stronger default ciphers (from [riseup.net](https://help.riseup.net/en/security/message-security/openpgp/best-practices)):
@ -94,9 +101,13 @@ default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB
My [gpg.conf](https://github.com/jclement/dotfiles/blob/master/other/gpg.conf) file is on github.
<div class="danger">Unplug your network cable now and verify that machine no longer has network connectivity.</div>
{{<alert>}}
Unplug your network cable now and verify that machine no longer has network connectivity.
{{</alert>}}
<div class="note">Most of the rest of this guide should be run as root. The default permissions on the Yubikey device under the Debian LiveCD don't allow non-root users to interact with it. We could fix it but this is a LiveCD and it won't survive the reboot so life is simpler just doing the key generation and key-to-card operations as root.</div>
{{<note>}}
Most of the rest of this guide should be run as root. The default permissions on the Yubikey device under the Debian LiveCD don't allow non-root users to interact with it. We could fix it but this is a LiveCD and it won't survive the reboot so life is simpler just doing the key generation and key-to-card operations as root.
{{</note>}}
## Generating the GPG keys
@ -517,13 +528,13 @@ $ gpg2 -a --export-secret-key 0x2896DB4A0E427716 >> /media/BACKUP/2896DB4A0E427
$ gpg2 -a --export-secret-subkeys 0x2896DB4A0E427716 >> /media/BACKUP/2896DB4A0E427716.sub.key
```
<div class="danger">
{{<alert>}}
Be absolutely sure you have a backup of your GPG keys and revocation certificate on a separate USB stick before you continue. Remember this is a LiveCD and any work you do on this environment is erased upon reboot. This backup is necessary for several reasons:
<ol>
<li>If your smart card is lost or damaged you can create a new one and (optionally) revoke the sub-keys that were in use on the previous card.
<li>The sub-keys on the smart card are limited and can not sign other keys, change your expiry date, or add UIDs to your existing key
</ol>
</div>
{{</alert>}}
## Configuring the smart card
@ -534,11 +545,15 @@ Use <kbd>gpg2 --card-edit</kbd> to edit the user information and PINs for the sm
* **Admin PIN** - this PIN is required to make changes to the smart card and is not used day-to-day
* **URL** - this is the location of your public key file and can be used my GnuPG to download your key in new installations. I really like <a href="https://keybase.io">keybase.io</a> so I've used that.
<div class="note">My environment uses the graphical PIN entry program so you don't see PIN prompts below but you will be prompted after many of the operations for a PIN.</div>
{{<note>}}
My environment uses the graphical PIN entry program so you don't see PIN prompts below but you will be prompted after many of the operations for a PIN.
{{</note>}}
<div class="note">The default admin PIN is usually '12345678' and the default PIN is usually '123456'.</div>
{{<note>}}
The default admin PIN is usually '12345678' and the default PIN is usually '123456'.
{{</note>}}
<div class="warning">
{{<alert>}}
If the machine you are using has a built-in Smartcard reader you may receive "<b>Card not present</b>" errors. If this happens you may need to change the default reader for scdaemon to your Yubikey. Find a list of ports with:
<pre>
@ -557,7 +572,7 @@ reader-port 1050:0116:X:0
</pre>
Alternatively, if you aren't using the built-in Smartcard reader, you can also just deactivate it in the OS.
</div>
{{</alert>}}
```
$ gpg2 --card-edit
@ -836,11 +851,13 @@ The smart card can now be used for encryption, signing and authentication (SSH).
$ sudo apt-get install gnupg2 gnupg-agent libpth20 pinentry-curses libccid pcscd scdaemon libksba8
```
<div class="warning"><p>To work with the Yubikey you must have gnupg2 &gt;= 2.0.22 and scdaemon &gt;= 2.0.22</p><p> If you are using Debian Wheezy you can install updated version of gnupg2 and scdaemon from backports with: <pre>
{{<alert>}}
To work with the Yubikey you must have gnupg2 &gt;= 2.0.22 and scdaemon &gt;= 2.0.22</p><p> If you are using Debian Wheezy you can install updated version of gnupg2 and scdaemon from backports with: <pre>
$ echo "deb http://http.debian.net/debian wheezy-backports main" >> /etc/apt/sources.list
$ apt-get update
$ apt-get -t wheezy-backports install gnugp2 scdaemon
</pre></div>
</pre>
{{</alert>}}
### Configuration
@ -866,7 +883,9 @@ $ gpg2 --card-status
Add the following to your *.bashrc* or *.zshrc* to pull in the gpg-agent environment variables when you open new terminals. This is required for SSH from the CLI to work properly (due to gnome-keyring issues).
<div class="note">The path to the .gpg-agent-info may vary. On my Ubuntu system it's ~/.gnupg/gpg-agent-info-$(HOSTNAME). I modified /etc/X11/Xsession.d/90gpg-agent and changed the PID_FILE path to be consistent between my machines in ~/.gpg-agent-info.</div>
{{<note>}}
The path to the .gpg-agent-info may vary. On my Ubuntu system it's ~/.gnupg/gpg-agent-info-$(HOSTNAME). I modified /etc/X11/Xsession.d/90gpg-agent and changed the PID_FILE path to be consistent between my machines in ~/.gpg-agent-info.
{{</note>}}
```bash
@ -898,7 +917,9 @@ The way I've managed to resolve it is:
Wrap /usr/bin/gnome-keyring-daemon to turn off gnome-keyring GPG and SSH (as root)
<div class="warning">This obviously seems like a hack. I'd appreciate any ideas on how better to resolve this.</div>
{{<alert>}}
This obviously seems like a hack. I'd appreciate any ideas on how better to resolve this.
{{</alert>}}
```
$ mv /usr/bin/gnome-keyring-daemon /usr/bin/gnome-keyring-daemon-wrapper

@ -2,8 +2,9 @@
title: Kub Kar Timer
date: 2016-02-14
template: article.jade
comments: true
toc: true
showTableOfContents: true
tags:
- arduino
---
My boys are in Boy Scouts and the annual Kub Kar races are a fun part of the program. Our group has a couple older wooden tracks and I wanted to add a timer mechanism to them that would time and rank each car for each race.
@ -36,7 +37,7 @@ The premise for my final solution was to dangle a conductive "wire" (lamp pull c
I ended up drilling a 1/2" hole in the underside of the timing platform and insert a 1" section of 1/2" copper pipe. Directly through the center I dangled a chunk of steel lamp pull-chain. The copper tubes are all tied to ground and the chains are tied to the Arduino's digital IO pins (with internal pull-up resistors enabled).
<p class="note">With this design it's important that the chain in each lane is a few links too long and dangles on the lane. This prevents the chain from swinging back and forth for ages and allows you to get on with the next race much faster.</p>
{{<note>}}With this design it's important that the chain in each lane is a few links too long and dangles on the lane. This prevents the chain from swinging back and forth for ages and allows you to get on with the next race much faster.{{</note>}}
For actually triggering the race I ran cables from the timer to the starting gate and then used some Allround as an improvised switch that was closed when the gate was up. As soon as the gate drops the timer starts.
@ -48,7 +49,7 @@ I opted for a small 20x4 LCD I2C display because that's what I had handy.
## The Software
The source code for this entire project is up on [Bitbucket](https://bitbucket.org/jclement/kub-kar-timer).
The source code for this entire project is [here](https://git.erraticbits.ca/jclement/kub-kar-timer).
## The Build

@ -3,7 +3,6 @@ title: QubesOS Presentation
date: 2017-03-02
comments: true
tags: [qubesos, presentation]
toc: true
---
I'm a huge fan of QubesOS. Here are some slides from an internal company presentation trying to inflict QubesOS on my coworkers.

@ -1,151 +0,0 @@
---
title: Deploying Ghost with Docker & NGINX
date: 2018-09-12
comments: true
tags: [server]
toc: false
---
It seemed like a good idea to try something new with this website. I settled on running the fancy blogging software Ghost because it looked pretty, has a wonderful editing experience (with markdown support), and (most importantly) I'd never used it before.
<!--more-->
I have a VPS through Digital Ocean hosting this site and a few other things. I wanted to use that VPS rather than maintaining yet another machine. There are a couple things about Ghost that make me uncomfortable installing it on the bare machine.
1. It requires a specific globally installed version of Node
2. It doesn't install using Ubuntu's packing system but is, instead, installed using its own deployment script
3. There is no support for running multiple Ghost instances on a single machine
I wanted to:
* Limit to impact on the bare machine
* Have the ability to run multiple Ghost instances
* Run Ghost behind NGINX and have NGINX handle SSL termination (LetsEncrypt)
* Have some sort of safe mechanism for performing upgrades
* Isolate the Ghost process as much as possible so that if it turns evil its impact on the rest of the system is minimized
This seemed like an ideal project to deploy under Docker. This post is mostly documentation of that process so that future Jeff (when he has to fix something) remembers how this worked.
Fortunately, I'm not the first to want to deploy Ghost using Docker so there are a number of Docker images. I picked the official one here.
Getting it up and running is fairly easy:
```
docker run --name ghost-forgesi \
-p 127.0.0.1:13003:2368 \
-d ghost:alpine
```
That starts the Ghost docker image and exposes it on port 13003 on localhost.
Next step is to configure NGINX to forward requests for my website through to my Ghost instance.
I added a new site on `sites-available` and symlinked that into `sites-enabled` for my website "forgesi.net".
In this case I have forgesi.net:80 www.forgesi.net:80 and forgesi.net:443 all redirecting to www.forgesi.net:443 so there are a couple bonus server containers in my configuration.
```
# Redirect HTTP traffic to www.forgesi.net:443
server {
listen 80;
listen [::]:80;
server_name forgesi.net www.forgesi.net;
return 301 https://www.forgesi.net$request_uri;
}
# Redirect forgesi.net HTTPS traffic to www.forgesi.net:443
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name forgesi.net;
ssl on;
ssl_certificate /etc/letsencrypt/live/forgesi.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/forgesi.net/privkey.pem; # managed by Certbot
include /etc/nginx/ssl_params;
return 301 https://www.forgesi.net$request_uri;
}
# Main Server Configuration
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.forgesi.net;
root /var/www/forgesi.net;
index index.html;
access_log /var/log/nginx/access.log;
ssl on;
ssl_certificate /etc/letsencrypt/live/forgesi.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/forgesi.net/privkey.pem; # managed by Certbot
include /etc/nginx/ssl_params;
# If a physical file exists in DocRoot host that, otherwise pass through to Ghost
location / {
try_files $uri $uri @forgesighost;
}
location @forgesighost {
proxy_pass http://127.0.0.1:13003;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
```
The SSL certificates in the above configuration were created by LetsEncrypt. When I originally created this file I just used whatever self-signed cert I had kicking around and then let LetsEncrypt take it over.
```
$ sudo apt install python-certbot-nginx
$ sudo certbot --nginx -d forgesi.net -d www.forgesi.net
$ sudo service nginx restart
```
The `/etc/nginx/ssl_params` file mentioned in the configuration are just some parameters that help be get a A+ rating on SSL labs:
![SSL Configuration](ssl.png)
```
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS;
ssl_dhparam /etc/nginx/dhparam.pem;
add_header Strict-Transport-Security "max-age=31536000";
ssl_prefer_server_ciphers on;
```
After making the above configuration changes, starting the Ghost Docker image, and restarting nginx, I can now connect to my Ghost test server.
There are, however, some problems I need to address:
* The Ghost image doesn't know its server name (many links are pointing to localhost)
* The Ghost image doesn't know how to send email (for lost passwords)
* The Ghost process is running as user 1000 which overlaps with a local user account on the host system
* All content is stored within the Ghost image and lost when I restart/upgrade the image.
The following script deletes and recreates my Docker container and resolves the above issues.
* It uses Mailgun for mail delivery (Mailgun has a good free tier)
* It runs as user 1001 (A dedicated user for this)
* All data is persisted to /var/lib/ghost/forgesi on the server machine (making it easy to backup, and allowing it to persist across upgrades)
```sh
#!/bin/sh
docker rm -f ghost-forgesi
docker run --name ghost-forgesi \
-p 127.0.0.1:13003:2368 \
--user 1001 \
-e url=https://www.forgesi.net \
-e mail__transport=SMTP \
-e mail__from=noreply@mg.forgesi.net \
-e mail__options__service=Mailgun \
-e mail__options__auth__user=postmaster@mg.forgesi.net \
-e mail__options__auth__pass=ZZZZZZ \
-v /var/lib/ghost/forgesi:/var/lib/ghost/content \
--restart=always \
-d ghost:alpine
```
Now, when Ghost is upgraded, I can update my image with a docker pull ghost:alpine and then rebuild my container with the above script.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@ -3,6 +3,7 @@ title: GPG/SSH with the YubiKey 5
date: 2018-10-07
comments: true
tags: [yubikey, crypto]
categories: ["tutorial"]
showToc: true
---
@ -108,9 +109,9 @@ At this point, we no longer need Internet access so unplug the cable or disconne
## Generating GPG Keys
{{% warning %}}
{{<alert>}}
Do not proceed until you have disconnected your machine from the Internet.
{{% /warning %}}
{{</alert>}}
Here is a sample set of keys that I’m generating. The master key is reserved for key signing and certifying the sub-keys. There are distinct sub-keys for signing, encryption and authentication (SSH). Each of these separate sub-keys will be loaded into the matching slot on the YubiKey OpenPGP module.

@ -3,6 +3,7 @@ title: NGINX Semi-private Site
date: 2018-12-20
comments: true
tags: [server]
categories: ["tutorial"]
toc: false
---

@ -3,7 +3,7 @@ title: Blog with Hugo, Gitlab CD, and Caddy
date: 2019-09-20
comments: true
tags: [server]
showToc: true
showTableOfContents: true
---
This post is an overview on how I setup this site (built using [Hugo](https://gohugo.io)) to be automatically deployed to my [Caddy](https://caddyserver.com/) server using [Gitlab's](https://gitlab.com) continuous deployment.
@ -153,9 +153,9 @@ It'll look something like this:
-----END OPENSSH PRIVATE KEY-----
```
{{% warning %}}
{{<alert>}}
The private key `~/deploy_erraticbits` needs to be protected. Anyone with that key can login to the `deploy_erraticbits` user on the webserver. Once Gitlab is setup (below), this file should be deleted from the webserver.
{{% /warning %}}
{{</alert>}}
# Setting up Gitlab CI/CD

@ -4,6 +4,7 @@ date: 2019-09-02
comments: true
tags: [server, crypto]
toc: true
categories: ["tutorial"]
---
I'm setting up a new Ubuntu 18.04 server and wanted the drives to be encrypted. Since the machine is headless in my basement, however, entering a password on boot is annoying.

@ -3,6 +3,7 @@ title: Wireguard Access Server
date: 2019-09-20
comments: true
tags: [server, wireguard, crypto]
categories: ["tutorial"]
showToc: true
---

@ -3,6 +3,7 @@ title: Yubikey Setup
date: 2019-10-05
comments: true
tags: [yubikey, raspberrypi]
categories: ["tutorial"]
showToc: true
draft: true
---

@ -1,10 +1,11 @@
---
title: "Adventures with Golang, WebSockets, Create-React-App and NGINX"
title: "Golang, WebSockets & React"
date: 2020-04-18
tags:
- golang
- react
- nginx
categories: ["tutorial"]
---
As part of my COVID friendly game project, [werdz.ca](https://werdz.ca), I've been working with [GoLang](https://golang.org), [Create-React-App](https://create-react-app.dev), WebSockets and NGINX (for production). Some of it has been "an adventure".
@ -67,9 +68,9 @@ and then add it to my Router
app.router.HandleFunc("/api/game/ws", app.apiGameWs)
```
{{% note %}}
{{<note>}}
Throughout this document, let's assume that my backend server binds to `localhost:8100`.
{{% /note %}}
{{</note>}}
The one part I never did figure out was how to detect a client connection closing in Go (closing the browser, for example). I'd like to be able to flag players as in-active when their connection closes so that they don't hold up the game play. I've tried a variety of things and eventually settled on a super cheesy client-side ping over HTTP (rather than the WebSocket).
@ -120,9 +121,9 @@ module.exports = app => {
Now, you may find yourself thinking, like I did, "Hey, I'm a Typescript app so I'll just create this as `setupProxy.ts`". Don't do it. This also, despite the documentation suggestion it should be fine, does [not work](https://github.com/facebook/create-react-app/issues/6794).
{{% warning %}}
{{<alert>}}
One last gotcha that had me stumped for a while. You need to remove the `proxy` property from `package.json` or Create-React-App will just ignore `setupProxy.js`.
{{% /warning %}}
{{</alert>}}
# NGINX Proxy

@ -34,9 +34,9 @@ $ apt install nyx tor deb.torproject.org-keyring
Once the software is installed, the Tor service is configured via. `/etc/tor/torrc`.
{{% warning %}}
{{<alert>}}
Tor relays can be configured as *exit nodes*. Exit nodes are the final hop on the Tor network before your traffic reenters the normal Internet. Running an Exit node can be dangerous because a lot of sketchy stuff happens on the Tor network and I'd rather not be the one that site operators come back to as the source for that sketchy stuff hitting their server. Adding `ExitRelay 0` to the `torrc` ensures that my Tor relay is not an exit node.
{{% /warning %}}
{{</alert>}}
My bandwidth cap is 1TB / month which, if my math is correct, gives me about 400,000 Bytes/sec sustained. Outside of Tor, the traffic to this server per month is negligible.

@ -1,14 +1,18 @@
---
title: "Moving my files in-house"
date: 2022-01-15
tags:
- privacy
- syncthing
- self-hosting
showTableOfContents: true
draft: false
categories: ["tutorial"]
slug: "advanced-customisation"
tags: ["privacy", "self-hosting"]
categories: ["tutorial"]
---
Every six months or so, when the position of the moon is just right, I flip-flop on the privacy/self-hosted vs. just-let-Google-handle-it issue. Today, I've flopped toward privacy and self-hosting.
<!--more-->
A couple of days ago, I was using Gmail for the majority of my email and Google Drive for syncing files between my various machines (Windows desktop, Windows laptop, MacbookPro, Chromebook, and iPhone).
This post is mostly focused on my migration from Google Drive to [SyncThing](https://www.syncthing.net), but I'll touch on what I did with email too.
@ -61,7 +65,7 @@ I am also running SyncThing on one of my cloud-hosted servers (alpha, in the dia
The whole picture looks like this:
```mermaid
{{< mermaid >}}
flowchart LR
subgraph LAN
anorak<-->mcsyncthing
@ -73,11 +77,11 @@ flowchart LR
mcsyncthing--encrypted-->alpha
end
iphone--ssh-->mcsyncthing
```
{{< /mermaid >}}
Although, it's actually more like this because the individual workstations will sync between themselves too.
```mermaid
{{< mermaid >}}
flowchart LR
subgraph LAN
anorak<-->mcsyncthing
@ -92,7 +96,7 @@ flowchart LR
mcsyncthing--encrypted-->alpha
end
iphone--ssh-->mcsyncthing
```
{{< /mermaid >}}
Quick screenshot of the management UI for SyncThing on *mcsyncthing* that I connect to via. an SSH Tunnel.

@ -0,0 +1,9 @@
---
title: Writings
description: "Assorted writings and blog posts"
cascade:
showEdit: false
showSummary: true
---
Here are a collection of things I've written over the years. While occasionally useful to others, the primary purpose of writing these is for me to document/remember how I set things up.

@ -1,18 +0,0 @@
---
title: First post!
date: 2019-01-05
tags: [python, stuff]
draft: true
---
This is my first post, how exciting!!
{{< highlight python >}}
import os
print("Hello")
{{< /highlight >}}
```python
import os
print("Hello")
```

@ -1,49 +0,0 @@
---
title: Code Sample
subtitle: Using Hugo or Pygments
date: 2016-03-08
tags: ["example", "code"]
draft: true
---
The following are two code samples using syntax highlighting.
<!--more-->
The following is a code sample using triple backticks ( ``` ) code fencing provided in Hugo. This is client side highlighting and does not require any special installation.
```javascript
var num1, num2, sum
num1 = prompt("Enter first number")
num2 = prompt("Enter second number")
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert("Sum = " + sum) // "+" means combine into a string
```
The following is a code sample using the "highlight" shortcode provided in Hugo. This is server side highlighting and requires Python and Pygments to be installed.
{{< highlight javascript >}}
var num1, num2, sum
num1 = prompt("Enter first number")
num2 = prompt("Enter second number")
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert("Sum = " + sum) // "+" means combine into a string
{{</ highlight >}}
{{< gallery caption-effect="fade" >}}
{{< figure thumb="-thumb" link="/img/hexagon.jpg" >}}
{{< figure thumb="-thumb" link="/img/sphere.jpg" caption="Sphere" >}}
{{< figure thumb="-thumb" link="/img/triangle.jpg" caption="Triangle" alt="This is a long comment about a triangle" >}}
{{< /gallery >}}
And here is the same code with line numbers:
{{< highlight javascript "linenos=inline">}}
var num1, num2, sum
num1 = prompt("Enter first number")
num2 = prompt("Enter second number")
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert("Sum = " + sum) // "+" means combine into a string
{{</ highlight >}}

@ -0,0 +1,10 @@
---
title: Projects
description: "Assorted writings and blog posts"
cascade:
showEdit: false
showSummary: true
groupByYear: false
---
A collection of interesting projects I've written over the years.

@ -0,0 +1,7 @@
---
title: "Werdz"
description: "something"
date: 2022-01-15
showTableOfContents: true
externalUrl: https://www.werdz.ca
---

@ -0,0 +1,18 @@
---
title: Services
description: "Various services is run"
cascade:
showEdit: false
showSummary: true
groupByYear: false
---
A collection of public services I'm self-hosting.
## [Pastebin](https://paste.erraticbits.ca)
ErraticBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bits AES.
## [Uploads](https://upload.erraticbits.ca)
A temporarily file hosting service based on [transfer.sh](https://transfer.sh).

@ -0,0 +1,4 @@
---
title: Tags
---

@ -0,0 +1,5 @@
---
title: privacy
---
Privacy stuff

@ -0,0 +1,5 @@
module git.erraticbits.ca/jclement/blog
go 1.18
require github.com/jpanther/congo/v2 v2.2.2 // indirect

@ -0,0 +1,2 @@
github.com/jpanther/congo/v2 v2.2.2 h1:7MvPHxY9fP+DtIiAaCk1yYAgdndbPhLf6syDixIg1+A=
github.com/jpanther/congo/v2 v2.2.2/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=

@ -0,0 +1,8 @@
{{ $jsHome := resources.Get "js/home.js" | resources.Minify | resources.Fingerprint "sha512" }}
<div id="page">
{{ partial "partials/home/page.html" . }}
</div>
<div id="profile" class="hidden h-full">
{{ partial "partials/home/profile.html" . }}
</div>
<script defer type="text/javascript" src="{{ $jsHome.RelPermalink }}" integrity="{{ $jsHome.Data.Integrity }}"></script>

@ -1,6 +0,0 @@
{{ $hugo_config := `{ "version": 1 }` }}
<div class="notices info">
<div class="inner">
{{ .Inner }}
</div>
</div>

@ -1,6 +1,8 @@
{{ $hugo_config := `{ "version": 1 }` }}
<div class="notices note">
<div class="inner">
{{ .Inner }}
</div>
</div>
<div class="flex px-4 py-3 rounded-md bg-primary-100 dark:bg-primary-900">
<span class="ltr:pr-3 rtl:pl-3 text-primary-400">
{{ partial "icon.html" (.Get 0 | default "edit") }}
</span>
<span class="dark:text-neutral-300">
{{- .Inner | markdownify -}}
</span>
</div>

@ -0,0 +1,14 @@
<div class="flex justify-between">
<span
class="w-full py-6 mr-3 rounded-md"
{{ with .Get 0 }}style="background-color: {{ . }}"{{ end }}
></span>
<span
class="w-full py-6 mr-3 rounded-md"
{{ with .Get 1 }}style="background-color: {{ . }}"{{ end }}
></span>
<span
class="w-full py-6 mr-3 rounded-md"
{{ with .Get 2 }}style="background-color: {{ . }}"{{ end }}
></span>
</div>

6