diff --git a/website/content/default.nix b/website/content/default.nix index 9687167..6f1a0e2 100644 --- a/website/content/default.nix +++ b/website/content/default.nix @@ -1,8 +1,6 @@ -{ config, ... }: +{ config, lib, ... }: let inherit (config) pages; - files = dir: - map (name: dir + /${name}) (with builtins; attrNames (readDir dir)); in { imports = [ @@ -10,7 +8,9 @@ in ./fediversity.nix ] ++ - (files ./partners) + lib.fileset.toList ./partners + ++ + lib.fileset.toList ./news ; pages.index = { @@ -46,6 +46,12 @@ in [Read more about ${partner.title}](./${partner}) '') (with pages; [ nlnet oid tweag nordunet ]))} + + # News + + ${lib.concatStringsSep "\n" (map (article: '' + - ${article.date} [${article.title}](./${article}) + '') config.collections.news.entry)} ''; }; } diff --git a/website/content/news/website-launch.nix b/website/content/news/website-launch.nix new file mode 100644 index 0000000..ae79c07 --- /dev/null +++ b/website/content/news/website-launch.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + collections.news.entry = { + title = "Fediversity website launch"; + description = "Announcing our new website for the Fediversity project"; + date = "2024-05-15"; + author = "Laurens Hof"; + locations = [ + "website-launch.html" + ]; + body = '' + We are pleased to introduce the launch of our new website dedicated to the Fediversity project. + + The project is broad in scope, and the website reflects this. Whether you are a developer, an individual interested in the project, or want to know how the grant money is spend, the website keeps you up to date with everything you need to know. + + We're excited to show you more of the progress of the Fediversity project, and how we can build a next generation of the open internet together! + ''; + }; +} diff --git a/website/default.nix b/website/default.nix index 96b2a4b..be41a93 100644 --- a/website/default.nix +++ b/website/default.nix @@ -6,16 +6,23 @@ overlays = [ ]; } , lib ? import "${sources.nixpkgs}/lib" -, }: +let + lib' = final: prev: import ./lib.nix { lib = final; }; + lib'' = lib.extend lib'; +in { build = let - result = pkgs.lib.evalModules { + result = lib''.evalModules { modules = [ ./structure ./content - { _module.args = { inherit pkgs; }; } + { + _module.args = { + inherit pkgs; + }; + } ]; }; in diff --git a/website/lib.nix b/website/lib.nix new file mode 100644 index 0000000..36ad68f --- /dev/null +++ b/website/lib.nix @@ -0,0 +1,40 @@ +{ lib }: +rec { + /** + Create a URL-safe slug from any string + */ + slug = str: + let + # Replace non-alphanumeric characters with hyphens + replaced = join "" + ( + builtins.map + (c: + if (c >= "a" && c <= "z") || (c >= "0" && c <= "9") + then c + else "-" + ) + (with lib; stringToCharacters (toLower str))); + + # Remove leading and trailing hyphens + trimHyphens = s: + let + matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s; + in + with lib; optionalString (!isNull matched) (builtins.elemAt matched 1); + + collapseHyphens = s: + let + result = builtins.replaceStrings [ "--" ] [ "-" ] s; + in + if result == s then s else collapseHyphens result; + in + trimHyphens (collapseHyphens replaced); + + join = lib.concatStringsSep; + + splitLines = s: with builtins; filter (x: !isList x) (split "\n" s); + + indent = prefix: s: + join "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s)); +} diff --git a/website/structure/default.nix b/website/structure/default.nix index dc7780c..4e9cdf0 100644 --- a/website/structure/default.nix +++ b/website/structure/default.nix @@ -5,71 +5,130 @@ let types ; cfg = config; + types' = import ./types.nix { inherit lib; } // { + article = { config, collectionName, ... }: { + imports = [ types'.page ]; + options = { + date = mkOption { + description = "Publication date"; + type = with types; nullOr str; + default = null; + }; + author = mkOption { + description = "Page author"; + type = with types; nullOr (either str (listOf str)); + default = null; + }; + }; + config.name = lib.slug config.title; + config.outPath = "${collectionName}/${lib.head config.locations}"; + config.template = cfg.templates.article; + }; + + page = { name, config, ... }: { + options = { + name = mkOption { + description = "Symbolic name for the page, used as a human-readable identifier"; + type = types.str; + default = name; + }; + title = mkOption { + description = "Page title"; + type = types.str; + default = name; + }; + locations = mkOption { + description = '' + List of historic output locations for the resulting file + + The first element is the canonical location. + All other elements are used to create redirects to the canonical location. + ''; + type = with types; nonEmptyListOf str; + }; + outPath = mkOption { + description = '' + Location of the page, used for transparently creating links + ''; + type = types.str; + default = lib.head config.locations; + }; + description = mkOption { + description = '' + One-sentence description of page contents + ''; + type = types.str; + }; + summary = mkOption { + description = '' + One-paragraph summary of page contents + ''; + type = types.str; + }; + body = mkOption { + description = '' + Page contents in CommonMark + ''; + type = types.str; + }; + template = mkOption + { + description = '' + Function that converts the page contents to files + ''; + type = with types; functionTo (functionTo options.files.type); + default = cfg.templates.page; + }; + }; + }; + }; in { + # TODO: split out: + # - extra module system types into lib' + # - page and article types into their own module values under structure/${page,article}.nix + # yes, actually. those types should probably be configurable + config.collections.news.type = types'.article; + options.pages = mkOption { description = '' Collection of pages on the site ''; - type = with types; attrsOf (submodule ({ name, config, ... }: - { - options = { - title = mkOption { - type = types.str; - }; - locations = mkOption { - description = '' - List of historic output locations for the resulting file + type = with types; attrsOf (submodule types'.page); + }; - The first element is the canonical location. - All other elements are used to create redirects to the canonical location. - ''; - type = with types; nonEmptyListOf str; + options.collections = mkOption + { + description = '' + Named collections of unnamed pages + ''; + type = with types; attrsOf (submodule ({ name, config, ... }: { + options = { + type = mkOption { + description = "Type of entries in the collection"; + type = types.deferredModule; }; - outPath = mkOption { - description = '' - Canonical location of the page - ''; - type = types.str; - default = lib.head config.locations; + entry = mkOption { + description = "An entry in the collection"; + type = types'.collection (types.submodule ({ + _module.args.collection = config.entry; + _module.args.collectionName = name; + imports = [ config.type ]; + })); }; - description = mkOption { - description = '' - One-sentence description of page contents - ''; - type = types.str; - }; - summary = mkOption { - description = '' - One-paragraph summary of page contents - ''; - type = types.str; - }; - body = mkOption { - description = '' - Page contents in CommonMark - ''; - type = types.str; - }; - template = mkOption - { - description = '' - Function that converts the page contents to files - ''; - type = with types; functionTo (functionTo (functionTo options.files.type)); - default = cfg.templates.default; - }; }; })); - }; + }; options.templates = mkOption { description = '' Collection of named functions to convert page contents to files + + Each template function takes the complete site `config` and the page data structure. ''; - type = with types; attrsOf (functionTo (functionTo (functionTo options.files.type))); + type = with types; attrsOf (functionTo (functionTo options.files.type)); }; - config.templates.default = + config.templates = let commonmark = name: markdown: pkgs.runCommand "${name}.html" { @@ -78,10 +137,10 @@ in cmark ${builtins.toFile "${name}.md" markdown} > $out ''; in - lib.mkDefault - (config: name: page: { - # TODO: create static redirects from the tail - ${lib.head page.locations} = builtins.toFile "${name}.html" '' + { + page = lib.mkDefault (config: page: { + # TODO: create static redirects from `tail page.locations` + ${page.outPath} = builtins.toFile "${page.name}.html" ''
@@ -90,14 +149,39 @@ in