From 0614c7c9c7a5717f5835fa6d914926a7c5dceacd Mon Sep 17 00:00:00 2001
From: Valentin Gagarin <valentin@gagarin.work>
Date: Thu, 7 Nov 2024 03:19:08 +0100
Subject: [PATCH] make page templates granularly overridable

---
 content/default.nix        | 14 ++++++++++---
 lib.nix                    |  3 +++
 presentation/default.nix   |  2 +-
 presentation/templates.nix | 22 ++++++++++----------
 structure/article.nix      | 10 +---------
 structure/collections.nix  |  2 +-
 structure/default.nix      |  6 ++----
 structure/page.nix         | 41 +++++++++++++++++---------------------
 8 files changed, 49 insertions(+), 51 deletions(-)

diff --git a/content/default.nix b/content/default.nix
index a6962907..649b5d38 100644
--- a/content/default.nix
+++ b/content/default.nix
@@ -1,13 +1,14 @@
 { config, lib, ... }:
 let
   inherit (config) pages;
+  cfg = config;
 in
 {
   imports = lib.nixFiles ./.;
 
-  collections.news.type = config.content-types.article;
+  collections.news.type = cfg.content-types.article;
 
-  pages.index = { link, ... }: {
+  pages.index = { config, link, ... }: {
     title = "Fediversity";
     description = "Fediversity web site";
     summary = ''
@@ -52,12 +53,19 @@ in
 
       ${
         let
-          sorted = with lib; reverseList (sortOn (entry: entry.date) config.collections.news.entry);
+          sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry);
         in
         lib.join "\n" (map (article: ''
           - ${article.date} [${article.title}](${link article})
         '') sorted)
       }
     '';
+    outputs.html = (cfg.templates.html.page config).override {
+      html.body.content = lib.mkForce [
+        # don't show the page title as a heading
+        (cfg.menus.main.outputs.html config)
+        (cfg.templates.html.markdown { inherit (config) name body; })
+      ];
+    };
   };
 }
diff --git a/lib.nix b/lib.nix
index f3cb7e4b..aa8cdaa7 100644
--- a/lib.nix
+++ b/lib.nix
@@ -1,5 +1,8 @@
 { lib }:
 rec {
+  template = g: f: x:
+    (g (f x)) // { override = o: g (lib.recursiveUpdate (f x) o); };
+
   /**
     Recursively replace occurrences of `from` with `to` within `string`
 
diff --git a/presentation/default.nix b/presentation/default.nix
index 3e9e266b..8f74b91c 100644
--- a/presentation/default.nix
+++ b/presentation/default.nix
@@ -21,7 +21,7 @@ in
       description = ''
         Collection of named helper functions for conversion different structured representations which can be rendered to a string
       '';
-      type = recursiveAttrs (with types; functionTo (coercedTo attrs toString str));
+      type = recursiveAttrs (with types; functionTo (either str attrs));
     };
 
   options.files = mkOption {
diff --git a/presentation/templates.nix b/presentation/templates.nix
index c76cd3a4..7a688f70 100644
--- a/presentation/templates.nix
+++ b/presentation/templates.nix
@@ -4,19 +4,21 @@ let
     mkOption
     types
     ;
-  # TODO: optionally run the whole thing through the validator
-  # https://github.com/validator/validator
-  render-html = document:
-    let
-      eval = lib.evalModules {
-        class = "DOM";
-        modules = [ document (import ./dom.nix) ];
-      };
-    in
-    toString eval.config;
 in
 {
   config.templates.html = {
+    dom = document:
+      let
+        eval = lib.evalModules {
+          class = "DOM";
+          modules = [ document (import ./dom.nix) ];
+        };
+      in
+      {
+        __toString = _: toString eval.config;
+        value = eval.config;
+      };
+
     markdown = { name, body }:
       let
         commonmark = pkgs.runCommand "${name}.html"
diff --git a/structure/article.nix b/structure/article.nix
index 67ec6045..04c6e65e 100644
--- a/structure/article.nix
+++ b/structure/article.nix
@@ -5,14 +5,6 @@ let
     types
     ;
   cfg = config;
-  render-html = document:
-    let
-      eval = lib.evalModules {
-        class = "DOM";
-        modules = [ document (import ../presentation/dom.nix) ];
-      };
-    in
-    toString eval.config;
 in
 {
   content-types.article = { config, collection, ... }: {
@@ -35,7 +27,7 @@ in
       };
     };
     config.name = lib.slug config.title;
-    config.outputs.html = lib.mkForce (render-html {
+    config.outputs.html = lib.mkForce (cfg.templates.html.dom {
       html = {
         head = {
           title.text = config.title;
diff --git a/structure/collections.nix b/structure/collections.nix
index b171c1f3..7dd12610 100644
--- a/structure/collections.nix
+++ b/structure/collections.nix
@@ -68,7 +68,7 @@ in
     in
     with lib; foldl
       (acc: elem: acc // {
-        "${head elem.locations}.html" = builtins.toFile "${elem.name}.html" elem.outputs.html;
+        "${head elem.locations}.html" = builtins.toFile "${elem.name}.html" "${elem.outputs.html}";
       })
       { }
       collections;
diff --git a/structure/default.nix b/structure/default.nix
index 4e1cdb9d..62102927 100644
--- a/structure/default.nix
+++ b/structure/default.nix
@@ -53,13 +53,11 @@ in
         #       names is soft.
         default = target: with lib; "${relativePath (head config.locations) (head target.locations)}.html";
       };
-      outputs.html = mkOption {
-        # TODO: make this of type DOM and convert to string at the output.
-        #       the output aggregator then only needs something string-coercible
+      outputs = mkOption {
         description = ''
           Representations of the document in different formats
         '';
-        type = with types; str;
+        type = with types; attrsOf (either str attrs);
       };
     };
   };
diff --git a/structure/page.nix b/structure/page.nix
index 8eef14e6..8d255c14 100644
--- a/structure/page.nix
+++ b/structure/page.nix
@@ -5,14 +5,6 @@ let
     types
     ;
   cfg = config;
-  render-html = document:
-    let
-      eval = lib.evalModules {
-        class = "DOM";
-        modules = [ document (import ../presentation/dom.nix) ];
-      };
-    in
-    toString eval.config;
 in
 {
   # TODO: enable i18n, e.g. via a nested attribute for language-specific content
@@ -27,7 +19,7 @@ in
       (acc: elem: acc // {
         # TODO: create static redirects from `tail page.locations`
         # TODO: the file name could correspond to the canonical location in the HTML representation
-        "${head elem.locations}.html" = builtins.toFile "${elem.name}.html" elem.outputs.html;
+        "${head elem.locations}.html" = builtins.toFile "${elem.name}.html" "${elem.outputs.html}";
       })
       { }
       (attrValues config.pages);
@@ -59,19 +51,22 @@ in
         type = types.str;
       };
     };
-    config.outputs.html = render-html {
-      html = {
-        head = {
-          title.text = config.title;
-          meta.description = config.description;
-          link.canonical = lib.head config.locations;
-        };
-        body.content = [
-          (cfg.menus.main.outputs.html config)
-          { section.heading.content = config.title; }
-          (cfg.templates.html.markdown { inherit (config) name body; })
-        ];
-      };
-    };
+
+    config.outputs.html = cfg.templates.html.page config;
   };
+
+  config.templates.html.page = lib.template cfg.templates.html.dom (page: {
+    html = {
+      head = {
+        title.text = page.title;
+        meta.description = page.description;
+        link.canonical = lib.head page.locations;
+      };
+      body.content = [
+        (cfg.menus.main.outputs.html page)
+        { section.heading.content = page.title; }
+        (cfg.templates.html.markdown { inherit (page) name body; })
+      ];
+    };
+  });
 }