forked from fediversity/fediversity
		
	implement collections
This commit is contained in:
		
							parent
							
								
									b435309994
								
							
						
					
					
						commit
						141242a86d
					
				
					 6 changed files with 286 additions and 65 deletions
				
			
		|  | @ -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…
	
	Add table
		
		Reference in a new issue
	
	 Valentin Gagarin
							Valentin Gagarin