init blog
This commit is contained in:
commit
0caaae801e
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build/
|
||||||
|
node_modules/
|
16
lib/css/main.css
Normal file
16
lib/css/main.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import 'highlight.js/styles/dark.css';
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
@tailwind variants;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
font-family: IBM Plex Mono, ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow > * + * {
|
||||||
|
margin-block-start: var(--flow-space, 1em);
|
||||||
|
}
|
23
lib/layouts/default.njk
Normal file
23
lib/layouts/default.njk
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="/main.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-white text-black dark:bg-black dark:text-white">
|
||||||
|
<section class="flow md:container md:mx-auto p-4">
|
||||||
|
{% block nav %}
|
||||||
|
<p>
|
||||||
|
<a href="/">{{ sitename }}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{{ contents | safe }}
|
||||||
|
{% endblock %}
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
lib/layouts/index.njk
Normal file
22
lib/layouts/index.njk
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "default.njk" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ contents | safe }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% for date, posts in collections.posts | groupby('pub.year') | dictsort | reverse %}
|
||||||
|
<h2 class="{{classes.h2}}">{{ date }}</h2>
|
||||||
|
<hr class="{{ classes.hr }}"/>
|
||||||
|
{% for post in posts %}
|
||||||
|
<div>
|
||||||
|
<h4 class="{{ classes.h4 }}"><a href="{{ post.permalink }}">{{ post.title | safe }}</a></h4>
|
||||||
|
<h5>{{ post.description | safe }}</h5>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
No posts
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
21
lib/layouts/post.njk
Normal file
21
lib/layouts/post.njk
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "default.njk" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{{ contents | safe }}
|
||||||
|
|
||||||
|
{% if previous %}
|
||||||
|
<p>
|
||||||
|
Previous:
|
||||||
|
<a href="/{{ previous.path }}">{{ previous.title }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next %}
|
||||||
|
<p>
|
||||||
|
Next:
|
||||||
|
<a href="/{{ next.path }}">{{ next.title }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
69
lib/tailwind-renderer.js
Normal file
69
lib/tailwind-renderer.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
export const classes = {
|
||||||
|
blockquote: 'flow px-4 border-l-8 border-purple-900',
|
||||||
|
footnote: 'list-disc flow',
|
||||||
|
footnote_section: 'mx-8 my-4',
|
||||||
|
bullet_list: 'list-disc mx-4',
|
||||||
|
h1: 'text-6xl',
|
||||||
|
h2: 'text-4xl',
|
||||||
|
h3: 'text-2xl',
|
||||||
|
h4: 'text-xl',
|
||||||
|
h5: 'text-lg',
|
||||||
|
h6: 'text-base',
|
||||||
|
hljs: 'hljs p-4 overflow-x-scroll',
|
||||||
|
hr: 'border-red-900',
|
||||||
|
code: 'text-slate-400',
|
||||||
|
image: 'grayscale',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rules = (md) => {
|
||||||
|
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
|
||||||
|
const defaultRenderer = (name) => md.renderer.rules[name] || proxy;
|
||||||
|
|
||||||
|
const defaultBulletListOpenRenderer = defaultRenderer('bullet_list_open')
|
||||||
|
const defaultBlockquoteOpenRenderer = defaultRenderer('blockquote_open');
|
||||||
|
const defaultHeadingOpenRenderer = defaultRenderer('heading_open');
|
||||||
|
const defaultCodeOpenRenderer = defaultRenderer('code_inline');
|
||||||
|
const defaultHrRenderer = defaultRenderer('hr');
|
||||||
|
const defaultImageRenderer = defaultRenderer('image');
|
||||||
|
|
||||||
|
return {
|
||||||
|
footnote_block_open: () => (
|
||||||
|
`<hr class="${classes.hr}">\n` +
|
||||||
|
`<section class="${classes.footnote_section}">\n` +
|
||||||
|
`<ol class="${classes.footnote}">\n`
|
||||||
|
),
|
||||||
|
|
||||||
|
bullet_list_open: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes.bullet_list);
|
||||||
|
|
||||||
|
return defaultBulletListOpenRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
|
||||||
|
blockquote_open: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes.blockquote);
|
||||||
|
return defaultBlockquoteOpenRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
|
||||||
|
heading_open: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes[tokens[idx].tag]);
|
||||||
|
return defaultHeadingOpenRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
|
||||||
|
code_inline: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes.code);
|
||||||
|
console.log(tokens[idx])
|
||||||
|
return defaultCodeOpenRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
|
||||||
|
hr: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes.hr);
|
||||||
|
return defaultHrRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
|
||||||
|
image: (tokens, idx, options, env, self) => {
|
||||||
|
tokens[idx].attrJoin("class", classes.image);
|
||||||
|
return defaultImageRenderer(tokens, idx, options, env, self);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
140
metalsmith.js
Normal file
140
metalsmith.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
import collections from '@metalsmith/collections';
|
||||||
|
import drafts from '@metalsmith/drafts';
|
||||||
|
import layouts from '@metalsmith/layouts';
|
||||||
|
import markdown from '@metalsmith/markdown';
|
||||||
|
import permalinks from '@metalsmith/permalinks';
|
||||||
|
import inplace from '@metalsmith/in-place';
|
||||||
|
import Metalsmith from 'metalsmith';
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
import MarkdownIt from 'markdown-it';
|
||||||
|
import MarkdownItFootnote from 'markdown-it-footnote';
|
||||||
|
|
||||||
|
import postcss from 'postcss';
|
||||||
|
import autoprefixer from 'autoprefixer';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import atImport from 'postcss-import';
|
||||||
|
|
||||||
|
import { rules, classes } from './lib/tailwind-renderer.js';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const t1 = performance.now();
|
||||||
|
|
||||||
|
let md
|
||||||
|
|
||||||
|
Metalsmith(__dirname)
|
||||||
|
.source('./src')
|
||||||
|
.destination('./build')
|
||||||
|
.clean(true)
|
||||||
|
.env({
|
||||||
|
DEBUG: process.env.DEBUG,
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
})
|
||||||
|
.metadata({
|
||||||
|
sitename: 'vlv',
|
||||||
|
siteurl: 'https://vlv.io',
|
||||||
|
description: 'vlv - a scatterbraindump',
|
||||||
|
generatorname: 'Metalsmith',
|
||||||
|
generatorurl: 'https://metalsmith.io/',
|
||||||
|
classes,
|
||||||
|
})
|
||||||
|
.use(drafts({
|
||||||
|
default: false,
|
||||||
|
include: false,
|
||||||
|
}))
|
||||||
|
.use(
|
||||||
|
markdown({
|
||||||
|
render: function (source, options, context) {
|
||||||
|
if (!md) {
|
||||||
|
md = new MarkdownIt(options);
|
||||||
|
md.use(MarkdownItFootnote);
|
||||||
|
md.renderer.rules = {
|
||||||
|
...md.renderer.rules,
|
||||||
|
...rules(md),
|
||||||
|
};
|
||||||
|
console.log(md.renderer.rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.key == 'contents') {
|
||||||
|
return md.render(source)
|
||||||
|
} else {
|
||||||
|
return md.renderInline(source)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
engineOptions: {
|
||||||
|
linkify: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return `<pre class="${classes.hljs}"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
|
||||||
|
} catch (__) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<pre class="${classes.hljs}"><code>${md.utils.escapeHtml(str)}</code></pre>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
collections({
|
||||||
|
posts: {
|
||||||
|
pattern: 'posts/**/*.html',
|
||||||
|
sortBy: 'date',
|
||||||
|
reverse: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
permalinks({
|
||||||
|
pattern: ':date/:title',
|
||||||
|
date: 'YYYY',
|
||||||
|
relative: false,
|
||||||
|
slug: {
|
||||||
|
remove: /[^a-z0-9- ]+/gi,
|
||||||
|
lower: true,
|
||||||
|
extend: {
|
||||||
|
"'": '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
inplace({
|
||||||
|
transform: 'nunjucks'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
layouts({
|
||||||
|
directory: 'lib/layouts',
|
||||||
|
default: 'post.njk',
|
||||||
|
engineOptions: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.build((err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
// run postcss with tailwind at the end
|
||||||
|
fs.readFile('./lib/css/main.css', (err, css) => {
|
||||||
|
console.log('running postcss ...');
|
||||||
|
postcss([tailwindcss, autoprefixer, atImport])
|
||||||
|
.process(css, { from: './lib/css/main.css', to: './build/main.css' })
|
||||||
|
.then((result) => {
|
||||||
|
fs.writeFile('./build/main.css', result.css, () => true);
|
||||||
|
if (result.map) {
|
||||||
|
fs.writeFile(
|
||||||
|
'./build/main.css.map',
|
||||||
|
result.map.toString(),
|
||||||
|
() => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log('completed postcss ...');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Build success in ${((performance.now() - t1) / 1000).toFixed(1)}s`,
|
||||||
|
);
|
||||||
|
});
|
1570
package-lock.json
generated
Normal file
1570
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "node metalsmith.js",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@metalsmith/collections": "1.3.0",
|
||||||
|
"@metalsmith/drafts": "1.3.0",
|
||||||
|
"@metalsmith/in-place": "5.0.0",
|
||||||
|
"@metalsmith/layouts": "2.7.0",
|
||||||
|
"@metalsmith/markdown": "1.10.0",
|
||||||
|
"@metalsmith/permalinks": "2.5.1",
|
||||||
|
"@metalsmith/postcss": "5.4.1",
|
||||||
|
"autoprefixer": "10.4.14",
|
||||||
|
"highlight.js": "11.8.0",
|
||||||
|
"jstransformer-nunjucks": "1.2.0",
|
||||||
|
"markdown-it": "13.0.1",
|
||||||
|
"markdown-it-footnote": "3.0.3",
|
||||||
|
"metalsmith": "2.6.1",
|
||||||
|
"postcss": "8.4.27",
|
||||||
|
"postcss-import": "15.1.0",
|
||||||
|
"prettier": "3.0.1",
|
||||||
|
"tailwindcss": "3.3.3"
|
||||||
|
}
|
||||||
|
}
|
3
prettier.config.js
Normal file
3
prettier.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
singleQuote: true,
|
||||||
|
};
|
BIN
src/assets/P7240102.jpg
Normal file
BIN
src/assets/P7240102.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 675 KiB |
6
src/index.md
Normal file
6
src/index.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: index
|
||||||
|
layout: index.njk
|
||||||
|
---
|
||||||
|
|
||||||
|
asdf
|
453
src/posts/2019/bash-powered-flat-file-blog.md
Normal file
453
src/posts/2019/bash-powered-flat-file-blog.md
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
---
|
||||||
|
title: Bash powered flat-file blog
|
||||||
|
date: 2019-08-01
|
||||||
|
pub:
|
||||||
|
year: 2019
|
||||||
|
---
|
||||||
|
# Bash powered flat-file blog
|
||||||
|
|
||||||
|
I've been meaning to put something on this space for a long time, yet I
|
||||||
|
couldn't bring myself to commit to a weblog software. I just want to
|
||||||
|
serve up flat markdown files and I am aware of that there are
|
||||||
|
[hundreds](https://github.com/myles/awesome-static-generators)
|
||||||
|
upon [hundreds](https://staticsitegenerators.net/) of potential
|
||||||
|
candidates to do this. Why reinvent the wheel?
|
||||||
|
|
||||||
|
Guess what, I did reinvent the wheel.
|
||||||
|
|
||||||
|
Multiple times: [1](https://github.com/varl/blorgh),
|
||||||
|
[2](https://github.com/varl/spine),
|
||||||
|
[3](https://github.com/varl/blog-elm),
|
||||||
|
[4](https://github.com/varl/galgen). _Spine_ actually works, and is
|
||||||
|
kinda neat. It's what powered this site four years ago, and I've used it
|
||||||
|
to deploy static sites for clients. _Blorgh_ was meant to replace
|
||||||
|
_Spine_ ... You know how it goes. In the meantime my [podcast
|
||||||
|
downloader](https://github.com/varl/pyphoid) broke and I decided to
|
||||||
|
rewrite that in a new language (Go) as an exercise in learning a new
|
||||||
|
language. Obviously that has priority over trying to fix the parser to
|
||||||
|
accept some changes that the feed provider did to their feeds. I thought
|
||||||
|
RSS was widely accepted, but I guess everyone has to add their own
|
||||||
|
extensions to it.
|
||||||
|
|
||||||
|
It is Java however and since I've reduced the specs for this virtual
|
||||||
|
server (cost optimization) and tried to outsource some of the tasks it
|
||||||
|
previously did, so Java had to go. It was also a chore updating and
|
||||||
|
recompiling the thing. *cough* Made up excuses. *cough*
|
||||||
|
|
||||||
|
It was hard enough to find a Wiki software that used Git as a backing
|
||||||
|
storage system. Eventually I settled on [Gitit]() that gives me the
|
||||||
|
Wiki data in a [nice and flat
|
||||||
|
format](https://git.vardevs.se/varl/wikidata.git/tree/).
|
||||||
|
|
||||||
|
I remembered that a former colleague of mine experimented with [Bash
|
||||||
|
CGI](https://github.com/ruudud/cgi), and hey, I have
|
||||||
|
[Pandoc](https://pandoc.org/) already since it was required for Gitit.
|
||||||
|
Let's begin to connect the pipes.
|
||||||
|
|
||||||
|
## Server setup
|
||||||
|
|
||||||
|
Primarily we need two things to do our thing: **nginx** and
|
||||||
|
**fcgiwrap**. We are going to use FastCGI (shipped with **nginx**) and
|
||||||
|
FCGI Wrap to make it easy for us to run our Bash scripts through the
|
||||||
|
Common Gateway Interface.
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install nginx fcgiwrap
|
||||||
|
```
|
||||||
|
|
||||||
|
### File-system
|
||||||
|
|
||||||
|
We need to serve our CGI scripts from somewhere, as well as have a root
|
||||||
|
for the public files. Throw in a log file directory for access logs and
|
||||||
|
what not.
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/www/vlv.io$ tree -L 1
|
||||||
|
.
|
||||||
|
├ cgi
|
||||||
|
├ logs
|
||||||
|
└ public
|
||||||
|
|
||||||
|
3 directories, 0 files
|
||||||
|
```
|
||||||
|
|
||||||
|
### NGINX
|
||||||
|
|
||||||
|
- `/etc/nginx/sites-available/vlv.io.vhost`:
|
||||||
|
|
||||||
|
```
|
||||||
|
server {
|
||||||
|
server_name .vlv.io;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
access_log /srv/www/vlv.io/logs/access.log;
|
||||||
|
error_log /srv/www/vlv.io/logs/error_log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /srv/www/vlv.io/public/;
|
||||||
|
autoindex on;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/cgi {
|
||||||
|
root /srv/www/vlv.io/cgi/;
|
||||||
|
rewrite ^/cgi/(.*) /$1 break;
|
||||||
|
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_pass unix:/var/run/fcgiwrap.socket;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /srv/www/vlv.io/cgi$fastcgi_script_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CGI Bash
|
||||||
|
|
||||||
|
The
|
||||||
|
[`httputils`](https://github.com/ruudud/cgi/blob/master/cgi-bin/httputils)
|
||||||
|
makes writing end-points easier, so let's use that for the foundation of
|
||||||
|
our scripts.
|
||||||
|
|
||||||
|
Additionally, we use [`jq`](https://stedolan.github.io/jq/) to generate
|
||||||
|
JSON, and as mentioned, [`Pandoc`](https://pandoc.org) to generate HTML
|
||||||
|
reponses:
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install jq pandoc
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's explore the scripts, shall we?
|
||||||
|
|
||||||
|
- The first script is to return a list of the articles, `/srv/www/vlv.io/cgi/ls`:
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ "$SCRIPT_FILENAME" ]]; then
|
||||||
|
. "$(dirname $SCRIPT_FILENAME)/httputils"
|
||||||
|
else
|
||||||
|
. "$(dirname $(pwd)$SCRIPT_NAME)/httputils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
METHOD_NOT_ALLOWED="405 Method Not Allowed"
|
||||||
|
METHOD_OK="200 OK"
|
||||||
|
|
||||||
|
PLAN_PATH="/srv/www/vlv.io/public/plan"
|
||||||
|
PLAN_PATH_LENGTH=${#PLAN_PATH}
|
||||||
|
|
||||||
|
do_GET() {
|
||||||
|
shopt -s nullglob globstar
|
||||||
|
local array=($PLAN_PATH/**/*)
|
||||||
|
local length=${#array[@]}
|
||||||
|
shopt -u nullglob globstar
|
||||||
|
|
||||||
|
echo "Status: ${METHOD_OK}"
|
||||||
|
echo ""
|
||||||
|
if [[ $length == 0 ]]; then
|
||||||
|
cat <<JSON
|
||||||
|
{
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
local json_array="["
|
||||||
|
local count=1
|
||||||
|
|
||||||
|
for i in "${array[@]}"; do
|
||||||
|
if [ -f "${i}" ]; then
|
||||||
|
local file=${i:PLAN_PATH_LENGTH}
|
||||||
|
local f="${file%.*}"
|
||||||
|
json_array="${json_array}\"${f}\""
|
||||||
|
|
||||||
|
if [[ $count -lt $length ]]; then
|
||||||
|
json_array="${json_array},"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
(( count += 1 ))
|
||||||
|
done
|
||||||
|
|
||||||
|
json_array="${json_array}]"
|
||||||
|
cat <<JSON
|
||||||
|
{
|
||||||
|
"files": $json_array
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Content-Type: application/json"
|
||||||
|
|
||||||
|
case $REQUEST_METHOD in
|
||||||
|
GET)
|
||||||
|
do_GET
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "No handle for $REQUEST_METHOD"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
- We pass in the file to load to the `/srv/www/vlv.io/cgi/view` script,
|
||||||
|
which loads and transforms the markdown to html:
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ "$SCRIPT_FILENAME" ]]; then
|
||||||
|
. "$(dirname $SCRIPT_FILENAME)/httputils"
|
||||||
|
else
|
||||||
|
. "$(dirname $(pwd)$SCRIPT_NAME)/httputils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
METHOD_NOT_ALLOWED="405 Method Not Allowed"
|
||||||
|
METHOD_OK="200 OK"
|
||||||
|
|
||||||
|
do_GET() {
|
||||||
|
echo "Status: ${METHOD_OK}"
|
||||||
|
echo ""
|
||||||
|
local FILE_CONTENTS=$(pandoc --html-q-tags --from=markdown --to=html "/srv/www/vlv.io/public/plan/${QUERY_STRING}.md")
|
||||||
|
local ENCODED_CONTENTS=$(jq -n --arg q "$QUERY_STRING" --arg f "$FILE_CONTENTS" '{"filename": $q, "contents": $f}')
|
||||||
|
cat <<JSON
|
||||||
|
$ENCODED_CONTENTS
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Content-Type: application/json"
|
||||||
|
|
||||||
|
case $REQUEST_METHOD in
|
||||||
|
GET)
|
||||||
|
do_GET
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "No handle for $REQUEST_METHOD"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
That's all we need server-side to return the list of articles and to
|
||||||
|
view an article's content:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -i https://vlv.io/cgi/ls
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Server: nginx/1.14.2
|
||||||
|
Date: Mon, 19 Aug 2019 07:41:34 GMT
|
||||||
|
Content-Type: application/json
|
||||||
|
Transfer-Encoding: chunked
|
||||||
|
Connection: keep-alive
|
||||||
|
|
||||||
|
{
|
||||||
|
"files":
|
||||||
|
["/2019/07/we-did-a-thing","/2019/08/bash-powered-flat-file-blog"]
|
||||||
|
}
|
||||||
|
|
||||||
|
$ curl -i https://vlv.io/cgi/view?/2019/07/we-did-a-thing
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Server: nginx/1.14.2
|
||||||
|
Date: Mon, 19 Aug 2019 07:42:57 GMT
|
||||||
|
Content-Type: application/json
|
||||||
|
Transfer-Encoding: chunked
|
||||||
|
Connection: keep-alive
|
||||||
|
|
||||||
|
{
|
||||||
|
"filename": "/2019/07/we-did-a-thing",
|
||||||
|
"contents": "<figure>\n<img
|
||||||
|
src=\"../../../share/2019/baby-online/P7240102.jpg\"
|
||||||
|
alt=\"Welcome to the world, little one.\" /><figcaption>Welcome
|
||||||
|
to the world, little one.</figcaption>\n</figure>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives us our API, but we still want to consume it.
|
||||||
|
|
||||||
|
## Client setup
|
||||||
|
|
||||||
|
### HTML/CSS
|
||||||
|
|
||||||
|
In `/srv/www/vlv.io/public/index.html` we keep a simple template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>vlv.io</title>
|
||||||
|
<script type="module" src="/static/vlv.io.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/vlv.io.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<viva-log></viva-log>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `vlv.io.css` CSS file is a [Normalize
|
||||||
|
CSS](https://github.com/necolas/normalize.css) file + some very basic
|
||||||
|
styles:
|
||||||
|
|
||||||
|
```
|
||||||
|
body {
|
||||||
|
background-color: #181818;
|
||||||
|
color: #aeaeae;
|
||||||
|
height: 100%;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
The logger itself is a self-contained custom element, and we can split
|
||||||
|
the `vlv.io.js` script into two parts. The `template` element and the
|
||||||
|
`viva-log` element.
|
||||||
|
|
||||||
|
The slotted template:
|
||||||
|
|
||||||
|
```
|
||||||
|
const template = document.createElement('template')
|
||||||
|
|
||||||
|
template.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
color: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #181818;
|
||||||
|
background-color: #aeaeae;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<slot name="list">Loading menu...</slot>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<slot name="view">Select a file to view...</slot>
|
||||||
|
</article>
|
||||||
|
`
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives us our boilerplate, which we use in the `viva-log` element:
|
||||||
|
|
||||||
|
```
|
||||||
|
customElements.define('viva-log', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: 'open' })
|
||||||
|
this.shadow.appendChild(template.content.cloneNode(true))
|
||||||
|
|
||||||
|
this.listSlot = this.shadow.querySelector('slot[name=list]')
|
||||||
|
this.viewSlot = this.shadow.querySelector('slot[name=view]')
|
||||||
|
|
||||||
|
const url = new URL(window.location)
|
||||||
|
|
||||||
|
if (url.pathname !== '/') {
|
||||||
|
this.view(url.pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
const data = await fetch(`/cgi/ls`)
|
||||||
|
const response = await data.json()
|
||||||
|
const files = response.files
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.setAttribute('href', `${file}`)
|
||||||
|
|
||||||
|
a.onclick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const url = new URL(e.target.href)
|
||||||
|
this.view(url.pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.appendChild(document.createTextNode(`${file}`))
|
||||||
|
|
||||||
|
div.appendChild(a)
|
||||||
|
div.appendChild(document.createElement('br'))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listSlot.replaceChild(div, this.listSlot.firstChild)
|
||||||
|
}
|
||||||
|
|
||||||
|
async view(file) {
|
||||||
|
this.viewSlot.innerHTML = `Loading '${file}'...`
|
||||||
|
|
||||||
|
const data = await fetch(`/cgi/view?${file}`)
|
||||||
|
const response = await data.json()
|
||||||
|
|
||||||
|
history.pushState({}, `vlv.io :: ${response.filename}`, `${file}`)
|
||||||
|
|
||||||
|
const text = response.contents
|
||||||
|
|
||||||
|
this.viewSlot.innerHTML = text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- The first improvement would be to generate the HTML from Markdown to a
|
||||||
|
temporary file, as right now each request triggers Pandoc to parse and
|
||||||
|
convert our Markdown file to HTML.
|
||||||
|
- - An alternative is to convert from Markdown to HTML on the client.
|
||||||
|
|
||||||
|
#### Update -- 2020-06-22
|
||||||
|
|
||||||
|
An additional script, `generate.sh` now converts the raw Markdown to
|
||||||
|
HTML using Pandoc statically, and then generates the JSON for the API
|
||||||
|
statically as well, effectively pre-rendering (if not caching) the
|
||||||
|
pages.
|
||||||
|
|
||||||
|
It has the benefit of being statically explorable in addition to the
|
||||||
|
JavaScript client: [blog/2020/06](https://vlv.io/blog/2020/06/)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Depends on how strict the Bash scripts are implemented, e.g. if not
|
||||||
|
jailed to a specific directory, and the FastCGI user has read-access,
|
||||||
|
a script could potentially return the contents of arbitrary files.
|
||||||
|
|
||||||
|
## F.A.Q
|
||||||
|
|
||||||
|
> Couldn't you have used SSI to accomplish the exact same behaviour?
|
||||||
|
|
||||||
|
Yes.
|
||||||
|
|
||||||
|
> ES Modules? Custom elements? ShadowRoot? Templates & slots? History
|
||||||
|
> API? All that for _this_ tiny little thing?
|
||||||
|
|
||||||
|
Yes.
|
||||||
|
|
||||||
|
> Babel? Polyfills? Transpilation?
|
||||||
|
|
||||||
|
No.
|
181
src/posts/2019/chili.md
Normal file
181
src/posts/2019/chili.md
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
---
|
||||||
|
title: World's best chili
|
||||||
|
date: 2019-11-01
|
||||||
|
pub:
|
||||||
|
year: 2019
|
||||||
|
---
|
||||||
|
|
||||||
|
_(at some point i might translate this amazing Chili recipe)_
|
||||||
|
|
||||||
|
# VÄRLDENS BÄSTA CHILI
|
||||||
|
|
||||||
|
Receptet hittade vi ursprungligen i en bok av Texas-journalisten Francis X
|
||||||
|
Tolbert, en man som vigde sitt liv åt denna eldiga köttgryta.
|
||||||
|
|
||||||
|
Det hela började på det tidiga 60-talet då Tolbert skrev en artikel som fick
|
||||||
|
rubriken "Jakten på den äkta chilin".
|
||||||
|
|
||||||
|
Under de kommande åren fick han 48 000 brevsvar från världens alla hörn. Han
|
||||||
|
läste, lagade, reste, provåt och intervjuade levande legender som Cap Warren,
|
||||||
|
den siste ranchkocken som fortfarande kokade mat åt sina cowboys på en vedeldad
|
||||||
|
spis bakpå den täckta kokvagnen.
|
||||||
|
|
||||||
|
Ganska snart kunde Tolbert slå fast vad som a b s o l u t inte får finnas med i
|
||||||
|
en chili:
|
||||||
|
|
||||||
|
- tomater
|
||||||
|
- vita bönor
|
||||||
|
- gul lök
|
||||||
|
- köttfärs
|
||||||
|
|
||||||
|
Äkta chili lagar man nämligen på hela köttbitar och rätt lagad ska den vara
|
||||||
|
just vad namnet chili con carne antyder: rödpeppar med en viss tillsats av
|
||||||
|
kött.
|
||||||
|
|
||||||
|
Det stora problemet visade sig vara att hitta den rätta blandningen av olika
|
||||||
|
pepparsorter så att chilin får en bred fyllig hetta som varar länge och inte
|
||||||
|
enbart blir olidligt skarp.
|
||||||
|
|
||||||
|
Till sist tvingades Tolbert arrangera ett världens första chili-VM och det
|
||||||
|
recept som vi publicerar här är en variant av det som segrade - NORTH TEXAS
|
||||||
|
RED.
|
||||||
|
|
||||||
|
Det recept som följer är en lätt försvanekad version. Ingen som prövat det har
|
||||||
|
klagat på styrkan, men många har haft svårt att hitta de rätta ingredienserna.
|
||||||
|
För er som bor i Stockholm rekommenderar vi en butik som heter BBQ & Chili som
|
||||||
|
har det mesta i chili-väg och dessutom en stor sortering salsor och såser.
|
||||||
|
|
||||||
|
Det här är vad du behöver:
|
||||||
|
|
||||||
|
- En mycket stor svart järngryta
|
||||||
|
- En stekpanna
|
||||||
|
- En liten kastrull
|
||||||
|
- En helflaska tequila
|
||||||
|
- Sex burkar ljust öl, helst det mexikanska Corona.
|
||||||
|
- 5 torkade ancho-pepparfrukter (De är stora, mörkt brunröda och finns i
|
||||||
|
affärer som säljer latinamerikansk mat. I nödfall kan de ersättas med en
|
||||||
|
blandning av mörkrött chilipulver och ett antal flådda, urkärnade röda
|
||||||
|
paprikor.)
|
||||||
|
- 1 chipotle-peppar (Röd, rökt. Finns konserverad i latinamerikanska affärer.)
|
||||||
|
- 3 birdseye-pepparfrukter (Små spetsiga klarröda. Kan ersättas av torkad, mald
|
||||||
|
piri-piri).
|
||||||
|
- 4 jalapeno-pepparfrukter (Knubbiga, gröna. Finns på burk i de flesta
|
||||||
|
välsorterade livsmedelsaffärer. Ta den starka varianten.)
|
||||||
|
- Baconfett (eller olja) att steka i.
|
||||||
|
- 10 stora vitlöksklyftor, grovt hackade.
|
||||||
|
- 5 kilo oxkött, skuret i centimeterstora tärningar.
|
||||||
|
- En halv kopp mjöl.
|
||||||
|
- En kopp chilipulver.
|
||||||
|
- Två koppar mörk oxbuljong.
|
||||||
|
- 2 matskedar spiskummin. (Kännarna kan inte komma överens om kryddan ska
|
||||||
|
rostas innan den används eller inte.)
|
||||||
|
- 2 matskedar oregano.
|
||||||
|
- 2 matskedar malda korianderfrön.
|
||||||
|
- 1/2 matsked socker.
|
||||||
|
- Salt, efter smak. Börja försiktigt!
|
||||||
|
- Lite grovt majsmjöl, masa harina.
|
||||||
|
|
||||||
|
Så här gör du:
|
||||||
|
|
||||||
|
1. Ta dig en rejäl tequila. En platta med gamla Hank Williams-låtar bidrar
|
||||||
|
också till så att det rätta chili-perspektivet på tillvaron infinner sig.
|
||||||
|
|
||||||
|
1. Börja sedan med pepparn. Rensa bort stjälkar och frön. Koka den torkade
|
||||||
|
pepparn 15 minuter under lock, ställ åt sidan och låtsvalna.
|
||||||
|
|
||||||
|
1. Rensa och hacka den övriga pepparn. Ställ åt sidan. (Här är en varning på
|
||||||
|
plats. Peppar är starkt. Den BRÄNNS. Se upp för ångorna när du kokar och tvätta
|
||||||
|
händerna noga efteråt. Och tänk noga på vad då gör med fingrarna det närmaste
|
||||||
|
dygnet. Om du petar dig i näsan kan du lika gärna göra det med lödkolven.)
|
||||||
|
|
||||||
|
1. Ta ett glas tequila till, ett rejält glas. Det kommer att behövas. Nu börjar
|
||||||
|
det nämligen dra ihop sig.
|
||||||
|
|
||||||
|
1. Fräs vitlöken mjuk och brun. Lägg i grytan.
|
||||||
|
|
||||||
|
1. Öka värmen i stekpannan och börja stek köttet. Ta lite i sänder och rör om
|
||||||
|
ordentligt så att bitarna steks på alla sidor. Lägg ner i grytan. Detta är ett
|
||||||
|
varmt, osigt och tidsödande slitgöra som kräver både tålamod och tequila.
|
||||||
|
|
||||||
|
1. Blanda mjöl och chilipulver. Strö över köttet i grytan.
|
||||||
|
|
||||||
|
1. Sila av den blötlagda pepparn, men spara vattnet. Mosa den kokta pepparn,
|
||||||
|
tillsätt sedan all peppar till köttet.
|
||||||
|
|
||||||
|
1. Häll på pepparvattnet, oxbuljongen och öl tills vätskan täcker köttet. Koka
|
||||||
|
upp.
|
||||||
|
|
||||||
|
1. Nu är chilin på väg. En nöjd kock kan ta ett steg tillbaka, beundra sin
|
||||||
|
skapelse och belönar sig själv med ytterligare en tequila, raskt åtföljd av det
|
||||||
|
resterande ölet.
|
||||||
|
|
||||||
|
1. Låt chilin småkoka. Rör ner spiskummin, oregano och koriander. Rör ofta så
|
||||||
|
att mästerverket inte bränns fast i botten.
|
||||||
|
|
||||||
|
1. Fortsätt kokningen tills köttet börjar falla sönder. Det bör ta två, tre
|
||||||
|
timmar.
|
||||||
|
|
||||||
|
1. Tequila!!!
|
||||||
|
|
||||||
|
1. Det kan hända att chilin är lite lös när den närmar sig slutkokningen.
|
||||||
|
Riktiga Texasbor reder den då med grovt majsmjöl.
|
||||||
|
|
||||||
|
1. Gör slut på den sista skvätten tequila (om du inte redan gjort det).
|
||||||
|
|
||||||
|
1. Ta av grytan och skumma bort det fett som samlats ovanpå.
|
||||||
|
|
||||||
|
Den här satsen räcker till ett tjugotal normala människor, men högst tio
|
||||||
|
chiliälskare.
|
||||||
|
|
||||||
|
Servera chilin i små, djupa tallrikar. Många gillar att äta den med majschips
|
||||||
|
till, en klick cremé fraiche eller lite grovt riven cheddarost ovanpå. Servera
|
||||||
|
sallad, bröd, guacamole (avocadoröra) och stora mängder ljust öl till.
|
||||||
|
|
||||||
|
Musiken är nästan lika viktig. Satsa på någon genuint: Hank Williams, Buddy
|
||||||
|
Holly, Joe Ely, Jerry Jeff Walker, Flaco Jiminez, Butch Hancock, Commander Cody
|
||||||
|
and his Lost Planet Airmen eller Gram Parsons. Fram mot natten passar det
|
||||||
|
utmärkt att spela Freddy Fenders odödliga "Before the next teardrops falls",
|
||||||
|
Doug Sahms "Wasted days and wasted nights" eller Creedende Clearwater Revivals
|
||||||
|
"Lodi".
|
||||||
|
|
||||||
|
Det går också att spela Wilco, Weeping Willows och helst bör alla sjunga
|
||||||
|
allsång i någon gammal Carter Family sång typ "Will The Circle Be Unbroken"
|
||||||
|
Volymen bör vara öronbedövande. Sjung med. Skråla gärna. Och kom ihåg, en äkta
|
||||||
|
chiliafton S K A spåra ur fram mot natten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
FOTNOT 1: Sedan första publiceringen har vi mottagit en rad klagomål mot
|
||||||
|
tequilan i detta recept. De som har hört av sig har varit rörande ense om att
|
||||||
|
en helflaska är alldeles för lite. Naturligvis var den mängden enbart en
|
||||||
|
rekommendation, anpassad för en person.
|
||||||
|
|
||||||
|
Och kollegan Peter Svensson har förslagit en intressant variant. Den följer
|
||||||
|
här:
|
||||||
|
|
||||||
|
SNABB-CHILI:
|
||||||
|
|
||||||
|
I nödfall kan alla ingredienser utom tequilan utgå. I sådana fall förkortas
|
||||||
|
koktiden avsevärt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
FOTNOT 2: Nyårsafton 1990 lagade Jan Gradvall och Stefan Lindström chili och
|
||||||
|
följde detta recept slaviskt. Ingen av dem minns någonting efter klockan 18.00
|
||||||
|
men överlevande har berättat att kvällen var ovanligt lyckad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
FOTNOT 3: Texten ovan är en lätt omarbetad och uppdaterad version av ett recept
|
||||||
|
som publicerats två gånger i Expressen. Först 1988 och sedan 1991. Under några
|
||||||
|
år var detta den mest efterfrågade artikeln i tidningens arkiv. Vännerna på
|
||||||
|
textarkivet berättar att de till och med fick en förfrågan på en kopia från
|
||||||
|
Saudiarabien under Desert Storm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
FOTNOT 4: denna text får fritt spridas och kopieras under förutsättning att ni
|
||||||
|
inte blandar bönor i chilin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
23
src/posts/2019/tao-te-ching.md
Normal file
23
src/posts/2019/tao-te-ching.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: Tao Te Ching
|
||||||
|
date: 2019-08-30
|
||||||
|
pub:
|
||||||
|
year: 2019
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tao Te Ching ([Stephen Mitchell](http://taoteching.org.uk/index.php?c=30&a=Stephen+Mitchell))
|
||||||
|
|
||||||
|
```
|
||||||
|
The Master does his job
|
||||||
|
and then stops.
|
||||||
|
He understands that the universe
|
||||||
|
is forever out of control,
|
||||||
|
and that trying to dominate events
|
||||||
|
goes against the current of the Tao.
|
||||||
|
Because he believes in himself,
|
||||||
|
he doesn't try to convince others.
|
||||||
|
Because he is content with himself,
|
||||||
|
he doesn't need others' approval.
|
||||||
|
Because he accepts himself,
|
||||||
|
the whole world accepts him.
|
||||||
|
```
|
8
src/posts/2019/we-did-a-thing.md
Normal file
8
src/posts/2019/we-did-a-thing.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: We did a thing
|
||||||
|
date: 2019-07-22
|
||||||
|
pub:
|
||||||
|
year: 2019
|
||||||
|
---
|
||||||
|
|
||||||
|
![Welcome to the world, little one.](/assets/P7240102.jpg)
|
31
src/posts/2020/house.md
Normal file
31
src/posts/2020/house.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
title: 2020 so far
|
||||||
|
date: 2020-06-01
|
||||||
|
pub:
|
||||||
|
year: 2020
|
||||||
|
---
|
||||||
|
# 2020 so far
|
||||||
|
|
||||||
|
This has been a stranger year than most, even when not taking COVID-19
|
||||||
|
into account.
|
||||||
|
|
||||||
|
Buying a house, selling our apartment, renovations, contractors taking
|
||||||
|
our money and then ghosting us, water damage under the floors -- and
|
||||||
|
more!
|
||||||
|
|
||||||
|
Shoulder dislocations aplenty; when moving appliances, hastily grabbing
|
||||||
|
a coffee cup before our boy did, when stretching out my chest, when
|
||||||
|
sleeping with my hand behind my head.
|
||||||
|
|
||||||
|
Funeral attendance over Zoom. I had to put my distrust of Zoom aside for
|
||||||
|
that, not my hill to die on. Most of our combined families are in Sweden
|
||||||
|
whereas we ourselves are in Norway. The lockdown has made the 600 km
|
||||||
|
divide between us accute.
|
||||||
|
|
||||||
|
This is a personal memo to remember these 6 months, and that freedom to
|
||||||
|
travel is a privilege and not a right. Throughout my life borders have
|
||||||
|
become looser and easier to traverse, and it has been an assumption of
|
||||||
|
mine that it would continue in that direction. One virus is all it took
|
||||||
|
to fundamentally challenge that.
|
||||||
|
|
||||||
|
Half the year to go, I wonder what lasting changes we'll see.
|
15
src/posts/2020/keep-up-the-good-work.md
Normal file
15
src/posts/2020/keep-up-the-good-work.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: Keep up the good work
|
||||||
|
date: 2020-06-30
|
||||||
|
pub:
|
||||||
|
year: 2020
|
||||||
|
---
|
||||||
|
|
||||||
|
"Keep up the good work", he said, fully intending it as words of
|
||||||
|
acknowledgement of the quality of work being done, and as encouragement
|
||||||
|
to carry on doing good work.
|
||||||
|
|
||||||
|
What is the implication though? That good things will come if you do
|
||||||
|
good work? Promotion? Money? Status? Are those good things?
|
||||||
|
|
||||||
|
As if.
|
17
src/posts/2020/key-to-success.md
Normal file
17
src/posts/2020/key-to-success.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: The Key to Success
|
||||||
|
date: 2020-06-01
|
||||||
|
pub:
|
||||||
|
year: 2020
|
||||||
|
---
|
||||||
|
# The Key to success
|
||||||
|
|
||||||
|
Wisdom from my grandparents:
|
||||||
|
|
||||||
|
> Nyckeln till framgång är att vid slutet av dagen vara nöjd med det man
|
||||||
|
> gjort.
|
||||||
|
|
||||||
|
Translation:
|
||||||
|
|
||||||
|
> The key to success is to at the end of the day, be content with what
|
||||||
|
> one has done.
|
95
src/posts/2020/monitor-with-irc.md
Normal file
95
src/posts/2020/monitor-with-irc.md
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
---
|
||||||
|
title: Monitoring with IRC
|
||||||
|
date: 2020-09-01
|
||||||
|
pub:
|
||||||
|
year: 2020
|
||||||
|
---
|
||||||
|
# Monitoring with IRC
|
||||||
|
|
||||||
|
I would like to keep an eye on my servers from the comfort of my IRC
|
||||||
|
client.
|
||||||
|
|
||||||
|
I have also been meaning to play around with
|
||||||
|
[ii](https://git.suckless.org/ii/file/README.html) for years and decided
|
||||||
|
that now is the time.
|
||||||
|
|
||||||
|
# Idea outline
|
||||||
|
|
||||||
|
- Host a private IRC server.
|
||||||
|
|
||||||
|
- A computer joins the IRC server and joins a specified channel.
|
||||||
|
|
||||||
|
- When a server task finishes, it sends a message to the channel about
|
||||||
|
what it did, and the result.
|
||||||
|
|
||||||
|
- A server can respond to commands sent to it by monitoring the channel
|
||||||
|
messages.
|
||||||
|
|
||||||
|
# Software
|
||||||
|
|
||||||
|
- [ii](https://git.suckless.org/ii/file/README.html)
|
||||||
|
Chosen for its simplicity. It is a file-based IRC client with a FIFO
|
||||||
|
"In" file for sending commands, and a normal "out" file for messages.
|
||||||
|
This characteristic makes it easy to build a bot using bash.
|
||||||
|
|
||||||
|
- [ngircd](https://github.com/ngircd/ngircd) Chosen for its simplicity.
|
||||||
|
It performs nicely as a small IRC server, and is very easy to
|
||||||
|
configure.
|
||||||
|
|
||||||
|
# Result
|
||||||
|
|
||||||
|
When the server "cc" successfully backups using rclone to Google Photos:
|
||||||
|
|
||||||
|
```
|
||||||
|
21:40 < cc> [OK] rclone backed up /data/media/pictures to photos:album/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
A failure message looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
21:56 < cc> [ERR] rclone failed to backup /data/media/pictures to photos:album/backups:
|
||||||
|
21:56 < cc> 2020/09/14 21:56:02 NOTICE: iphone_2/Europa turné 2012/Thumbs.db: There was an error while trying to create this media item. (3)
|
||||||
|
21:56 < cc> full logs in /home/varl/logs/rclone
|
||||||
|
```
|
||||||
|
|
||||||
|
# Code
|
||||||
|
|
||||||
|
I use a helper command `say` that redirects the given arguments to the
|
||||||
|
"in" file to send them over IRC:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
: "${ircdir:=$HOME/irc}"
|
||||||
|
: "${network:=irc.server.tld}"
|
||||||
|
: "${channel:=#channel}"
|
||||||
|
|
||||||
|
printf -- "%s\n" "$@" > "$ircdir/$network/$channel/in"
|
||||||
|
```
|
||||||
|
|
||||||
|
And the backup script is also kept small and runs with `cron` every
|
||||||
|
night.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
: "${src:=/path/to/stuff}"
|
||||||
|
: "${dest:=photos:album/backups}"
|
||||||
|
: "${logfile:=/path/to/logs/rclone}"
|
||||||
|
|
||||||
|
rclone copy "$src" "$dest" --log-file "$logfile" --log-level NOTICE
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
say "[OK] rclone backed up ${src} to ${dest}"
|
||||||
|
else
|
||||||
|
say "[ERR] rclone failed to backup ${src} to ${dest}:"
|
||||||
|
say "$(tail -n1 $logfile)"
|
||||||
|
say "full logs in ${logfile}"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it for now, currently it's a proof of concept, and I will be
|
||||||
|
trying out how this works for me and add more things to monitor over
|
||||||
|
time. Infrastructre is in place and works nicely for now though.
|
||||||
|
|
||||||
|
Over and out.
|
393
src/posts/2023/varls-nixtape-vol-1.md
Normal file
393
src/posts/2023/varls-nixtape-vol-1.md
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
---
|
||||||
|
title: varl's nixtapes vol.1
|
||||||
|
date: 2023-08-08
|
||||||
|
pub:
|
||||||
|
year: 2023
|
||||||
|
collection: nixtapes
|
||||||
|
---
|
||||||
|
|
||||||
|
> #### TL;DR
|
||||||
|
>
|
||||||
|
> At the end of this post we will have reached a point where we can load a
|
||||||
|
> shell with specific tools, and when we leave that shell, the global
|
||||||
|
> environment is untouched.
|
||||||
|
|
||||||
|
# volume 1: Setting up Nix
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Over the last year or so I have experimented with using `nix` to manage
|
||||||
|
my development environments across multiple machines.
|
||||||
|
|
||||||
|
I like to keep e.g. my personal projects and work projects separated as
|
||||||
|
much as possible, so on my work projects I don't want my personal
|
||||||
|
development stack (tools, versions, etc.) to be loaded and when I work
|
||||||
|
on my personal projects the same applies.
|
||||||
|
|
||||||
|
I also want the development environments to be declarative, but the
|
||||||
|
actual use case is that I want them to be portable to other machines, so
|
||||||
|
I can spin up a new development environment fairly quickly.
|
||||||
|
|
||||||
|
There are other ways to achieve full isolation, e.g. a complete virtual
|
||||||
|
machine, but I don't like the overhead that brings.
|
||||||
|
|
||||||
|
I also _like_ the way I have my global environment setup, and prefer to
|
||||||
|
use that as a base, and then bring whatever tools I need for the context
|
||||||
|
I am working in into it.
|
||||||
|
|
||||||
|
I'm not sure when it happened, but `nix` the package manager (not "nix the
|
||||||
|
language", and not "nix the operating system"), is quite mature[^1]
|
||||||
|
across both Linux and MacOS, so I decided to brush off my "nix the
|
||||||
|
language" concerns and dive into it. Again.
|
||||||
|
|
||||||
|
[^1]: Well, `nix-env` and other `nix-*` commands _are_ mature, but the
|
||||||
|
next-gen all-in-one `nix` CLI is definitely _not_ mature. It's not
|
||||||
|
even final:
|
||||||
|
https://nixos.org/manual/nix/stable/command-ref/experimental-commands
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A supported OS:
|
||||||
|
- Linux
|
||||||
|
- MacOS
|
||||||
|
- Windows with WSL2 with `systemd` enabled[^2]
|
||||||
|
- Comfortable using a shell
|
||||||
|
- A high tolerance for obtuse languages[^3]
|
||||||
|
|
||||||
|
[^2]: Configuration guide:
|
||||||
|
https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/
|
||||||
|
|
||||||
|
[^3]: We will be using a bare minimum of "nix the language" to
|
||||||
|
accomplish our goals, so there are better ways to do most things I
|
||||||
|
do, but I don't understand any of them so I'm sticking to what I can
|
||||||
|
only describe as "simple but dumb".
|
||||||
|
|
||||||
|
## A note on "Nixes"
|
||||||
|
|
||||||
|
Nix is somewhat of an overloaded term, as it can mean:
|
||||||
|
|
||||||
|
- Nix the language
|
||||||
|
- Nix the CLI
|
||||||
|
- Nix the OS[^4]
|
||||||
|
- Nix the package manager
|
||||||
|
- Nix the utilities
|
||||||
|
- Nix the store
|
||||||
|
|
||||||
|
[^4]: Most often called NixOS but searching for "nix" often winds up in
|
||||||
|
NixOS-land which is not always helpful.
|
||||||
|
|
||||||
|
I will use a subset of those in this guide, and do my best to
|
||||||
|
differeniate them.
|
||||||
|
|
||||||
|
The in-scope Nix terms will be:
|
||||||
|
|
||||||
|
- Nix the language (try as I might, there is no avoiding it)
|
||||||
|
- Nix the package manager
|
||||||
|
- Nix the utilities
|
||||||
|
|
||||||
|
This will give us all we need to accomplish our goal.
|
||||||
|
|
||||||
|
## A note on Nix installers
|
||||||
|
|
||||||
|
There are many ways to install Nix (the package manager) without NixOS,
|
||||||
|
and it works on any Linux distribution alongside the system package
|
||||||
|
manager.
|
||||||
|
|
||||||
|
It also works on MacOS, but there are a few rough patches of terrain. We
|
||||||
|
can mostly avoid them, or implement workarounds for the nasty ones.
|
||||||
|
|
||||||
|
### Official installer
|
||||||
|
|
||||||
|
> https://nixos.org/download
|
||||||
|
|
||||||
|
The official installer is available for Linux, MacOS, and Windows and is
|
||||||
|
the most unopinionated of the lot. It provides a predictable
|
||||||
|
environment, with opt-in to experimental functionality (that we will
|
||||||
|
_not_ use in this article anyway), and is managed by the Nix developers.
|
||||||
|
|
||||||
|
### Distro installer
|
||||||
|
|
||||||
|
> Arch Linux, Gentoo, Debian, Alpine, etc. all provide packages for
|
||||||
|
> their distro that can be used with various caveats.
|
||||||
|
|
||||||
|
I use the Arch Linux `nix` package on my workstation, for example, and
|
||||||
|
haven't had issues with it. It may be slightly out-of-date versus the
|
||||||
|
official one, but the package maintainer has been fast with updates so I
|
||||||
|
have never had issues.
|
||||||
|
|
||||||
|
I would be more sceptical of the Debian stable package version, and
|
||||||
|
perhaps opt for the official installer on that distro. Your mileage may
|
||||||
|
vary, but you are on Linux so you should be used to research and inform
|
||||||
|
your own choices.
|
||||||
|
|
||||||
|
### Determinate Nix installer
|
||||||
|
|
||||||
|
> https://github.com/DeterminateSystems/nix-installer
|
||||||
|
|
||||||
|
This installer is developed by [Determinate
|
||||||
|
Systems](https://determinate.systems/)[^5] and is an opinionated
|
||||||
|
installer, in that it makes different choices on how Nix should be
|
||||||
|
installed and setup than the official installer does. It leans into the
|
||||||
|
advanced functionality (often experimental) that the official installer
|
||||||
|
leaves off by default, and goes as far as to disable or dissuade users
|
||||||
|
from using the stable nix utilities.
|
||||||
|
|
||||||
|
[^5]: A consultancy that provides services related to Nix training for
|
||||||
|
teams and companies.
|
||||||
|
|
||||||
|
It's a great installer, but if used, some configuration options must be
|
||||||
|
manually reverted to the official defaults. I tried this installer, but
|
||||||
|
wound up uninstalling and using the official one to avoid diving into
|
||||||
|
all the changes and assumptions they make.
|
||||||
|
|
||||||
|
## Installing using the official installer
|
||||||
|
|
||||||
|
If you decided to install Nix using the distro package manager or the
|
||||||
|
determinate systems' installer, feel free to jump ahead.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Given you have Linux running `systemd`, with SELinux disabled, and can
|
||||||
|
authenticate with `sudo`, the multi-user installation method is
|
||||||
|
recommended:
|
||||||
|
|
||||||
|
```
|
||||||
|
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
If you cannot use the multi-user installation, you must use the
|
||||||
|
single-user installation method:
|
||||||
|
|
||||||
|
```
|
||||||
|
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow along the installation, answering the prompts, and you will end
|
||||||
|
up with a Nix installation.
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
For MacOS a multi-user installation is recommended:
|
||||||
|
|
||||||
|
```
|
||||||
|
sh <(curl -L https://nixos.org/nix/install)
|
||||||
|
```
|
||||||
|
|
||||||
|
Same deal here, follow along the instructions and the result should be a
|
||||||
|
working Nix installation.
|
||||||
|
|
||||||
|
## Using the environment
|
||||||
|
|
||||||
|
Now that we have Nix setup, we can try out a few tricks. If you haven't,
|
||||||
|
you will need to open a new shell.
|
||||||
|
|
||||||
|
### Starting a shell with specific tools
|
||||||
|
|
||||||
|
> `nix-shell` reference manual:
|
||||||
|
> https://nixos.org/manual/nix/stable/command-ref/nix-shell
|
||||||
|
|
||||||
|
`nix-shell` starts an interactive shell based on a Nix expression, but
|
||||||
|
we will stick to our guns and define packages instead of an expression.
|
||||||
|
|
||||||
|
We can start a pure environment that inherits nothing from the global
|
||||||
|
environment:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-shell --pure
|
||||||
|
```
|
||||||
|
|
||||||
|
In which we don't even have `ping` defined:
|
||||||
|
|
||||||
|
```
|
||||||
|
[nix-shell:~/dev]$ ping
|
||||||
|
bash: ping: command not found
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `exit` to go back to the global shell, and try this instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-shell --pure --packages cowsay
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
[nix-shell:~/dev]$ cowsay "hi nixer !"
|
||||||
|
____________
|
||||||
|
< hi nixer ! >
|
||||||
|
------------
|
||||||
|
\ ^__^
|
||||||
|
\ (oo)\_______
|
||||||
|
(__)\ )\/\
|
||||||
|
||----w |
|
||||||
|
|| ||
|
||||||
|
```
|
||||||
|
|
||||||
|
Hit `Ctrl-D` or type `exit` again. Multiple packages can be listed on
|
||||||
|
the commandline separated by spaces:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-shell --pure --packages less vim
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that when we request packages we don't have, we get these
|
||||||
|
messages:
|
||||||
|
|
||||||
|
```
|
||||||
|
these 2 paths will be fetched (8.06 MiB download, 38.84 MiB unpacked):
|
||||||
|
/nix/store/h5m8kaai6x64j1q6r7ffvq20f06r77m3-less-633
|
||||||
|
/nix/store/6j38m8vm8gp9a8qpw3b7dj9g50x1w95n-vim-9.0.1562
|
||||||
|
copying path '/nix/store/h5m8kaai6x64j1q6r7ffvq20f06r77m3-less-633' from 'https://cache.nixos.org'...
|
||||||
|
copying path '/nix/store/6j38m8vm8gp9a8qpw3b7dj9g50x1w95n-vim-9.0.1562' from 'https://cache.nixos.org'...
|
||||||
|
```
|
||||||
|
|
||||||
|
The short version is that each package and version is hashed and stored
|
||||||
|
in the Nix store (`/nix`). The consequence of this is that multiple
|
||||||
|
versions of the same package can exist at the same time (at the cost of
|
||||||
|
disk space) without causing conflicts across the environment. There is
|
||||||
|
also a long version[^6]. Probably a longer version somewhere as well.
|
||||||
|
|
||||||
|
[^6]: Understanding the Nix store by looking at how Nix works:
|
||||||
|
https://nixos.org/guides/how-nix-works.html
|
||||||
|
|
||||||
|
### Installing packages into the environment
|
||||||
|
|
||||||
|
> `nix-shell` reference manual:
|
||||||
|
> https://nixos.org/manual/nix/stable/command-ref/nix-env#name
|
||||||
|
|
||||||
|
Being able to run a shell with specific packages is handy, and
|
||||||
|
`nix-shell` will be a underlying driver further into the nixtapes.
|
||||||
|
Instead let's take a look at the `nix-env` command.
|
||||||
|
|
||||||
|
Using `nix-env` we can manipulate a Nix user environment, which means
|
||||||
|
un/installing packages, searching for packages, and, rolling back the
|
||||||
|
environment.
|
||||||
|
|
||||||
|
Let's find some packages to install.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --query --available vim
|
||||||
|
|
||||||
|
vim-9.0.1562
|
||||||
|
```
|
||||||
|
|
||||||
|
Great, love that version. Let's install it:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --install vim-9.0.1562
|
||||||
|
|
||||||
|
installing 'vim-9.0.1562'
|
||||||
|
building '/nix/store/08xr544knfcahpn9xykql2xzsg374pxl-user-environment.drv'...
|
||||||
|
|
||||||
|
which vim
|
||||||
|
|
||||||
|
/home/varl/.nix-profile/bin/vim
|
||||||
|
|
||||||
|
readlink /home/varl/.nix-profile/bin/vim
|
||||||
|
|
||||||
|
/nix/store/6j38m8vm8gp9a8qpw3b7dj9g50x1w95n-vim-9.0.1562/bin/vim
|
||||||
|
```
|
||||||
|
|
||||||
|
Given that `/home/varl/.nix-profile/bin` is added to my `PATH` before
|
||||||
|
any paths that has my system version of `vim`, it will be picked up
|
||||||
|
correctly and when I run `vim` I will get the nix-installed version. No
|
||||||
|
shell-trickery and `vim` will be available across any other interactive
|
||||||
|
shells I open and use.
|
||||||
|
|
||||||
|
It is also possible to preserve installed alternative derivations of a
|
||||||
|
package, but I haven't had to use it much.
|
||||||
|
|
||||||
|
One thing we will use a lot though is the `--remove-all` flag. This
|
||||||
|
removes _all_ the installed packages in an environment, and only
|
||||||
|
installs the target package in the environment, so if we run:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --install vim nodejs python3
|
||||||
|
|
||||||
|
installing 'vim-9.0.1562'
|
||||||
|
installing 'nodejs-18.16.1'
|
||||||
|
installing 'python3-3.12.0b3'
|
||||||
|
these 3 paths will be fetched (65.51 MiB download, 219.36 MiB unpacked):
|
||||||
|
/nix/store/pxv7fa7ysw18kqrlvs1g0f9q66l7paz3-nodejs-18.16.1-libv8
|
||||||
|
/nix/store/3v1hjf626mh7mdii28m0srdbl8ch3dka-python3-3.12.0b3
|
||||||
|
/nix/store/b9a3j1rvcgj4wxpb30yzdi7ba62g3ha8-python3-3.12.0b3-debug
|
||||||
|
copying path '/nix/store/pxv7fa7ysw18kqrlvs1g0f9q66l7paz3-nodejs-18.16.1-libv8' from 'https://cache.nixos.org'...
|
||||||
|
copying path '/nix/store/b9a3j1rvcgj4wxpb30yzdi7ba62g3ha8-python3-3.12.0b3-debug' from 'https://cache.nixos.org'...
|
||||||
|
copying path '/nix/store/3v1hjf626mh7mdii28m0srdbl8ch3dka-python3-3.12.0b3' from 'https://cache.nixos.org'...
|
||||||
|
building '/nix/store/jid36dzng4pjqph3f0bdyzmsvaq5fl0h-user-environment.drv'...
|
||||||
|
```
|
||||||
|
|
||||||
|
And then do:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --install --remove-all vim
|
||||||
|
```
|
||||||
|
|
||||||
|
Only `vim` will be installed, and `python3` and `nodejs` are purged from
|
||||||
|
the environment. This is an important feature, as it allows us to
|
||||||
|
install any tools and versions we want to play around with `nix-env --install`, and then go back to scratch without worrying about leaving a
|
||||||
|
mess in our environments.
|
||||||
|
|
||||||
|
### Uninstalling packages from the environment
|
||||||
|
|
||||||
|
Uninstalling is without drama:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --uninstall vim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment generations
|
||||||
|
|
||||||
|
Every time we use `nix-env` to modify our environment, Nix creates a new
|
||||||
|
generation. We can jump between generations, rollback, and delete them.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
This rolls back the current environment one generation, and is just a
|
||||||
|
convenience wrapper around `--list-generations` and
|
||||||
|
`--switch-generation`.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --list-generations
|
||||||
|
|
||||||
|
...
|
||||||
|
113 2023-06-21 16:39:06
|
||||||
|
114 2023-08-08 16:25:50
|
||||||
|
115 2023-08-08 16:36:01
|
||||||
|
116 2023-08-08 16:37:42 (current)
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the id in the left column, we can jump to different generations of
|
||||||
|
our environment.
|
||||||
|
|
||||||
|
Deleting generations (they add up) can be done on a one-by-one basis, or
|
||||||
|
using smart values like `+5` (save last 5) and `30d` (save last 30
|
||||||
|
days).
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --delete-generations 113
|
||||||
|
nix-env --delete-generations +5
|
||||||
|
nix-env --delete-generations 30d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where are we now ?
|
||||||
|
|
||||||
|
We have installed Nix (the package manager) and the Nix utilities
|
||||||
|
(`nix-*` commands) that we need.
|
||||||
|
|
||||||
|
We have explored how to create ad-hoc shell environments that only have
|
||||||
|
specific packages available, which is useful for trying out new things and
|
||||||
|
switching between versions of e.g. language runtimes.
|
||||||
|
|
||||||
|
We have learned how to manipulate the user's environment by installing
|
||||||
|
packages that persist in the environment (as opposed to the ephemeral
|
||||||
|
ones in `nix-shell`).
|
||||||
|
|
||||||
|
We've seen that Nix creates generations of the user's environments, and
|
||||||
|
that we can switch between generations or simply rollback the
|
||||||
|
environment to a previous state.
|
||||||
|
|
||||||
|
These are the building blocks for managing our user's environment, as
|
||||||
|
well as the various development environments that we strive for.
|
||||||
|
|
||||||
|
/v.
|
464
src/posts/2023/varls-nixtape-vol-2.md
Normal file
464
src/posts/2023/varls-nixtape-vol-2.md
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
---
|
||||||
|
title: varl's nixtapes vol.2
|
||||||
|
date: 2023-08-09
|
||||||
|
pub:
|
||||||
|
year: 2023
|
||||||
|
collection: nixtapes
|
||||||
|
---
|
||||||
|
|
||||||
|
> #### TL;DR
|
||||||
|
>
|
||||||
|
> Now that we have a working nix installation and understand how the nix
|
||||||
|
> utilities (`nix-*` commands) work at a basic level, we can integrate nix
|
||||||
|
> more deeply into our user environment to manage packages.
|
||||||
|
>
|
||||||
|
> For MacOS we will end up with a decent replacement for
|
||||||
|
> [Homebrew](https://brew.sh), and for Linux, we will have a package
|
||||||
|
> manager that is independent from the system package manager that we can
|
||||||
|
> use to manage packages for our user.
|
||||||
|
|
||||||
|
# volume 2: integrating nix with the environment
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A working nix installation, see [vol.1](varl-s-nixtapes-vol1)
|
||||||
|
|
||||||
|
## Nix Channels
|
||||||
|
|
||||||
|
> A new concept has appeared !
|
||||||
|
|
||||||
|
To find Nix packages, there are different channels that exist under the
|
||||||
|
Git repository [`nixpkgs`](https://github.com/nixos/nixpkgs).
|
||||||
|
|
||||||
|
There are stable and unstable, large and small, channels that have
|
||||||
|
different use cases.
|
||||||
|
|
||||||
|
I like to be able to lock my channel to a specific commit, so I clone
|
||||||
|
nixpkgs to `~/.nix-defexpr` so I can manipulate it at will by switch
|
||||||
|
branches, updating, making local changes to try out (read: break)
|
||||||
|
various things.[^1]
|
||||||
|
|
||||||
|
[^1]:
|
||||||
|
I use the `master` branch, which is unstable. For my usage I
|
||||||
|
haven't had any real breakages. Simply switch branch to swap the
|
||||||
|
channel. Simply `git switch nixos-23.05` to use the stable branch.
|
||||||
|
|
||||||
|
All in all, I find it handy, so I would recommend doing the same:
|
||||||
|
|
||||||
|
```
|
||||||
|
rm -rd ~/.nix-defexpr
|
||||||
|
git clone --depth=1 https://github.com/NixOS/nixpkgs.git ~/.nix-defexpr
|
||||||
|
```
|
||||||
|
|
||||||
|
To make it stick immediately (we will make it more permanent later) run:
|
||||||
|
|
||||||
|
```
|
||||||
|
export NIX_PATH="nixpkgs=$HOME/.nix-defexpr"
|
||||||
|
```
|
||||||
|
|
||||||
|
Channels can be listed with:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-channel --list
|
||||||
|
```
|
||||||
|
|
||||||
|
`nix-channel` also provides `--add`, `--update`, and `--remove`
|
||||||
|
commands.
|
||||||
|
|
||||||
|
I would recommend removing all existing channels if you go along with
|
||||||
|
having a local clone of `nixpkgs`.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-channel --remove {channel-alias}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is by far the simplest way, so until you need multiple channels,
|
||||||
|
sticking with one is the most predictable. Each channel also has the
|
||||||
|
concept of generations, and it is possible to rollback an `--update` to
|
||||||
|
a previous generation.
|
||||||
|
|
||||||
|
Quite powerful, and overkill for my ends. Onwards !
|
||||||
|
|
||||||
|
## Configure user packages
|
||||||
|
|
||||||
|
A promise I made is declarative package management, and that boils down
|
||||||
|
being able to state what packages we want, and have the package manager
|
||||||
|
make sure that we have those in our environment.
|
||||||
|
|
||||||
|
Let's take a look at how to configure at the user level, as opposed to
|
||||||
|
system, and project, level.
|
||||||
|
|
||||||
|
If you don't have the file `~/.config/nixpkgs/config.nix` we need to
|
||||||
|
create it:
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p ~/.config/nixpkgs
|
||||||
|
touch ~/.config/nixpkgs/config.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the `config.nix` file in an editor and let's dip into
|
||||||
|
Nix-the-language.
|
||||||
|
|
||||||
|
Paste the following into the file:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
pkgs : {
|
||||||
|
allowUnfree = true;
|
||||||
|
packageOverrides = pkgs: with pkgs; rec {
|
||||||
|
nixUserProfile = writeText "nix-user-profile" ''
|
||||||
|
export PATH=$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
export NIX_PATH="nixpkgs=$HOME/.nix-defexpr"
|
||||||
|
'';
|
||||||
|
userBasePkgs = pkgs.buildEnv {
|
||||||
|
name = "user-base";
|
||||||
|
paths = [
|
||||||
|
(runCommand "profile" {} ''
|
||||||
|
mkdir -p $out/etc/profile.d
|
||||||
|
cp ${nixUserProfile} $out/etc/profile.d/nix-user-profile.sh
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
pathsToLink = [
|
||||||
|
"/share"
|
||||||
|
"/bin"
|
||||||
|
"/etc"
|
||||||
|
];
|
||||||
|
extraOutputsToInstall = [ "man" "doc" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm not going to go into the semantics of the language, but rather talk
|
||||||
|
a bit about what we are doing and why it is helpful.[^2]
|
||||||
|
|
||||||
|
[^2]:
|
||||||
|
You can dive into the language yourself if you wish:
|
||||||
|
https://nixos.org/manual/nix/stable/language/index.html
|
||||||
|
|
||||||
|
`allowUnfree` determines if we are able to use non-free, as in
|
||||||
|
proprietrary, packages from `nixpkgs`. By default only free software is
|
||||||
|
allowed to install, and your mileage will vary if you need non-free
|
||||||
|
software or not. I do, so I have it set to `true`.
|
||||||
|
|
||||||
|
`packageOverrides` is where we will define our own packages to
|
||||||
|
accomplish a one-shot command to install all the packages we want in our
|
||||||
|
environment.
|
||||||
|
|
||||||
|
The first package we define is `nixUserProfile` which uses a `writeText`
|
||||||
|
helper function to build a package that consists of a file with the text
|
||||||
|
we have defined.
|
||||||
|
|
||||||
|
The output file will have the contents:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export PATH=$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
export NIX_PATH="nixpkgs=$HOME/.nix-defexpr"
|
||||||
|
```
|
||||||
|
|
||||||
|
This does two things.
|
||||||
|
|
||||||
|
First, it sets up our `PATH` variable to include the `bin` folders that
|
||||||
|
contain our nix package binaries. We want them first in `PATH` to ensure
|
||||||
|
that the nix packages are used before the system packages that may
|
||||||
|
already exist on the system.
|
||||||
|
|
||||||
|
Second, it sets the `NIX_PATH` to the `~/.nix-defexpr` folder where we
|
||||||
|
cloned the `nixpkgs` repo to. I mentioned that we did a one-off `export`
|
||||||
|
to set this, and this is the more permanent fix to make sure it sticks
|
||||||
|
across reboots and all interactive shells we use.
|
||||||
|
|
||||||
|
However. This simply builds a package that consists of a file, but it
|
||||||
|
doesn't run it.
|
||||||
|
|
||||||
|
The next package we define is our `userBasePkgs`, and here is a trimmed
|
||||||
|
version of the above to make it easier to deconstruct.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
userBasePkgs = pkgs.buildEnv {
|
||||||
|
name = "user-base";
|
||||||
|
paths = [
|
||||||
|
(runCommand "profile" {} ''
|
||||||
|
mkdir -p $out/etc/profile.d
|
||||||
|
cp ${nixUserProfile} $out/etc/profile.d/nix-user-profile.sh
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
We have to give it a `name`, so we can refer to it, and I chose
|
||||||
|
`user-base`. The convention will make more sense down the line.
|
||||||
|
|
||||||
|
In `paths` it is a bit murkier. Right off the bat we invoke another nix
|
||||||
|
helper function to run a command to trigger a side effect on our system.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
(runCommand "profile" {} ''
|
||||||
|
mkdir -p $out/etc/profile.d
|
||||||
|
cp ${nixUserProfile} $out/etc/profile.d/nix-user-profile.sh
|
||||||
|
'')
|
||||||
|
```
|
||||||
|
|
||||||
|
`$out` refers to our `~/.nix-profile` folder, so it creates the folder
|
||||||
|
`~/.nix-profile/etc/profile.d` directory and copies the output from the
|
||||||
|
`nixUserProfile` package we built before to it.
|
||||||
|
|
||||||
|
Now that we have a package that has a build output, we need to install
|
||||||
|
the package to reflect the change to our environment.
|
||||||
|
|
||||||
|
## Install `user-base` with `nix-env`
|
||||||
|
|
||||||
|
Now that we have our `user-base` package, let us install it and continue
|
||||||
|
hooking up our user environment with the nix user environment.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env --install --remove-all user-base
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, short form:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -ir user-base
|
||||||
|
```
|
||||||
|
|
||||||
|
This evaluates our nix package definition in
|
||||||
|
`~/.config/nixpkgs/config.nix` and builds our derivation, and installs
|
||||||
|
it into our environment. We didn't define any additional software yet so
|
||||||
|
we can check if `~/.nix-profile/etc/profile.d/nix-user-profile.sh`
|
||||||
|
contains the two lines we expect:
|
||||||
|
|
||||||
|
```
|
||||||
|
cat ~/.nix-profile/etc/profile.d/nix-user-profile.sh
|
||||||
|
|
||||||
|
export PATH=$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
export NIX_PATH="nixpkgs=$HOME/.nix-defexpr"
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing in our shell knows this file exists though, so we need to wire
|
||||||
|
up a few more things.
|
||||||
|
|
||||||
|
## Loading scripts in `~/.nix-profile/etc/profile.d`
|
||||||
|
|
||||||
|
Add this chunk to `~/.zprofile` (zsh), `~/.bash_profile` (bash), or
|
||||||
|
`~/.profile` (bash, sh).
|
||||||
|
|
||||||
|
It loads scripts in `~/.nix-profile/etc/profile.d`, and this will handle
|
||||||
|
our script as well, and ensure that we have `NIX_PATH` and `PATH` setup
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
if [ -d $HOME/.nix-profile/etc/profile.d ]; then
|
||||||
|
for i in $HOME/.nix-profile/etc/profile.d/*.sh; do
|
||||||
|
if [ -r $i ]; then
|
||||||
|
. $i
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Source the profile file, or start a new shell, and then test it:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo $PATH
|
||||||
|
/home/varl/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
|
||||||
|
echo $NIX_PATH
|
||||||
|
nixpkgs=/home/varl/.nix-defexpr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Managing packages
|
||||||
|
|
||||||
|
I promised package management for this volume, and it shall be done. We
|
||||||
|
have the right amount of environment integration to make it all work.
|
||||||
|
|
||||||
|
Back in the `~/.config/nixpkgs/config.nix` file, let's extend it to
|
||||||
|
cover:
|
||||||
|
|
||||||
|
- **user-base**: packages that are cross-platform and I want available
|
||||||
|
always, both on mac and linux.
|
||||||
|
- **user-linux**: linux specific packages that do not exist, or I don't
|
||||||
|
want, on MacOS.
|
||||||
|
- **user-macos**: macos specific packages that i don't want/need on other
|
||||||
|
OSes.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
pkgs : {
|
||||||
|
allowUnfree = true;
|
||||||
|
packageOverrides = pkgs: with pkgs; rec {
|
||||||
|
nixUserProfile = writeText "nix-user-profile" ''
|
||||||
|
export PATH=$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
export NIX_PATH="nixpkgs=$HOME/.nix-defexpr"
|
||||||
|
'';
|
||||||
|
userBasePkgs = pkgs.buildEnv {
|
||||||
|
name = "user-base";
|
||||||
|
paths = [
|
||||||
|
(runCommand "profile" {} ''
|
||||||
|
mkdir -p $out/etc/profile.d
|
||||||
|
cp ${nixUserProfile} $out/etc/profile.d/nix-user-profile.sh
|
||||||
|
'')
|
||||||
|
fzf
|
||||||
|
fd
|
||||||
|
ripgrep
|
||||||
|
vim
|
||||||
|
rsync
|
||||||
|
gnupg
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
];
|
||||||
|
pathsToLink = [
|
||||||
|
"/share"
|
||||||
|
"/bin"
|
||||||
|
"/etc"
|
||||||
|
];
|
||||||
|
extraOutputsToInstall = [ "man" "doc" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
userLinuxPkgs = pkgs.buildEnv {
|
||||||
|
name = "user-linux";
|
||||||
|
paths = [
|
||||||
|
userBasePkgs
|
||||||
|
bitwarden
|
||||||
|
bitwarden-cli
|
||||||
|
signal-desktop
|
||||||
|
xcolor
|
||||||
|
];
|
||||||
|
pathsToLink = [] ++ (userBasePkgs.pathsToLink or []);
|
||||||
|
extraOutputsToInstall = []
|
||||||
|
++ (userBasePkgs.extraOutputsToInstall or []);
|
||||||
|
};
|
||||||
|
|
||||||
|
userMacOSPkgs = pkgs.buildEnv {
|
||||||
|
name = "user-macos";
|
||||||
|
paths = [
|
||||||
|
userBasePkgs
|
||||||
|
bash
|
||||||
|
zsh
|
||||||
|
zsh-completions
|
||||||
|
alacritty
|
||||||
|
karabiner-elements
|
||||||
|
coreutils
|
||||||
|
podman
|
||||||
|
podman-compose
|
||||||
|
];
|
||||||
|
pathsToLink = [
|
||||||
|
"/Applications"
|
||||||
|
] ++ (userBasePkgs.pathsToLink or []);
|
||||||
|
extraOutputsToInstall = []
|
||||||
|
++ (userBasePkgs.extraOutputsToInstall or []);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key here is to notice that we defined two more packages, `userLinuxPkgs`
|
||||||
|
and `userMacOSPkgs`[^3], and the first item in the `paths` list is
|
||||||
|
`userBasePkgs`.
|
||||||
|
|
||||||
|
[^3]:
|
||||||
|
Notice that the `pathsToLink` for `userMacOSPkgs` includes
|
||||||
|
`/Applications`. This is an attempt to link the package to the
|
||||||
|
`/Applications` to allow e.g. Spotlight to find the application we
|
||||||
|
installed with Nix.
|
||||||
|
|
||||||
|
This is there so that when we install `user-linux` or `user-macos`, it
|
||||||
|
installs the `user-base` package which includes any common packages that
|
||||||
|
we might have. We definitely have one in common, the package that
|
||||||
|
creates the script to update our environment, but the list can be as
|
||||||
|
long or short as we want it.
|
||||||
|
|
||||||
|
On Linux I would run:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -ir user-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
And on MacOS I would run:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -ir user-macos
|
||||||
|
```
|
||||||
|
|
||||||
|
`nix-env` would then rebuild my environment and install any packages
|
||||||
|
defined in `~/.config/nixpkgs/config.nix`.
|
||||||
|
|
||||||
|
To remove packages, we just delete them from the `config.nix` file, and
|
||||||
|
re-run the relevant `nix-env -ir` command.
|
||||||
|
|
||||||
|
## Finding package names to use in `config.nix`
|
||||||
|
|
||||||
|
Since we can pin our `nixpkgs` to a commit, it is nice to avoid some
|
||||||
|
versions on packages in `config.nix`.
|
||||||
|
|
||||||
|
When we search with `nix-env -qa vim`, we would get back a string like
|
||||||
|
`vim-9.0.1642`. We can add that to the `paths` list, but when there is a
|
||||||
|
new version it would stop working.
|
||||||
|
|
||||||
|
Instead we can search for packages with the `-P` flag to find
|
||||||
|
the unambiguous attribute path.
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -qaP vim
|
||||||
|
|
||||||
|
vim vim-9.0.1642
|
||||||
|
```
|
||||||
|
|
||||||
|
The first column, `vim`, is the path we can use to refer to the `vim`
|
||||||
|
package without the version.
|
||||||
|
|
||||||
|
For NodeJS it can look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -qaP nodejs
|
||||||
|
nodejs-14_x nodejs-14.21.3
|
||||||
|
nodejs_14 nodejs-14.21.3
|
||||||
|
nodejs-16_x nodejs-16.20.1
|
||||||
|
nodejs_16 nodejs-16.20.1
|
||||||
|
elmPackages.nodejs nodejs-18.17.0
|
||||||
|
nodejs-18_x nodejs-18.17.0
|
||||||
|
nodejs_18 nodejs-18.17.0
|
||||||
|
nodejs_20 nodejs-20.5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we could opt for `nodejs_18` to get the latest 18.x version.
|
||||||
|
|
||||||
|
When looking for multiple packages, I would recommend doing something
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-env -qaP | fzf
|
||||||
|
```
|
||||||
|
|
||||||
|
`nix-env -qa` is a very slow command that loads all the packages before
|
||||||
|
searching according to a pattern, so loading the packages once, then
|
||||||
|
piping them into `fzf` provides a nice and fast search interface over
|
||||||
|
all the packages in `nixpkgs`.
|
||||||
|
|
||||||
|
## Where are we now ?
|
||||||
|
|
||||||
|
We have set up our `NIX_PATH` to refer to a local clone of `nixpkgs`
|
||||||
|
that we use to pin software versions.
|
||||||
|
|
||||||
|
We have also configured our `PATH` to include directories where nix
|
||||||
|
places binaries that we want to take precedence.
|
||||||
|
|
||||||
|
We did so by leveraging a custom package that generates a script to
|
||||||
|
update our environment.
|
||||||
|
|
||||||
|
We still needed to manually wire up our shell to actually run the
|
||||||
|
scripts when the shell starts, and we did that in our `.profile` file
|
||||||
|
(different for sh/bash/zsh).
|
||||||
|
|
||||||
|
We then added more packages to our `config.nix` and looked at how to
|
||||||
|
manage packages in two ways: 1) common packages that are cross-platform
|
||||||
|
and 2) packages that are specific to an operating system (mac/linux).
|
||||||
|
|
||||||
|
We can install all the common + operating system specific packages with
|
||||||
|
a single command using nested package evaluation in Nix the language.
|
||||||
|
|
||||||
|
And finally we took a look at how to search for packages and refer to
|
||||||
|
them in `config.nix`.
|
||||||
|
|
||||||
|
All this means that you can declare packages in `config.nix`, and move
|
||||||
|
that file across computers. It will work the same regardless of machine.
|
||||||
|
Put it in a Git repo and keep your user environments in sync.
|
||||||
|
|
||||||
|
/v.
|
15
src/posts/2023/varls-nixtape-vol-3.md
Normal file
15
src/posts/2023/varls-nixtape-vol-3.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: varl's nixtapes vol.3
|
||||||
|
date: 2023-08-15
|
||||||
|
pub:
|
||||||
|
year: 2023
|
||||||
|
collection: nixtapes
|
||||||
|
---
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
- Declarative development environments
|
||||||
|
- Nested nix environments.
|
||||||
|
- Setting up WARP and `app` command
|
15
src/posts/foo.md
Normal file
15
src/posts/foo.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: foo
|
||||||
|
keywords:
|
||||||
|
- foo
|
||||||
|
- bar
|
||||||
|
draft: true
|
||||||
|
pub:
|
||||||
|
year: 2023
|
||||||
|
---
|
||||||
|
|
||||||
|
asdf
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const foo = true;
|
||||||
|
```
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default {
|
||||||
|
// we need to run the class extraction on the build result
|
||||||
|
content: ['./build/**/*.html'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
Loading…
Reference in a new issue