diff --git a/nix/hosts/misc/rss.nix b/nix/hosts/misc/rss.nix
index 3f15345..55b41cf 100644
--- a/nix/hosts/misc/rss.nix
+++ b/nix/hosts/misc/rss.nix
@@ -6,20 +6,52 @@
}:
let
domain = "rss.${lib.our.domains.priv}";
+ cfg = config.services.tinyfeed;
in
{
-
services.tinyfeed = {
enable = true;
name = "Ariadne's RSS feed";
description = "Things i like";
- files = [
- {
- out = "index.html";
- }
- ];
+ files =
+ let
+ template = "${../../../web/tinyfeed/index.html.gtpl}";
+ stylesheet = "${../../../web/tinyfeed/css.css}";
+ in
+ [
+ {
+ name = "Ariadne's blogroll";
+ description = ''
+ Things I find interesting, from a variety of domains.
+ '';
+ out = "index.html";
+ feedsPath = "${cfg.outputPath}/feeds.txt";
+ inherit template stylesheet;
+ limit = 100;
+ }
+ {
+ name = "Ariadne's tech feed";
+ description = ''
+ Tech-focused blogroll.
+ '';
+ out = "tech.html";
+ feedsPath = "${cfg.outputPath}/tech.txt";
+ inherit template stylesheet;
+ limit = 100;
+ }
+ {
+ name = "Ariadne's video feed";
+ description = ''
+ Videos from people I find interesting. Mostly video essays.
+ '';
+ out = "videos.html";
+ feedsPath = "${cfg.outputPath}/videos.txt";
+ inherit template stylesheet;
+ limit = 20;
+ }
+ ];
};
services.nginx = {
diff --git a/nix/modules/services/tinyfeed.nix b/nix/modules/services/tinyfeed.nix
index 7311d87..c3a6f79 100644
--- a/nix/modules/services/tinyfeed.nix
+++ b/nix/modules/services/tinyfeed.nix
@@ -125,9 +125,9 @@ in
concatStringsSep " " [
"tinyfeed "
"-n"
- (escapeShellArg cfg.name)
+ (escapeShellArg file.name)
"-d"
- (escapeShellArg cfg.description)
+ (escapeShellArg file.description)
"-t"
(escapeShellArg file.template)
"-l"
@@ -136,8 +136,8 @@ in
(escapeShellArg file.feedsPath)
"--output"
(escapeShellArg file.out)
- (optionalString (file.script != null) "-s ${toString i}.js")
- (optionalString (file.stylesheet != null) "-S ${toString i}.css")
+ (optionalString (file.script != null) "-S ${toString i}.js")
+ (optionalString (file.stylesheet != null) "-s ${toString i}.css")
]
) cfg.files
);
@@ -163,17 +163,14 @@ in
systemd.tmpfiles.rules = [
"d ${cfg.outputPath} 0755 ${cfg.user} -"
"f ${cfg.feedsPath} 0644 ${cfg.user} -"
- "l ${cfg.outputPath}/ 0644 ${cfg.user} -"
]
++ (concatLists (
imap (
i: file:
- (optional (
- file.script != null
- ) "l ${cfg.outputPath}/${toString i}.js 0644 ${cfg.user} - ${file.script}")
+ (optional (file.script != null) "L+ ${cfg.outputPath}/${toString i}.js - - - - ${file.script}")
++ (optional (
file.stylesheet != null
- ) "l ${cfg.outputPath}/${toString i}.css 0644 ${cfg.user} - ${file.stylesheet}")
+ ) "L ${cfg.outputPath}/${toString i}.css - - - - ${file.stylesheet}")
) cfg.files
));
users.users.${cfg.user} = { };
diff --git a/nix/pkgs/tinyfeed-template-directory-fix.patch b/nix/pkgs/tinyfeed-template-directory-fix.patch
new file mode 100644
index 0000000..85b42b8
--- /dev/null
+++ b/nix/pkgs/tinyfeed-template-directory-fix.patch
@@ -0,0 +1,21 @@
+diff --git a/internal/main.go b/internal/main.go
+index e1bedcd..8c8defe 100644
+--- a/internal/main.go
++++ b/internal/main.go
+@@ -3,6 +3,7 @@ package tinyfeed
+ import (
+ _ "embed"
+ "flag"
++ "path/filepath"
+ "fmt"
+ "html"
+ "html/template"
+@@ -218,7 +219,7 @@ func printHTML(feeds []*gofeed.Feed, items []Item) error {
+ Funcs(templateFuncs).
+ Parse(opmlTemplate)
+ default:
+- ts, err = template.New(templatePath).
++ ts, err = template.New(filepath.Base(templatePath)).
+ Funcs(templateFuncs).
+ ParseFiles(templatePath)
+ }
diff --git a/nix/pkgs/tinyfeed.nix b/nix/pkgs/tinyfeed.nix
index ad91f3a..e36a58b 100644
--- a/nix/pkgs/tinyfeed.nix
+++ b/nix/pkgs/tinyfeed.nix
@@ -14,5 +14,7 @@ buildGoModule (finalAttrs: {
hash = "sha256-qeHg7beGdTf0EaUAShyki8t7c7VG0iG9e0WX2Wc50RQ=";
};
+ patches = [ ./tinyfeed-template-directory-fix.patch ];
+
vendorHash = "sha256-lwT2eD+zFI49wvdlLcZILa2X/NsEi/jDzshngH3CLyQ=";
})
diff --git a/web/tinyfeed/.gitignore b/web/tinyfeed/.gitignore
new file mode 100644
index 0000000..2389fd7
--- /dev/null
+++ b/web/tinyfeed/.gitignore
@@ -0,0 +1,3 @@
+index.html
+result
+.envrc
diff --git a/web/tinyfeed/.project.el b/web/tinyfeed/.project.el
new file mode 100644
index 0000000..e69de29
diff --git a/web/tinyfeed/css.css b/web/tinyfeed/css.css
new file mode 100644
index 0000000..f0904e0
--- /dev/null
+++ b/web/tinyfeed/css.css
@@ -0,0 +1,1060 @@
+ol.items-list {
+ list-style-type: none;
+ padding: 0;
+ > :not(:last-child) {
+ /* border-bottom: 1px solid var(--colour-border); */
+ margin-bottom: var(--spacing-6);
+ }
+}
+
+.items-list li h3 {
+ margin-bottom: var(--spacing-2);
+}
+
+p.secondary {
+ color: var(--colour-text-secondary);
+ font-size: var(--font-size-12);
+ line-height: var(--line-height-12);
+ margin: 0;
+}
+
+/* GIC.css Version 1.4.0
+ * (c) Copyright Ariadne (me@aria.rip) 2026.
+ * Made available under the MIT license */
+:root {
+ --font-variable: "Inter", sans-serif;
+ --font-weight-normal: 400;
+ --font-weight-bold: 700;
+
+ /* Colour palette */
+ --blue: #1d70b8;
+ --blue-tint-25: #5694ca;
+ --blue-tint-50: #8eb8dc;
+ --blue-tint-80: #d2e2f1;
+ --blue-tint-95: #f4f8fb;
+ --blue-shade-25: #16548a;
+ --blue-shade-50: #0f385c;
+ --blue-shade-10: #1a65a6;;
+ --blue-shade-95: #030B12;
+ --green: #0f7a52;
+ --green-tint-25: #4b9b7d;
+ --green-tint-50: #87bca8;
+ --green-tint-80: #cfe4dc;
+ --green-tint-95: #f3f8f6;
+ --green-shade-25: #0b5c3e;
+ --green-shade-50: #083d29;
+ --teal: #158187;
+ --teal-tint-25: #50a1a5;
+ --teal-tint-50: #8ac0c3;
+ --teal-tint-80: #d0e6e7;
+ --teal-tint-95: #f3f9f9;
+ --teal-shade-25: #106165;
+ --teal-shade-50: #0b4144;
+ --teal-accent: #00ffe0;
+ --purple: #54319f;
+ --purple-tint-25: #7f65b7;
+ --purple-tint-50: #aa98cf;
+ --purple-tint-80: #ddd6ec;
+ --purple-tint-95: #f6f5fa;
+ --purple-shade-25: #3f2577;
+ --purple-shade-50: #2a1950;
+ --magenta: #ca357c;
+ --magenta-tint-25: #d7689d;
+ --magenta-tint-50: #e59abe;
+ --magenta-tint-80: #f4d7e5;
+ --magenta-tint-95: #fcf5f8;
+ --magenta-shade-25: #98285d;
+ --magenta-shade-50: #651b3e;
+ --red: #ca3535;
+ --red-tint-25: #d76868;
+ --red-tint-50: #e59a9a;
+ --red-tint-80: #f4d7d7;
+ --red-tint-95: #fcf5f5;
+ --red-shade-25: #982828;
+ --red-shade-50: #651b1b;
+ --orange: #f47738;
+ --orange-tint-25: #f7996a;
+ --orange-tint-50: #fabb9c;
+ --orange-tint-80: #fde4d7;
+ --orange-tint-95: #fef8f5;
+ --orange-shade-25: #b7592a;
+ --orange-shade-50: #7a3c1c;
+ --yellow: #ffdd00;
+ --yellow-tint-25: #ffe640;
+ --yellow-tint-50: #ffee80;
+ --yellow-tint-80: #fff8cc;
+ --yellow-tint-95: #fffdf2;
+ --yellow-shade-25: #bfa600;
+ --yellow-shade-50: #806f00;
+ --brown: #99704a;
+ --brown-tint-25: #b39477;
+ --brown-tint-50: #ccb8a5;
+ --brown-tint-95: #faf8f6;
+ --black: #0b0c0c;
+ --black-tint-25: #484949;
+ --black-tint-50: #858686;
+ --black-tint-80: #cecece;
+ --black-tint-95: #f3f3f3;
+ --white: #ffffff;
+
+ --colour-brand: var(--red);
+ --colour-brand-text: var(--white);
+
+ --colour-text: var(--black);
+ --colour-body-background: var(--white);
+
+ /* Used in for example 'muted' text and help text. */
+ --colour-text-secondary: var(--black-tint-25);
+
+ --colour-focus: var(--yellow);
+ --colour-focus-text: var(--black);
+
+ --colour-error: var(--red);
+ --colour-success: var(--green);
+
+ /* Used in for example borders, separators, rules and keylines. */
+ --colour-border: var(--black-tint-80);
+ /* Used for form inputs and controls */
+ --colour-input-border: var(--black);
+ /* Used for hover states on form controls */
+ --colour-hover: var(--black-tint-80);
+ /* Standard links (on white) */
+ --colour-link: var(--blue-shade-10);
+ --colour-link-visited: var(--purple);
+ --colour-link-hover: var(--blue-shade-50);
+ --colour-link-active: var(--black);
+
+ /* 'Surfaces' are our name for components that have different colour */
+ /* palettes to typical page content. This is the generic surface. */
+ --colour-surface-background: var(--blue-tint-95);
+ --colour-surface-text: var(--black);
+ --colour-surface-board: var(--blue-tint-50);
+ --colour-button-text: var(--white);
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+
+ /* Dark theme, from catppuccin mocha */
+ --blue: #89b4fa;
+ --blue-base: #89b4fa;
+ --blue-tint-25: #6787bc;
+ --blue-tint-50: #455a7d;
+ --blue-tint-80: #1b2432;
+ --blue-tint-95: #07090d;
+ --blue-shade-10: #95bcfb;
+ --blue-shade-25: #a7c7fb;
+ --blue-shade-50: #c4dafd;
+ --blue-shade-95: #f9fbff;
+ --green: #a6e3a1;
+ --green-tint-25: #7daa79;
+ --green-tint-50: #537251;
+ --green-tint-80: #212d20;
+ --green-tint-95: #080b08;
+ --green-shade-25: #bceab9;
+ --green-shade-50: #d3f1d0;
+ --teal: #94e2d5;
+ --teal-tint-25: #6faaa0;
+ --teal-tint-50: #4a716b;
+ --teal-tint-80: #1e2d2b;
+ --teal-tint-95: #070b0b;
+ --teal-shade-25: #afe9e0;
+ --teal-shade-50: #caf1ea;
+ --purple: #cba6f7;
+ --purple-tint-25: #987db9;
+ --purple-tint-50: #66537c;
+ --purple-tint-80: #292131;
+ --purple-tint-95: #0a080c;
+ --purple-shade-25: #d8bcf9;
+ --purple-shade-50: #e5d3fb;
+ --pink: #f5c2e7;
+ --pink-tint-25: #b892ad;
+ --pink-tint-50: #7b6174;
+ --pink-tint-80: #31272e;
+ --pink-tint-95: #0c0a0c;
+ --pink-shade-25: #f8d1ed;
+ --pink-shade-50: #fae1f3;
+ --red: #f38ba8;
+ --red-tint-25: #b6687e;
+ --red-tint-50: #7a4654;
+ --red-tint-80: #311c22;
+ --red-tint-95: #0c0708;
+ --red-shade-25: #f6a8be;
+ --red-shade-50: #f9c5d4;
+ --orange: #fab387;
+ --orange-tint-25: #bc8665;
+ --orange-tint-50: #7d5a44;
+ --orange-tint-80: #32241b;
+ --orange-tint-95: #0d0907;
+ --orange-shade-25: #fbc6a5;
+ --orange-shade-50: #fdd9c3;
+ --yellow: #f9e2af;
+ --yellow-tint-25: #bbaa83;
+ --yellow-tint-50: #7d7158;
+ --yellow-tint-80: #322d23;
+ --yellow-tint-95: #0c0b09;
+ --yellow-shade-25: #fbe9c3;
+ --yellow-shade-50: #fcf1d7;
+ --brown: #f2cdcd;
+ --brown-tint-25: #b69a9a;
+ --brown-tint-50: #796767;
+ --brown-tint-95: #0c0a0a;
+ --black: #1e1e2e;
+ --black-tint-25: #171723;
+ --black-tint-50: #0f0f17;
+ --black-tint-80: #060609;
+ --black-tint-95: #020202;
+ --white: #cdd6f4;
+ --white-shade-25: #9aa1b7;
+
+ /* Alternative Dark theme, from modus operandi (Vivendi) */
+ /* A bit higher contrast for those who like that */
+ /* --blue: #2fafff; */
+ /* --blue-tint-25: #2383bf; */
+ /* --blue-tint-50: #185880; */
+ /* --blue-tint-80: #092333; */
+ /* --blue-tint-95: #02090d; */
+ /* --blue-shade-10: #44b7ff; */
+ /* --blue-shade-25: #63c3ff; */
+ /* --blue-shade-50: #97d7ff; */
+ /* --blue-shade-95: #f5fbff; */
+ /* --green: #44bc44; */
+ /* --green-tint-25: #338d33; */
+ /* --green-tint-50: #225e22; */
+ /* --green-tint-80: #0e260e; */
+ /* --green-tint-95: #030903; */
+ /* --green-shade-25: #73cd73; */
+ /* --green-shade-50: #a2dea2; */
+ /* --teal: #00d3d0; */
+ /* --teal-tint-25: #009e9c; */
+ /* --teal-tint-50: #006a68; */
+ /* --teal-tint-80: #002a2a; */
+ /* --teal-tint-95: #000b0a; */
+ /* --teal-shade-25: #40dedc; */
+ /* --teal-shade-50: #80e9e8; */
+ /* --purple: #9099d9; */
+ /* --purple-tint-25: #6c73a3; */
+ /* --purple-tint-50: #484d6d; */
+ /* --purple-tint-80: #1d1f2b; */
+ /* --purple-tint-95: #07080b; */
+ /* --purple-shade-25: #acb3e3; */
+ /* --purple-shade-50: #c8ccec; */
+ /* --pink: #feacd0; */
+ /* --pink-tint-25: #bf819c; */
+ /* --pink-tint-50: #7f5668; */
+ /* --pink-tint-80: #33222a; */
+ /* --pink-tint-95: #0d090a; */
+ /* --pink-shade-25: #fec1dc; */
+ /* --pink-shade-50: #ffd6e8; */
+ /* --red: #ff5f59; */
+ /* --red-tint-25: #bf4743; */
+ /* --red-tint-50: #80302d; */
+ /* --red-tint-80: #331312; */
+ /* --red-tint-95: #0d0504; */
+ /* --red-shade-25: #ff8783; */
+ /* --red-shade-50: #ffafac; */
+ /* --orange: #db7b5f; */
+ /* --orange-tint-25: #a45c47; */
+ /* --orange-tint-50: #6e3e30; */
+ /* --orange-tint-80: #2c1913; */
+ /* --orange-tint-95: #0b0605; */
+ /* --orange-shade-25: #e49c87; */
+ /* --orange-shade-50: #edbdaf; */
+ /* --yellow: #d0bc00; */
+ /* --yellow-tint-25: #9c8d00; */
+ /* --yellow-tint-50: #685e00; */
+ /* --yellow-tint-80: #2a2600; */
+ /* --yellow-tint-95: #0a0900; */
+ /* --yellow-shade-25: #dccd40; */
+ /* --yellow-shade-50: #e8de80; */
+ /* --brown: #f2cdcd; */
+ /* --brown-tint-25: #b69a9a; */
+ /* --brown-tint-50: #796767; */
+ /* --brown-tint-95: #0c0a0a; */
+ /* --black: #000000; */
+ /* --black-tint-25: #404040; */
+ /* --black-tint-50: #808080; */
+ /* --black-tint-80: #cccccc; */
+ /* --black-tint-95: #f2f2f2; */
+ /* --white: #ffffff; */
+ /* --white-shade-250: #bfbfbf; */
+
+ --colour-text: var(--white);
+ --colour-body-background: var(--black);
+ --colour-text-secondary: var(--white-shade-25);
+ --colour-border: var(--white);
+ --colour-input-border: var(--white);
+ --colour-focus-text: var(--white);
+ --colour-link-active: var(--white);
+ --colour-surface-background: var(--blue-tint-80);
+ --colour-surface-text: #fff;
+ --colour-surface-board: var(--blue-tint-50);
+ --colour-button-text: var(--black);
+ --colour-brand-text: var(--black);
+ }
+
+
+ --width-input-border: 2px;
+ --width-form-group-error: 5px;
+ --width-focus: 3px;
+
+ --font-size-80: 53px;
+ --line-height-80: 55px;
+ --font-size-48: 32px;
+ --line-height-48: 35px;
+ --font-size-36: 27px;
+ --line-height-36: 30px;
+ --font-size-27: 21px;
+ --line-height-27: 25px;
+ --font-size-24: 21px;
+ --line-height-24: 25px;
+ --font-size-19: 19px;
+ --line-height-19: 25px;
+ --font-size-16: 16px;
+ --line-height-16: 20px;
+
+ --spacing-0: 0;
+ --spacing-1: 5px;
+ --spacing-2: 10px;
+ --spacing-3: 15px;
+ --spacing-4: 15px;
+ --spacing-5: 15px;
+ --spacing-6: 20px;
+ --spacing-7: 25px;
+ --spacing-8: 30px;
+ --spacing-9: 40px;
+
+ @media screen and (min-width: 641px) { /* tablet */
+ --font-size-80: 80px;
+ --line-height-80: 80px;
+ --font-size-48: 48px;
+ --line-height-48: 50px;
+ --font-size-36: 36px;
+ --line-height-36: 40px;
+ --font-size-27: 27px;
+ --line-height-27: 30px;
+ --font-size-24: 24px;
+ --line-height-24: 30px;
+ --spacing-4: 20px;
+ --spacing-5: 25px;
+ --spacing-6: 30px;
+ --spacing-7: 40px;
+ --spacing-8: 50px;
+ --spacing-9: 60px;
+ }
+
+ @media print {
+ --colour-text: #000000;
+ --font-size-80: 53pt;
+ --line-height-80: 1.1;
+ --font-size-48: 32pt;
+ --line-height-48: 1.15;
+ --font-size-36: 24pt;
+ --line-height-36: 1.05;
+ --font-size-27: 18pt;
+ --line-height-27: 1.15;
+ --font-size-24: 18pt;
+ --line-height-24: 1.15;
+ --font-size-19: 14pt;
+ --line-height-19: 1.15;
+ --font-size-16: 14pt;
+ --line-height-16: 1.2;
+ }
+}
+
+
+/* eeeee e e eeeee eeeee eeeee eeeee eeeee eeeee e e e e */
+/* 8 8 8 8 8 8 88 8 8 8 8 8 8 8 8 8 8 8 8 */
+/* 8e 8eeee8 8eee8 8 8 8e 8eee8e 8eee8 8eee8 8eee8 8eeee8 */
+/* 88 88 88 8 8 88 "8 88 8 88 8 88 88 8 88 */
+/* 88 88 88 8eee8 88ee8 88 8 88 8 88 88 8 88 */
+html { background: var(--colour-body-background); }
+html:has(footer) { background: var(--colour-surface-background); }
+
+body {
+ font-family: var(--font-variable);
+ font-weight: var(--font-weight-normal);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ color: var(--colour-text);
+ background: var(--colour-body-background);
+
+ margin: 0;
+
+ /* Prevent automatic text sizing, as we already cater for small devices and */
+ /* would like the browser to stay on 100% text zoom by default. */
+ text-size-adjust: 100%;
+}
+
+h1,h2,h3,h4 { font-weight: var(--font-weight-bold); margin-top: 0; }
+span.caption { color: var(--colour-text-secondary); display: block; }
+
+p {
+ margin-top: 0;
+}
+
+/* sizing stuff. */
+h1 { font-size: var(--font-size-48); line-height: var(--line-height-48); margin-bottom: var(--spacing-8); }
+h2 { font-size: var(--font-size-36); line-height: var(--line-height-36); margin-bottom: var(--spacing-6); }
+h3 { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-4); }
+h4 { font-size: var(--font-size-19); line-height: var(--line-height-19); margin-bottom: var(--spacing-4); }
+p { font-size: var(--font-size-19); line-height: var(--line-height-19); margin-bottom: var(--spacing-4); }
+p.large { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-6); }
+p.small { font-size: var(--font-size-16); line-height: var(--line-height-16); margin-bottom: var(--spacing-4); }
+
+span.caption:has(+ h1) { font-size: var(--font-size-27); line-height: var(--line-height-27); margin-bottom: var(--spacing-1); }
+span.caption:has(+ h2) { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-1); }
+span.caption:has(+ h3) { font-size: var(--font-size-19); line-height: var(--line-height-19); }
+
+/* Add top padding to headings that appear directly after paragraphs. */
+p.large + h2 { padding-top: var(--spacing-1); }
+
+p + h2,
+ul + h2,
+ol + h2 { padding-top: var(--spacing-4); }
+
+p + h3,
+ul + h3,
+ol + h3,
+p + h4,
+ul + h4,
+ol + h4 { padding-top: var(--spacing-1); }
+
+a {
+ text-decoration: underline;
+ text-decoration-thickness: max(1px, .0625rem);
+ text-decoration-offset: 0.1578em;
+ color: var(--colour-link);
+
+ &:not(.no-visited):visited {
+ color: var(--colour-link-visited);
+ }
+
+ &:hover {
+ color: var(--colour-link-hover);
+ text-decoration-thickness: max(3px, .1875rem, .12em);
+ text-decoration-skip-ink: none;
+ text-decoration-skip: none;
+ }
+
+ &:active {
+ color: var(--colour-link-active);
+ }
+
+ /* When focussed, the text colour needs to be darker to ensure that colour */
+ /* contrast is still acceptable */
+ &:focus {
+ color: var(--colour-focus-text);
+ }
+
+ /* Show URL when printing */
+ @media print {
+ &[href^="/"],
+ &[href^="http://"],
+ &[href^="https://"] {
+ &::after {
+ content: " (" attr(href) ")";
+ font-size: 90%;
+ word-wrap: break-word;
+ }
+ }
+ }
+
+ &.error {
+ &,
+ &:visited {
+ color: var(--colour-error);
+ }
+
+ &:hover {
+ color: var(--red-shade-50);
+ }
+
+ &:active {
+ color: var(--colour-error);
+ }
+
+ &:focus {
+ color: var(--colour-focus-text);
+ }
+ }
+
+ &.success {
+ &,
+ &:visited {
+ color: var(--colour-success);
+ }
+
+ &:hover {
+ color: var(--green-shade-50);
+ }
+
+ &:active {
+ color: var(--colour-success);
+ }
+
+ &:focus {
+ color: var(--colour-focus-text);
+ }
+ }
+
+
+ &.no-underline:not(:hover):not(:active) {
+ text-decoration: none;
+ }
+}
+
+
+ul, ol {
+ font-size: var(--font-size-19);
+ line-spacing: var(--line-spacing-19);
+
+ margin-top: 0;
+ margin-bottom: var(--spacing-4);
+
+ color: var(--colour-text);
+
+ padding-left: var(--spacing-4);
+ /* list-style-type: disc; */
+ &.no-bullet {
+ padding-left: 0;
+ list-style-type: none;
+ > li {
+ margin-bottom: var(--spacing-1);
+ }
+ }
+
+ /* Add a top margin for nested lists */
+ ul, ol {
+ margin-top: var(--spacing-2);
+ }
+
+
+ /* Lists without numbers or bullets should always have extra space between */
+ /* list items. Lists with numbers or bullets only have this extra space on */
+ /* tablet and above */
+ > li {
+ margin-bottom: 0;
+ @media screen and (min-width: 641px) { /* tablet */
+ margin-bottom: var(--spacing-1);
+ }
+ }
+
+ &.extra-space > li {
+ margin-bottom: var(--spacing-2);
+ @media screen and (min-width: 641px) { /* tablet */
+ margin-bottom: var(--spacing-3);
+ }
+ }
+}
+
+
+/* section spacer */
+hr { margin: var(--spacing-4) 0; border: 0; }
+hr.xl { margin: var(--spacing-8) 0; }
+hr.l { margin: var(--spacing-6) 0; }
+hr.visible { border-bottom: 1px solid var(--colour-border) }
+
+/* utilities */
+.break-word { word-break: break-word; }
+
+/* oo dP */
+/* 88 */
+/* .d8888b. 88d888b. dP .d888b88 .d8888b. */
+/* 88' `88 88' `88 88 88' `88 Y8ooooo. */
+/* 88. .88 88 88 88. .88 88 */
+/* `8888P88 dP dP `88888P8 `88888P' */
+/* .88 */
+/* d8888P */
+main {
+ padding: var(--spacing-6) 0;
+}
+
+grid-container {
+ --size-gutter: 30px;
+ --size-gutter-half: calc(var(--size-gutter) / 2);
+ --size-page-width: 960px;
+
+ max-width: var(--size-page-width);
+
+ margin-left: var(--size-gutter-half);
+ margin-right: var(--size-gutter-half);
+
+ /* On tablet, add full width gutters */
+ @media screen and (min-width: 641px) { /* tablet */
+ margin-right: var(--size-gutter);
+ margin-left: var(--size-gutter);
+ }
+
+ /* As soon as the viewport is greater than the width of the page plus the */
+ /* gutters, just centre the content instead of adding gutters. */
+ @media (min-width: 1020px) {
+ margin-right: auto;
+ margin-left: auto;
+ }
+
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ grid-gap: var(--size-gutter);
+}
+
+
+/* By default, column spans only apply past the tablet breakpoint */
+.whole-row { grid-column-start: span 12; }
+.one-third { grid-column-start: span 12; }
+.two-thirds { grid-column-start: span 12; }
+.one-half { grid-column-start: span 12; }
+.one-quarter { grid-column-start: span 12; }
+.two-quarters { grid-column-start: span 12; }
+.three-quarters { grid-column-start: span 12; }
+
+/* Force applying the whole time */
+.mobile-whole-row { grid-column-start: span 12; }
+.mobile-one-third { grid-column-start: span 4; }
+.mobile-two-thirds { grid-column-start: span 8; }
+.mobile-one-half { grid-column-start: span 6; }
+.mobile-one-quarter { grid-column-start: span 3; }
+.mobile-two-quarters { grid-column-start: span 6; }
+.mobile-three-quarters { grid-column-start: span 9; }
+
+@media screen and (min-width: 641px) { /* tablet */
+ .whole-row { grid-column-start: span 12; }
+ .one-third { grid-column-start: span 4; }
+ .two-thirds { grid-column-start: span 8; }
+ .one-half { grid-column-start: span 6; }
+ .one-quarter { grid-column-start: span 3; }
+ .two-quarters { grid-column-start: span 6; }
+ .three-quarters { grid-column-start: span 9; }
+
+ .tablet-whole-row { grid-column-start: span 12; }
+ .tablet-one-third { grid-column-start: span 4; }
+ .tablet-two-thirds { grid-column-start: span 8; }
+ .tablet-one-half { grid-column-start: span 6; }
+ .tablet-one-quarter { grid-column-start: span 3; }
+ .tablet-two-quarters { grid-column-start: span 6; }
+ .tablet-three-quarters { grid-column-start: span 9; }
+}
+
+@media screen and (min-width: 769px) { /* desktop */
+ .desktop-whole-row { grid-column-start: span 12; }
+ .desktop-one-third { grid-column-start: span 4; }
+ .desktop-two-thirds { grid-column-start: span 8; }
+ .desktop-one-half { grid-column-start: span 6; }
+ .desktop-one-quarter { grid-column-start: span 3; }
+ .desktop-two-quarters { grid-column-start: span 6; }
+ .desktop-three-quarters { grid-column-start: span 9; }
+}
+
+/* _ _ ____ ____ ___ ____ ____ ____ ____ ____ ___ ____ ____ */
+/* |__| |___ |__| | \ |___ |__/ |___ | | | | | |___ |__/ */
+/* | | |___ | | |__/ |___ | \ | |__| |__| | |___ | \ */
+/* */
+header.masthead {
+ font-size: var(--font-size-16);
+ font-weight: var(--font-weight-bold);
+ line-height: 1;
+
+ border-bottom: 1px solid transparent;
+ color: var(--colour-brand-text);
+ background: var(--colour-brand);
+
+ &:has(> a) { padding: 16px; }
+
+ > a {
+ display: inline;
+ margin-right: var(--spacing-2);
+ font-size: var(--font-size-27);
+ line-height: var(--line-height-27);
+ text-decoration: none;
+ color: inherit;
+
+ &:visited { color: inherit; }
+ }
+
+ nav {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ &:has(input:checked) {
+ padding-top: var(--spacing-3);
+ flex-direction: column;
+ }
+
+ ul {
+ list-style-type: none;
+ align-items: center;
+ margin: 0; padding: 0;
+ height: 100%;
+
+ /* On mobile layouts, hide items by default (except for the first one, which is a checkbox in disguise).
+ When checkbox is checked, flex in a column */
+ display: flex;
+ flex-grow: 1;
+ flex-direction: row;
+ justify-content: flex-end;
+ li:not(:first-child) { display: none; }
+ &:has(input:checked){
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ margin-top: var(--spacing-3);
+ li {
+ display: block;
+ width: 100%;
+ text-align: center;
+ border-top: 1px solid var(--colour-text);
+ }
+ }
+
+ /* On tablet and above, show in a row */
+ @media screen and (min-width: 641px) { /* tablet */
+ li:has(input) { display: none !important; }
+ li { display: block !important; }
+ flex-direction: row;
+ justify-content: flex-start;
+ }
+
+ li {
+ margin: 0;
+ box-sizing: border-box;
+
+ /* Checkbox used for show/hide on mobile */
+ input {
+ appearance: none;
+ display: block; width: 100%; height: 100%;
+ outline: none;
+ border: none;
+ margin: 0 auto;
+ &::before {
+ font-size: var(--font-size-16);
+ display: block;
+ width: 100%;
+ text-align: center;
+ content: "Open Navbar";
+ font-weight: var(--font-weight-bold);
+ }
+ &:checked::before { content: "Close navbar"; }
+ }
+
+ a,input {
+ padding: var(--spacing-4);
+ display: block;
+ box-sizing: border-box;
+ width: 100%; height: 100%;
+
+ color: var(--colour-brand-text) !important;
+ text-decoration: none;
+
+ &:hover, &:focus {
+ background: var(--colour-hover);
+ color: var(--colour-text) !important;
+ }
+ }
+ }
+ }
+ }
+}
+
+footer {
+ font-size: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ padding: var(--spacing-5) 0;
+
+ border-top: 10px solid var(--colour-brand);
+ color: var(--colour-footer-text);
+ background: var(--colour-surface-background);
+}
+
+/* ______ ___ ____ __ ____ __ */
+/* | || | // \\ || )) || || (( \ */
+/* || ||=|| ||=) || ||== \\ */
+/* || || || ||_)) ||__| ||___ \_)) */
+/* */
+table {
+ font-size: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ margin-bottom: var(--spacing-6);
+
+ width: 100%;
+ display: block;
+ overflow: auto;
+
+ border-spacing: 0;
+ border-collapse: collapse;
+
+ thead { font-weight: var(--font-weight-bold); }
+
+ td,th {
+ padding: var(--spacing-2) var(--spacing-4) var(--spacing-2) 0;
+ border-bottom: 1px solid var(--colour-border);
+ text-align: left;
+ vertical-align: top;
+
+ &.numeric { text-align: right; }
+ &:last-child { padding-right: 0; }
+ }
+
+
+ caption {
+ display: table-caption;
+ font-weight: var(--font-weight-bold);
+ text-align: left;
+
+
+ &.xl, &.l, &.m { margin-bottom: var(--spacing-3); }
+
+ &.xl { font-size: var(--font-size-48); line-height: var(--line-height-48); }
+ &.l { font-size: var(--font-size-36); line-height: var(--line-height-36); }
+ &.m { font-size: var(--font-size-24); line-height: var(--line-height-24); }
+ }
+}
+
+/* ____ ____ ____ _ _ ____ */
+/* |___ | | |__/ |\/| [__ */
+/* | |__| | \ | | ___] */
+/* */
+input {
+ font-size: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ box-sizing: border-box;
+ width: 100%;
+ margin-top: 0;
+ padding: var(--spacing-1);
+
+
+ border: var(--width-input-border) var(--colour-input-border) solid;
+ border-radius: 0;
+ appearance: none;
+
+ &:focus {
+ outline: var(--width-focus) solid var(--colour-focus);
+ outline-offset: 0;
+ /* Double the border by adding its width again. Use `box-shadow` for this */
+ /* instead of changing `border-width` - this is for consistency with */
+ /* components such as textarea where we avoid changing `border-width` as */
+ /* it will change the element size. Also, `outline` cannot be utilised */
+ /* here as it is already used for the yellow focus state. */
+ box-shadow: inset 0 0 0 var(--width-input-border) var(--colour-input-border);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ color: inherit;
+ background-color: transparent;
+ cursor: not-allowed;
+ }
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ margin: 0;
+ -webkit-appearance: none;
+ }
+
+ &[type="number"] {
+ -moz-appearance: textfield;
+ }
+
+ &.extra-letter-spacing {
+ font-variant-numeric: tabular-nums;
+ letter-spacing: 0.05em;
+ }
+
+
+ @media screen and (max-width: 640px) { /* until mobile */
+ display: block;
+ max-width: 100%;
+ }
+}
+
+input-wrapper {
+ display: flex;
+ input { flex: 0 1 auto; }
+}
+
+input-prefix, input-suffix {
+ font-family: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ box-sizing: border-box;
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ min-width: 1rem;
+ padding: var(--spacing-1);
+ border: var(--width-input-border) var(--colour-input-border) solid;
+
+ background-color: var(--black-tint-95);
+ text-align: center;
+ white-space: nowrap;
+ cursor: default;
+
+}
+input-prefix { border-right: 0; }
+input-suffix { border-left: 0; }
+
+.width-30 { max-width: 29.5em; }
+.width-20 { max-width: 20.5em; }
+.width-10 { max-width: 11.5em; }
+.width-5 { max-width: 5.5em; }
+.width-4 { max-width: 4.5em; }
+.width-3 { max-width: 3.75em; }
+.width-2 { max-width: 2.75em; }
+
+
+form-group {
+ margin-bottom: var(--spacing-6);
+ display: block;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ &:has(.error), &:has(input:invalid) {
+ padding-left: var(--spacing-3);
+ border-left: var(--width-form-group-error) solid var(--colour-error);
+ }
+}
+
+input:invalid,
+textarea:invalid,
+form-group:has(.error) input,
+form-group:has(.error) textarea {
+ border-color: var(--colour-error);
+ &:focus { border-color: var(--colour-input-border); }
+}
+
+.error:not(a) {
+ font-size: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ font-weight: var(--font-weight-bold);
+
+ display: block;
+ margin-top: 0;
+ margin-bottom: var(--spacing-3);
+ clear: both;
+ color: var(--colour-error);
+}
+
+label {
+ display: block;
+ margin-bottom: var(--spacing-1);
+ color: var(--color-text);
+}
+
+h1 > label,
+h2 > label,
+h3 > label {
+ margin-bottom: var(--spacing-3);
+}
+
+h1 > label { font-size: var(--font-size-48); }
+
+h1:has(> label),
+h2:has(> label),
+h3:has(> label) {
+ margin: 0;
+}
+
+.hint {
+ font-size: var(--font-size-19);
+ line-height: var(--line-height-19);
+
+ display: block;
+ margin-bottom: var(--spacing-3);
+
+ color: var(--colour-secondary-text);
+}
+
+textarea {
+ font-size: var(--font-size-19);
+ line-height: 1.25;
+
+ display: block;
+ width: 100%;
+ min-height: 50px;
+ margin-bottom: var(--spacing-6);
+ padding: var(--spacing-1);
+
+ resize: vertical;
+ border: var(--width-input-border) solid var(--colour-input-border);
+ border-radius: 0;
+ -webkit-appearance: none;
+
+ &:focus {
+ outline: var(--width-focus) solid var(--colour-focus);
+ outline-offset: 0;
+ box-shadow: inset 0 0 0 var(--width-input-border) var(--colour-input-border);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ color: inherit;
+ background-color: transparent;
+ cursor: not-allowed;
+ }
+}
+
+button {
+ --button-bg: var(--green-shade-25);
+ --button-bg-darker: var(--green-shade-50);
+ --text: var(--colour-button-text);
+
+ text-decoration: none;
+ font-weight: var(--font-weight-normal);
+ font-size: var(--font-size-80)
+ line-height: 1;
+ text-align: center;
+
+ display: inline-block;
+ position: relative;
+
+ margin: 0 0 22px;
+ margin-bottom: 22px;
+ padding: 8px 10px 7px;
+ border: 2px solid transparent;
+
+ color: var(--text);
+ box-shadow: 0 2px 0 var(--button-bg-darker);
+ background-color: var(--button-bg);
+
+ &:hover {
+ background-color: var(--button-bg-darker);
+ }
+
+ &:active {
+ top: 2px;
+ }
+
+ &:focus {
+ border-color: var(--colour-focus);
+ box-shadow: 0 var(--width-input-border) 0 var(--colour-focus-text);
+ outline: var(--width-focus) solid transparent;
+ }
+
+ &.secondary {
+ --button-bg: var(--black-tint-95);
+ --button-bg-darker: var(--black-tint-80);
+ --text: var(--colour-text);
+ }
+ &.warning {
+ --button-bg: var(--red-shade-25);
+ --button-bg-darker: var(--red-shade-50);
+ }
+ &[disabled] {
+ opacity: .5;
+ cursor: notallowed;
+
+ &:hover {
+ background-color: var(--button-bg);
+ }
+ &:active {
+ top: 0;
+ }
+ }
+}
diff --git a/web/tinyfeed/feeds.txt b/web/tinyfeed/feeds.txt
new file mode 100644
index 0000000..9a5aea6
--- /dev/null
+++ b/web/tinyfeed/feeds.txt
@@ -0,0 +1 @@
+https://fasterthanli.me/index.xml
diff --git a/web/tinyfeed/index.html.gtpl b/web/tinyfeed/index.html.gtpl
new file mode 100644
index 0000000..9be5f8b
--- /dev/null
+++ b/web/tinyfeed/index.html.gtpl
@@ -0,0 +1,87 @@
+
+
+
+
+
+ {{.Metadata.name}}
+
+
+
+
+
+
+
+
+
+ {{range .Stylesheets}}
+
+ {{end}}
+
+ {{range .Scripts}}
+
+ {{end}}
+
+
+
+
+
+
+
+
{{.Metadata.name}}
+
+
+ {{range .Items}}
+ -
+
+ {{domain .}}
+
+
+
+ {{end}}
+
+
+
+
{{.Metadata.description}}
+
This page aggregate the following feeds:
+
+
+ Last Updated:
+
+
+
+
+
+
+
+
+
diff --git a/web/tinyfeed/justfile b/web/tinyfeed/justfile
new file mode 100644
index 0000000..36fe655
--- /dev/null
+++ b/web/tinyfeed/justfile
@@ -0,0 +1,8 @@
+build:
+ tinyfeed -i feeds.txt -o index.html -l 20 -s css.css -t index.html.gtpl
+
+watch:
+ watchexec -w index.html.gtpl just build
+
+open: build
+ firefox $(pwd)/index.html
diff --git a/web/tinyfeed/shell.nix b/web/tinyfeed/shell.nix
new file mode 100644
index 0000000..c221718
--- /dev/null
+++ b/web/tinyfeed/shell.nix
@@ -0,0 +1,12 @@
+{
+ inputs ? import ../../nix/npins,
+ pkgs ? import { overlays = [ (import ../../nix/pkgs inputs) ]; },
+}:
+pkgs.mkShell {
+
+ buildInputs = [
+ pkgs.tinyfeed
+ pkgs.watchexec
+ pkgs.just
+ ];
+}