Import into git

This commit is contained in:
Thibault “Adædra” Hamel 2024-06-12 01:57:32 +02:00
parent 1b184b62e4
commit bdec4993ab
26 changed files with 6100 additions and 0 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
root = true
[*]
indent_size = 4
max_line_length = 100

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/dist/
/node_modules/
/tmp/

View File

@ -0,0 +1,182 @@
= Arch Linux with UKI & SecureBoot
The Linux boot sequence can be seen as a bit convoluted, as multiple options exist, and the more recent advent of UEFI has shuffled this space again. While GRUB is still a very popular option, especially since it is compatible with both traditional BIOS boot and more recent UEFI firmwares, it is not the only one.
But the step of going through a third party software to boot your Linux kernel is not a compulsory step anymore. Linux kernels have been able to be booted as standalone UEFI images for a while now, and so as long as you had a firmware capable of providing your boot arguments, you could bypass the bootloader stage. Even more recently, Linux now has something called *UKI*, or "`Unified Kernel Image`". It is a way of taking a Linux kernel, its initramfs, and its command line, and make it a truly standalone bootable EFI image that can be simply booted from anything capable of booting EFI images.
Arch Linux is a generic distribution geared towards tinkerers. It notably has no defaults in regard to its bootloader. And it fully supports (albeit with a bit of tinkering) booting from an UKI image.
== Prerequisites
We are going to make an Arch Linux installation boot from UEFI. This can be done either from an existing installation or during the initial installation process. The only true requirement is to be on an UEFI-compatible machine and https://wiki.archlinux.org/title/Installation_guide#Verify_the_boot_mode[booting into this mode]. Of course, this also means having an https://wiki.archlinux.org/title/EFI_system_partition[UEFI boot partition]. In this guide, I have mounted mine at `+/boot/efi+`, although some installs mount it directly into `+/boot+`. It doesnt matter much, but you might have to correct some paths if its the case for you.
This guide will also show how to make it compatible with https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot[SecureBoot]. SecureBoot is a bit of a problematic point in the Linux community and some people prefer to leave it out. I am not going to argue for or against it, I am just presenting how to make it compatible, but this part is completely optional. However, if you want to go with SecureBoot, you need to boot your machine with SecureBoot enabled and in setup mode.
Note: Arch Linux now provides multiple choices for the https://wiki.archlinux.org/title/Arch_boot_process#initramfs[initramfs]. https://wiki.archlinux.org/title/Mkinitcpio[mkinitcpio] has been the historical and still default choice today, but it is also possible to use https://wiki.archlinux.org/title/Dracut[dracut] or https://wiki.archlinux.org/title/Booster[booster]. dracut supports making an UKI image directly, and there is `+systemd-ukify+` that can make an UKI image from separate kernel and initramfs. However, this article will focus on mkinitcpio.
== Making the install
=== mkinitcpio
On your existing system, or on a system at the boot loader phase of the install, we will need to configure mkinitcpio to write to an UKI image. Ensure the `+mkinitcpio+` package is installed. Then, we will edit the preset file for mkinitcpio, located at `+/etc/mkinitcpio.d/linux.preset+` (or a similar name depending on your kernel flavour of choice like `+linux-lts.preset+`). Disable the classical initramfs generation by commenting the `+default_image+` and `+fallback_image+` options, then enable UKI generation by uncommenting the `+default_uki+` and `+fallback_uki+` options. Also, ensure the paths are correct. For a UEFI boot partition mounted at `+/boot/efi+`:
[source,bash]
----
default_uki="/boot/efi/EFI/Linux/arch-linux.efi"
fallback_uki="/boot/efi/EFI/Linux/arch-linux-fallback.efi"
----
Also, mkinitcpio expects the parent folder to already exist which is not by default the case. Use `+mkdir+` to ensure the folder is created.
=== Command Line
In traditional Linux setup, the kernel options or command line is stored and passed by the bootloader, and usually configured there. Here, we have no bootloader, but still need to provide the command line options.
The UKI build process assemble the command line from files in `+/etc/cmdline.d+`. You can create `+.conf+` files in this folder containing command line arguments (which also allows you to group related arguments together). Simplest example is to create a `+/etc/cmdline.d/root.conf+` with the root configuration:
....
root="/dev/vda2" rw
....
Of course, adapt it to your configuration and needs.
=== Building the image
Now that things are configured, you can just build the image like you do traditionally:
....
mkinitcpio -P
....
The process should end without errors, and you should find the new files at the location indicated above.
=== Registering
UEFI boot programs are normally registered into the firmware settings as boot entries. If we want to boot our kernel, we need to register our kernel in the firmware with `+efibootmgr+`. First, ensure the `+efibootmgr+` package is installed.
....
efibootmgr --create --disk=/dev/vda --part=1 --label="Arch Linux" --loader='\EFI\Linux\arch-linux.efi'
efibootmgr --create-only --disk=/dev/vda --part=1 --label="Arch Linux Fallback" --loader='\EFI\Linux\arch-linux-fallback.efi'
....
=== Rebooting
Everything is set up! Now you can finalise the installation steps (if applicable) and reboot your machine, then check that your system boots directly into Arch Linux. You can also trigger the UEFI boot menu to check for the additional entry for the fallback image.
Congratulations! You now boot entirely through your UKI image.
== SecureBoot
Note: As said above, this step is optional, if you do not want to use SecureBoot.
SecureBoot is an addition to UEFI that ensures that your boot sequence has not been tampered with, by requiring all boot images to be signed with a valid certificate. By default, machines have two Microsoft-provided certificates, one for Windows, one for third party systems, that some distributions use to provide some level of support to SecureBoot. You can also register your own certificate into the machine to sign your own binaries, which is what we are going to setup here. Once setup correctly, the signing process is automatic.
Install the `+sbctl+` package. Ensure that your machine has SecureBoot is in setup mode by issuing `+sbctl status+`:
....
Installed: ✓ sbctl is installed
Owner GUID: 00000000-0000-4000-0000-000000000000
Setup Mode: ✗ Enabled
Secure Boot: ✗ Disabled
Vendor Keys: none
....
Note: `+sbctl+` might also indicate that it is _not_ installed, but this will correct itself in the next steps.
If it is not, you will have to reboot your machine into the firmware interface to enable it. `+sbctl+` is a nice command to simplify the process of dealing with SecureBoot. To create a certificate and record it into our firmware, we issue the following commands:
....
sbctl create-keys
sbctl enroll-keys -m
....
Warning: `+-m+` tells `+sbctl+` to also insert the standard Microsoft certificates, which allows to boot Windows. Even if you plan on never booting Windows, some systems might have UEFI-drivers to load on boot that use one of these certificates. Not including them could break your boot sequence. You can technically do without, but be sure of what you are doing there.
Now that we have our certificates, we need to sign our UKI images. Fortunately, `+sbctl+` also includes a hook for `+mkinitcpio+` to do it for you. You should just have to regenerate images with `+mkinitcpio -P+`. Once this done, ensure it is all good with `+sbctl verify+`:
....
Verifying file database and EFI images in /boot/efi...
✗ /boot/efi/EFI/Linux/arch-linux.efi is not signed
✗ /boot/efi/EFI/Linux/arch-linux-fallback.efi is not signed
....
If you have static UEFI binaries, like the UEFI Shell or a third party bootloader like rEFInd, you will have to sign those too. They should be given to you by `+sbctl verify+`. For each binary that is to be signed, issue the following command:
....
sbctl sign -s /boot/efi/EFI/…
....
The `+-s+` flag tells `+sbctl+` to remember this path. In the future, if you modify any of those files, you can just issue `+sbctl sign -TODO+` to check and resign every known file.
Warning: Ensure that `+sbctl verify+` validates your boot files before rebooting! If you reboot without signing your files, your system will refuse to boot, now that the certificates are registered.
You can now reboot your machine to check that it still boots. Once you reboot, `+sbctl status+` should tell you that all is well:
....
Installed: ✓ sbctl is installed
Owner GUID: 00000000-0000-4000-0000-000000000000
Setup Mode: ✓ Disabled
Secure Boot: ✓ Enabled
Vendor Keys: microsoft
....
== Bonuses
While this is enough for a functional setup, I have some personal tweaks to this that may be useful to you.
=== Automatic root decryption
Note: Requires SecureBoot, a TPM, and a LUKS 2 encrypted root.
If your root partition is encrypted, you have to enter the passphrase at every boot. If you have a TPM, you can use it to automatically provide this passphrase to the system. While this seem like a downgrade in security, your system is normally still password protected at the login prompt, so your data should be safe, but it is your choice between more security and ease of use. However, it becomes dangerous if coupled with auto-login!
The boot process and the TPM will ensure that your system has not been tempered with. It means that it will compare SecureBoot certificates at boot and will refuse to use the saved credentials if they are modified.
Note: This does not save your passphrase in the firmware. It uses a different LUKS slot with its own data saved into the TPM. The safety of the passphrase itself is not affected.
systemd provides the `+systemd-cryptenroll+` command to make the process easy. First, check that your TPM device is detected:
....
systemd-cryptenroll --tpm2-device=list
....
If it is visible, you can then use it with your root partition:
....
systemd-cryptenroll --tpm2-device=auto /dev/vda2
....
Replace `+/dev/vda2+` by your root partition physical device (the encrypted layer, not the `+/dev/mapper+` mapping). It should ask you for the current encryption passphrase before registering the new TPM slot.
Reboot, and you should not have to input your passphrase anymore.
Note: Security of this depends on what is allowed to boot on your machine. I would advise to lock down your UEFI settings with a password, including the boot menu if possible, making changing the default boot device impossible for an external person. Of course, they can reset the firmware with a physical access, but that would also reset the boot certificates and make the TPM decryption impossible. If you still keep a bootloader, be careful of what it allows to load.
=== rEFInd
If you have an other system you often use besides your Arch Linux, you may want to still keep a bootloader, depending on how tedious your UEFI firmware is to override the default boot setting. rEFInd is a generic UEFI boot loader, but a very capable one, as well as providing a graphical user interface to choose where to boot from.
While rEFInd will find and boot your Linux configuration in the current state, a few tweaks will improve the experience a bit.
First step is to install the `+refind+` package, then run its built-in script to make the install automatically:
....
refind-install
....
Now, we need to tweak mkinitcpio configuration a bit. If you already have your UKI images generated at this point, I would advise you to remove completely the `+/boot/efi/EFI/Linux+` folder.
rEFInd has some automatic heuristics to try to guess what kind of system is an image for to display an appropriate icon. However, it seems to not be good with UKI images (yet), but we can help it. If it cannot determine the type of an image, it will use the name of the parent folder as a hint. In the current configuration, it will see the `+Linux+` folder, and will display a generic Tux icon. If you want to have Arch Linux branding, we need to rename the folder to `+arch+`. Go back to the `+linux.preset+` file from earlier, and change the two lines to replace `+Linux+` with `+arch+`. Do not close the file yet, we have another change to make.
For distributions that may install different kernel versions at the same time, rEFInd will try to determine which version is the latest to use it as default. However, it gets a bit lost with Arch Linux and believes that the default and fallback images are of the same system, but since no kernel version is given in the file name, it will fallback to the file creation date to determine the newer (and default) kernel. By default, mkinitcpio creates the default image first, and the fallback one in second, which, in our case, would make the fallback image the default.
There is a little trick to solve this: make mkinitcpio build the images the other way around. In the `+.preset+` file, simply switch the presets around:
[source,bash]
----
PRESETS=('fallback' 'default')
----
You can now close the preset file. Recreate the parent folder with `+mkdir+`, then run `+mkinitcpio -P+` to re-create the images in the right order.
Note: If you have SecureBoot set up, do not forget to sign its binary with `+sbctl+`.

8
bin/import-bundle.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
name=$(basename $1 .textbundle)
mkdir -p articles/$name
pandoc \
-f markdown -t asciidoc \
-i $1/text.md -o articles/$name/$name.asciidoc \
-s --wrap=none --shift-heading-level-by=-1

59
flake.lock Normal file
View File

@ -0,0 +1,59 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717646450,
"narHash": "sha256-KE+UmfSVk5PG8jdKdclPVcMrUB8yVZHbsjo7ZT1Bm3c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "818dbe2f96df233d2041739d6079bb616d3e5597",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

12
flake.nix Normal file
View File

@ -0,0 +1,12 @@
{
inputs.flake-utils.url = github:numtide/flake-utils;
outputs = { self, flake-utils, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; };
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_20 python312Packages.fonttools python312Packages.brotli libavif pandoc imagemagick ];
};
});
}

189
gulpfile.js Normal file
View File

@ -0,0 +1,189 @@
import { src, dest, parallel, watch as _watch } from "gulp";
import postcss from "gulp-postcss";
import { Transform } from "node:stream";
import pug from "pug";
import asciidoctor from "asciidoctor";
import { basename, dirname, join } from "node:path";
import Vinyl from "vinyl";
import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
import rehypePresetMinify from "rehype-preset-minify";
import rehypeExternalLinks from "rehype-external-links";
import rehypePrism from "@mapbox/rehype-prism";
import { DateTime } from "luxon";
import { visit } from "unist-util-visit";
import { readFileSync, unlinkSync } from "node:fs";
import { env } from "node:process";
import tmp from "tmp";
import { execFile } from "node:child_process";
const PRODUCTION = env.NODE_ENV === "production";
const DEFAULT_DATE = DateTime.fromSeconds(Number(env.SOURCE_DATE_EPOCH)).toUTC();
const FONT_PRESETS = {
mono: { ranges: ["20-7F", "2E22-2E25", "2713", "2717"] },
text: { ranges: ["20-7F", "A0-FF", "2000-206F", "20AC"] },
};
const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
EXTENSION_REGISTRY.inlineMacro("abbr", function () {
this.process((parent, target, attributes) => {
return this.createInline(
parent,
"quoted",
`<abbr title="${attributes["$positional"]}">${target}</abbr>`,
);
});
});
const extractImages = (gulp, file) => () => (tree) => {
visit(tree, "element", (node) => {
if (node.tagName !== "img") {
return;
}
const path = join(dirname(file.path), node.properties.src);
const image = new Vinyl({
path: join(basename(file.path, ".asciidoc"), node.properties.src),
contents: readFileSync(path),
});
gulp.push(image);
});
};
const asset = PRODUCTION
? (path) => join("https://assets.adaedra.eu/", path)
: (path) => join("/_assets/", path);
const renderArticle = () => {
const allArticles = [];
const renderLayout = pug.compileFile("src/layout.pug");
const renderArticleLayout = pug.compileFile("src/article.pug");
return new Transform({
readableObjectMode: true,
writableObjectMode: true,
async transform(file, _, callback) {
try {
const slug = basename(file.path, ".asciidoc");
const article = Asciidoctor.load(file.contents, {
extension_registry: EXTENSION_REGISTRY,
});
const date = DateTime.fromISO(article.getAttribute("docdate"), {
zone: "UTC",
});
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
callback(null);
return;
}
allArticles.push([slug, date, article]);
const vfile = await unified()
.use(rehypeParse)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(this, file))
.use(rehypePrism)
.use(rehypePresetMinify)
.use(rehypeStringify)
.process(article.convert());
const content = renderLayout({
asset,
title: article.getDoctitle(),
render() {
return renderArticleLayout({
asset,
article,
date,
DateTime,
body: vfile.toString(),
});
},
});
file.contents = Buffer.from(content);
file.path = join(slug, "index.html");
file.base = null;
callback(null, file);
} catch (e) {
console.error(e);
callback(e);
}
},
final(callback) {
try {
allArticles.sort(([, a], [, b]) => b.diff(a).toMillis());
const renderIndex = pug.compileFile("src/index.pug");
const contents = renderLayout({
asset,
render() {
return renderIndex({ articles: allArticles, asset });
},
});
this.push(
new Vinyl({
path: "index.html",
contents: Buffer.from(contents),
}),
);
callback(null);
} catch (e) {
console.error(e);
callback(e);
}
},
});
};
export const articles = () =>
src("articles/**/*.asciidoc").pipe(renderArticle()).pipe(dest("dist/"));
export const images = () => src("src/*.avif", { encoding: false }).pipe(dest("dist/_assets/"));
export const svg = () => src("src/*.svg").pipe(dest("dist/_assets/"));
export const css = () => src("src/index.css").pipe(postcss()).pipe(dest("dist/_assets/"));
const compileFont = () =>
new Transform({
readableObjectMode: true,
writableObjectMode: true,
transform(chunk, _, callback) {
const [, variant, weight] = /([A-Z][a-z]+)-(\w+)\.ttf$/.exec(chunk.basename);
const tmpOutput = tmp.fileSync({ discardDescriptor: true });
const unicodes = FONT_PRESETS[variant.toLowerCase()].ranges;
execFile("pyftsubset", [
chunk.path,
`--unicodes=${unicodes.join(",")}`,
`--output-file=${tmpOutput.name}`,
"--layout-features=*",
"--flavor=woff2",
]).once("exit", () => {
const file = new Vinyl({
path: `iosevka-adaedra-${variant.toLowerCase()}-${weight.toLowerCase()}.woff2`,
contents: readFileSync(tmpOutput.name),
});
unlinkSync(tmpOutput.name);
callback(null, file);
});
},
});
export const fonts = () => src("vendor/*.ttf").pipe(compileFont()).pipe(dest("dist/_assets/"));
export default parallel(fonts, articles, images, svg, css);
export const watch = () => {
_watch(["src/*.pug", "articles/**/*.asciidoc"], articles);
_watch("src/*.css", css);
_watch("src/*.avif", images);
_watch("src/*.svg", svg);
};

5208
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "@adaedra/blog",
"version": "0.1.0",
"private": true,
"type": "module",
"dependencies": {
"@mapbox/rehype-prism": "^0.9.0",
"asciidoctor": "^3.0.4",
"cssnano": "^7.0.2",
"gulp": "^5.0.0",
"gulp-cli": "^3.0.0",
"gulp-postcss": "^10.0.0",
"luxon": "^3.4.4",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5",
"pug": "^3.0.3",
"rehype": "^13.0.1",
"rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0",
"tmp": "^0.2.3",
"unified": "^11.0.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/gulp": "^4.0.17",
"@types/luxon": "^3.4.2",
"@types/pug": "^2.0.10",
"@types/tmp": "^0.2.6",
"prettier": "^3.3.1"
},
"scripts": {
"build": "gulp",
"watch": "gulp watch",
"dev": "python -m http.server -d dist"
}
}

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
import importPlugin from "postcss-import";
import nestingPlugin from "postcss-nesting";
import cssnanoPlugin from "cssnano";
/** @type {import('postcss-load-config').Config} */
export default {
plugins: [importPlugin(), nestingPlugin(), cssnanoPlugin({ preset: "default" })],
};

12
src/article.pug Normal file
View File

@ -0,0 +1,12 @@
nav
a(href="/" title="Back to home page")
img(src=asset("avatar.avif") alt="Adædra's avatar")
| Adædra
header.article-header
= date.toLocaleString(DateTime.DATE_FULL)
h1!= article.getDocumentTitle()
main
a(name="content")
!= body

BIN
src/avatar.avif Normal file

Binary file not shown.

BIN
src/cariboudev.avif Normal file

Binary file not shown.

33
src/gruvbox.css Normal file
View File

@ -0,0 +1,33 @@
:root {
--gruvbox-bg0: #282828;
--gruvbox-bg0-hard: #1d2021;
--gruvbox-bg0-soft: #32302f;
--gruvbox-bg1: #3c3836;
--gruvbox-bg2: #504945;
--gruvbox-bg3: #665c54;
--gruvbox-bg4: #7c6f64;
--gruvbox-fg0: #fbf1c7;
--gruvbox-fg1: #ebdbb2;
--gruvbox-fg2: #d5c4a1;
--gruvbox-fg3: #bdae93;
--gruvbox-fg4: #a89984;
--gruvbox-dark-red: #cc241d;
--gruvbox-dark-green: #98971a;
--gruvbox-dark-yellow: #d79921;
--gruvbox-dark-blue: #458588;
--gruvbox-dark-purple: #b16286;
--gruvbox-dark-aqua: #689d6a;
--gruvbox-dark-orange: #d65d0e;
--gruvbox-dark-gray: #928374;
--gruvbox-light-red: #fb4934;
--gruvbox-light-green: #b8bb26;
--gruvbox-light-yellow: #fabd2f;
--gruvbox-light-blue: #83a598;
--gruvbox-light-purple: #d3869b;
--gruvbox-light-aqua: #8ec07c;
--gruvbox-light-orange: #f38019;
--gruvbox-light-gray: #a89984;
}

179
src/index.css Normal file
View File

@ -0,0 +1,179 @@
@import "../vendor/iosevka.css";
@import "gruvbox.css";
@import "prism.css";
html, body {
padding: 0;
margin: 0;
}
html {
--brand-color: #DDCBA3;
background-color: var(--gruvbox-bg0-hard);
color: var(--gruvbox-fg0);
font-family: iosevka-adaedra-text;
font-size: 16px;
@media print {
--brand-color: #9A806A;
background-color: transparent;
color: black;
font-size: 10pt;
}
}
.main {
max-width: 100ch;
margin: 0 auto;
padding: 1em;
@media print {
max-width: unset;
margin: 0;
padding: 0;
}
}
.index-header {
display: flex;
align-items: center;
margin-bottom: 2rem;
img {
width: 10rem;
max-width: 20vw;
}
h1 {
color: var(--brand-color);
font-size: 300%;
}
}
.article-header {
color: var(--gruvbox-fg4);
margin: 5rem 0 2rem 0;
@media print {
color: var(--gruvbox-bg4);
}
}
h1 {
padding: 0;
margin: 0;
font-weight: normal;
font-size: 300%;
color: var(--gruvbox-fg0);
@media print {
color: var(--gruvbox-bg0);
}
}
h2 {
font-weight: normal;
font-size: 200%;
margin: 2rem 0 1rem 0;
}
nav {
font-size: 120%;
color: var(--brand-color);
margin-bottom: 2rem;
img {
height: 2em;
margin-inline-end: 0.5em;
vertical-align: middle;
border-radius: 10%;
}
a, a:visited {
color: inherit;
text-decoration: none;
}
}
.index-list {
list-style-type: none;
margin: 0;
padding: 0;
a {
text-decoration: none;
}
svg {
height: 1em;
width: 1em;
vertical-align: middle;
margin-inline-end: 1ch;
}
li {
margin: 0.5rem 0;
}
}
p {
text-align: justify;
hyphens: auto;
line-height: 2;
margin: 0 0 1em 0;
padding: 0;
}
pre {
overflow-x: auto;
border: 1px solid var(--gruvbox-fg4);
background-color: var(--gruvbox-bg0);
padding: 0.5rem;
direction: ltr;
@media print {
background-color: transparent;
}
}
code {
white-space: nowrap;
&::before {
content: '\2E22';
}
&::after {
content: '\2E25';
}
&::before, &::after {
color: var(--gruvbox-fg4);
pre & {
display: none;
}
}
pre & {
white-space: pre;
}
}
pre, code {
font-family: iosevka-adaedra-mono;
}
a {
color: var(--gruvbox-light-blue);
&:visited {
color: var(--gruvbox-light-purple);
}
}
.imageblock img {
max-width: 100%;
margin: 0 auto;
}

24
src/index.pug Normal file
View File

@ -0,0 +1,24 @@
header.index-header
div(style="flex: 1;")
h1 Adædra
| Software developper<br />
| Rust, Ruby, TypeScript, Linux<br />
| Paris, France
img(src=asset("cariboudev.avif") alt="Adædra's mascot")/
h2 Latest articles
ul.index-list
- for ([slug, date, article] of articles)
li
= date.toISODate()
| &nbsp;
a(href=`/${slug}/`)!= article.getDocumentTitle()
h2 Find me elsewhere
ul.index-list
li: a(href="https://gitea.adaedra.eu/adaedra" target="_blank")
svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-label="Gitea Logo"): use(href=`${asset("logos.svg")}#gitea`)/
| Gitea
li: a(href="https://nerdculture.de/@adaedra" target="_blank" rel="noopener")
svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 74 79" aria-label="Mastodon Logo"): use(href=`${asset("logos.svg")}#mastodon`)/
| Mastodon

13
src/layout.pug Normal file
View File

@ -0,0 +1,13 @@
doctype html
html(lang="en")
head
meta(charset="utf-8")/
title
if title
!= `${title} &ndash; `
| Adædra's blog
meta(name="viewport" content="width=device-width, initial-scale=1")/
meta(name="theme-color" content="#DDCBA3")/
link(rel="stylesheet" type="text/css" href=asset('index.css'))/
body: .main
!= render()

9
src/logos.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg">
<g id="mastodon">
<path d="M73.7014 17.9592C72.5616 9.62034 65.1774 3.04876 56.424 1.77536C54.9472 1.56019 49.3517 0.7771 36.3901 0.7771H36.2933C23.3281 0.7771 20.5465 1.56019 19.0697 1.77536C10.56 3.01348 2.78877 8.91838 0.903306 17.356C-0.00357857 21.5113 -0.100361 26.1181 0.068112 30.3439C0.308275 36.404 0.354874 42.4535 0.91406 48.489C1.30064 52.498 1.97502 56.4751 2.93215 60.3905C4.72441 67.6217 11.9795 73.6395 19.0876 76.0945C26.6979 78.6548 34.8821 79.0799 42.724 77.3221C43.5866 77.1245 44.4398 76.8953 45.2833 76.6342C47.1867 76.0381 49.4199 75.3714 51.0616 74.2003C51.0841 74.1839 51.1026 74.1627 51.1156 74.1382C51.1286 74.1138 51.1359 74.0868 51.1368 74.0592V68.2108C51.1364 68.185 51.1302 68.1596 51.1185 68.1365C51.1069 68.1134 51.0902 68.0932 51.0695 68.0773C51.0489 68.0614 51.0249 68.0503 50.9994 68.0447C50.9738 68.0391 50.9473 68.0392 50.9218 68.045C45.8976 69.226 40.7491 69.818 35.5836 69.8087C26.694 69.8087 24.3031 65.6569 23.6184 63.9285C23.0681 62.4347 22.7186 60.8764 22.5789 59.2934C22.5775 59.2669 22.5825 59.2403 22.5934 59.216C22.6043 59.1916 22.621 59.1702 22.6419 59.1533C22.6629 59.1365 22.6876 59.1248 22.714 59.1191C22.7404 59.1134 22.7678 59.1139 22.794 59.1206C27.7345 60.2936 32.799 60.8856 37.8813 60.8843C39.1036 60.8843 40.3223 60.8843 41.5447 60.8526C46.6562 60.7115 52.0437 60.454 57.0728 59.4874C57.1983 59.4628 57.3237 59.4416 57.4313 59.4098C65.3638 57.9107 72.9128 53.2051 73.6799 41.2895C73.7086 40.8204 73.7803 36.3758 73.7803 35.889C73.7839 34.2347 74.3216 24.1533 73.7014 17.9592ZM61.4925 47.6918H53.1514V27.5855C53.1514 23.3526 51.3591 21.1938 47.7136 21.1938C43.7061 21.1938 41.6988 23.7476 41.6988 28.7919V39.7974H33.4078V28.7919C33.4078 23.7476 31.3969 21.1938 27.3894 21.1938C23.7654 21.1938 21.9552 23.3526 21.9516 27.5855V47.6918H13.6176V26.9752C13.6176 22.7423 14.7157 19.3795 16.9118 16.8868C19.1772 14.4 22.1488 13.1231 25.8373 13.1231C30.1064 13.1231 33.3325 14.7386 35.4832 17.9662L37.5587 21.3949L39.6377 17.9662C41.7884 14.7386 45.0145 13.1231 49.2765 13.1231C52.9614 13.1231 55.9329 14.4 58.2055 16.8868C60.4017 19.3772 61.4997 22.74 61.4997 26.9752L61.4925 47.6918Z" fill="currentColor"/>
</g>
<g id="gitea">
<path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z" fill="currentcolor" />
<path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z" fill="currentcolor" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

78
src/prism.css Normal file
View File

@ -0,0 +1,78 @@
.token.comment,
.token.prolog,
.token.cdata {
color: var(--gruvbox-light-gray);
}
.token.delimiter,
.token.boolean,
.token.keyword,
.token.selector,
.token.important,
.token.atrule {
color: var(--gruvbox-light-red);
}
.token.operator,
.token.punctuation,
.token.attr-name {
color: var(--gruvbox-light-gray);
}
.token.tag,
.token.tag .punctuation,
.token.doctype,
.token.builtin {
color: var(--gruvbox-light-yellow);
}
.token.entity,
.token.number,
.token.symbol {
color: var(--gruvbox-light-purple);
}
.token.property,
.token.constant,
.token.variable {
color: var(--gruvbox-light-red);
}
.token.string,
.token.char {
color: var(--gruvbox-light-green);
}
.token.attr-value,
.token.attr-value .punctuation {
color: var(--gruvbox-light-gray);
}
.token.url {
color: var(--gruvbox-light-green);
text-decoration: underline;
}
.token.function {
color: var(--gruvbox-light-yellow);
}
.token.regex {
background: var(--gruvbox-light-green);
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.inserted {
background: var(--gruvbox-light-gray);
}
.token.deleted {
background: var(--gruvbox-light-red);
}

BIN
vendor/IosevkaAdaedraMono-Bold.ttf vendored Normal file

Binary file not shown.

BIN
vendor/IosevkaAdaedraMono-Regular.ttf vendored Normal file

Binary file not shown.

BIN
vendor/IosevkaAdaedraText-Bold.ttf vendored Normal file

Binary file not shown.

BIN
vendor/IosevkaAdaedraText-BoldItalic.ttf vendored Normal file

Binary file not shown.

BIN
vendor/IosevkaAdaedraText-Italic.ttf vendored Normal file

Binary file not shown.

BIN
vendor/IosevkaAdaedraText-Regular.ttf vendored Normal file

Binary file not shown.

41
vendor/iosevka.css vendored Normal file
View File

@ -0,0 +1,41 @@
@font-face {
font-family: iosevka-adaedra-text;
font-style: normal;
font-weight: normal;
src: url("iosevka-adaedra-text-regular.woff2") format("woff2");
}
@font-face {
font-family: iosevka-adaedra-text;
font-style: normal;
font-weight: bold;
src: url("iosevka-adaedra-text-bold.woff2") format("woff2");
}
@font-face {
font-family: iosevka-adaedra-text;
font-style: italic;
font-weight: normal;
src: url("iosevka-adaedra-text-italic.woff2") format("woff2");
}
@font-face {
font-family: iosevka-adaedra-text;
font-style: italic;
font-weight: bold;
src: url("iosevka-adaedra-text-bolditalic.woff2") format("woff2");
}
@font-face {
font-family: iosevka-adaedra-mono;
font-style: normal;
font-weight: normal;
src: url("iosevka-adaedra-mono-regular.woff2") format("woff2");
}
@font-face {
font-family: iosevka-adaedra-mono;
font-style: normal;
font-weight: bold;
src: url("iosevka-adaedra-mono-bold.woff2") format("woff2");
}