forked from Fediversity/fediversity.eu
implement collections
This commit is contained in:
parent
e807aec04b
commit
f4b3a08a37
|
@ -1,8 +1,6 @@
|
||||||
{ config, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (config) pages;
|
inherit (config) pages;
|
||||||
files = dir:
|
|
||||||
map (name: dir + /${name}) (with builtins; attrNames (readDir dir));
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
@ -10,7 +8,9 @@ in
|
||||||
./fediversity.nix
|
./fediversity.nix
|
||||||
]
|
]
|
||||||
++
|
++
|
||||||
(files ./partners)
|
lib.fileset.toList ./partners
|
||||||
|
++
|
||||||
|
lib.fileset.toList ./news
|
||||||
;
|
;
|
||||||
|
|
||||||
pages.index = {
|
pages.index = {
|
||||||
|
@ -46,6 +46,12 @@ in
|
||||||
|
|
||||||
[Read more about ${partner.title}](./${partner})
|
[Read more about ${partner.title}](./${partner})
|
||||||
'') (with pages; [ nlnet oid tweag nordunet ]))}
|
'') (with pages; [ nlnet oid tweag nordunet ]))}
|
||||||
|
|
||||||
|
# News
|
||||||
|
|
||||||
|
${lib.concatStringsSep "\n" (map (article: ''
|
||||||
|
- ${article.date} [${article.title}](./${article})
|
||||||
|
'') config.collections.news.entry)}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
19
content/news/website-launch.nix
Normal file
19
content/news/website-launch.nix
Normal file
|
@ -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!
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
13
default.nix
13
default.nix
|
@ -6,16 +6,23 @@
|
||||||
overlays = [ ];
|
overlays = [ ];
|
||||||
}
|
}
|
||||||
, lib ? import "${sources.nixpkgs}/lib"
|
, lib ? import "${sources.nixpkgs}/lib"
|
||||||
,
|
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
lib' = final: prev: import ./lib.nix { lib = final; };
|
||||||
|
lib'' = lib.extend lib';
|
||||||
|
in
|
||||||
{
|
{
|
||||||
build =
|
build =
|
||||||
let
|
let
|
||||||
result = pkgs.lib.evalModules {
|
result = lib''.evalModules {
|
||||||
modules = [
|
modules = [
|
||||||
./structure
|
./structure
|
||||||
./content
|
./content
|
||||||
{ _module.args = { inherit pkgs; }; }
|
{
|
||||||
|
_module.args = {
|
||||||
|
inherit pkgs;
|
||||||
|
};
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
40
lib.nix
Normal file
40
lib.nix
Normal file
|
@ -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));
|
||||||
|
}
|
|
@ -5,17 +5,37 @@ let
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
cfg = config;
|
cfg = config;
|
||||||
in
|
types' = import ./types.nix { inherit lib; } // {
|
||||||
{
|
article = { config, collectionName, ... }: {
|
||||||
options.pages = mkOption {
|
imports = [ types'.page ];
|
||||||
description = ''
|
|
||||||
Collection of pages on the site
|
|
||||||
'';
|
|
||||||
type = with types; attrsOf (submodule ({ name, config, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
options = {
|
||||||
title = mkOption {
|
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;
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
title = mkOption {
|
||||||
|
description = "Page title";
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
};
|
};
|
||||||
locations = mkOption {
|
locations = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
@ -28,7 +48,7 @@ in
|
||||||
};
|
};
|
||||||
outPath = mkOption {
|
outPath = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Canonical location of the page
|
Location of the page, used for transparently creating links
|
||||||
'';
|
'';
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = lib.head config.locations;
|
default = lib.head config.locations;
|
||||||
|
@ -56,8 +76,45 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Function that converts the page contents to files
|
Function that converts the page contents to files
|
||||||
'';
|
'';
|
||||||
type = with types; functionTo (functionTo (functionTo options.files.type));
|
type = with types; functionTo (functionTo options.files.type);
|
||||||
default = cfg.templates.default;
|
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 types'.page);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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 ];
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
@ -66,10 +123,12 @@ in
|
||||||
options.templates = mkOption {
|
options.templates = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Collection of named functions to convert page contents to files
|
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
|
let
|
||||||
commonmark = name: markdown: pkgs.runCommand "${name}.html"
|
commonmark = name: markdown: pkgs.runCommand "${name}.html"
|
||||||
{
|
{
|
||||||
|
@ -78,10 +137,10 @@ in
|
||||||
cmark ${builtins.toFile "${name}.md" markdown} > $out
|
cmark ${builtins.toFile "${name}.md" markdown} > $out
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
lib.mkDefault
|
{
|
||||||
(config: name: page: {
|
page = lib.mkDefault (config: page: {
|
||||||
# TODO: create static redirects from the tail
|
# TODO: create static redirects from `tail page.locations`
|
||||||
${lib.head page.locations} = builtins.toFile "${name}.html" ''
|
${page.outPath} = builtins.toFile "${page.name}.html" ''
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
@ -90,14 +149,39 @@ in
|
||||||
|
|
||||||
<title>${page.title}</title>
|
<title>${page.title}</title>
|
||||||
<meta name="description" content="${page.description}" />
|
<meta name="description" content="${page.description}" />
|
||||||
<link rel="canonical" href="${lib.head page.locations}" />
|
<link rel="canonical" href="${page.outPath}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${builtins.readFile (commonmark name page.body)}
|
${lib.indent " " (builtins.readFile (commonmark page.name page.body))}
|
||||||
<body>
|
<body>
|
||||||
</html>
|
</html>
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
article = lib.mkDefault (config: page: {
|
||||||
|
# TODO: create static redirects from `tail page.locations`
|
||||||
|
${page.outPath} = builtins.toFile "${page.name}.html" ''
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>${page.title}</title>
|
||||||
|
<meta name="description" content="${page.description}" />
|
||||||
|
${with lib;
|
||||||
|
if ! isNull page.author then
|
||||||
|
''<meta name="author" content="${if isList page.author then join ", " page.author else page.author}" />''
|
||||||
|
else ""
|
||||||
|
}
|
||||||
|
<link rel="canonical" href="${page.outPath}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${lib.indent " " (builtins.readFile (commonmark page.name page.body))}
|
||||||
|
<body>
|
||||||
|
</html>
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
options.files = mkOption {
|
options.files = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
@ -108,9 +192,27 @@ in
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf path;
|
type = with types; attrsOf path;
|
||||||
};
|
};
|
||||||
config.files = lib.concatMapAttrs
|
config.files =
|
||||||
(name: page: page.template config name page)
|
let
|
||||||
|
pages = lib.concatMapAttrs
|
||||||
|
(name: page: page.template config page)
|
||||||
config.pages;
|
config.pages;
|
||||||
|
collections =
|
||||||
|
let
|
||||||
|
byCollection = with lib; mapAttrs
|
||||||
|
(_: collection:
|
||||||
|
map (entry: entry.template config entry) collection.entry
|
||||||
|
)
|
||||||
|
config.collections;
|
||||||
|
in
|
||||||
|
with lib; concatMapAttrs
|
||||||
|
(collection: entries:
|
||||||
|
foldl' (acc: entry: acc // entry) { } entries
|
||||||
|
)
|
||||||
|
byCollection;
|
||||||
|
in
|
||||||
|
pages // collections;
|
||||||
|
|
||||||
|
|
||||||
options.build = mkOption {
|
options.build = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
@ -121,7 +223,7 @@ in
|
||||||
let
|
let
|
||||||
script = ''
|
script = ''
|
||||||
mkdir $out
|
mkdir $out
|
||||||
'' + lib.concatStringsSep "\n" copy;
|
'' + lib.join "\n" copy;
|
||||||
copy = lib.mapAttrsToList
|
copy = lib.mapAttrsToList
|
||||||
(
|
(
|
||||||
path: file: ''
|
path: file: ''
|
||||||
|
|
47
structure/types.nix
Normal file
47
structure/types.nix
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) types;
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
collection = elemType:
|
||||||
|
let
|
||||||
|
unparenthesize = class: class == "noun";
|
||||||
|
desc = type:
|
||||||
|
types.optionDescriptionPhrase unparenthesize type;
|
||||||
|
desc' = type:
|
||||||
|
let
|
||||||
|
typeDesc = types.optionDescriptionPhrase unparenthesize type;
|
||||||
|
in
|
||||||
|
if type.descriptionClass == "noun"
|
||||||
|
then
|
||||||
|
typeDesc + "s"
|
||||||
|
else
|
||||||
|
"many instances of ${typeDesc}";
|
||||||
|
in
|
||||||
|
types.mkOptionType {
|
||||||
|
name = "collection";
|
||||||
|
description = "separately specified ${desc elemType} for a collection of ${desc' elemType}";
|
||||||
|
merge = loc: defs:
|
||||||
|
map
|
||||||
|
(def:
|
||||||
|
let
|
||||||
|
merged = lib.mergeDefinitions
|
||||||
|
(loc ++ [ "[definition ${toString def.file}]" ])
|
||||||
|
elemType
|
||||||
|
[{ inherit (def) file; value = def.value; }];
|
||||||
|
in
|
||||||
|
if merged ? mergedValue then merged.mergedValue else merged.value
|
||||||
|
)
|
||||||
|
defs;
|
||||||
|
check = elemType.check;
|
||||||
|
getSubOptions = elemType.getSubOptions;
|
||||||
|
getSubModules = elemType.getSubModules;
|
||||||
|
substSubModules = m: collection (elemType.substSubModules m);
|
||||||
|
functor = (lib.defaultFunctor "collection") // {
|
||||||
|
type = collection;
|
||||||
|
wrapped = elemType;
|
||||||
|
payload = { };
|
||||||
|
};
|
||||||
|
nestedTypes.elemType = elemType;
|
||||||
|
};
|
||||||
|
}
|
Reference in a new issue