forked from Fediversity/fediversity.eu
Compare commits
7 commits
61709abeb5
...
765e34754b
Author | SHA1 | Date | |
---|---|---|---|
765e34754b | |||
e962f92db8 | |||
2e2bf6307b | |||
656fd790a2 | |||
5303997e9a | |||
6fc4ad6293 | |||
2ce52f9530 |
21
content/navigation.nix
Normal file
21
content/navigation.nix
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{ config, ... }:
|
||||||
|
let
|
||||||
|
inherit (config) pages;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
menus.main = {
|
||||||
|
label = "Main";
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
menu.label = "Consortium";
|
||||||
|
menu.items = map (page: { inherit page; }) (with pages; [ nlnet oid tweag nordunet ]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page = pages.fediversity;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page = pages.grants;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ in
|
||||||
modules = [
|
modules = [
|
||||||
./structure
|
./structure
|
||||||
./content
|
./content
|
||||||
|
./presentation
|
||||||
{
|
{
|
||||||
_module.args = {
|
_module.args = {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
|
|
115
presentation/default.nix
Normal file
115
presentation/default.nix
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
{ config, options, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
|
types
|
||||||
|
;
|
||||||
|
templates = import ./templates.nix { inherit lib; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
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 options.files.type));
|
||||||
|
};
|
||||||
|
|
||||||
|
config.templates =
|
||||||
|
let
|
||||||
|
commonmark = name: markdown: pkgs.runCommand "${name}.html"
|
||||||
|
{
|
||||||
|
buildInputs = [ pkgs.cmark ];
|
||||||
|
} ''
|
||||||
|
cmark ${builtins.toFile "${name}.md" markdown} > $out
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
page = lib.mkDefault (config: page: {
|
||||||
|
# TODO: create static redirects from `tail page.locations`
|
||||||
|
# TODO: reconsider using `page.outPath` and what to put into `locations`.
|
||||||
|
# maybe we can avoid having ".html" suffixes there.
|
||||||
|
# since templates can output multiple files, `html` is merely one of many things we *could* produce.
|
||||||
|
# TODO: maybe it would even make sense to split routing and rendering altogether
|
||||||
|
${page.outPath} = builtins.toFile "${page.name}.html" (templates.html {
|
||||||
|
head = ''
|
||||||
|
<title>${page.title}</title>
|
||||||
|
<meta name="description" content="${page.description}" />
|
||||||
|
<link rel="canonical" href="${page.outPath}" />
|
||||||
|
'';
|
||||||
|
body = ''
|
||||||
|
${templates.nav { menu = { menu = config.menus.main; }; }}
|
||||||
|
${builtins.readFile (commonmark page.name page.body)}
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
article = lib.mkDefault (config: page: {
|
||||||
|
# TODO: create static redirects from `tail page.locations`
|
||||||
|
${page.outPath} = builtins.toFile "${page.name}.html" (templates.html {
|
||||||
|
head = ''
|
||||||
|
<title>${page.title}</title>
|
||||||
|
<meta name="description" content="${page.description}" />
|
||||||
|
<meta name="author" content="${with lib; if isList page.author then join ", " page.author else page.author}" />
|
||||||
|
'';
|
||||||
|
body = ''
|
||||||
|
${templates.nav { menu = { menu = config.menus.main; }; }}
|
||||||
|
${builtins.readFile (commonmark page.name page.body)}
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
options.files = mkOption {
|
||||||
|
description = ''
|
||||||
|
Files that make up the site, mapping from output path to contents
|
||||||
|
|
||||||
|
By default, all elements in `option`{pages} are converted to files using their template or the default template.
|
||||||
|
Add more files to the output by assigning to this attribute set.
|
||||||
|
'';
|
||||||
|
type = with types; attrsOf path;
|
||||||
|
};
|
||||||
|
|
||||||
|
config.files =
|
||||||
|
let
|
||||||
|
pages = lib.concatMapAttrs
|
||||||
|
(name: page: page.template config page)
|
||||||
|
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 {
|
||||||
|
description = ''
|
||||||
|
The final output of the web site
|
||||||
|
'';
|
||||||
|
type = types.package;
|
||||||
|
default =
|
||||||
|
let
|
||||||
|
script = ''
|
||||||
|
mkdir $out
|
||||||
|
'' + lib.join "\n" copy;
|
||||||
|
copy = lib.mapAttrsToList
|
||||||
|
(
|
||||||
|
path: file: ''
|
||||||
|
mkdir -p $out/$(dirname ${path})
|
||||||
|
cp -r ${file} $out/${path}
|
||||||
|
''
|
||||||
|
)
|
||||||
|
config.files;
|
||||||
|
in
|
||||||
|
pkgs.runCommand "source" { } script;
|
||||||
|
};
|
||||||
|
}
|
37
presentation/templates.nix
Normal file
37
presentation/templates.nix
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{ lib }:
|
||||||
|
rec {
|
||||||
|
html = { head, body }: ''
|
||||||
|
<!DOCTYPE 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" />
|
||||||
|
${lib.indent " " head}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${lib.indent " " body}
|
||||||
|
<body>
|
||||||
|
</html>
|
||||||
|
'';
|
||||||
|
nav = { menu }:
|
||||||
|
let
|
||||||
|
render-item = item:
|
||||||
|
if item ? menu then
|
||||||
|
''
|
||||||
|
<li>${item.menu.label}
|
||||||
|
${lib.indent " " (nav { menu = item; })}
|
||||||
|
''
|
||||||
|
else
|
||||||
|
if item ? page then ''<li><a href="${item.page}">${item.page.title}</a></li>''
|
||||||
|
else ''<li><a href="${item.link.url}">${item.link.label}</a></li>''
|
||||||
|
;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
${with lib; indent " " (join "\n" (map render-item menu.menu.items))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
'';
|
||||||
|
}
|
|
@ -14,15 +14,11 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.content-types = {
|
config.content-types = {
|
||||||
page = { name, config, ... }: {
|
document = { name, config, ... }: {
|
||||||
options = {
|
options = {
|
||||||
|
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
description = "Symbolic name for the page, used as a human-readable identifier";
|
description = "Symbolic name, used as a human-readable identifier";
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
};
|
|
||||||
title = mkOption {
|
|
||||||
description = "Page title";
|
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = name;
|
default = name;
|
||||||
};
|
};
|
||||||
|
@ -40,6 +36,7 @@ in
|
||||||
type = with types; functionTo str;
|
type = with types; functionTo str;
|
||||||
default = target: "TODO: compute the relative path based on `locations`";
|
default = target: "TODO: compute the relative path based on `locations`";
|
||||||
};
|
};
|
||||||
|
# TODO: may not need it when using `link`; could repurpose it to render the default template
|
||||||
outPath = mkOption {
|
outPath = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Location of the page, used for transparently creating links
|
Location of the page, used for transparently creating links
|
||||||
|
@ -47,6 +44,28 @@ in
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = lib.head config.locations;
|
default = lib.head config.locations;
|
||||||
};
|
};
|
||||||
|
# TODO: maybe it would even make sense to split routing and rendering altogether.
|
||||||
|
# in that case, templates would return strings, and a different
|
||||||
|
# piece of the machinery resolves rendering templates to files
|
||||||
|
# using `locations`.
|
||||||
|
# then we'd have e.g. `templates.html` and `templates.atom` for
|
||||||
|
# different output formats.
|
||||||
|
template = mkOption {
|
||||||
|
description = ''
|
||||||
|
Function that converts the page contents to files
|
||||||
|
'';
|
||||||
|
type = with types; functionTo (functionTo options.files.type);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
page = { name, config, ... }: {
|
||||||
|
imports = [ cfg.content-types.document ];
|
||||||
|
options = {
|
||||||
|
title = mkOption {
|
||||||
|
description = "Page title";
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
description = mkOption {
|
description = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
One-sentence description of page contents
|
One-sentence description of page contents
|
||||||
|
@ -65,14 +84,8 @@ in
|
||||||
'';
|
'';
|
||||||
type = types.str;
|
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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
config.template = cfg.templates.page;
|
||||||
};
|
};
|
||||||
|
|
||||||
article = { config, collectionName, ... }: {
|
article = { config, collectionName, ... }: {
|
||||||
|
@ -91,7 +104,43 @@ in
|
||||||
};
|
};
|
||||||
config.name = lib.slug config.title;
|
config.name = lib.slug config.title;
|
||||||
config.outPath = "${collectionName}/${lib.head config.locations}";
|
config.outPath = "${collectionName}/${lib.head config.locations}";
|
||||||
config.template = cfg.templates.article;
|
config.template = lib.mkForce cfg.templates.article;
|
||||||
|
};
|
||||||
|
|
||||||
|
named-link = { ... }: {
|
||||||
|
options = {
|
||||||
|
label = mkOption {
|
||||||
|
description = "Link label";
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
url = mkOption {
|
||||||
|
description = "Link URL";
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
navigation = { name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
description = "Symbolic name, used as a human-readable identifier";
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
label = mkOption {
|
||||||
|
description = "Menu label";
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
items = mkOption {
|
||||||
|
description = "List of menu items";
|
||||||
|
type = with types; listOf (attrTag {
|
||||||
|
menu = mkOption { type = submodule cfg.content-types.navigation; };
|
||||||
|
page = mkOption { type = submodule cfg.content-types.page; };
|
||||||
|
link = mkOption { type = submodule cfg.content-types.named-link; };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ in
|
||||||
{
|
{
|
||||||
imports = [ ./content-types.nix ];
|
imports = [ ./content-types.nix ];
|
||||||
|
|
||||||
|
# TODO: enable i18n, e.g. via a nested attribute for language-specific content
|
||||||
options.pages = mkOption {
|
options.pages = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Collection of pages on the site
|
Collection of pages on the site
|
||||||
|
@ -20,6 +21,20 @@ in
|
||||||
{
|
{
|
||||||
description = ''
|
description = ''
|
||||||
Named collections of unnamed pages
|
Named collections of unnamed pages
|
||||||
|
|
||||||
|
Define the content type of a new collection `example` to be `article`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.collections.example.type = config.types.article;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a new entry to the `example` collection:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.collections.example.entry = {
|
||||||
|
# contents here
|
||||||
|
}
|
||||||
|
```
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
||||||
options = {
|
options = {
|
||||||
|
@ -39,122 +54,10 @@ in
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
options.templates = mkOption {
|
options.menus = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Collection of named functions to convert page contents to files
|
Collection navigation menus
|
||||||
|
|
||||||
Each template function takes the complete site `config` and the page data structure.
|
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf (functionTo (functionTo options.files.type));
|
type = with types; attrsOf (submodule config.content-types.navigation);
|
||||||
};
|
|
||||||
# TODO: split out templates and all related helper junk into `../presentation`
|
|
||||||
config.templates =
|
|
||||||
let
|
|
||||||
commonmark = name: markdown: pkgs.runCommand "${name}.html"
|
|
||||||
{
|
|
||||||
buildInputs = [ pkgs.cmark ];
|
|
||||||
} ''
|
|
||||||
cmark ${builtins.toFile "${name}.md" markdown} > $out
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
page = lib.mkDefault (config: page: {
|
|
||||||
# TODO: create static redirects from `tail page.locations`
|
|
||||||
# TODO: reconsider using `page.outPath` and what to put into `locations`.
|
|
||||||
# maybe we can avoid having ".html" suffixes there.
|
|
||||||
# since templates can output multiple files, `html` is merely one of many things we *could* produce.
|
|
||||||
${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}" />
|
|
||||||
<link rel="canonical" href="${page.outPath}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${lib.indent " " (builtins.readFile (commonmark page.name page.body))}
|
|
||||||
<body>
|
|
||||||
</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 {
|
|
||||||
description = ''
|
|
||||||
Files that make up the site, mapping from output path to contents
|
|
||||||
|
|
||||||
By default, all elements in `option`{pages} are converted to files using their template or the default template.
|
|
||||||
Add more files to the output by assigning to this attribute set.
|
|
||||||
'';
|
|
||||||
type = with types; attrsOf path;
|
|
||||||
};
|
|
||||||
config.files =
|
|
||||||
let
|
|
||||||
pages = lib.concatMapAttrs
|
|
||||||
(name: page: page.template config page)
|
|
||||||
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 {
|
|
||||||
description = ''
|
|
||||||
The final output of the web site
|
|
||||||
'';
|
|
||||||
type = types.package;
|
|
||||||
default =
|
|
||||||
let
|
|
||||||
script = ''
|
|
||||||
mkdir $out
|
|
||||||
'' + lib.join "\n" copy;
|
|
||||||
copy = lib.mapAttrsToList
|
|
||||||
(
|
|
||||||
path: file: ''
|
|
||||||
mkdir -p $out/$(dirname ${path})
|
|
||||||
cp -r ${file} $out/${path}
|
|
||||||
''
|
|
||||||
)
|
|
||||||
config.files;
|
|
||||||
in
|
|
||||||
pkgs.runCommand "source" { } script;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue