forked from Fediversity/Fediversity
implement collections
This commit is contained in:
parent
b435309994
commit
141242a86d
|
@ -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)}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
19
website/content/news/website-launch.nix
Normal file
19
website/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!
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
|
40
website/lib.nix
Normal file
40
website/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,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" ''
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
@ -90,14 +149,39 @@ in
|
|||
|
||||
<title>${page.title}</title>
|
||||
<meta name="description" content="${page.description}" />
|
||||
<link rel="canonical" href="${lib.head page.locations}" />
|
||||
<link rel="canonical" href="${page.outPath}" />
|
||||
</head>
|
||||
<body>
|
||||
${builtins.readFile (commonmark name page.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 = ''
|
||||
|
@ -108,9 +192,27 @@ in
|
|||
'';
|
||||
type = with types; attrsOf path;
|
||||
};
|
||||
config.files = lib.concatMapAttrs
|
||||
(name: page: page.template config name page)
|
||||
config.pages;
|
||||
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 = ''
|
||||
|
@ -121,7 +223,7 @@ in
|
|||
let
|
||||
script = ''
|
||||
mkdir $out
|
||||
'' + lib.concatStringsSep "\n" copy;
|
||||
'' + lib.join "\n" copy;
|
||||
copy = lib.mapAttrsToList
|
||||
(
|
||||
path: file: ''
|
||||
|
|
47
website/structure/types.nix
Normal file
47
website/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;
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue