initial commit

trunk
Adam Veldhousen 2 years ago
commit d7698e7dd1
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

2
.gitignore vendored

@ -0,0 +1,2 @@
resources
public

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "themes/digitalgarden"]
path = themes/digitalgarden
url = https://github.com/apvarun/digital-garden-hugo-theme.git

@ -0,0 +1 @@
hugo 0.93.3

@ -0,0 +1,13 @@
FROM alpine as builder
COPY . /opt/
WORKDIR /opt
RUN apk add --no-cache hugo && \
hugo --verbose --minify --enableGitInfo
FROM nginx
COPY --from=builder /opt/public /usr/share/nginx/html
EXPOSE 80

@ -0,0 +1,56 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
---
-------------------------
# Default post template
## Writing tips
1. Find a good topic and commit to it
eg how to get started blogging
2. Make your goals and audience specific
Who is my Audience: eg People who want to start blogging, especially about technical topics, but havent done it yet.
What is my Goal: eg. Give people a concrete set of steps and pointers so they can get started.
3. Have a beginning, middle, and end
4. Get feedback and iterate
5. Add finishing touches: packaging, publication, and promotion
From [freeCodeCamp: How to write a great technical blog post][1]
[1]: https://www.freecodecamp.org/news/how-to-write-a-great-technical-blog-post-414c414b67f6/
## Syntax highlighting example
```go {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
// GetTitleFunc returns a func that can be used to transform a string to
// title case.
//
// The supported styles are
//
// - "Go" (strings.Title)
// - "AP" (see https://www.apstylebook.com/)
// - "Chicago" (see https://www.chicagomanualofstyle.org/home.html)
//
// If an unknown or empty style is provided, AP style is what you get.
func GetTitleFunc(style string) func(s string) string {
switch strings.ToLower(style) {
case "go":
return strings.Title
case "chicago":
tc := transform.NewTitleConverter(transform.ChicagoStyle)
return tc.Title
default:
tc := transform.NewTitleConverter(transform.APStyle)
return tc.Title
}
}
```

@ -0,0 +1,73 @@
baseURL = "https://vdhsn.com"
languageCode = "en-us"
title = "Adam Veldhousen - Raised Beds"
theme = "digitalgarden"
pygmentsStyle = "solarized-light" # solarized-light, -dark or -dark256
summaryLength = 32
enableRobotsTXT = true
enableEmoji = true
googleAnalytics = "UA-PROPERTY_ID"
[params]
favicon = "/party-tp.gif"
[markup]
[markup.highlight]
codeFences = true
guessSyntax = false
anchorlinenos = true
lineNoStart = 1
lineNos = true
# lineNumbersInTable = true
# noClasses = false
style = "solarized-light"
tabWidth = 2
[menu]
[[menu.main]]
name = 'Home'
url = '/'
weight = 1
[[menu.main]]
name = 'Health'
url = '/health'
weight = 2
[[menu.main]]
name = 'Software'
url = '/software'
weight = 2
[[menu.social]]
name = 'Twitter'
url = 'https://twitter.com/adamveld12'
weight = 1
[[menu.social]]
name = 'GitHub'
url = 'https://github.com/adamveld12'
weight = 2
[[menu.social]]
name = 'LinkedIn'
url = 'https://www.linkedin.com/in/aveldhousen/'
weight = 3
[[menu.social]]
name = "Keybase"
url = "https://keybase.io/aveldhousen"
weight = 3
[[menu.social]]
name = 'Email'
url = 'mailto:garden@vdhsn.com'
weight = 8
[[menu.social]]
name = 'RSS'
url = '/index.xml'
weight = 9

@ -0,0 +1,27 @@
---
title: Homepage
---
# Hello 👋
**Welcome to my digital garden!**
I'm Adam V. and I write software for a living, and sometimes for fun.
[Resume](/resume_2019.pdf) | PGP: [A466CEE1415C0B9C](/gpg.pub)
A digital garden is like a personal wiki and a knowledge database of thoughts and ideas. Similar to a traditional garden, a digital one will also container various kinds of content (plants), of which may even be unrelated to each other. Ideas are not refined, thoughts are not tailored. Here is an excellent write-up about the [history of digital gardens](https://maggieappleton.com/garden-history)
Twitter, for some, is also equivalent to a digital garden. It lets you share thoughts and ideas with everyone. But how often do you go back to those tweets? Not often. That's why you need a space for your ideas on the internet **that you own**. Check out `Digital gardens let you cultivate your own little bit of the internet` [post](https://www.technologyreview.com/2020/09/03/1007716/digital-gardens-let-you-cultivate-your-own-little-bit-of-the-internet/) by MIT technology review
<hr />
Building your own digital garden is not a fad. It's a necessity. Tools like Roam Research, Obsidian and Notion provided means to interlink content, even over a graphical way. Still not sold? Check out [The Digital Garden](https://dev.to/jbranchaud/the-digital-garden-l10) by Josh Branchaud.
Start collecting your ideas 💡, curate thought provoking & interesting content 💬&nbsp; and learn.
→ [Software](/software)
→ [Health](/health)

@ -0,0 +1,7 @@
---
title: Health
description: Health related notes. Physical, Mental, Spiritual, Social etc.
---

@ -0,0 +1,56 @@
---
title: "ADHD"
date: 2022-03-10T20:19:09-06:00
draft: true
tags: [health, mental]
---
-------------------------
# Default post template
## Writing tips
1. Find a good topic and commit to it
eg how to get started blogging
2. Make your goals and audience specific
Who is my Audience: eg People who want to start blogging, especially about technical topics, but havent done it yet.
What is my Goal: eg. Give people a concrete set of steps and pointers so they can get started.
3. Have a beginning, middle, and end
4. Get feedback and iterate
5. Add finishing touches: packaging, publication, and promotion
From [freeCodeCamp: How to write a great technical blog post][1]
[1]: https://www.freecodecamp.org/news/how-to-write-a-great-technical-blog-post-414c414b67f6/
## Syntax highlighting example
```go {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
// GetTitleFunc returns a func that can be used to transform a string to
// title case.
//
// The supported styles are
//
// - "Go" (strings.Title)
// - "AP" (see https://www.apstylebook.com/)
// - "Chicago" (see https://www.chicagomanualofstyle.org/home.html)
//
// If an unknown or empty style is provided, AP style is what you get.
func GetTitleFunc(style string) func(s string) string {
switch strings.ToLower(style) {
case "go":
return strings.Title
case "chicago":
tc := transform.NewTitleConverter(transform.ChicagoStyle)
return tc.Title
default:
tc := transform.NewTitleConverter(transform.APStyle)
return tc.Title
}
}
```

@ -0,0 +1,56 @@
---
title: "Sleep"
date: 2022-03-10T21:00:10-06:00
draft: true
tags: [health, mental, sleep]
---
-------------------------
# Default post template
## Writing tips
1. Find a good topic and commit to it
eg how to get started blogging
2. Make your goals and audience specific
Who is my Audience: eg People who want to start blogging, especially about technical topics, but havent done it yet.
What is my Goal: eg. Give people a concrete set of steps and pointers so they can get started.
3. Have a beginning, middle, and end
4. Get feedback and iterate
5. Add finishing touches: packaging, publication, and promotion
From [freeCodeCamp: How to write a great technical blog post][1]
[1]: https://www.freecodecamp.org/news/how-to-write-a-great-technical-blog-post-414c414b67f6/
## Syntax highlighting example
```go {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
// GetTitleFunc returns a func that can be used to transform a string to
// title case.
//
// The supported styles are
//
// - "Go" (strings.Title)
// - "AP" (see https://www.apstylebook.com/)
// - "Chicago" (see https://www.chicagomanualofstyle.org/home.html)
//
// If an unknown or empty style is provided, AP style is what you get.
func GetTitleFunc(style string) func(s string) string {
switch strings.ToLower(style) {
case "go":
return strings.Title
case "chicago":
tc := transform.NewTitleConverter(transform.ChicagoStyle)
return tc.Title
default:
tc := transform.NewTitleConverter(transform.APStyle)
return tc.Title
}
}
```

@ -0,0 +1,4 @@
---
title: Software
description: Software I use
---

@ -0,0 +1,53 @@
---
title: "Set variables at build time in Golang"
date: 2020-01-05T21:00:00Z
tags: [golang, programming]
draft: false
---
For some CLI tools I use regularly I noticed that when I run the `version` flag get some version info that includes a
git commit hash or a git tag to the command line:
```shell
> kubectl version
Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.3", GitCommit:"b3cbbae08ec52a7fc73d334838e18d17e8512749", GitTreeState:"clean", BuildDate:"2019-11-13T11:23:11Z", GoVersion:"go1.12.12", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
```
I discovered a nice way to do this for go programs. You can specify a value for a global variable at build time using
the `-ldflags "-X"` flag.
For this program:
```go
package main
import "fmt"
var Version = "dev"
func main(){
fmt.Printf("I'm version <%s>", Version)
}
```
You can use this one liner that builds your go program while setting the *Version* variable to the *sha* hash of the latest
commit.
```shell
> go build -ldflags "-X main.Version=$(git rev-parse HEAD)" -o hello_app main.go
> hello_app
I'm version ba83f7c418c669f705a3cce0c58a1f9129a3de14
```
Note that you have to specify the full path of the package in your project, for example:
```bash
# Your project looks like
# - main.go <- module `github.com/myrepo/test`
# - version <- `github.com/myrepo/test/version`
# - sha.go <- has `var Version = "dev"`
> go build -ldflags "-X github.com/myrepo/test/version.Version=$(git rev-parse HEAD)" -o hello_app main.go
> hello_app
I'm version ba83f7c418c669f705a3cce0c58a1f9129a3de14
```

@ -0,0 +1,35 @@
---
title: "Git tips - lint + test pre-commit hook"
date: 2020-01-01T21:00:39Z
tags: [git, bash]
---
One of my favorite inventions is a `pre-commit` hook that auto runs test and lint commands from a `makefile` or
`package.json` if they're found:
```bash {linenos=table}
#!/usr/bin/env bash
if [ -f "$PWD/makefile" ] && [ ! -z "$(cat $PWD/makefile | grep '^lint:')" ]; then
echo "running make lint"
make lint
elif [ -f "$PWD/package.json" ] && [ ! -z "$(cat $PWD/package.json | grep "^\"lint\":")" ]; then
echo "running npm run lint"
npm run lint
fi
if [ -f "$PWD/makefile" ] && [ ! -z "$(cat $PWD/makefile | grep '^test:')" ]; then
echo "running make test"
make test
elif [ -f "$PWD/package.json" ] && [ ! -z "$(cat $PWD/package.json | grep "^\"test\":")" ]; then
echo "running npm run test"
npm run test
fi
```
The `/usr/bin/env bash` piece ensures that the script has access to all of the environment variables you expect in your
regular shell.
If the test or lint command fails then the `git commit` command fails. If I absolutely need to commit something in spite
of the lint/test results I can do `git commit --no-verify` to skip the `pre-commit` hook.

@ -0,0 +1,57 @@
---
title: "Go Modules: How to replace a dependency with a local copy"
date: 2020-03-20T20:35:25Z
draft: false
tags: [golang, modules]
---
Lets say you're working on a go package and you need to replace a dependency with a specific version for testing. You can use the awesome `go mod edit -replace old=new[@version]` command to do this.
This command adds a `replace` directive to your `go.mod` that overrides any require statement versions for the matching module:
```bash {hl_lines=[13]}
$ go mod edit -replace github.com/gobuffalo/packr=github.com/gobuffalo/packr@v2.8.0
$ cat go.mod
module github.com/adamveld12/riffraff
go 1.14
require (
github.com/gobuffalo/packr v1.30.1
github.com/golangci/golangci-lint v1.21.0 // indirect
github.com/satori/go.uuid v1.2.0
)
replace github.com/gobuffalo/packr => github.com/gobuffalo/packr v2.8.0
```
Now when you build your go app, it will use `v2.8.0` of packr in place of the version specified in the `require` block.
But what if you want to use it similarly to `npm link`, where you want to replace a module with a local working copy?
Run the same command but omit the `@version` on the new package like so:
```bash {hl_lines=[15]}
# clone your own copy and make some edits at ~/projects/packr
$ cd ~/projects && git clone https://github.com/gobuffalo/packr
$ cd ~/projects/riffraff && go mod edit -replace github.com/gobuffalo/packr=../packr
$ cat go.mod
module github.com/adamveld12/riffraff
go 1.14
require (
github.com/gobuffalo/packr v1.30.1
github.com/golangci/golangci-lint v1.21.0 // indirect
github.com/satori/go.uuid v1.2.0
)
replace github.com/gobuffalo/packr => ../packr #now points at your local copy
```
When you're all finished up you can remove the `replace` directive with the following command:
```bash
$ go mod edit -dropreplace github.com/gobuffalo/packr
```

@ -0,0 +1,120 @@
---
title: "Makefiles for Golang"
date: 2020-01-07T00:50:32Z
tags: [make, golang]
draft: false
---
[Make is a build automation tool from the late 70's][make-wiki] that's pretty popular in C and C++ world. Thanks to its age and
popularity you can find tons of tutorials and Make is supported on basically every platform out there. I'm going to
demonstrate how to set up a basic Makefile for Golang projects that will build, lint and test your code.
Make has a few simple rules that make it powerful, it expects that each task you create will be the name of an output file
on disk. This is nice because if a file already exists with the same name as a task then Make will skip doing the work
for that task.
## Building
For example if you create the following `Makefile` below and place it in the root of your project and run `make`, you will
see a new `hello_world` binary built:
```makefile
hello_world:
go build -o hello_world main.go
```
But at this point if you run it again, you'll see:
```sh
make: 'hello_world' is up to date.
```
So lets add a clean command to clean up the build output:
```makefile {linenos=table,hl_lines=["4-5"]}
hello_world:
go build -o hello_world main.go
clean:
rm -rf ./hello_world
```
One issue here is that the `clean` task will only work as long as there isn't a file in the project also named `clean`.
If you want Make to ignore the file system for this task then you can add an entry to the `.PHONY` list:
```makefile {linenos=table,hl_lines=[7]}
hello_world:
go build -o hello_world main.go
clean:
rm -rf ./hello_world
.PHONY: clean
```
## Testing
Next we can run tests. You can define variables in your makefile that run shell commands for their value. I'm running
`go list` and filtering out the `vendor` folder so we can run tests for every package in our project. Remember to add
that `test` task to the `.PHONY` list:
```makefile
PKGS := $(shell go list ./... | grep -v vendor)
test:
go test -v -cover $(PKGS)
.PHONY: test
```
## Linting
Now that we can build and test our code, lets try to lint it. My lint tool of choice is [golangci-lint][golangcilint]
so I like to add an install task that runs `go get` to install it. To do this I take advantage of a Make feature called
prerequisite tasks, where you can list tasks that are required to execute before another task runs. This makes it easy
to set up the install task as a dependency of our `lint` command, ensuring its installed every time we run it:
```makefile
LINT_BIN := $(GOPATH)/bin/golangci-lint
$(LINT_BIN):
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
lint: $(LINT_BIN)
@$(LINT_BIN) run -p format -p unused -p bugs # The '@' symbol hides the command in the output
.PHONY: lint
```
## Putting it together
Below is the complete makefile. I added the `.SHELLFLAGS` variable, which sets various flags in the shell that Make executes
your commands in. The `-euo pipefail` runs your commands in a type of [strict mode in Bash][strict-mode] which will catch
errors as they happen and make your life debugging shells scripts generally much easier.
```makefile
.SHELLFLAGS := -euo pipefail
PKGS := $(shell go list ./... | grep -v vendor)
LINT_BIN := $(GOPATH)/bin/golangci-lint
hello_world:
go build -o hello_world main.go
lint: $(LINT_BIN)
@$(LINT_BIN) run -p format -p unused -p bugs # The '@' symbol hides the command in the output
test:
go test -v -cover $(PKGS)
$(LINT_BIN):
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
clean:
rm -rf ./hello_world
.PHONY: clean lint test
```
[make-wiki]: https://en.wikipedia.org/wiki/Make_(software)
[strict-mode]: http://redsymbol.net/articles/unofficial-bash-strict-mode/

@ -0,0 +1,92 @@
---
title: "Kubernetes load balancing with Metal LB"
date: 2020-01-02T20:57:36Z
tags: ["kubernetes", "homelab", "networking"]
draft: false
---
If you run Kubernetes on an IaaS provider like AWS or GCE and create a service with the *LoadBalancer* type, there is glue code included in kubernetes itself that will provision an ELB/ALB for you automatically. When you're running k8s on prem or at home any service you create with the *LoadBalancer* service type will hang indefinitely since there is no way to provision external IPs on your router out of the box. This is where Metal LB comes in.
[Metal LB][metallb] is a project that implements load balancing for on premises based Kubernetes clusters by responding to ARP requests directly on your network with the MAC address of the worker nodes. This means no setup is required in most cases and you get a nice internal IP that you can port forward on your router. In this post I will walk you through high level set up so you can get traffic from the internet hitting your service in a scalable way.
## Setup Metal LB
Installation is easy but you have to make sure you're using a compatible networking add on. I would recommend [Flannel][flannel] or [Kube Router][kuberouter] but there are many others supported with caveats that you can [look in their compatibility table][metallbcompattable].
Next you can install Metal LB on your cluster like so:
```bash
kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml
```
Then set up a config map with an IP address pool. This IP address pool should be in the subnet that is set up on your router or traffic will be dropped. This means that if your router is set up to give out IPs in the range of `192.168.0.2-192.168.0.254` then you should make sure the pool is in that range.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.0.240-192.168.0.250
```
Now lets run a pod and service to see this in action. Apply the following with *kubectl*:
```yaml
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
ports:
- protocol: TCP
name: web
port: 80
selector:
app: whoami
type: LoadBalancer
---
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: default
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
ports:
- name: web
containerPort: 80
```
Finally get the external IP address by doing `kubectl get svc whoami`. Visit that IP on port 80 and you should see some output.
And that's all there is to it. From here you should be able to port forward 80 to that IP and access the service from the internet with the IP given to you by your ISP.
Next I will show how to set up [Traefik][traefik], a popular and powerful loadbalancer. We'll be able to port forward to traefik and route to multiple services in any way we want.
[metallb]: https://metallb.universe.tf/
[metallbcompattable]: https://metallb.universe.tf/installation/network-addons/
[kuberouter]: https://www.kube-router.io/
[flannel]: https://github.com/coreos/flannel/blob/master/Documentation/kubernetes.md
[cillium]: https://github.com/cilium/cilium
[traefik]: https://docs.traefik.io/v2.0/

@ -0,0 +1,76 @@
---
title: "How to setup a local persistent volume in kubernetes"
date: 2020-01-04T23:14:35Z
draft: false
tags: [kubernetes, configuration, storage]
---
I'm running a single node kubernetes cluster and one of the first things I needed was persistent storage. To create a volume that you can mount into your containers in a pod you have to create a *PersistentVolume (PV)* and then request it with a *PersistentVolumeClaim (PVC)*.
Create a *PersistentVolume (PV)* object, pointing at a path on your host. Note the `spec.capacity.storage`, `spec.hostPath.path` and change these accordingly.
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: persistent-test-volume
labels:
name: persistent-test-volume
spec:
volumeMode: Filesystem
storageClassName: standard
accessModes:
- ReadWriteOnce # type of access
capacity:
storage: 100Gi # Size of the volume
hostPath:
path: "/storage/volumes/test-volume"
```
Next you must create a *PersistentVolumeClaim (PVC)* to request access to the resources of the *PersistentVolume (PV)*.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: persistent-test-volume-claim
spec:
volumeMode: Filesystem
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
selector:
matchLabels:
name: persistent-test-volume
```
Now that we've set these two resources up, we can create a pod with a container that references the *PVC* we made above in the `spec.volumes`
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pv-tester
namespace: default
spec:
restartPolicy: Never
containers:
- name: pv-tester
image: busybox
command: ["/bin/sh", "-c", "echo 'Hello volume' > /test_vol/hello.txt"]
volumeMounts:
- name: vol
mountPath: /test_vol
volumes:
- name: vol
persistentVolumeClaim:
claimName: persistent-test-volume-claim
```
You now should be able to see the `hello.txt` file at the path `/storage/volumes/` on the host machine.

@ -0,0 +1,12 @@
---
title: "Reading List"
date: 2022-03-09
draft: false
description: Cookie sweet donut candy pastry apple dolor orange lollipop biscuit. Muffin cream ipsum ipsum sprinkles sugar tiramisu pastry sweet tiramisu.
---
## Kubernetes
- [Kubernetes in Production: The Ultimate Guide to Monitoring Resource Metrics with Prometheus](https://www.replex.io/blog/kubernetes-in-production-the-ultimate-guide-to-monitoring-resource-metrics)
- [K8s Monitor Pod CPU and memory usage with Prometheus](https://itnext.io/k8s-monitor-pod-cpu-and-memory-usage-with-prometheus-28eec6d84729)
- [Understanding machine cpu usage](https://www.robustperception.io/understanding-machine-cpu-usage)

@ -0,0 +1,62 @@
---
title: "Using credentials in your Makefiles"
date: 2020-01-14T05:57:04Z
draft: false
tags: [make, programming]
---
Recently while working on a project I needed a way to include secrets in my repo for local development that integrated
with my *Make* based build setup. I found that a nice way to do this is with the `include` command in *Make*.
The `include` command allows you to include external files to augment your makefile. First I created a `secrets.mk` and
declared variables in it for my API keys.
```makefile {linenos=table}
# secrets.mk
github_access_token := 'xxxxxxxxxxxxxxxxx'
slack_access_token := 'xxxxxxxxxxxxxxxxx'
```
Then in my makefile I added `include secrets.mk`. Don't forget to add `secrets.mk` to your `.gitignore`!
```makefile
include secrets.mk
.PHONY: dev
dev: app
GITHUB_ACCESS_TOKEN=$(github_access_token) \
SLACK_ACCESS_TOKEN=$(slack_access_token) \
./app
app:
go build -o app main.go
```
One issue with this I ran into was in my CI build I didn't have a `secrets.mk` file. *Make* will fail if it cannot find an
include file on disk or if it *Make* unable to find a rule to generate one. Luckily you can preprend a dash to the include
statement to make it optional, so the rest of your tasks that don't require an include will still be usable.
```makefile
-include secrets.mk
```
Lastly, another neat thing about using this technique is that if an include isn't found, *Make* will look for a task that
can generate it. I added a `secrets.mk` task to my makefile that creates a stub include file that can get filled out with
the correct credentials if needed.
```makefile
include secrets.mk
.PHONY: dev
dev: app
GITHUB_ACCESS_TOKEN=$(github_access_token) \
SLACK_ACCESS_TOKEN=$(slack_access_token) \
./app
app:
go build -o app main.go
secrets.mk:
echo "github_access_token:='xxxxxxxx'" > ./secrets.mk
echo "slack_access_token:='xxxxxxxxx'" >> ./secrets.mk
```

@ -0,0 +1 @@
Nothing here yet.

@ -0,0 +1,35 @@
{{ define "main" }}
{{ $description := .Description }}
<div class="flex h-screen relative">
<section
class="w-full h-full md:min-w-[400px] md:w-1/4 bg-slate-50 dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col py-3 overflow-y-auto scroll-area">
<a href="{{ .Permalink }}">
<h2 class="font-bold mb-5 py-1 pl-12 pr-3 md:px-3">{{ .Title }}</h2>
</a>
<div class="space-y-2.5">
{{ range .Data.Pages -}}
<a class="block px-3 py-4 hover:bg-slate-200 dark:hover:bg-slate-700" href="{{ .RelPermalink }}">
{{ $title := .Params.title }}
{{ with .Params.images }}
{{- range first 1 . }}
<img class="rounded max-w-full mb-4" src="{{ . }}" alt="{{ $title }}" />
{{ end -}}
{{ end }}
<h3 class="text-lg font-semibold mb-0.5">{{ $title }}</h3>
<div class="text-sm text-slate-500 dark:text-slate-400 line-clamp-2">
{{ $description }}
</div>
</a>
{{ end -}}
</div>
</section>
<main class="hidden md:grid h-screen place-items-center flex-1">
<p class="text-center p-8 text-2xl text-slate-300 dark:text-slate-700">Select a post to read</p>
</main>
</div>
{{ end }}

@ -0,0 +1,58 @@
{{ define "main" }}
{{ $permalink := .RelPermalink }}
<div class="flex h-screen relative">
<section
class="will-change-transform transform transition-transform -translate-x-[200%] absolute top-0 left-0 lg:relative
lg:translate-x-0 lg:min-w-[400px] lg:w-1/4 h-full bg-slate-50 dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 lg:flex flex-col py-3 overflow-y-auto scroll-area">
{{$Section := .Site.GetPage "section" .Section }}
{{ with $Section }}
<a href="{{ .Permalink }}">
<h2 class="font-bold mb-5 py-1 pl-12 pr-3 md:px-3">{{ .Title }}</h2>
</a>
<div class="space-y-2.5">
{{ range .Data.Pages -}}
{{ $isCurrentPage := eq .RelPermalink $permalink }}
<a class="block px-3 py-4 {{ if $isCurrentPage }} bg-slate-900 dark:bg-slate-700 text-slate-50 {{ else }} hover:bg-slate-200 dark:hover:bg-slate-700 {{ end }}"
href="{{ .RelPermalink }}">
{{ $title := .Params.title }}
{{ with .Params.images }}
{{- range first 1 . }}
<img class="rounded max-w-full mb-4" src="{{ . }}" alt="{{ $title }}" />
{{ end -}}
{{ end }}
<h3 class="text-lg font-semibold mb-0.5">{{ $title }}</h3>
<div
class="text-sm {{ if $isCurrentPage }} text-slate-400 {{ else }} text-slate-500 dark:text-slate-400 {{ end }} line-clamp-2">
{{ .Summary }}
</div>
</a>
{{ end -}}
</div>
{{ end -}}
</section>
<div class="overflow-y-auto h-screen w-full">
<article class="px-6 py-20 w-full mx-auto prose lg:prose-lg h-fit dark:prose-invert prose-img:mx-auto">
{{ $title := .Params.title }}
<h1 class="!mb-2">{{ $title }}</h1>
{{ if .Date }}
<p class="text-sm text-slate-500 !mb-8">{{ .Date.Format "January 2, 2006" }}</p>
{{ end }}
{{ with .Params.images }}
{{- range first 1 . }}
<img class="rounded max-w-full mx-auto mb-4" src="{{ . }}" alt="{{ $title }}" />
{{ end -}}
{{ end }}
{{ .Content }}
</article>
</div>
</div>
{{ end }}

@ -0,0 +1,6 @@
<svg class="h-4 w-4" viewBox="0 0 448 512" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" >
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,83 @@
<aside
class="will-change-transform transform transition-transform -translate-x-full absolute top-0 left-0 md:relative md:translate-x-0 w-3/4 md:w-60 h-full min-h-screen p-3 bg-slate-50 dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col gap-2.5 z-20 sidebar">
<p class="font-bold mb-5 flex items-center gap-2">
<button
aria-label="Close sidebar"
class="md:hidden menu-trigger-close p-1 rounded text-slate-800 dark:text-slate-50 hover:bg-slate-200 dark:hover:bg-slate-700">
{{- partial "icon/closeIcon.html" . -}}
</button>
<a href="{{ .Site.BaseURL }}" class="px-2">
<span>{{ .Site.Title }}</span>
</a>
<button
aria-label="Toggle dark mode"
class="dark-mode-toggle p-2 rounded border dark:border-slate-700 hover:bg-slate-200 dark:hover:bg-slate-700">
{{- partial "icon/sunIcon.html" . -}}
</button>
</p>
{{ $permalink := cond (eq (len .RelPermalink) 1) .RelPermalink (strings.TrimRight "/" .RelPermalink) }}
<ul class="list-none flex flex-col gap-1">
{{ range .Site.Menus.main }}
<li>
<a class="px-2 py-1.5 rounded-md text-sm flex items-center justify-between {{ if eq .URL $permalink }} bg-slate-800 text-white dark:bg-slate-50 dark:text-slate-800 dark:selection:bg-slate-400 {{ else }} hover:bg-slate-200 dark:hover:bg-slate-700 {{ end }}"
href="{{ .URL }}" {{ if strings.HasPrefix .URL "http" }} target="_blank" rel="noopener" {{ end }}>
<span>{{ .Name }}</span>
{{ if strings.HasPrefix .URL "http" }}
<span>
{{- partial "icon/externalIcon.html" . -}}
</span>
{{ end }}
</a>
</li>
{{ end }}
</ul>
<div class="flex-1"></div>
{{ if .Site.Params.newsletter }}
{{- partial "newsletter-widget.html" . -}}
{{ end }}
<ul class="list-none flex flex-wrap justify-center gap-1 pt-2 border-t border-slate-200 dark:border-slate-600">
{{ range .Site.Menus.social }}
<li>
<a class="px-2 py-1.5 rounded-md text-sm block text-slate-800 dark:text-slate-50 {{ if eq (.URL|absURL) $permalink }} bg-slate-800 text-white dark: {{ else }} hover:bg-slate-200 dark:hover:bg-slate-700 {{ end }}"
href="{{ .URL }}" target="_blank" rel="noopener noreferrer">
<span class="sr-only">{{ .Name }}</span>
{{ if eq .Name "GitHub" }}
<span>{{- partial "icon/githubIcon.html" . -}}</span>
{{ else if eq .Name "Keybase" }}
<span>{{- partial "icon/keybaseIcon.html" . -}}</span>
{{ else if eq .Name "Twitter" }}
<span>{{- partial "icon/twitterIcon.html" . -}}</span>
{{ else if eq .Name "LinkedIn" }}
<span>{{- partial "icon/linkedinIcon.html" . -}}</span>
{{ else if eq .Name "Instagram" }}
<span>{{- partial "icon/instagramIcon.html" . -}}</span>
{{ else if eq .Name "Dribbble" }}
<span>{{- partial "icon/dribbbleIcon.html" . -}}</span>
{{ else if eq .Name "Codepen" }}
<span>{{- partial "icon/codepenIcon.html" . -}}</span>
{{ else if eq .Name "Twitch" }}
<span>{{- partial "icon/twitchIcon.html" . -}}</span>
{{ else if eq .Name "Email" }}
<span>{{- partial "icon/emailIcon.html" . -}}</span>
{{ else if eq .Name "RSS" }}
<span>{{- partial "icon/rssfeedIcon.html" . -}}</span>
{{ end }}
</a>
</li>
{{ end }}
</ul>
</aside>
<div
class="fixed bg-slate-700 bg-opacity-5 transition duration-200 ease-in-out inset-0 z-10 pointer-events-auto md:hidden left-0 top-0 w-full h-full hidden menu-overlay">
</div>
<button
aria-label="Toggle Sidebar"
class="md:hidden absolute top-3 left-3 z-10 menu-trigger p-1 rounded text-slate-800 dark:text-slate-50 hover:bg-slate-100">
{{- partial "icon/menuIcon.html" . -}}
</button>

@ -0,0 +1,23 @@
SHA=$(shell git rev-parse --short=6 HEAD)
BUILD_DATE=$(shell date --iso-8601=minutes)
.PHONY: build docker install dev
dev:
hugo serve -D --enableGitInfo
docker:
docker build \
--label="org.opencontainers.image.created=${BUILD_DATE}" \
--label="org.opencontainers.image.source=https://git.vdhsn.com/adam/garden.git" \
--label="org.opencontainers.image.url=https://git.vdhsn.com/adam/garden" \
--label="org.opencontainers.image.revision=${SHA}" \
--label="org.opencontainers.image.licenses=MIT" \
--label="org.opencontainers.image.authors=Adam Veldhousen <adam@vdhsn.com>" \
-t vdhsn/garden:latest .
build:
hugo --verbose --minify --enableGitInfo
install:
asdf local

@ -0,0 +1,85 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=YqU0
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

@ -0,0 +1 @@
Subproject commit b8339def522c34047459c8a25938c02f8ccbfbb5
Loading…
Cancel
Save