summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Still Thinking About It.lektorproject11
-rw-r--r--assets/static/css/.djlintrc14
-rw-r--r--assets/static/css/blog.css10
-rw-r--r--assets/static/css/categories.css61
-rw-r--r--assets/static/css/category.css17
-rw-r--r--assets/static/css/libre_baskerville.css73
-rw-r--r--assets/static/css/modern_normalize.css194
-rw-r--r--assets/static/css/style.css753
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500.woff2bin0 -> 36692 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500italic.woff2bin0 -> 38664 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600.woff2bin0 -> 36916 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600italic.woff2bin0 -> 38864 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700.woff2bin0 -> 35944 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700italic.woff2bin0 -> 38192 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-italic.woff2bin0 -> 37936 bytes
-rw-r--r--assets/static/fonts/libre-baskerville-v24-latin_latin-ext-regular.woff2bin0 -> 35232 bytes
-rw-r--r--assets/static/images/auto.svg3
-rw-r--r--assets/static/images/dark.svg3
-rw-r--r--assets/static/images/email.svg8
-rw-r--r--assets/static/images/favicon.pngbin0 -> 1132 bytes
-rw-r--r--assets/static/images/light.svg3
-rw-r--r--assets/static/images/logo.svg21
-rw-r--r--assets/static/images/mastodon.svg8
-rw-r--r--assets/static/images/opengraph.pngbin0 -> 27513 bytes
-rw-r--r--assets/static/images/rss.svg4
-rw-r--r--assets/static/images/xmpp.svg8
-rw-r--r--configs/atom.ini18
-rw-r--r--configs/index-pages.ini23
-rw-r--r--configs/tags.ini4
-rw-r--r--content/about/contents.lr29
-rw-r--r--content/about/me.jpgbin0 -> 211145 bytes
-rw-r--r--content/blog/back-to-blogging/contents.lr43
-rw-r--r--content/blog/contents.lr3
-rw-r--r--content/categories/contents.lr1
-rw-r--r--content/categories/site-notes/contents.lr1
-rw-r--r--content/contents.lr3
-rw-r--r--databags/blog.ini2
-rw-r--r--justfile14
-rw-r--r--models/blog-post.ini39
-rw-r--r--models/blog.ini17
-rw-r--r--models/categories.ini9
-rw-r--r--models/category.ini11
-rw-r--r--models/index.ini8
-rw-r--r--models/page.ini11
-rw-r--r--packages/blog-markdown-headings/lektor_blog_markdown_headings.py152
-rw-r--r--packages/blog-markdown-headings/setup.py13
-rw-r--r--templates/.djlintrc14
-rw-r--r--templates/about.html11
-rw-r--r--templates/base.html167
-rw-r--r--templates/blog-month-index.html54
-rw-r--r--templates/blog-post.html31
-rw-r--r--templates/blog-year-index.html54
-rw-r--r--templates/blog.html15
-rw-r--r--templates/categories.html53
-rw-r--r--templates/category.html51
-rw-r--r--templates/index.html15
-rw-r--r--templates/macros/blog.html100
-rw-r--r--templates/macros/pagination.html221
-rw-r--r--templates/page.html3
-rw-r--r--templates/tag.html59
60 files changed, 2440 insertions, 0 deletions
diff --git a/Still Thinking About It.lektorproject b/Still Thinking About It.lektorproject
new file mode 100644
index 0000000..92e8963
--- /dev/null
+++ b/Still Thinking About It.lektorproject
@@ -0,0 +1,11 @@
+[project]
+name = Still Thinking About It
+url = https://dennisfink.me
+url_style = absolute
+output_path = build
+
+[packages]
+lektor-atom = 0.4.0
+lektor-markdown-image-attrs = 0.2.1
+lektor-index-pages = 1.1.0
+lektor-tags = 0.5.2
diff --git a/assets/static/css/.djlintrc b/assets/static/css/.djlintrc
new file mode 100644
index 0000000..5882faa
--- /dev/null
+++ b/assets/static/css/.djlintrc
@@ -0,0 +1,14 @@
+{
+"blank_line_before_tag": "macro",
+"blank_line_after_tag": "endmacro,extends,import,from",
+"profile": "jinja",
+"format_css": true,
+"css": {
+"indent_size": 2
+},
+"format_js": true,
+"js": {
+"indent_size": 2
+},
+"indent": "2"
+}
diff --git a/assets/static/css/blog.css b/assets/static/css/blog.css
new file mode 100644
index 0000000..48a716b
--- /dev/null
+++ b/assets/static/css/blog.css
@@ -0,0 +1,10 @@
+@charset "UTF-8";
+
+article > p:first-of-type:first-letter {
+ color: var(--blue);
+ float: left;
+ font-weight: 700;
+ font-size: 3.8rem;
+ line-height: 3.8rem;
+ margin: 0.2em 0.1em 0 0;
+}
diff --git a/assets/static/css/categories.css b/assets/static/css/categories.css
new file mode 100644
index 0000000..5e26021
--- /dev/null
+++ b/assets/static/css/categories.css
@@ -0,0 +1,61 @@
+@charset "UTF-8";
+
+article > ul {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ @media (max-width: 500px) {
+ grid-template-columns: 1fr;
+ }
+
+ li {
+ max-inline-size: unset;
+ }
+
+ a {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 1rem;
+ border: 1px solid var(--foreground);
+ border-radius: 0.5rem;
+ height: 100%;
+
+ &:hover,
+ &:hover > dl {
+ border-color: var(--blue);
+ /*background-color: var(--blue);*/
+ color: var(--blue);
+ }
+
+ h3 {
+ margin: 0;
+ }
+
+ dl {
+ display: flex;
+ flex-direction: column;
+ color: var(--dim-foreground);
+ gap: 0.25rem;
+ font-size: 1.2rem;
+ font-style: italic;
+ line-height: initial;
+ margin: 0;
+
+ div {
+ display: flex;
+ flex-direction: row;
+ gap: 0.25rem;
+ }
+
+ dd {
+ margin: 0;
+ max-inline-size: unset;
+ }
+ }
+ }
+}
diff --git a/assets/static/css/category.css b/assets/static/css/category.css
new file mode 100644
index 0000000..e3567d8
--- /dev/null
+++ b/assets/static/css/category.css
@@ -0,0 +1,17 @@
+@charset "UTF-8";
+
+article > table {
+ border-collapse: collapse;
+
+ td {
+ vertical-align: top;
+ padding-bottom: 0.25rem;
+ }
+
+ td:first-child {
+ color: var(--dim-foreground);
+ font-style: italic;
+ padding-right: 2rem;
+ white-space: nowrap;
+ }
+}
diff --git a/assets/static/css/libre_baskerville.css b/assets/static/css/libre_baskerville.css
new file mode 100644
index 0000000..fa61e73
--- /dev/null
+++ b/assets/static/css/libre_baskerville.css
@@ -0,0 +1,73 @@
+@charset "UTF-8";
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: normal;
+ font-weight: 400;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-regular.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: italic;
+ font-weight: 400;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-italic.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: normal;
+ font-weight: 500;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-500.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: italic;
+ font-weight: 500;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-500italic.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: normal;
+ font-weight: 600;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-600.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: italic;
+ font-weight: 600;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-600italic.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: normal;
+ font-weight: 700;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-700.woff2")
+ format("woff2");
+}
+
+@font-face {
+ font-display: swap;
+ font-family: "Libre Baskerville";
+ font-style: italic;
+ font-weight: 700;
+ src: url("/static/fonts/libre-baskerville-v24-latin_latin-ext-700italic.woff2")
+ format("woff2");
+}
diff --git a/assets/static/css/modern_normalize.css b/assets/static/css/modern_normalize.css
new file mode 100644
index 0000000..708b6a2
--- /dev/null
+++ b/assets/static/css/modern_normalize.css
@@ -0,0 +1,194 @@
+/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */
+/*
+Document
+========
+*/
+/**
+Use a better box model (opinionated).
+*/
+*,
+::before,
+::after {
+ box-sizing: border-box;
+}
+
+html {
+ /* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */
+ font-family:
+ system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji";
+ line-height: 1.15;
+ /* 1. Correct the line height in all browsers. */
+ -webkit-text-size-adjust: 100%;
+ /* 2. Prevent adjustments of font size after orientation changes in iOS. */
+ tab-size: 4;
+ /* 3. Use a more readable tab size (opinionated). */
+}
+
+/*
+Sections
+========
+*/
+body {
+ margin: 0;
+ /* Remove the margin in all browsers. */
+}
+
+/*
+Text-level semantics
+====================
+*/
+/**
+Add the correct font weight in Chrome and Safari.
+*/
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
+2. Correct the odd 'em' font sizing in all browsers.
+*/
+code,
+kbd,
+samp,
+pre {
+ font-family:
+ ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
+ /* 1 */
+ font-size: 1em;
+ /* 2 */
+}
+
+/**
+Add the correct font size in all browsers.
+*/
+small {
+ font-size: 80%;
+}
+
+/**
+Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
+*/
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/*
+Tabular data
+============
+*/
+/**
+Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
+*/
+table {
+ border-color: currentcolor;
+}
+
+/*
+Forms
+=====
+*/
+/**
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+*/
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit;
+ /* 1 */
+ font-size: 100%;
+ /* 1 */
+ line-height: 1.15;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+}
+
+/**
+Correct the inability to style clickable types in iOS and Safari.
+*/
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
+*/
+legend {
+ padding: 0;
+}
+
+/**
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+progress {
+ vertical-align: baseline;
+}
+
+/**
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ outline-offset: -2px;
+ /* 2 */
+}
+
+/**
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to 'inherit' in Safari.
+*/
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+}
+
+/*
+Interactive
+===========
+*/
+/*
+Add the correct display in Chrome and Safari.
+*/
+summary {
+ display: list-item;
+}
diff --git a/assets/static/css/style.css b/assets/static/css/style.css
new file mode 100644
index 0000000..ca7443a
--- /dev/null
+++ b/assets/static/css/style.css
@@ -0,0 +1,753 @@
+@charset "UTF-8";
+
+/*
+=============================================================================
+CUSTOM PROPERTIES
+=============================================================================
+*/
+:root {
+ /* Named palette */
+ --black: light-dark(#575279, #393b44);
+ --red: light-dark(#b4637a, #c94f6d);
+ --green: light-dark(#618774, #81b29a);
+ --yellow: light-dark(#ea9d34, #dbc074);
+ --blue: light-dark(#286983, #719cd6);
+ --magenta: light-dark(#907aa9, #9d79d6);
+ --cyan: light-dark(#56949f, #63cdcf);
+ --white: light-dark(#e5e9f0, #dfdfe0);
+ --orange: light-dark(#d7827e, #f4a261);
+ --pink: light-dark(#d685af, #d67ad2);
+ /* Dim palette */
+ --dim-black: light-dark(#504c6b, #30323a);
+ --dim-red: light-dark(#a5576d, #ab435d);
+ --dim-green: light-dark(#597668, #6e9783);
+ --dim-yellow: light-dark(#dd9024, #baa363);
+ --dim-blue: light-dark(#295e73, #6085b6);
+ --dim-magenta: light-dark(#816b9a, #8567b7);
+ --dim-cyan: light-dark(#50848c, #54aeb0);
+ --dim-white: light-dark(#c8cfde, #bebebe);
+ --dim-foreground: light-dark(#4c4769, #aeafb0);
+ /* Bright palette */
+ --bright-black: light-dark(#5f5695, #575860);
+ --bright-red: light-dark(#c26d85, #d16983);
+ --bright-green: light-dark(#629f81, #8ebaa4);
+ --bright-yellow: light-dark(#eea846, #e0c989);
+ --bright-blue: light-dark(#2d81a3, #86abdc);
+ --bright-magenta: light-dark(#9a80b9, #baa1e2);
+ --bright-cyan: light-dark(#5ca7b4, #7ad5d6);
+ --bright-white: light-dark(#e6ebf3, #e4e4e5);
+ --bright-foreground: light-dark(#625c87, #d6d6d7);
+ /* Semantic */
+ --foreground: light-dark(#575279, #cdcecf);
+ --background: light-dark(#faf4ed, #192330);
+ /* Layout */
+ --measure: 60ch;
+ --column-min-width: calc(1/3 * 100vw);
+ --column-max-width: 100%;
+ /* Gradient border — reused in header, footer */
+ --gradient-border: linear-gradient(
+ to right,
+ var(--foreground),
+ var(--green),
+ var(--foreground)
+ );
+
+ /* Responsive layout */
+ @media (min-width: 500px) {
+ --column-max-width: var(--measure);
+ }
+
+ &:has(input[name="theme-switcher"][value="light dark"]:checked) {
+ color-scheme: light dark;
+ }
+
+ &:has(input[name="theme-switcher"][value="light"]:checked) {
+ color-scheme: light;
+ }
+
+ &:has(input[name="theme-switcher"][value="dark"]:checked) {
+ color-scheme: dark;
+ }
+}
+
+/*
+=============================================================================
+BASE
+=============================================================================
+ */
+html {
+ font-size: 14px;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ min-width: var(--column-min-width);
+ max-width: var(--column-max-width);
+ margin: 0 auto;
+ background-color: var(--background);
+ color: var(--foreground);
+ font-family: "Libre Baskerville", Georgia, serif;
+ font-size: clamp(1rem, 1.5vw, 1.4rem);
+ line-height: 1.6;
+
+ @media (max-width: 500px) {
+ padding: 0 1.5rem;
+ }
+
+ & > footer {
+ border-image-slice: 1;
+ border-image-source: var(--gradient-border);
+ border-image-width: 1px 0 0 0;
+ border-top: 1px solid;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-bottom: 2rem;
+ margin-top: auto;
+ padding-top: 0.5rem;
+
+ @media (max-width: 500px) {
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ h2 {
+ font-size: clamp(1.2rem, 2.5vw, 1.7rem);
+ font-style: italic;
+ font-weight: 400;
+ line-height: 1;
+ }
+
+ section div {
+ display: flex;
+ flex-direction: row;
+ gap: 0.25rem;
+ }
+
+ & > section[aria-label="Follow me"] a {
+ background-color: var(--green);
+ display: inline-block;
+ height: 3rem;
+ mask-size: cover;
+ width: 3rem;
+
+ &:hover {
+ background-color: var(--blue);
+ }
+
+ &#atom {
+ mask-image: url("/static/images/rss.svg");
+ }
+
+ &#mastodon {
+ mask-image: url("/static/images/mastodon.svg");
+ }
+
+ &#xmpp {
+ mask-image: url("/static/images/xmpp.svg");
+ }
+
+ &#email {
+ mask-image: url("/static/images/email.svg");
+ }
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ }
+}
+
+/*
+=============================================================================
+TYPOGRAPHY
+=============================================================================
+*/
+
+/* --- Headings --- */
+h1 {
+ font-size: clamp(2rem, 4vw, 3.2rem);
+ font-weight: 700;
+ line-height: 1;
+ margin: 0 0 0.5rem 0;
+}
+
+h2 {
+ font-size: clamp(1.5rem, 3vw, 2.2rem);
+ font-style: italic;
+ font-weight: 500;
+ line-height: 1;
+ margin: 1rem 0;
+}
+
+h3 {
+ font-size: clamp(1.2rem, 2.5vw, 1.7rem);
+ font-style: italic;
+ font-weight: 400;
+ line-height: 1;
+ margin-bottom: 1rem;
+}
+
+/* --- Body text --- */
+p {
+ font-size: clamp(1rem, 1.5vw, 1.4rem);
+ line-height: 1.5em;
+ margin: 0;
+ text-align: justify;
+ text-wrap: pretty;
+ word-spacing: 0.16em;
+
+ &.center {
+ text-align: center;
+ }
+}
+
+blockquote {
+ font-style: italic;
+ border-left: 2px solid var(--blue);
+ margin: 1em 0 1em 0.5em;
+ padding-left: 0.5em;
+}
+
+/* --- Lists --- */
+ul,
+ol,
+dl {
+ font-size: clamp(1rem, 1.5vw, 1.4rem);
+ line-height: 2rem;
+}
+
+li,
+dd {
+ max-inline-size: var(--measure);
+}
+
+/* --- Rules --- */
+hr {
+ border: 0;
+ border-top: 2px solid var(--blue);
+ margin: 1em 0;
+}
+
+/* --- Links --- */
+a {
+ color: var(--green);
+ text-decoration: none;
+ text-decoration-thickness: 0.05em;
+ text-underline-offset: 0.1em;
+
+ &:hover {
+ color: var(--blue);
+ }
+}
+
+/*
+=============================================================================
+SKIP LINK (accessibility)
+=============================================================================
+ */
+.skip-link {
+ background: #fff;
+ color: #000;
+ left: 0;
+ padding: 0.5rem 1rem;
+ position: absolute;
+ text-decoration: none;
+ top: 0;
+ transform: translateY(-120%);
+ transition: transform 0.2s ease;
+ z-index: 1000;
+
+ &:focus,
+ &:focus-visible {
+ transform: translateY(0);
+ }
+}
+
+.visually-hidden {
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+
+/*
+=============================================================================
+SITE HEADER
+=============================================================================
+*/
+body > header {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ justify-content: space-evenly;
+ margin-top: 4rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid;
+ border-image-slice: 1;
+ border-image-source: var(--gradient-border);
+ border-image-width: 0 0 1px 0;
+
+ & > div {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 0.25rem;
+ }
+
+ svg {
+ max-width: 4em;
+ max-height: 4em;
+ fill: var(--green);
+
+ @media (max-width: 500px) {
+ display: none;
+ }
+ }
+
+ p {
+ font-size: clamp(1.1rem, 2vw, 1.75rem);
+ font-style: italic;
+ max-width: unset;
+ }
+
+ /* --- Navigation --- */
+ nav {
+ ul {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ a[aria-current="page"] {
+ color: var(--blue);
+
+ &:hover {
+ color: var(--bright-blue);
+ }
+ }
+ }
+
+ /* --- Theme switcher --- */
+ fieldset {
+ align-items: center;
+ border: 0;
+ display: flex;
+ flex-direction: row;
+ gap: 0.25rem;
+ justify-content: flex-start;
+ margin: 0;
+ padding: 0;
+
+ input[type="radio"] {
+ display: none;
+
+ &:checked + span {
+ background-color: var(--yellow);
+ }
+ }
+
+ label span {
+ background-color: var(--foreground);
+ cursor: pointer;
+ display: block;
+ height: 1.5rem;
+ mask-size: cover;
+ width: 1.5rem;
+ }
+ }
+}
+
+#lightmode + span,
+#automode + span,
+#darkmode + span {
+ mask-repeat: no-repeat;
+ mask-position: 50% 50%;
+}
+
+#lightmode + span {
+ mask-image: url("/static/images/light.svg");
+}
+
+#automode + span {
+ mask-image: url("/static/images/auto.svg");
+}
+
+#darkmode + span {
+ mask-image: url("/static/images/dark.svg");
+}
+
+/*
+=============================================================================
+MAIN
+=============================================================================
+*/
+main {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+/*
+=============================================================================
+ARTICLES
+=============================================================================
+*/
+article {
+ max-width: var(--column-max-width);
+ min-width: var(--column-min-width);
+
+ & > header {
+ display: flex;
+ flex-direction: row;
+ align-items: start;
+ justify-content: space-between;
+
+ &:not(:has(nav)) {
+ margin-bottom: 2rem;
+ }
+
+ & > div {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ max-width: calc(2 / 3 * 100%);
+
+ /* --- Article metadata --- */
+ & > dl {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ font-size: 1rem;
+ gap: calc(1 / 3 * 1rem);
+ margin: 0;
+
+ div {
+ align-items: center;
+ border: 1px solid var(--foreground);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: row;
+ }
+
+ dt {
+ color: var(--bright-blue);
+ padding-left: calc(1 / 3 * 1rem);
+ padding-right: calc(1 / 3 * 1rem);
+ }
+
+ dd {
+ margin: 0;
+ padding-right: calc(1 / 3 * 1rem);
+
+ a {
+ color: var(--green);
+ }
+ }
+
+ @media (max-width: 700px) {
+ margin-top: 2rem;
+ }
+
+ @media (max-width: 500px) {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ }
+ }
+
+ h2 {
+ margin-bottom: 0.25rem;
+ margin-top: 0;
+ }
+
+ /* --- TOC --- */
+ & > nav {
+ border: 1px solid var(--foreground);
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ line-height: 1.4;
+ padding: 0.75rem;
+ max-width: calc(1 / 3 * 100%);
+
+ & > p {
+ color: var(--blue);
+ font-size: 1em;
+ font-weight: 700;
+ line-height: 1.2;
+ margin: 0 0 0.5rem 0;
+ }
+
+ ol {
+ font-size: 1em;
+ line-height: 1.4;
+ margin: 0;
+ padding-left: 1.5em;
+
+ ol {
+ margin-top: 0.25rem;
+ padding-left: 1.25em;
+ }
+ }
+
+ li {
+ max-inline-size: none;
+
+ & + li {
+ margin-top: 0.25rem;
+ }
+ }
+
+ a {
+ overflow-wrap: anywhere;
+ }
+
+ @media (max-width: 700px) {
+ margin-top: 2rem;
+ }
+ }
+ }
+
+ /* --- Article body --- */
+ p {
+ max-inline-size: var(--measure);
+
+ & + p {
+ margin-top: 2rem;
+ }
+ }
+
+ & > h3:not(:first-of-type) {
+ margin-top: 2rem;
+ }
+
+ & > footer {
+ color: var(--dim-foreground);
+ font-style: italic;
+ margin-top: 1.5rem;
+
+ p {
+ display: inline;
+ font-size: 1.2rem;
+ line-height: 1.5;
+ margin: 0;
+ padding: 0;
+ }
+
+ ul {
+ font-size: 1.2rem;
+ line-height: 1.5;
+ margin: 0;
+ padding: 0;
+ display: inline-flex;
+ flex-wrap: wrap;
+ list-style: none;
+ }
+
+ li {
+ display: flex;
+ max-inline-size: none;
+ }
+ }
+
+ /* --- Article separator --- */
+ & + hr {
+ align-items: center;
+ background: none;
+ border: 0;
+ display: flex;
+ height: 1em;
+ justify-content: center;
+ margin: 4em 0;
+ position: relative;
+ width: 100%;
+
+ &::before {
+ background-image: var(--gradient-border);
+ content: "";
+ height: 1px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 50%;
+ }
+
+ &::after {
+ background-color: var(--background);
+ color: var(--blue);
+ content: "§";
+ padding: 0 1rem;
+ position: relative;
+ transform: rotate(90deg);
+ z-index: 1;
+ }
+ }
+}
+
+/*
+=============================================================================
+PAGINATION
+=============================================================================
+ */
+nav:is(
+ [aria-label="Page navigation"],
+ [aria-label="Year archive navigation"],
+ [aria-label="Monthly archive navigation"]
+) {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ ol {
+ align-items: center;
+ border: 1px solid var(--foreground);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: row;
+ gap: 0.25rem;
+ list-style: none;
+ margin: 2rem 0;
+ padding: 0.25rem;
+ }
+
+ a,
+ span {
+ align-items: center;
+ display: flex;
+ height: 2rem;
+ justify-content: center;
+ min-width: 2rem;
+ padding-inline: 0.5rem;
+ }
+
+ & a {
+ border-radius: 0.5rem;
+
+ &:hover,
+ &[aria-current="page"]:hover {
+ background-color: var(--blue);
+ color: var(--foreground);
+ }
+
+ &[aria-current="page"] {
+ color: var(--blue);
+ }
+ }
+
+ span[aria-disabled="true"] {
+ color: var(--dim-foreground);
+ }
+}
+
+/*
+=============================================================================
+IMAGES
+=============================================================================
+*/
+img {
+ height: auto;
+ max-width: 100%;
+
+ &.center {
+ display: block;
+ margin-inline: auto;
+ }
+
+ &.round {
+ border-radius: 100%;
+ }
+
+ &.w25 {
+ width: 25%;
+ }
+
+ &.w33 {
+ width: calc(1 / 3 * 100%);
+ }
+
+ &.w50 {
+ width: 50%;
+ }
+
+ &.w66 {
+ width: calc(2 / 3 * 100%);
+ }
+
+ &.w75 {
+ width: 75%;
+ }
+
+ &.breakout-xs {
+ width: calc(100% + 16rem);
+ transform: translateX(-8rem);
+ }
+
+ &.breakout-sm {
+ width: calc(100% + 24rem);
+ transform: translateX(-12rem);
+ }
+
+ &.breakout-md {
+ width: calc(100% + 32rem);
+ transform: translateX(-16rem);
+ }
+
+ &.breakout-lg {
+ width: calc(100% + 48rem);
+ transform: translateX(-24rem);
+ }
+
+ &.breakout-xl {
+ width: calc(100% + 64rem);
+ transform: translateX(-32rem);
+ }
+
+ &.breakout-full {
+ width: 100vw;
+ margin-left: calc(50% - 50vw);
+ }
+
+ &.breakout-xs,
+ &.breakout-sm,
+ &.breakout-md,
+ &.breakout-lg,
+ &.breakout-xl,
+ &.breakout-full {
+ max-width: 100vw;
+ }
+
+ @media (max-width: 500px) {
+ &.breakout-xs,
+ &.breakout-sm,
+ &.breakout-md,
+ &.breakout-lg,
+ &.breakout-xl {
+ width: 100vw;
+ transform: none;
+ margin-left: calc(50% - 50vw);
+ }
+ }
+}
+
+.missing-publication-date-warning {
+ background: var(--red);
+ color: var(--foreground);
+ font-weight: 700;
+ font-size: 1.1rem;
+}
+
+.missing-publication-date-warning > dt {
+ color: var(--foreground);
+}
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500.woff2
new file mode 100644
index 0000000..4c373fe
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500italic.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500italic.woff2
new file mode 100644
index 0000000..7e780a9
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-500italic.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600.woff2
new file mode 100644
index 0000000..f94f429
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600italic.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600italic.woff2
new file mode 100644
index 0000000..067b7fb
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-600italic.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700.woff2
new file mode 100644
index 0000000..4e0f4e7
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700italic.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700italic.woff2
new file mode 100644
index 0000000..5351437
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-700italic.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-italic.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-italic.woff2
new file mode 100644
index 0000000..733d0dd
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-italic.woff2
Binary files differ
diff --git a/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-regular.woff2 b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-regular.woff2
new file mode 100644
index 0000000..3c2170e
--- /dev/null
+++ b/assets/static/fonts/libre-baskerville-v24-latin_latin-ext-regular.woff2
Binary files differ
diff --git a/assets/static/images/auto.svg b/assets/static/images/auto.svg
new file mode 100644
index 0000000..5032980
--- /dev/null
+++ b/assets/static/images/auto.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
+ <path d="M276-280h76l40-112h176l40 112h76L520-720h-80zm138-176 64-182h4l64 182zm66 376q-83 0-156-31.5T197-197t-85.5-127T80-480t31.5-156T197-763t127-85.5T480-880t156 31.5T763-763t85.5 127T880-480t-31.5 156T763-197t-127 85.5T480-80m0-80q133 0 226.5-93.5T800-480t-93.5-226.5T480-800t-226.5 93.5T160-480t93.5 226.5T480-160"/>
+</svg>
diff --git a/assets/static/images/dark.svg b/assets/static/images/dark.svg
new file mode 100644
index 0000000..ccb82f5
--- /dev/null
+++ b/assets/static/images/dark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
+ <path d="M480-120q-150 0-255-105T120-480t105-255 255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120m0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82m-10-270"/>
+</svg>
diff --git a/assets/static/images/email.svg b/assets/static/images/email.svg
new file mode 100644
index 0000000..2c197d4
--- /dev/null
+++ b/assets/static/images/email.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640">
+ <defs>
+ <clipPath id="a" clipPathUnits="userSpaceOnUse">
+ <path fill="#fff" d="M-5-5h650v650H-5Zm145.8 65A81 81 0 0 0 60 140.8v2.3L291.4 345a43.3 43.3 0 0 0 57.2 0L580 143.1v-2.3A81 81 0 0 0 499.2 60ZM60 210.1v242l138.8-121Zm520 0-138.8 121 138.8 121ZM237.2 364.6 62 517.2a81 81 0 0 0 78.7 62.8h358.4a81 81 0 0 0 78.7-62.8L403 364.6 381.6 383a94 94 0 0 1-123.4 0z" class="powerclip"/>
+ </clipPath>
+ </defs>
+ <path d="M91.4 0C41 0 0 41 0 91.4v457.2C0 599 41 640 91.4 640h457.2c50.4 0 91.4-41 91.4-91.4V91.4C640 41 599 0 548.6 0Z" clip-path="url(#a)"/>
+</svg>
diff --git a/assets/static/images/favicon.png b/assets/static/images/favicon.png
new file mode 100644
index 0000000..cb4a2f8
--- /dev/null
+++ b/assets/static/images/favicon.png
Binary files differ
diff --git a/assets/static/images/light.svg b/assets/static/images/light.svg
new file mode 100644
index 0000000..56e1084
--- /dev/null
+++ b/assets/static/images/light.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
+ <path d="M480-360q50 0 85-35t35-85-35-85-85-35-85 35-35 85 35 85 85 35m0 80q-83 0-141.5-58.5T280-480t58.5-141.5T480-680t141.5 58.5T680-480t-58.5 141.5T480-280M200-440H40v-80h160zm720 0H760v-80h160zM440-760v-160h80v160zm0 720v-160h80v160zM256-650l-101-97 57-59 96 100zm492 496-97-101 53-55 101 97zm-98-550 97-101 59 57-100 96zM154-212l101-97 55 53-97 101zm326-268"/>
+</svg>
diff --git a/assets/static/images/logo.svg b/assets/static/images/logo.svg
new file mode 100644
index 0000000..a3090ad
--- /dev/null
+++ b/assets/static/images/logo.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="800"
+ height="800"
+ viewBox="0 0 800 800"
+ version="1.1"
+ id="svg1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs1" />
+ <g
+ id="layer1">
+ <path
+ id="logo"
+ style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;stroke-linecap:round;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1"
+ d="M 89.449219 0 C 40.058022 0 1.8947806e-14 40.058022 0 89.449219 L 0 710.55078 C 0 759.94198 40.058022 800 89.449219 800 L 710.55078 800 C 759.94198 800 800 759.94198 800 710.55078 L 800 89.449219 C 800 40.058022 759.94198 1.8947806e-14 710.55078 0 L 89.449219 0 z M 89.449219 18.898438 L 710.55078 18.898438 C 749.79958 18.898438 781.10156 50.200416 781.10156 89.449219 L 781.10156 710.55078 C 781.10156 749.79958 749.79958 781.10156 710.55078 781.10156 L 89.449219 781.10156 C 50.200416 781.10156 18.898438 749.79958 18.898438 710.55078 L 18.898438 89.449219 C 18.898438 50.200416 50.200416 18.898438 89.449219 18.898438 z M 237.22852 89.824219 C 225.30581 89.764099 212.63778 89.813149 199.9668 89.984375 C 174.62483 90.326834 149.28529 91.151988 129.875 92.552734 C 120.16986 93.253107 111.95683 94.094899 105.89258 95.111328 C 102.86045 95.619543 100.37354 96.163921 98.419922 96.806641 C 96.466308 97.44936 94.86156 97.846472 93.806641 99.865234 C 93.279181 100.87462 93.231332 101.55582 93.056641 102.66797 C 92.881949 103.78011 92.722961 105.17836 92.570312 106.88086 C 92.265016 110.28586 91.986428 114.89969 91.728516 120.58594 C 91.212692 131.95844 90.783451 147.59876 90.4375 166.10156 C 89.745598 203.10716 89.388911 251.55165 89.359375 299.99805 C 89.329835 348.44444 89.628091 396.88867 90.246094 433.89258 C 90.555095 452.39453 90.944683 468.0332 91.414062 479.40234 C 91.648753 485.08692 91.901424 489.70206 92.179688 493.10156 C 92.318821 494.80131 92.46257 496.19404 92.621094 497.29883 C 92.779617 498.40362 92.824569 499.06954 93.294922 500.05469 C 94.235628 502.02499 95.788862 502.51581 97.603516 503.17188 C 99.418169 503.82794 101.70553 504.38069 104.48633 504.90039 C 110.04793 505.93978 117.5577 506.81642 126.42773 507.5918 C 144.16781 509.14256 167.31086 510.26508 190.44922 511.20312 C 213.58758 512.14117 236.7154 512.89431 254.34375 513.69336 C 263.15792 514.09289 270.60218 514.50483 275.94727 514.95312 C 278.61981 515.17728 280.7727 515.41242 282.25781 515.64648 C 282.90195 515.74801 283.35182 515.84713 283.70117 515.93359 C 283.78347 516.22135 283.87385 516.54876 283.97266 517.07031 C 284.22082 518.38022 284.47263 520.31606 284.70312 522.72266 C 285.16413 527.53585 285.55582 534.26272 285.86914 542.23047 C 286.49577 558.16597 286.81533 579.08637 286.83008 600.00195 C 286.84478 620.91754 286.55476 641.83795 285.96484 657.77539 C 285.66989 665.74411 285.29805 672.47212 284.85938 677.28906 C 284.64004 679.69754 284.40142 681.63565 284.16406 682.95117 C 284.07046 683.46982 283.98337 683.79442 283.9043 684.08594 C 283.58194 684.16714 283.19736 684.25852 282.61719 684.35352 C 281.21411 684.58325 279.16828 684.81373 276.62695 685.02734 C 271.5443 685.45456 264.45709 685.825 256.06445 686.14258 C 239.27918 686.77773 217.24939 687.20155 195.21289 687.48438 C 151.13989 688.05004 107.02734 688.05078 104.52734 688.05078 C 102.02734 688.05078 98.641913 688.05141 95.818359 688.11133 C 94.406583 688.14133 93.141642 688.18323 92.148438 688.25586 C 91.651836 688.29216 91.23338 688.33186 90.820312 688.40234 C 90.61378 688.43764 90.414344 688.47274 90.121094 688.57227 C 89.827843 688.67177 89.236367 688.53317 88.550781 689.9043 C 88.207986 690.58988 88.31307 690.61039 88.292969 690.74414 C 88.272869 690.87789 88.263066 690.97709 88.253906 691.08398 C 88.235576 691.29777 88.222867 691.52748 88.210938 691.80273 C 88.187088 692.35324 88.169277 693.07184 88.154297 693.91797 C 88.124347 695.61022 88.109375 697.80361 88.109375 700 C 88.109375 702.19639 88.124347 704.38978 88.154297 706.08203 C 88.169277 706.92816 88.187088 707.64676 88.210938 708.19727 C 88.222868 708.47252 88.235576 708.70223 88.253906 708.91602 C 88.263106 709.02291 88.272869 709.12211 88.292969 709.25586 C 88.313069 709.38961 88.207989 709.41012 88.550781 710.0957 C 89.236367 711.46688 89.829462 711.32826 90.123047 711.42773 C 90.416631 711.52723 90.615224 711.56244 90.822266 711.59766 C 91.236349 711.66806 91.655986 711.70786 92.15625 711.74414 C 93.156778 711.81674 94.437764 711.8587 95.878906 711.88867 C 98.761191 711.94857 102.26377 711.94922 105 711.94922 C 107.73623 711.94922 156.24228 711.95001 204.73047 711.26562 C 228.97457 710.92344 253.21138 710.41009 271.75 709.63867 C 281.01931 709.25296 288.8579 708.80451 294.60352 708.27344 C 297.47632 708.0079 299.81921 707.72376 301.61523 707.40039 C 302.51324 707.23871 303.27067 707.07151 303.94531 706.85938 C 304.61996 706.64724 305.2084 706.55281 306.01953 705.70508 C 306.83066 704.85735 306.89408 704.29292 307.0918 703.60547 C 307.28951 702.91802 307.44677 702.13619 307.59961 701.20508 C 307.90529 699.34285 308.17506 696.89835 308.42383 693.90039 C 308.92137 687.90447 309.33027 679.71432 309.66016 670.0293 C 310.31992 650.65926 310.65515 625.33315 310.66992 600.00195 C 310.68472 574.67076 310.37858 549.34304 309.75586 529.97461 C 309.4445 520.2904 309.05597 512.10021 308.58203 506.10742 C 308.34506 503.11103 308.08984 500.6699 307.79883 498.8125 C 307.65332 497.8838 307.50174 497.10586 307.31445 496.42383 C 307.12717 495.7418 307.08097 495.19821 306.29492 494.33789 C 305.50888 493.47757 304.9182 493.36892 304.26172 493.15234 C 303.60524 492.93577 302.8759 492.7654 302.01367 492.60156 C 300.28921 492.27388 298.04949 491.98609 295.30469 491.71094 C 289.81508 491.16063 282.33094 490.67502 273.48438 490.21484 C 255.79127 489.29449 232.66667 488.48402 209.55078 487.54688 C 186.43489 486.60974 163.32537 485.54426 145.74414 484.125 C 136.95353 483.41537 129.53535 482.61374 124.26367 481.71484 C 121.62783 481.2654 119.5206 480.78292 118.13281 480.32617 C 117.33987 480.0652 116.84141 479.69833 116.63477 479.57617 C 116.5799 479.36568 116.51307 479.08883 116.46484 478.7832 C 116.33281 477.94625 116.19434 476.75342 116.06055 475.26758 C 115.79295 472.2959 115.53859 468.14179 115.30469 463.00781 C 114.93817 454.96289 114.65616 442.90145 114.38281 430.66406 C 111.81701 367.27316 111.8204 241.56099 114.04883 173.92188 C 114.31219 159.38466 114.61266 146.17097 114.99023 136.98047 C 115.20123 131.84464 115.42929 127.68866 115.67188 124.71094 C 115.79317 123.22208 115.92033 122.02473 116.04102 121.17969 C 116.08887 120.84462 116.15604 120.54034 116.20898 120.31836 C 116.38248 120.20406 116.78698 119.894 117.45508 119.65234 C 118.67592 119.21077 120.56406 118.73745 122.93555 118.30078 C 127.67852 117.42745 134.38147 116.66617 142.32812 116.03906 C 158.22144 114.78484 179.12946 114.0481 200.0332 113.76562 C 220.93695 113.48314 241.85112 113.65282 257.78125 114.20508 C 265.74631 114.48121 272.472 114.85497 277.2832 115.30664 C 279.68881 115.53248 281.62259 115.78039 282.93164 116.02734 C 283.45251 116.12564 283.77897 116.21676 284.06641 116.29883 C 284.15271 116.64787 284.25061 117.09658 284.35156 117.74023 C 284.58436 119.22454 284.81786 121.37722 285.03711 124.04883 C 285.4756 129.39204 285.86809 136.8329 286.22852 145.64453 C 286.94936 163.26779 287.54486 186.39045 288.16992 209.51758 C 288.79498 232.6447 289.44871 255.7764 290.29102 273.47461 C 290.71217 282.32372 291.18012 289.80734 291.7207 295.29883 C 291.99099 298.04457 292.27505 300.28644 292.60156 302.01172 C 292.76482 302.87436 292.93586 303.60489 293.15234 304.26172 C 293.36883 304.91855 293.47757 305.50888 294.33789 306.29492 C 295.19821 307.08097 295.74181 307.12716 296.42383 307.31445 C 297.10584 307.50174 297.88382 307.6533 298.8125 307.79883 C 300.66986 308.08989 303.11107 308.34483 306.10742 308.58203 C 312.10013 309.05643 320.29051 309.44656 329.97461 309.75977 C 349.34282 310.38618 374.66929 310.69922 400 310.69922 C 425.33071 310.69922 450.65718 310.38618 470.02539 309.75977 C 479.70949 309.44656 487.89987 309.05643 493.89258 308.58203 C 496.88893 308.34483 499.33014 308.08989 501.1875 307.79883 C 502.11618 307.6533 502.89416 307.50174 503.57617 307.31445 C 504.25819 307.12716 504.80179 307.08097 505.66211 306.29492 C 506.52243 305.50888 506.63117 304.91855 506.84766 304.26172 C 507.06414 303.60489 507.23518 302.87436 507.39844 302.01172 C 507.72495 300.28644 508.00901 298.04457 508.2793 295.29883 C 508.81988 289.80734 509.28783 282.32372 509.70898 273.47461 C 510.55129 255.7764 511.20502 232.6447 511.83008 209.51758 C 512.45514 186.39045 513.05064 163.26779 513.77148 145.64453 C 514.13191 136.8329 514.5244 129.39204 514.96289 124.04883 C 515.18214 121.37722 515.41564 119.22454 515.64844 117.74023 C 515.74939 117.09658 515.84734 116.64787 515.93359 116.29883 C 516.22103 116.21673 516.54749 116.12561 517.06836 116.02734 C 518.37741 115.78039 520.31119 115.53248 522.7168 115.30664 C 527.528 114.85497 534.25369 114.48121 542.21875 114.20508 C 558.14888 113.65282 579.06305 113.48313 599.9668 113.76562 C 620.87054 114.04811 641.77857 114.78484 657.67188 116.03906 C 665.61853 116.66617 672.32148 117.42745 677.06445 118.30078 C 679.43594 118.73745 681.32408 119.21077 682.54492 119.65234 C 683.21303 119.894 683.61752 120.20407 683.79102 120.31836 C 683.84396 120.54034 683.91114 120.84462 683.95898 121.17969 C 684.07967 122.02473 684.20684 123.22208 684.32812 124.71094 C 684.57071 127.68866 684.79877 131.84464 685.00977 136.98047 C 685.38734 146.17097 685.68781 159.38466 685.95117 173.92188 C 688.1796 241.56099 688.18299 367.27316 685.61719 430.66406 C 685.34384 442.90145 685.06183 454.96289 684.69531 463.00781 C 684.46141 468.14179 684.20705 472.2959 683.93945 475.26758 C 683.80566 476.75342 683.66719 477.94625 683.53516 478.7832 C 683.48695 479.08881 683.4201 479.36569 683.36523 479.57617 C 683.15861 479.6983 682.66017 480.06514 681.86719 480.32617 C 680.4794 480.78292 678.37217 481.2654 675.73633 481.71484 C 670.46465 482.61374 663.04647 483.41537 654.25586 484.125 C 636.67463 485.54426 613.56511 486.60975 590.44922 487.54688 C 567.33333 488.48401 544.20875 489.29449 526.51562 490.21484 C 517.66908 490.67502 510.18492 491.16063 504.69531 491.71094 C 501.95051 491.98609 499.71079 492.27388 497.98633 492.60156 C 497.1241 492.7654 496.39476 492.93577 495.73828 493.15234 C 495.0818 493.36892 494.49112 493.47757 493.70508 494.33789 C 492.91903 495.19821 492.87283 495.7418 492.68555 496.42383 C 492.49826 497.10586 492.34668 497.8838 492.20117 498.8125 C 491.91016 500.6699 491.65494 503.11103 491.41797 506.10742 C 490.94403 512.10021 490.5555 520.2904 490.24414 529.97461 C 489.62142 549.34304 489.3153 574.67076 489.33008 600.00195 C 489.34488 625.33315 489.68008 650.65926 490.33984 670.0293 C 490.66973 679.71432 491.07863 687.90447 491.57617 693.90039 C 491.82494 696.89835 492.09471 699.34285 492.40039 701.20508 C 492.55323 702.13619 492.71049 702.91802 492.9082 703.60547 C 493.10592 704.29292 493.16934 704.85735 493.98047 705.70508 C 494.7916 706.55281 495.38004 706.64725 496.05469 706.85938 C 496.72933 707.07151 497.48676 707.23871 498.38477 707.40039 C 500.18079 707.72376 502.52368 708.0079 505.39648 708.27344 C 511.1421 708.80451 518.98069 709.25296 528.25 709.63867 C 546.78862 710.41009 571.02543 710.92344 595.26953 711.26562 C 643.75772 711.95002 692.26377 711.94922 695 711.94922 C 697.73623 711.94922 701.23881 711.94857 704.12109 711.88867 C 705.56224 711.85867 706.84322 711.81667 707.84375 711.74414 C 708.34401 711.70784 708.76365 711.66804 709.17773 711.59766 C 709.38478 711.56246 709.58337 711.52716 709.87695 711.42773 C 710.17054 711.32823 710.76363 711.46683 711.44922 710.0957 C 711.79201 709.41012 711.68693 709.38961 711.70703 709.25586 C 711.72713 709.12211 711.73689 709.02291 711.74609 708.91602 C 711.76439 708.70224 711.77716 708.47252 711.78906 708.19727 C 711.81296 707.64676 711.8308 706.92816 711.8457 706.08203 C 711.8756 704.38978 711.89062 702.19639 711.89062 700 C 711.89062 697.80361 711.8756 695.61022 711.8457 693.91797 C 711.8307 693.07184 711.81286 692.35324 711.78906 691.80273 C 711.77716 691.52748 711.76449 691.29776 711.74609 691.08398 C 711.73709 690.97709 711.72713 690.87789 711.70703 690.74414 C 711.68693 690.61039 711.79203 690.58988 711.44922 689.9043 C 710.76363 688.53312 710.17216 688.67177 709.87891 688.57227 C 709.58566 688.47277 709.38622 688.4376 709.17969 688.40234 C 708.76662 688.33184 708.34816 688.29217 707.85156 688.25586 C 706.85836 688.18326 705.59342 688.14131 704.18164 688.11133 C 701.35809 688.05133 697.97266 688.05078 695.47266 688.05078 C 692.97266 688.05078 648.86011 688.05004 604.78711 687.48438 C 582.75061 687.20155 560.72082 686.77773 543.93555 686.14258 C 535.54291 685.825 528.4557 685.45456 523.37305 685.02734 C 520.83172 684.81373 518.78589 684.58325 517.38281 684.35352 C 516.80264 684.25852 516.41806 684.16713 516.0957 684.08594 C 516.0166 683.79442 515.92951 683.46982 515.83594 682.95117 C 515.59858 681.63565 515.35996 679.69754 515.14062 677.28906 C 514.70195 672.47212 514.33011 665.74411 514.03516 657.77539 C 513.44524 641.83795 513.15517 620.91754 513.16992 600.00195 C 513.18462 579.08637 513.50423 558.16597 514.13086 542.23047 C 514.44418 534.26272 514.83588 527.53585 515.29688 522.72266 C 515.52738 520.31606 515.77918 518.38022 516.02734 517.07031 C 516.12614 516.54876 516.21652 516.22135 516.29883 515.93359 C 516.64818 515.84709 517.09805 515.74801 517.74219 515.64648 C 519.2273 515.41242 521.38019 515.17727 524.05273 514.95312 C 529.39782 514.50483 536.84208 514.09289 545.65625 513.69336 C 563.2846 512.89431 586.41242 512.14117 609.55078 511.20312 C 632.68914 510.26508 655.83219 509.14256 673.57227 507.5918 C 682.4423 506.81642 689.95207 505.93978 695.51367 504.90039 C 698.29447 504.38069 700.58183 503.82794 702.39648 503.17188 C 704.21114 502.51581 705.76437 502.02499 706.70508 500.05469 C 707.17543 499.06954 707.22038 498.40362 707.37891 497.29883 C 707.53743 496.19404 707.68118 494.80131 707.82031 493.10156 C 708.09858 489.70206 708.35125 485.08692 708.58594 479.40234 C 709.05532 468.0332 709.44491 452.39453 709.75391 433.89258 C 710.37191 396.88867 710.67017 348.44444 710.64062 299.99805 C 710.61113 251.55165 710.2544 203.10716 709.5625 166.10156 C 709.21655 147.59876 708.78731 131.95844 708.27148 120.58594 C 708.01357 114.89969 707.73499 110.28586 707.42969 106.88086 C 707.27704 105.17836 707.11805 103.78011 706.94336 102.66797 C 706.76867 101.55582 706.72082 100.87462 706.19336 99.865234 C 705.13844 97.846472 703.53369 97.44936 701.58008 96.806641 C 699.62646 96.163921 697.13955 95.619543 694.10742 95.111328 C 688.04317 94.094899 679.83014 93.253107 670.125 92.552734 C 650.71471 91.151988 625.37517 90.326834 600.0332 89.984375 C 574.69124 89.641916 549.35769 89.783367 529.98438 90.324219 C 520.29773 90.594645 512.10787 90.964009 506.11328 91.427734 C 503.11599 91.659597 500.67262 91.913357 498.81445 92.203125 C 497.88537 92.348009 497.10619 92.498373 496.42383 92.685547 C 495.74146 92.87272 495.19821 92.919034 494.33789 93.705078 C 493.47757 94.491122 493.36883 95.08145 493.15234 95.738281 C 492.93586 96.395112 492.76482 97.125644 492.60156 97.988281 C 492.27505 99.713557 491.99099 101.95543 491.7207 104.70117 C 491.18012 110.19266 490.71217 117.67628 490.29102 126.52539 C 489.44871 144.2236 488.79498 167.3553 488.16992 190.48242 C 487.54486 213.60955 486.94936 236.73221 486.22852 254.35547 C 485.86809 263.1671 485.4756 270.60796 485.03711 275.95117 C 484.81786 278.62278 484.58436 280.77546 484.35156 282.25977 C 484.25061 282.90342 484.15266 283.35213 484.06641 283.70117 C 483.77867 283.78347 483.45122 283.87387 482.92969 283.97266 C 481.61982 284.22076 479.68389 284.47287 477.27734 284.70312 C 472.46425 285.16366 465.73715 285.55376 457.76953 285.86523 C 441.8343 286.48818 420.91499 286.80078 400 286.80078 C 379.08501 286.80078 358.1657 286.48818 342.23047 285.86523 C 334.26285 285.55376 327.53575 285.16366 322.72266 284.70312 C 320.31611 284.47287 318.38018 284.22076 317.07031 283.97266 C 316.54878 283.87386 316.22133 283.78347 315.93359 283.70117 C 315.84729 283.35213 315.74939 282.90342 315.64844 282.25977 C 315.41564 280.77546 315.18214 278.62278 314.96289 275.95117 C 314.5244 270.60796 314.13191 263.1671 313.77148 254.35547 C 313.05064 236.73221 312.45514 213.60955 311.83008 190.48242 C 311.20502 167.3553 310.55129 144.2236 309.70898 126.52539 C 309.28783 117.67628 308.81988 110.19266 308.2793 104.70117 C 308.00901 101.95543 307.72495 99.713557 307.39844 97.988281 C 307.23518 97.125644 307.06414 96.395112 306.84766 95.738281 C 306.63117 95.08145 306.52243 94.491122 305.66211 93.705078 C 304.80179 92.919034 304.25854 92.87272 303.57617 92.685547 C 302.89381 92.498373 302.11463 92.348009 301.18555 92.203125 C 299.32738 91.913357 296.88401 91.659597 293.88672 91.427734 C 287.89213 90.964009 279.70229 90.594645 270.01562 90.324219 C 260.32898 90.053793 249.15122 89.884343 237.22852 89.824219 z M 95.96875 147.48047 C 92.795425 212.48563 92.806701 378.38905 95.611328 448.07031 C 95.498822 442.87459 95.343065 439.61492 95.246094 433.80859 C 94.628861 396.85074 94.329853 348.42582 94.359375 300.00195 C 94.388895 251.57808 94.746523 203.15147 95.4375 166.19531 C 95.579972 158.57537 95.797801 154.02401 95.96875 147.48047 z M 704.03125 147.48047 C 704.2022 154.02401 704.42003 158.57537 704.5625 166.19531 C 705.25348 203.15147 705.6111 251.57808 705.64062 300.00195 C 705.67012 348.42582 705.37114 396.85074 704.75391 433.80859 C 704.65693 439.61492 704.50118 442.87459 704.38867 448.07031 C 707.1933 378.38905 707.20458 212.48563 704.03125 147.48047 z M 323.76367 304.44922 C 360.24537 307.13718 439.75463 307.13718 476.23633 304.44922 C 473.87002 304.54468 472.46282 304.6796 469.86328 304.76367 C 450.58223 305.38726 425.28955 305.69922 400 305.69922 C 374.71045 305.69922 349.41777 305.38726 330.13672 304.76367 C 327.53718 304.6796 326.12998 304.54468 323.76367 304.44922 z M 304.41797 523.19141 C 304.52261 525.74502 304.6687 527.30239 304.75977 530.13477 C 305.37967 549.41559 305.68467 574.70898 305.66992 599.99805 C 305.65522 625.28712 305.32074 650.58016 304.66406 669.85938 C 304.55103 673.17803 304.37449 675.06296 304.24219 677.99414 C 307.16881 642.74268 307.16449 559.65308 304.41797 523.19141 z M 495.58203 523.19141 C 492.83551 559.65308 492.83119 642.74268 495.75781 677.99414 C 495.62551 675.06296 495.44897 673.17803 495.33594 669.85938 C 494.67926 650.58016 494.34483 625.28712 494.33008 599.99805 C 494.31538 574.70898 494.62033 549.41559 495.24023 530.13477 C 495.3313 527.30239 495.47739 525.74502 495.58203 523.19141 z " />
+ </g>
+</svg>
diff --git a/assets/static/images/mastodon.svg b/assets/static/images/mastodon.svg
new file mode 100644
index 0000000..f6b8203
--- /dev/null
+++ b/assets/static/images/mastodon.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
+ <defs>
+ <clipPath id="a" clipPathUnits="userSpaceOnUse">
+ <path fill="#fff" d="M-5-5h650v650H-5Zm324.4 45q-57.1.6-102 6.8-49.7 6.7-78.6 20.2l-8.6 5q-9.8 6-19.6 15.3-13.6 12.9-23.4 28.9-12.9 20.2-19.6 44.8-8.6 28.9-8.6 63.3V247q0 47.3.6 73 1.8 69.5 11 115.6 11.7 58.3 38.1 94.5 30.7 41.2 82.3 54.7 67 17.8 129 14.7 28.3-1.8 54-7.3 18.5-4.3 33.8-10.5l11.7-4.9-2.5-46-13.5 3.6q-17.2 3.7-35 6.2-25.2 3.7-47.9 2.4-32.5-.6-49.1-4.3-26.4-4.9-41.2-18.4-17.8-15.3-21.5-44.2-1.2-8.6-1.2-17.8L223 462q19 3.7 39.9 6.1 29.5 4.3 57.1 5.5 25.8 1.3 56.5-1.2 19.7-1.2 54-5.5 34.5-4.3 65.8-18.4 30-14.2 50-33.5t23.7-39q5.5-27.7 8-67 2.4-28.2 3-59.6v-25.1q0-34.4-8.6-63.3-6.7-24.6-19.6-44.8-9.8-16-23.4-28.9-9.2-9.2-19.6-15.4-7.4-4.9-8.6-4.9-28.3-13.5-78-20.2-45.5-6.2-102-6.8zm-81 95.2q-38.8 0-62.1 26.4t-23.4 71.9v146.8h58.4V237.8q0-45.5 37.5-45.5 42.3 0 42.3 54v78H349v-78q0-54 42.3-54 37.5 0 37.5 45.5v142.5h58.4V233.5q0-45.5-23.4-71.9t-62-26.4q-44.8 0-67 34.4l-14.7 24-14.7-24q-22.2-34.4-67-34.4" class="powerclip"/>
+ </clipPath>
+ </defs>
+ <path d="M91.4 0C41 0 0 41 0 91.4v457.2C0 599 41 640 91.4 640h457.2c50.4 0 91.4-41 91.4-91.4V91.4C640 41 599 0 548.6 0Z" clip-path="url(#a)"/>
+</svg>
diff --git a/assets/static/images/opengraph.png b/assets/static/images/opengraph.png
new file mode 100644
index 0000000..c44994f
--- /dev/null
+++ b/assets/static/images/opengraph.png
Binary files differ
diff --git a/assets/static/images/rss.svg b/assets/static/images/rss.svg
new file mode 100644
index 0000000..0a6bed9
--- /dev/null
+++ b/assets/static/images/rss.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
+<!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
+ <path d="M91.4 0C41 0 0 41 0 91.4v457.2C0 599 41 640 91.4 640h457.2c50.4 0 91.4-41 91.4-91.4V91.4C640 41 599 0 548.6 0Zm45.7 148.6c0-19 15.3-34.3 34.3-34.3a354 354 0 0 1 354.3 354.3 34.2 34.2 0 1 1-68.6 0 285.6 285.6 0 0 0-285.7-285.7c-19 0-34.3-15.3-34.3-34.3m0 137.1c0-19 15.3-34.3 34.3-34.3a217 217 0 0 1 217.2 217.2 34.2 34.2 0 1 1-68.6 0c0-82-66.6-148.6-148.6-148.6-19 0-34.3-15.3-34.3-34.3m0 171.4a45.7 45.7 0 1 1 91.4 0 45.7 45.7 0 0 1-91.4 0"/>
+</svg>
diff --git a/assets/static/images/xmpp.svg b/assets/static/images/xmpp.svg
new file mode 100644
index 0000000..9d33519
--- /dev/null
+++ b/assets/static/images/xmpp.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
+ <defs>
+ <clipPath id="a" clipPathUnits="userSpaceOnUse">
+ <path fill="#fff" d="M-5-5h650v650H-5Zm428.5 526.2c36.8 7.8 54.7 3.4 24.7-6.2a288 288 0 0 1-83-43.6l-8.4-6.4 26.8-17.7c92.7-61.1 166.8-150.5 199-240.3 8-22 17.4-66 17.4-81v-10.4L557.5 132c-23.4 9-61.6 22-85 29L430 173.2v20.2c0 69.5-40.4 165.3-95.4 226.1l-11.8 13-12-13c-27.3-30-58.4-83.7-74-127.7a281 281 0 0 1-18.1-86.6l-1.8-28.6-44.5-13.7c-39-12-65.2-21.5-118.5-42.6L40 114.8l1.6 15.1c9.6 87.8 54 171 132.6 248.5a489 489 0 0 0 86.7 70.7l25.7 16.9-12.2 8.6a345 345 0 0 1-71.3 36.3c-22.2 7.5-25.4 13.5-5.8 10.9a513 513 0 0 0 97.6-27.4l25.7-10.5 19.7 9a462 462 0 0 0 83.2 28.3" class="powerclip"/>
+ </clipPath>
+ </defs>
+ <path d="M91.4 0C41 0 0 41 0 91.4v457.2C0 599 41 640 91.4 640h457.2c50.4 0 91.4-41 91.4-91.4V91.4C640 41 599 0 548.6 0Z" clip-path="url(#a)"/>
+</svg>
diff --git a/configs/atom.ini b/configs/atom.ini
new file mode 100644
index 0000000..84b78ec
--- /dev/null
+++ b/configs/atom.ini
@@ -0,0 +1,18 @@
+[latest]
+name = Still Thinking About It — Latest posts
+source_path = /blog
+url_path = /blog/latest.xml
+item_model = blog-post
+item_date_field = publication_date
+blog_author_field = author
+item_author_field = author
+
+[all]
+name = Still Thinking About It — All posts
+source_path = /blog
+url_path = /blog/all.xml
+item_model = blog-post
+item_date_field = publication_date
+blog_author_field = author
+item_author_field = author
+limit = 0
diff --git a/configs/index-pages.ini b/configs/index-pages.ini
new file mode 100644
index 0000000..bc428ce
--- /dev/null
+++ b/configs/index-pages.ini
@@ -0,0 +1,23 @@
+[year]
+parent_path = /blog
+key = '{.year:04d}'.format(item.publication_date)
+template = blog-year-index.html
+subindex = month
+
+[year.fields]
+date = this.children.first().publication_date.replace(month=1, day=1)
+year = this.key|int
+previous_year_page = site.get('/blog@index-pages/year/{:04d}'.format((this.key|int) - 1))
+next_year_page = site.get('/blog@index-pages/year/{:04d}'.format((this.key|int) + 1))
+first_year_page = index_pages('year')|sort(attribute='year')|first
+last_year_page = index_pages('year')|sort(attribute='year')|last
+
+[year.month]
+key = '{.month:02d}'.format(item.publication_date)
+template = blog-month-index.html
+
+[year.month.fields]
+# this.parent is the year-index page this month-index belongs to.
+date = this.parent.date.replace(month=this.key|int)
+year = this.parent.year
+month = this.key|int
diff --git a/configs/tags.ini b/configs/tags.ini
new file mode 100644
index 0000000..3ecc49a
--- /dev/null
+++ b/configs/tags.ini
@@ -0,0 +1,4 @@
+parent = /blog
+items = this.parent.children.filter(F.tags.contains(tag)).order_by('-publication_date')
+template = tag.html
+url_path = {{ this.parent.url_path }}tag/{{ tag|trim|lower|replace(" ", "-") }}
diff --git a/content/about/contents.lr b/content/about/contents.lr
new file mode 100644
index 0000000..9a65a65
--- /dev/null
+++ b/content/about/contents.lr
@@ -0,0 +1,29 @@
+_model: page
+---
+_template: about.html
+---
+title: About
+---
+body:
+
+<p class="center">
+Hi, I'm Dennis. Welcome!
+</p>
+
+![](me.jpg "class='center w33 round'")
+
+I am originally from Luxembourg and grew up in the south of the country. During
+my formative years, I spent a lot of time in a local scouts group, which was a
+fantastic way to develop a deep connection to nature and a desire to learn
+through exploration and experimentation. I later got involved in the local
+hacker and maker movement, where the development and exploration of technology
+often went hand in hand with discussions around privacy, security and the role
+of technology in society. I'm a trained educator and now I get to do what I
+love most: sharing my passion for science with others through education and
+communication.
+
+This blog is my place to capture my thoughts before they disappear. It'll most
+likely include little things from my day-to-day life, things I've been reading
+about and recurring ideas, as well as the occasional rabbit hole I've fallen
+down. There won't be a theme or a schedule, but it will be a space where I can
+collect all the things that I find interesting and share them with you.
diff --git a/content/about/me.jpg b/content/about/me.jpg
new file mode 100644
index 0000000..300ffd5
--- /dev/null
+++ b/content/about/me.jpg
Binary files differ
diff --git a/content/blog/back-to-blogging/contents.lr b/content/blog/back-to-blogging/contents.lr
new file mode 100644
index 0000000..a68236a
--- /dev/null
+++ b/content/blog/back-to-blogging/contents.lr
@@ -0,0 +1,43 @@
+title: Back to Blogging
+---
+publication_date: 2026-06-17
+---
+render_toc: no
+---
+body:
+
+Welcome back to my blog! Yes, it's time to get back to basics again. But why
+now? There are several reasons for this, which I may discuss in more detail in
+future posts. For now, let me recap on two things.
+
+Firstly, the recent rise of the ["Digital Independence Day"](https://di.day/en)
+events in Germany, has inspired me to become even more digitally independent.
+Since joining the [local Chaos Computer Club](https://c3l.lu) over 15 years
+ago, I have been on a mission to leave big tech behind. This has led me to
+self-host most of my digital tools and start blogging again.
+
+The main reason, though, is that the web is fundamentally about sharing
+information, not just consuming it. At a time when content generated by LLMs
+seems to dominate the web, it's more important than ever to have content
+written by humans. This is my attempt to contribute to this. I may blog about
+my views on AI in the future.
+
+What can you find here? As I write on the ["About" page](/about/):
+
+> This blog is my place to capture my thoughts before they disappear. It'll most
+> likely include little things from my day-to-day life, things I've been reading
+> about and recurring ideas, as well as the occasional rabbit hole I've fallen
+> down. There won't be a theme or a schedule, but it will be a space where I can
+> collect all the things that I find interesting and share them with you.
+
+Thank you for reading my blog, and I hope to see you again soon! You might want to subscribe to the [Atom feed](/blog/latest.xml)!
+---
+tags:
+
+blogging
+personal web
+digital independence
+human-written web
+atom feed
+---
+category: site-notes
diff --git a/content/blog/contents.lr b/content/blog/contents.lr
new file mode 100644
index 0000000..8aa3c00
--- /dev/null
+++ b/content/blog/contents.lr
@@ -0,0 +1,3 @@
+_model: blog
+---
+author: Dennis Fink
diff --git a/content/categories/contents.lr b/content/categories/contents.lr
new file mode 100644
index 0000000..01afcc8
--- /dev/null
+++ b/content/categories/contents.lr
@@ -0,0 +1 @@
+_model: categories
diff --git a/content/categories/site-notes/contents.lr b/content/categories/site-notes/contents.lr
new file mode 100644
index 0000000..75971e1
--- /dev/null
+++ b/content/categories/site-notes/contents.lr
@@ -0,0 +1 @@
+name: Site Notes
diff --git a/content/contents.lr b/content/contents.lr
new file mode 100644
index 0000000..cd8811e
--- /dev/null
+++ b/content/contents.lr
@@ -0,0 +1,3 @@
+_model: index
+---
+title: Index
diff --git a/databags/blog.ini b/databags/blog.ini
new file mode 100644
index 0000000..fdf0986
--- /dev/null
+++ b/databags/blog.ini
@@ -0,0 +1,2 @@
+name = Still Thinking About It
+tagline = Personal notes on things that linger.
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..e40389a
--- /dev/null
+++ b/justfile
@@ -0,0 +1,14 @@
+set shell := ["bash", "-uc"]
+
+serve:
+ lektor serve -h 0.0.0.0
+
+clean:
+ lektor clean --yes
+
+build:
+ lektor build
+
+deploy: build
+ rsync --archive --compress --verbose build/ draconis:/home/dennis/blog/
+ ssh -t draconis -- /home/dennis/bin/,deploy_blog
diff --git a/models/blog-post.ini b/models/blog-post.ini
new file mode 100644
index 0000000..4f36c1e
--- /dev/null
+++ b/models/blog-post.ini
@@ -0,0 +1,39 @@
+[model]
+name = Blog Post
+label = {{ this.title }}
+
+[fields.title]
+label = Title
+type = string
+size = large
+
+[fields.publication_date]
+label = Publication date
+type = date
+width = 1/2
+
+[fields.updated_date]
+label = Updated date
+type = date
+width = 1/2
+
+[fields.category]
+label = Category
+type = select
+source = site.query('/categories')
+width = 1/2
+
+[fields.render_toc]
+label = Render TOC
+type = boolean
+checkbox_label = If true, then a TOC will be rendered in the webpage.
+default = false
+width = 1/2
+
+[fields.body]
+label = Body
+type = markdown
+
+[fields.tags]
+label = Tags
+type = strings
diff --git a/models/blog.ini b/models/blog.ini
new file mode 100644
index 0000000..0d612ff
--- /dev/null
+++ b/models/blog.ini
@@ -0,0 +1,17 @@
+[model]
+name = Blog
+label = Blog
+hidden = yes
+
+[fields.author]
+label = Author
+type = string
+
+[children]
+model = blog-post
+order_by = -publication_date, title
+slug_format = {{ (this.publication_date|dateformat('YYYY/MM/') if this.publication_date) ~ this._id }}
+
+[pagination]
+enabled = yes
+per_page = 5
diff --git a/models/categories.ini b/models/categories.ini
new file mode 100644
index 0000000..2b4c222
--- /dev/null
+++ b/models/categories.ini
@@ -0,0 +1,9 @@
+[model]
+name = Categories
+label = Categories
+hidden = yes
+protected = yes
+
+[children]
+model = category
+order_by = name
diff --git a/models/category.ini b/models/category.ini
new file mode 100644
index 0000000..3a6e10d
--- /dev/null
+++ b/models/category.ini
@@ -0,0 +1,11 @@
+[model]
+name = Category
+label = {{ this.name }}
+hidden = yes
+
+[children]
+replaced_with = site.query('/blog').filter(F.category.contains(this))
+
+[fields.name]
+label = Name
+type = string
diff --git a/models/index.ini b/models/index.ini
new file mode 100644
index 0000000..475c4a1
--- /dev/null
+++ b/models/index.ini
@@ -0,0 +1,8 @@
+[model]
+name = Index
+label = index
+
+[pagination]
+enabled = yes
+per_page = 5
+items = site.query('/blog')
diff --git a/models/page.ini b/models/page.ini
new file mode 100644
index 0000000..dcddfe8
--- /dev/null
+++ b/models/page.ini
@@ -0,0 +1,11 @@
+[model]
+name = Page
+label = {{ this.title }}
+
+[fields.title]
+label = Title
+type = string
+
+[fields.body]
+label = Body
+type = markdown
diff --git a/packages/blog-markdown-headings/lektor_blog_markdown_headings.py b/packages/blog-markdown-headings/lektor_blog_markdown_headings.py
new file mode 100644
index 0000000..5293676
--- /dev/null
+++ b/packages/blog-markdown-headings/lektor_blog_markdown_headings.py
@@ -0,0 +1,152 @@
+import uuid
+from collections import namedtuple
+from html.parser import HTMLParser
+
+import mistune
+from lektor.pluginsystem import Plugin
+from lektor.utils import slugify
+from markupsafe import Markup, escape
+
+TocEntry = namedtuple("TocEntry", ["anchor", "title", "children"])
+
+
+def get_int_config(config, key, default, minimum=None, maximum=None):
+ value = config.get(key, str(default))
+
+ try:
+ value = int(value)
+ except (TypeError, ValueError):
+ value = default
+
+ if minimum is not None:
+ value = max(value, minimum)
+
+ if maximum is not None:
+ value = min(value, maximum)
+
+ return value
+
+
+class BlogMarkdownHeadingsPlugin(Plugin):
+ name = "Blog Markdown Headings"
+ description = (
+ "Adds Markdown heading anchors, TOC metadata, and configurable heading offsets."
+ )
+
+ def on_markdown_config(self, config, **extra):
+ plugin_config = self.get_config()
+
+ anchor_type = plugin_config.get("anchors.type", "slug")
+
+ heading_offset = get_int_config(
+ plugin_config,
+ "headings.offset",
+ default=2,
+ minimum=-5,
+ maximum=5,
+ )
+
+ max_heading_level = get_int_config(
+ plugin_config,
+ "headings.max_level",
+ default=6,
+ minimum=1,
+ maximum=6,
+ )
+
+ min_heading_level = get_int_config(
+ plugin_config,
+ "headings.min_level",
+ default=1,
+ minimum=1,
+ maximum=6,
+ )
+
+ def adjusted_level(level):
+ return min(
+ max(level + heading_offset, min_heading_level),
+ max_heading_level,
+ )
+
+ config.renderer_mixins.append(
+ renderer_mixin_factory(
+ anchor_type=anchor_type,
+ adjusted_level=adjusted_level,
+ )
+ )
+
+ def on_markdown_meta_init(self, meta, **extra):
+ meta["toc"] = []
+
+ def on_markdown_meta_postprocess(self, meta, **extra):
+ prev_level = None
+ toc = []
+ stack = [toc]
+
+ for level, anchor, title in meta["toc"]:
+ if prev_level is None:
+ prev_level = level
+ elif prev_level == level - 1:
+ stack.append(stack[-1][-1].children)
+ prev_level = level
+ elif prev_level > level:
+ while prev_level > level:
+ if len(stack) > 1:
+ stack.pop()
+ prev_level -= 1
+
+ stack[-1].append(TocEntry(anchor, title, []))
+
+ meta["toc"] = toc
+
+
+def renderer_mixin_factory(anchor_type, adjusted_level):
+ def render_heading(text, original_level, meta, raw=None):
+ level = adjusted_level(original_level)
+
+ if anchor_type == "random":
+ anchor = uuid.uuid4().hex[:6]
+ else:
+ if raw is None:
+ raw = escape(_strip_tags(text))
+ anchor = slugify(raw)
+
+ meta["toc"].append((level, anchor, Markup(text)))
+
+ return '<h%d id="%s">%s</h%d>' % (level, anchor, text, level)
+
+ class RendererMixinMistune0(object):
+ def header(self, text, level, raw):
+ return render_heading(text, level, self.meta, raw)
+
+ class RendererMixinMistune2(object):
+ def heading(self, text, level):
+ meta = self.lektor.meta
+ return render_heading(text, level, meta)
+
+ if mistune.__version__.startswith("0."):
+ return RendererMixinMistune0
+
+ if mistune.__version__.startswith("2."):
+ return RendererMixinMistune2
+
+ raise RuntimeError("Unsupported mistune version: %s" % mistune.__version__)
+
+
+def _strip_tags(inline_html):
+ parser = _StripTagsParser()
+ parser.feed(inline_html)
+ return parser.close()
+
+
+class _StripTagsParser(HTMLParser):
+ def reset(self):
+ super().reset()
+ self._data = []
+
+ def handle_data(self, data):
+ self._data.append(data)
+
+ def close(self):
+ super().close()
+ return "".join(self._data)
diff --git a/packages/blog-markdown-headings/setup.py b/packages/blog-markdown-headings/setup.py
new file mode 100644
index 0000000..59f9895
--- /dev/null
+++ b/packages/blog-markdown-headings/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup
+
+setup(
+ name="lektor-blog-markdown-headings",
+ version="0.1",
+ py_modules=["lektor_blog_markdown_headings"],
+ entry_points={
+ "lektor.plugins": [
+ "blog-markdown-headings = lektor_blog_markdown_headings:BlogMarkdownHeadingsPlugin",
+ ]
+ },
+ install_requires=[],
+)
diff --git a/templates/.djlintrc b/templates/.djlintrc
new file mode 100644
index 0000000..5882faa
--- /dev/null
+++ b/templates/.djlintrc
@@ -0,0 +1,14 @@
+{
+"blank_line_before_tag": "macro",
+"blank_line_after_tag": "endmacro,extends,import,from",
+"profile": "jinja",
+"format_css": true,
+"css": {
+"indent_size": 2
+},
+"format_js": true,
+"js": {
+"indent_size": 2
+},
+"indent": "2"
+}
diff --git a/templates/about.html b/templates/about.html
new file mode 100644
index 0000000..55a0778
--- /dev/null
+++ b/templates/about.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="profile">
+ <meta property="profile:first_name" content="Dennis">
+ <meta property="profile:last_name" content="Fink">
+{% endblock %}
+{% block content %}
+ <article>{{ this.body }}</article>
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..1000014
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<html lang="en"
+ prefix="og: https://ogp.me/ns# article: https://ogp.me/ns/article# profile: https://ogp.me/ns/profile#">
+ <head>
+ {% block head %}
+ {% block meta %}
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="color-scheme" content="dark light">
+ <meta name="generator" content="lektor">
+ <meta name="description"
+ property="og:description"
+ content="{{ bag('blog.tagline') }}">
+ <meta property="og:site_name" content="dennisfink.me">
+ <meta property="og:title"
+ content="{%- if (this.title or title) -%}{{ (this.title or title) | safe }} — {% endif -%}{{ bag('blog.name') }}">
+ <meta property="og:url"
+ content="{{ this | url(absolute=True, external=True) }}">
+ <meta property="og:image"
+ content="{{ '/static/images/opengraph.png' | url(absolute=True, external=True) }}">
+ <meta property="og:image:type" content="image/png">
+ <meta property="og:image:width" content="32">
+ <meta property="og:image:height" content="32">
+ <meta property="og:image:alt"
+ content="Logo of the site showing a second-order Hilbert curve segment drawn as a continuous right-angled line inside a rounded square, with both colored in muted green">
+ <meta name="fediverse:creator" content="@me@dennisfink.me">
+ {% endblock meta %}
+ {% block styles %}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/modern_normalize.css' | asseturl }}">
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/libre_baskerville.css' | asseturl }}">
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/style.css' | asseturl }}">
+ {% endblock styles %}
+ {% block meta_links %}
+ <link rel="canonical" href="{{ this|url(absolute=True, external=True) }}">
+ <link rel="alternate"
+ type="application/atom+xml"
+ title="{{ bag('blog.name') }} — Latest posts"
+ href="{{ '/blog@atom/latest'|url(absolute=True) }}">
+ <link rel="alternate"
+ type="application/atom+xml"
+ title="{{ bag('blog.name') }} — All posts"
+ href="{{ '/blog@atom/all'|url(absolute=True) }}">
+ <link rel="shortcut icon"
+ type="image/png"
+ href="{{ '/static/images/favicon.png' | asseturl }}">
+ {% endblock meta_links %}
+ <title>
+ {% block meta_title %}
+ {%- if (this.title or title) -%}{{ (this.title or title) | safe }} —
+ {% endif -%}
+ {{ bag("blog.name") }} — dennisfink.me
+ {% endblock meta_title %}
+ </title>
+ {% endblock head %}
+ </head>
+ <body>
+ {% block body %}
+ <a href="#main" class="skip-link">Skip to main content</a>
+ <header>
+ {% block site_header %}
+ <div>
+ <hgroup>
+ <h1>
+ <a href="/">{{ bag("blog.name") }}</a>
+ </h1>
+ <p>{{ bag("blog.tagline") }}</p>
+ </hgroup>
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
+ <path d="M89.4 0C40.1 0 0 40 0 89.4v621.2C0 759.9 40 800 89.4 800h621.2c49.3 0 89.4-40 89.4-89.4V89.4C800 40.1 760 0 710.6 0zm0 18.9h621.2A70 70 0 0 1 781 89.4v621.2a70 70 0 0 1-70.5 70.5H89.4A70 70 0 0 1 19 710.6V89.4A70 70 0 0 1 89.4 19m147.8 71c-11.9-.1-24.6 0-37.2 0-25.4.4-50.7 1.3-70.1 2.7-9.7.7-18 1.5-24 2.5q-4.5.7-7.5 1.7c-2 .6-3.5 1-4.6 3q-.6 1.4-.7 2.9-.3 1.6-.5 4.2l-.9 13.7c-.5 11.4-1 27-1.3 45.5a8909 8909 0 0 0-.2 267.8 1985 1985 0 0 0 2 59.2l.4 4.2q.1 1.5.7 2.8c1 2 2.5 2.4 4.3 3q2.7 1 6.9 1.8 8.4 1.6 22 2.7a1491 1491 0 0 0 64 3.6c23 1 46.2 1.7 63.8 2.5a614 614 0 0 1 28 2l1.4.2.3 1.2.7 5.6c.5 4.8.9 11.6 1.2 19.5a1632 1632 0 0 1 .1 115.5 390 390 0 0 1-1.8 25.3l-.3 1q-.4.3-1.3.4l-6 .6q-7.7.7-20.5 1.1c-16.8.7-38.9 1.1-60.9 1.4a7900 7900 0 0 1-103 .8l-2 .3c-.4 0-1 0-1.6 1.3q-.5.9-.3.8v1.1L88 700l.2 9.3.3.8c.6 1.4 1.2 1.2 1.5 1.3l.7.2 1.4.1 3.7.2h9.1c2.7 0 51.2 0 99.7-.6 24.3-.4 48.5-.9 67-1.7a485 485 0 0 0 30-2.2l2.2-.5c.7-.3 1.3-.3 2.1-1.2q1-1.1 1-2 .4-1.2.6-2.5.5-2.8.8-7.3.8-9.2 1.3-23.9a2277 2277 0 0 0 .1-140 547 547 0 0 0-2-31.2l-.5-2.4c-.2-.7-.2-1.2-1-2-.8-1-1.4-1-2-1.2q-1-.4-2.3-.6l-6.7-.9q-8.4-.7-21.8-1.5c-17.7-1-40.8-1.7-64-2.7-23-.9-46.2-2-63.8-3.4-8.7-.7-16.2-1.5-21.4-2.4q-4-.7-6.2-1.4-1.2-.4-1.5-.7l-.1-.8-.4-3.5q-.5-4.6-.8-12.3c-.4-8-.6-20.1-1-32.3a4578 4578 0 0 1 1.4-306l.5-4.4 1.3-.6q1.8-.8 5.4-1.4 7.4-1.3 19.4-2.3a1269 1269 0 0 1 115.5-1.8 355 355 0 0 1 25.1 1.8l1.2.3.3 1.4.6 6.3q.8 8.2 1.2 21.6c.7 17.7 1.3 40.8 2 64 .6 23 1.2 46.2 2 63.9a465 465 0 0 0 2.4 28.5q.2 1.3.6 2.3.1.9 1.1 2c1 1.1 1.4.8 2.1 1l2.4.5 7.3.8q9.2.7 23.9 1.2a2305 2305 0 0 0 163.9-1.2 101 101 0 0 0 9.7-1.3c.7-.2 1.2-.2 2-1 1-.8 1-1.4 1.2-2q.4-1 .6-2.3l.9-6.7q.8-8.4 1.4-21.8c.9-17.7 1.5-40.9 2.1-64s1.3-46.2 2-63.9a571 571 0 0 1 1.8-27.9l.3-1.4 1.2-.3 5.6-.7a355 355 0 0 1 19.5-1 1269 1269 0 0 1 115.5 1.7q12.1 1 19.4 2.3 3.5.7 5.4 1.4l1.3.6.2.9.3 3.5q.4 4.5.7 12.3a4578 4578 0 0 1-1 338.3l-.6 4.3q-.3.3-1.5.7-2.1.8-6.2 1.4c-5.2 1-12.7 1.7-21.4 2.4-17.6 1.4-40.7 2.5-63.9 3.4-23 1-46.2 1.8-63.9 2.7a493 493 0 0 0-28.5 2.4q-1.3.2-2.3.6c-.6.2-1.2.3-2 1.1s-.8 1.4-1 2.1l-.5 2.4-.8 7.3q-.7 9.2-1.2 23.9a2277 2277 0 0 0 1.4 163.9 97 97 0 0 0 1.3 9.7q.1 1 1 2.1c.9.9 1.5 1 2.2 1.2l2.3.5 7 .9q8.8.8 22.9 1.3c18.5.8 42.7 1.3 67 1.7 48.5.7 97 .6 99.7.6l14.2-.3.7-.2c.3 0 .9 0 1.5-1.3q.5-.9.3-.8v-1.1l.2-8.2-.2-9.3-.3-.8c-.6-1.4-1.2-1.2-1.5-1.3l-.7-.2-1.3-.1-3.7-.2h-8.7c-2.5 0-46.6 0-90.7-.6-22-.3-44-.7-60.9-1.4a483 483 0 0 1-26.5-1.7l-1.3-.3-.3-1.1-.7-5.7a390 390 0 0 1-1-19.5 1632 1632 0 0 1 1.2-135l1-6.9 1.4-.3 6.4-.6 21.6-1.3c17.6-.8 40.7-1.6 63.9-2.5 23-1 46.2-2 64-3.6q13.4-1.2 22-2.7 4-.7 6.8-1.7c1.8-.7 3.4-1.2 4.3-3.1q.6-1.3.7-2.8l.4-4.2q.5-5.1.8-13.7c.5-11.4.8-27 1.2-45.5a8229 8229 0 0 0-1.5-313.3 331 331 0 0 0-1.4-18q0-1.3-.7-2.7c-1-2-2.7-2.5-4.6-3q-3-1-7.5-1.8c-6-1-14.3-1.8-24-2.5a1786 1786 0 0 0-140-2.2 517 517 0 0 0-31.2 1.8l-2.4.5c-.7.2-1.2.2-2 1-1 .8-1 1.4-1.2 2q-.4 1-.6 2.3l-.9 6.7q-.8 8.4-1.4 21.8c-.9 17.7-1.5 40.9-2.1 64s-1.3 46.2-2 63.9a571 571 0 0 1-1.8 27.9l-.3 1.4-1.2.3-5.6.7c-4.8.5-11.6.9-19.5 1.2a1575 1575 0 0 1-135-1.2l-6.9-1-.3-1.4-.6-6.3q-.7-8.2-1.2-21.6c-.7-17.7-1.3-40.8-2-64-.6-23-1.2-46.2-2-63.9a465 465 0 0 0-2.4-28.5q-.2-1.3-.6-2.3-.1-.8-1.1-2c-1-1.2-1.4-.8-2.1-1l-2.4-.5-7.3-.8q-9.2-.7-23.9-1t-32.8-.6M96 147.4c-3.2 65-3.2 230.9-.4 300.6l-.4-14.3a8224 8224 0 0 1 .2-267.6zm608 0 .6 18.7a8887 8887 0 0 1 .2 267.6l-.4 14.3a5034 5034 0 0 0-.4-300.6m-380.2 157c36.4 2.6 116 2.6 152.4 0-2.3 0-3.7.2-6.3.3a2302 2302 0 0 1-139.8 0zm-19.4 218.7.4 7a2273 2273 0 0 1-.1 139.7c-.1 3.3-.3 5.2-.5 8 3-35.2 3-118.2.2-154.7m191.2 0c-2.8 36.5-2.8 119.5.2 154.8-.2-3-.4-4.8-.5-8.1a2274 2274 0 0 1-.1-139.7z" />
+ </svg>
+ </div>
+ <div>
+ <nav aria-label="Site navigation">
+ {% block site_navigation %}
+ <ul>
+ {% block site_navigation_items %}
+ <li>
+ <a href="{{ '/blog' | url(absolute=True) }}"
+ {% if "/blog@" in this.path %}aria-current="page"{% endif %}>Blog</a>
+ </li>
+ <li>
+ <a href="{{ '/categories' | url(absolute=True) }}"
+ {% if this.path == "/categories" %}aria-current="page"{% endif %}>Categories</a>
+ </li>
+ <li>
+ <a href="{{ '/about' | url(absolute=True) }}"
+ {% if this.path == "/about" %}aria-current="page"{% endif %}>About</a>
+ </li>
+ {% endblock site_navigation_items %}
+ </ul>
+ {% endblock site_navigation %}
+ </nav>
+ <fieldset class="theme-switcher">
+ <legend class="visually-hidden">Color theme</legend>
+ <label title="Light mode">
+ <input type="radio"
+ name="theme-switcher"
+ value="light"
+ id="lightmode"
+ aria-label="Light Mode">
+ <span aria-hidden="true"></span>
+ </label>
+ <label title="Auto mode">
+ <input type="radio"
+ name="theme-switcher"
+ value="light dark"
+ id="automode"
+ aria-label="Auto Mode"
+ checked>
+ <span aria-hidden="true"></span>
+ </label>
+ <label title="Dark mode">
+ <input type="radio"
+ name="theme-switcher"
+ value="dark"
+ id="darkmode"
+ aria-label="Dark Mode">
+ <span aria-hidden="true"></span>
+ </label>
+ </fieldset>
+ </div>
+ {% endblock site_header %}
+ </header>
+ <main>
+ {% block content required %}
+ {% endblock content %}
+ </main>
+ <footer>
+ {% block site_footer %}
+ <section aria-label="Follow me">
+ <h2>Follow me:</h2>
+ <div>
+ <a href="{{ '/blog@atom/latest' | url(absolute=True) }}"
+ id="atom"
+ aria-label="Atom feed — Latest posts"></a>
+ <a rel="nofollow"
+ href="/email/"
+ id="email"
+ aria-label="Mail address: me plus blog at dennisfink.me"></a>
+ <a rel="me"
+ href="https://social.dennisfink.me/@me"
+ id="mastodon"
+ aria-label="Mastodon profile: me on social.dennisfink.me"></a>
+ <a href="xmpp:me@dennisfink.me"
+ id="xmpp"
+ aria-label="XMPP address: me on dennisfink.me"></a>
+ </div>
+ </section>
+ <section>
+ <h2>Blogroll</h2>
+ <ul>
+ <li>
+ <a href="https://blog.janguth.eu/">Broadsheet (Jan Guth)</a>
+ </li>
+ </ul>
+ </section>
+ {% endblock %}
+ </footer>
+ {% endblock body %}
+ </body>
+</html>
diff --git a/templates/blog-month-index.html b/templates/blog-month-index.html
new file mode 100644
index 0000000..d114628
--- /dev/null
+++ b/templates/blog-month-index.html
@@ -0,0 +1,54 @@
+{% extends "base.html" %}
+
+{% from "macros/pagination.html" import render_month_archive_navigation %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/category.css'|asseturl }}">
+{% endblock %}
+{% set title = "Monthly archive: " + this.date | dateformat('MMMM y') %}
+{% block content %}
+ {% set post_count = this.children.count() %}
+ <article>
+ <header>
+ <hgroup>
+ <h2>Monthly archive: {{ this.date | dateformat("MMMM y") }}</h2>
+ <p>
+ ({{ post_count }}
+ {% if post_count > 1 %}
+ posts)
+ {% else %}
+ post)
+ {% endif %}
+ </p>
+ </hgroup>
+ </header>
+ <table>
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for post in this.children.order_by('-publication_date') %}
+ <tr>
+ <td>
+ <time datetime="{{ post.publication_date }}">{{ post.publication_date }}</time>
+ </td>
+ <td>
+ <a href="{{ post|url }}">{{ post.title }}</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {{ render_month_archive_navigation(this) }}
+ </article>
+{% endblock %}
diff --git a/templates/blog-post.html b/templates/blog-post.html
new file mode 100644
index 0000000..6ffb600
--- /dev/null
+++ b/templates/blog-post.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+
+{% from "macros/blog.html" import render_blog_post %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="article">
+ <meta property="article:author" content="https://dennisfink.me/about">
+ {% if this.publication_date %}
+ <meta property="article:published_time"
+ content="{{ this.publication_date }}T00:00:00Z">
+ {% endif %}
+ {% if this.updated_date %}
+ <meta property="article:modified_time"
+ content="{{ this.updated_date }}T00:00:0Z">
+ {% endif %}
+ {% if this.category %}
+ {% set category = site.get("/categories/" + this.category) %}
+ <meta property="article:section" content="{{ category.name }}">
+ {% endif %}
+ {% for t in this.tags %}
+ <meta property="article:tag" content="{{ t }}">
+ {% endfor %}
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/blog.css'|asseturl }}">
+{% endblock %}
+{% block content %}{{ render_blog_post(this) }}{% endblock %}
diff --git a/templates/blog-year-index.html b/templates/blog-year-index.html
new file mode 100644
index 0000000..138cc86
--- /dev/null
+++ b/templates/blog-year-index.html
@@ -0,0 +1,54 @@
+{% extends "base.html" %}
+
+{% from "macros/pagination.html" import render_year_archive_navigation %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/category.css'|asseturl }}">
+{% endblock %}
+{% set title = "Yearly archive: " + this.key %}
+{% block content %}
+ {% set post_count = this.children.count() %}
+ <article>
+ <header>
+ <hgroup>
+ <h2>Yearly archive: {{ this.key }}</h2>
+ <p>
+ ({{ post_count }}
+ {% if post_count > 1 %}
+ posts)
+ {% else %}
+ post)
+ {% endif %}
+ </p>
+ </hgroup>
+ </header>
+ <table>
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for post in this.children.order_by('-publication_date') %}
+ <tr>
+ <td>
+ <time datetime="{{ post.publication_date }}">{{ post.publication_date }}</time>
+ </td>
+ <td>
+ <a href="{{ post|url }}">{{ post.title }}</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {{ render_year_archive_navigation(this) }}
+ </article>
+{% endblock %}
diff --git a/templates/blog.html b/templates/blog.html
new file mode 100644
index 0000000..0ee96c7
--- /dev/null
+++ b/templates/blog.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% from "macros/blog.html" import render_blog_index %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/blog.css'|asseturl }}">
+{% endblock %}
+{% block content %}{{ render_blog_index(this.pagination) }}{% endblock %}
diff --git a/templates/categories.html b/templates/categories.html
new file mode 100644
index 0000000..4dbdf1a
--- /dev/null
+++ b/templates/categories.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/categories.css'|asseturl }}">
+{% endblock %}
+{% set title = "Categories" %}
+{% block content %}
+ <article>
+ <header>
+ <h2>Categories</h2>
+ </header>
+ <ul>
+ {% set sorted = [] %}
+ {% for category in this.children.order_by('name') %}
+ {% set post_count = category.children.count() %}
+ {% if post_count > 0 %}
+ {% set _ = sorted.append((post_count, category)) %}
+ {% endif %}
+ {% endfor %}
+ {% for post_count, category in sorted|sort(attribute='0', reverse=True) %}
+ {% set last_post = category.children.order_by('-publication_date').first() %}
+ <li>
+ <a href="{{ category|url }}">
+ <h3>{{ category.name }}</h3>
+ <dl>
+ <div>
+ <dt>Posts:</dt>
+ <dd>
+ {{ post_count }}
+ </dd>
+ </div>
+ {% if last_post %}
+ <div>
+ <dt>Last post:</dt>
+ <dd>
+ <time datetime="{{ last_post.publication_date }}">{{ last_post.publication_date }}</time>
+ </dd>
+ </div>
+ {% endif %}
+ </dl>
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ </article>
+{% endblock %}
diff --git a/templates/category.html b/templates/category.html
new file mode 100644
index 0000000..9d6b35d
--- /dev/null
+++ b/templates/category.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/category.css'|asseturl }}">
+{% endblock %}
+{% set title = "Category " + this.name %}
+{% block content %}
+ {% set post_count = this.children.count() %}
+ <article>
+ <header>
+ <hgroup>
+ <h2>Category: {{ this.name }}</h2>
+ <p>
+ ({{ post_count }}
+ {% if post_count > 1 %}
+ posts)
+ {% else %}
+ post)
+ {% endif %}
+ </p>
+ </hgroup>
+ </header>
+ <table>
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for post in this.children.order_by('-publication_date') %}
+ <tr>
+ <td>
+ <time datetime="{{ post.publication_date }}">{{ post.publication_date }}</time>
+ </td>
+ <td>
+ <a href="{{ post|url }}">{{ post.title }}</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </article>
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..0ee96c7
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% from "macros/blog.html" import render_blog_index %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/blog.css'|asseturl }}">
+{% endblock %}
+{% block content %}{{ render_blog_index(this.pagination) }}{% endblock %}
diff --git a/templates/macros/blog.html b/templates/macros/blog.html
new file mode 100644
index 0000000..b4caafe
--- /dev/null
+++ b/templates/macros/blog.html
@@ -0,0 +1,100 @@
+{% from "macros/pagination.html" import render_pagination %}
+
+{% macro render_toc(toc, maxdepth=2) %}
+ <nav aria-label="Table of contents">
+ <p>Table of contents</p>
+ <ol>
+ {% for item in toc recursive %}
+ <li>
+ <a href="#{{ item.anchor }}">{{ item.title }}</a>
+ {% if item.children and loop.depth < maxdepth %}
+ <ol>
+ {{ loop(item.children) }}
+ </ol>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ol>
+ </nav>
+{% endmacro %}
+
+{% macro render_blog_post(post, show_toc=True) %}
+ <article>
+ <header>
+ <div>
+ <h2>
+ <a href="{{ post|url }}">{{ post.title }}</a>
+ </h2>
+ <dl>
+ {% if post.publication_date %}
+ <div>
+ <dt>Posted:</dt>
+ <dd>
+ <a href="{{ ('/blog@index-pages/year/{}'.format(post.publication_date.year) ) | url }}">
+ <time datetime="{{ post.publication_date | dateformat('yyyy-MM-dd') }}">{{ post.publication_date | dateformat("d MMMM yyyy") }}</time>
+ </a>
+ </dd>
+ </div>
+ {% else %}
+ <div class="missing-publication-date-warning" role="alert">
+ <dt>⚠</dt>
+ <dd>
+ Missing publication date
+ </dd>
+ </div>
+ {% endif %}
+ {% if post.updated_date and post.publication_date and (post.updated_date > post.publication_date) %}
+ <div>
+ <dt>Last updated:</dt>
+ <dd>
+ <time datetime="{{ post.updated_date | dateformat('yyyy-MM-dd') }}">{{ post.updated_date | dateformat("d MMMM yyyy") }}</time>
+ </dd>
+ </div>
+ {% endif %}
+ {% if post.category %}
+ <div>
+ <dt>Category:</dt>
+ <dd>
+ {% set category = site.get('/categories/'+post.category) %}
+ <a href="{{ category|url }}">{{ category.name }}</a>
+ </dd>
+ </div>
+ {% endif %}
+ </dl>
+ </div>
+ {% if show_toc and post.render_toc %}
+ {% set toc = post.body.toc|default([]) %}
+ {{ render_toc(toc) }}
+ {% endif %}
+ </header>
+ {{ post.body }}
+ {% if post.tags %}
+ <footer>
+ <p>Tagged with</p>
+ <ul aria-label="Tags">
+ {% for tag in post.tags %}
+ <li>
+ {% set tag_slug = tag|trim|lower|replace(" ", "-") %}
+ <a href="{{ ('/blog/tag/' ~ tag_slug ~ '/')|url }}" rel="tag">{{ tag }}</a>
+ {%- if not loop.last -%}, {%- else -%}.{%- endif -%}
+ </li>
+ {% endfor %}
+ </ul>
+ </footer>
+ {% endif %}
+ </article>
+{% endmacro %}
+
+{% macro render_blog_index(pagination) %}
+ {% for post in pagination.items %}
+ {{ render_blog_post(post, False) }}
+ {% if not loop.last %}
+ <hr>
+ {% endif %}
+ {% endfor %}
+ {% if pagination.pages > 1 %}
+ <footer>
+ {{ render_pagination(pagination) }}
+ </footer>
+ {% endif %}
+{% endmacro %}
diff --git a/templates/macros/pagination.html b/templates/macros/pagination.html
new file mode 100644
index 0000000..adb1b5c
--- /dev/null
+++ b/templates/macros/pagination.html
@@ -0,0 +1,221 @@
+{% macro render_pagination(pagination, pagination_window=2) %}
+ {% set current = pagination.page %}
+ {% set total = pagination.pages %}
+ {% set window_start = [current - pagination_window, 1] | max %}
+ {% set window_end = [current + pagination_window, total] | min %}
+ <nav aria-label="Page navigation">
+ <ol>
+ <li>
+ {% if pagination.has_prev %}
+ <a href="{{ pagination.prev | url }}" aria-label="Previous page">❮</a>
+ {% else %}
+ <span aria-disabled="true">❮</span>
+ {% endif %}
+ </li>
+ {% if window_start > 1 %}
+ <li>
+ <a href="{{ pagination.for_page(1) | url }}" aria-label="Page 1">1</a>
+ </li>
+ {% if window_start > 2 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ {% endif %}
+ {% for page_num in range(window_start, window_end + 1) %}
+ <li>
+ {% if page_num == current %}
+ <a href="{{ pagination.for_page(page_num) | url }}" aria-current="page">{{ page_num }}</a>
+ {% else %}
+ <a href="{{ pagination.for_page(page_num) | url }}"
+ aria-label="Page {{ page_num }}">{{ page_num }}</a>
+ {% endif %}
+ </li>
+ {% endfor %}
+ {% if window_end < total %}
+ {% if window_end < total - 1 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ <li>
+ <a href="{{ pagination.for_page(total) | url }}"
+ aria-label="Page {{ total }}">{{ total }}</a>
+ </li>
+ {% endif %}
+ <li>
+ {% if pagination.has_next %}
+ <a href="{{ pagination.next | url }}" aria-label="Next page">❯</a>
+ {% else %}
+ <span aria-disabled="true">❯</span>
+ {% endif %}
+ </li>
+ </ol>
+ </nav>
+{% endmacro %}
+
+{% macro render_year_archive_navigation(
+ current_year_page,
+ archive_window=2
+ ) %}
+ {% set current_year = current_year_page.year %}
+ {% set first_year_page = current_year_page.first_year_page %}
+ {% set last_year_page = current_year_page.last_year_page %}
+ {% set previous_year_page = current_year_page.previous_year_page %}
+ {% set next_year_page = current_year_page.next_year_page %}
+ {% set first_year = first_year_page.year %}
+ {% set last_year = last_year_page.year %}
+ {% set window_start = [current_year - archive_window, first_year] | max %}
+ {% set window_end = [current_year + archive_window, last_year] | min %}
+ <nav aria-label="Year archive navigation">
+ <ol>
+ <li>
+ {% if previous_year_page %}
+ <a href="{{ previous_year_page | url }}"
+ rel="prev"
+ aria-label="Previous year: {{ previous_year_page.year }}">❮</a>
+ {% else %}
+ <span aria-disabled="true">❮</span>
+ {% endif %}
+ </li>
+ {% if window_start > first_year %}
+ <li>
+ <a href="{{ first_year_page | url }}" aria-label="Year {{ first_year }}">{{ first_year }}</a>
+ </li>
+ {% if window_start > first_year + 1 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ {% endif %}
+ {% for year in range(window_start, window_end + 1) %}
+ {% set year_page = site.get(
+ '/blog@index-pages/year/{:04d}'.format(year)
+ ) %}
+ {% if year_page %}
+ <li>
+ {% if year == current_year %}
+ <a href="{{ year_page | url }}" aria-current="page">{{ year }}</a>
+ {% else %}
+ <a href="{{ year_page | url }}" aria-label="Year {{ year }}">{{ year }}</a>
+ {% endif %}
+ </li>
+ {% endif %}
+ {% endfor %}
+ {% if window_end < last_year %}
+ {% if window_end < last_year - 1 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ <li>
+ <a href="{{ last_year_page | url }}" aria-label="Year {{ last_year }}">{{ last_year }}</a>
+ </li>
+ {% endif %}
+ <li>
+ {% if next_year_page %}
+ <a href="{{ next_year_page | url }}"
+ rel="next"
+ aria-label="Next year: {{ next_year_page.year }}">❯</a>
+ {% else %}
+ <span aria-disabled="true">❯</span>
+ {% endif %}
+ </li>
+ </ol>
+ </nav>
+{% endmacro %}
+
+{% macro render_month_archive_navigation(
+ current_month_page,
+ archive_window=2
+ ) %}
+ {% set year_pages = index_pages('year') | sort(attribute='year') %}
+ {% set ns = namespace(
+ month_pages=[],
+ current_position=none
+ ) %}
+ {% for year_page in year_pages %}
+ {% set ns.month_pages = ns.month_pages + (year_page.subindexes | sort(attribute='month')) %}
+ {% endfor %}
+ {% for month_page in ns.month_pages %}
+ {% if month_page.year == current_month_page.year
+ and month_page.month == current_month_page.month %}
+ {% set ns.current_position = loop.index0 %}
+ {% endif %}
+ {% endfor %}
+ {% set total = ns.month_pages | length %}
+ {% set current_position = ns.current_position %}
+ {% set window_start = [current_position - archive_window, 0] | max %}
+ {% set window_end = [current_position + archive_window, total - 1] | min %}
+ {% set first_month_page = ns.month_pages[0] %}
+ {% set last_month_page = ns.month_pages[total - 1] %}
+ {% set previous_month_page = none %}
+ {% set next_month_page = none %}
+ {% if current_position > 0 %}
+ {% set previous_month_page = ns.month_pages[current_position - 1] %}
+ {% endif %}
+ {% if current_position < total - 1 %}
+ {% set next_month_page = ns.month_pages[current_position + 1] %}
+ {% endif %}
+ <nav aria-label="Monthly archive navigation">
+ <ol>
+ <li>
+ {% if previous_month_page %}
+ <a href="{{ previous_month_page | url }}"
+ rel="prev"
+ aria-label="Previous month: {{ previous_month_page.date | dateformat('yyyy-MM') }}">❮</a>
+ {% else %}
+ <span aria-disabled="true">❮</span>
+ {% endif %}
+ </li>
+ {% if window_start > 0 %}
+ <li>
+ <a href="{{ first_month_page | url }}"
+ aria-label="{{ first_month_page.date | dateformat('yyyy-MM') }}">
+ {{ first_month_page.date | dateformat("MMMM y") }}
+ </a>
+ </li>
+ {% if window_start > 1 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ {% endif %}
+ {% for position in range(window_start, window_end + 1) %}
+ {% set month_page = ns.month_pages[position] %}
+ <li>
+ {% if position == current_position %}
+ <a href="{{ month_page | url }}" aria-current="page">{{ month_page.date | dateformat("MMMM y") }}</a>
+ {% else %}
+ <a href="{{ month_page | url }}"
+ aria-label="{{ month_page.date | dateformat('yyyy-MM') }}">
+ {{ month_page.date | dateformat("MMMM y") }}
+ </a>
+ {% endif %}
+ </li>
+ {% endfor %}
+ {% if window_end < total - 1 %}
+ {% if window_end < total - 2 %}
+ <li>
+ <span aria-hidden="true">&hellip;</span>
+ </li>
+ {% endif %}
+ <li>
+ <a href="{{ last_month_page | url }}"
+ aria-label="{{ last_month_page.date | dateformat('yyyy-MM') }}">
+ {{ last_month_page.date | dateformat("MMMM y") }}
+ </a>
+ </li>
+ {% endif %}
+ <li>
+ {% if next_month_page %}
+ <a href="{{ next_month_page | url }}"
+ rel="next"
+ aria-label="Next month: {{ next_month_page.date | dateformat('yyyy-MM') }}">❯</a>
+ {% else %}
+ <span aria-disabled="true">❯</span>
+ {% endif %}
+ </li>
+ </ol>
+ </nav>
+{% endmacro %}
diff --git a/templates/page.html b/templates/page.html
new file mode 100644
index 0000000..dd20955
--- /dev/null
+++ b/templates/page.html
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block content %}<article>{{ this.body }}</article>{% endblock %}
diff --git a/templates/tag.html b/templates/tag.html
new file mode 100644
index 0000000..b433616
--- /dev/null
+++ b/templates/tag.html
@@ -0,0 +1,59 @@
+{% extends "base.html" %}
+
+{% block meta %}
+ {{ super() }}
+ <meta property="og:type" content="website">
+{% endblock %}
+{% block styles %}
+ {{ super() }}
+ <link rel="stylesheet"
+ type="text/css"
+ href="{{ '/static/css/category.css'|asseturl }}">
+{% endblock %}
+{% set title = "Tag " + this.tag %}
+{% block content %}
+ {% set post_count = this.items.count() %}
+ <article>
+ <header>
+ <hgroup>
+ <h2>Tag: {{ this.tag }}</h2>
+ <p>
+ ({{ post_count }}
+ {% if post_count > 1 %}
+ posts)
+ {% else %}
+ post)
+ {% endif %}
+ </p>
+ </hgroup>
+ </header>
+ <table>
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for post in this.items %}
+ <tr>
+ <td>
+ {% if post.publication_date %}
+ <time datetime="{{ post.publication_date }}">{{ post.publication_date }}</time>
+ {% endif %}
+ </td>
+ <td>
+ <a href="{{ post|url }}">{{ post.title }}</a>
+ </td>
+ </tr>
+ {% else %}
+ <tr>
+ <td colspan="2">
+ <em>No posts for this tag.</em>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </article>
+{% endblock %}