commit 54bf9da2a32efc119c84d8dc76b57a7fee670f82 Author: Kiara Grouwstra Date: Mon Apr 14 11:20:58 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726d2d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +result +.direnv diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc5ac99 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Fediversity web site + +This web site is built with a static site generator based on the Nix language [module system](https://nix.dev/tutorials/module-system/). +It has unique features such as: +- correct-by-construction relative links, automatic redirects for moved pages +- correct-by-construction content fields +- customisable templating and content structure, all seamlessly expressed in the Nix language +- correct-by-construction spec-compliant HTML output +- content source organisation independent of output structure + +Structured content is managed through Nix expressions, and copy is written in [CommonMark](https://commonmark.org/). + +# Contributing + +- [Install Nix](https://nix.dev/install-nix) +- [Set up `direnv`](https://github.com/nix-community/nix-direnv#installation) +- Run `direnv allow` in the directory where repository is stored on your machine + + > **Note** + > + > This is a security boundary, and allows automatically running code from this repository on your machine. + +- Start a live preview in a different terminal: + + ```bash + devmode + ``` + + This will open your default web browser and automatically reload the page when the source changes. + +- Edit any of the files, see [repository layout](#repository-layout) for guidance + +# Testing + +As a derivation, e.g. for CI: + +```bash +nix-build -A tests +``` + +In the development shell: + +```bash +run-tests +``` + +Running tests in a loop on source code changes: + +```bash +test-loop +``` + +# Repository layout + +- [content](./content) + + Content of the web site is managed here. + The entry point is [`content/default.nix`](./content/default.nix) and is built to correspond to `index.html` in the result. + All other content sources are automatically included in `imports`, and can be accessed though the `config` module argument. + +- [structure](./structure) + + Definitions of content data structures, such as pages, articles, menus, collections, etc. + +- [presentation](./presentation) + + Code specific to how the web site is rendered. + In particular, it encodes the mechanism for distributing content to files, and for putting together files for the final result. + + In principle, different output formats (such as RSS feeds) are possible, and would be implemented there. + +- [default.nix](./default.nix) + + Entry point for building the project. + This is where content, structure, and presentation are wired up. + +- [shell.nix](./shell.nix) + + Convenience wrapper to enable running `nix-shell` without arguments. + +- [lib.nix](./lib.nix) + + Reusable convenience functions. + Also exposed under the `lib` attribute in [default.nix](./default.nix). + +- [npins](./npins) + + Dependencies, managed with [`npins`](https://github.com/andir/npins/). + +- [README.md](./README.md) + + This file. diff --git a/assets/images/avatar-sm.png b/assets/images/avatar-sm.png new file mode 100644 index 0000000..e1699dc Binary files /dev/null and b/assets/images/avatar-sm.png differ diff --git a/assets/images/avatar.png b/assets/images/avatar.png new file mode 100755 index 0000000..387b035 Binary files /dev/null and b/assets/images/avatar.png differ diff --git a/assets/images/avhuffelenmastodonpin.jpg b/assets/images/avhuffelenmastodonpin.jpg new file mode 100644 index 0000000..ff8d07d Binary files /dev/null and b/assets/images/avhuffelenmastodonpin.jpg differ diff --git a/assets/images/banner.png b/assets/images/banner.png new file mode 100644 index 0000000..5f8a9e8 Binary files /dev/null and b/assets/images/banner.png differ diff --git a/assets/images/bergen-airport.jpeg b/assets/images/bergen-airport.jpeg new file mode 100644 index 0000000..1c02622 Binary files /dev/null and b/assets/images/bergen-airport.jpeg differ diff --git a/assets/images/call-to-action.png b/assets/images/call-to-action.png new file mode 100755 index 0000000..ec2d625 Binary files /dev/null and b/assets/images/call-to-action.png differ diff --git a/assets/images/checkbox-illustration-scaled.png b/assets/images/checkbox-illustration-scaled.png new file mode 100644 index 0000000..e4bb37d Binary files /dev/null and b/assets/images/checkbox-illustration-scaled.png differ diff --git a/assets/images/checkbox-illustration.png b/assets/images/checkbox-illustration.png new file mode 100644 index 0000000..0bd2196 Binary files /dev/null and b/assets/images/checkbox-illustration.png differ diff --git a/assets/images/code.png b/assets/images/code.png new file mode 100644 index 0000000..763b5a2 Binary files /dev/null and b/assets/images/code.png differ diff --git a/assets/images/dc1.jpg b/assets/images/dc1.jpg new file mode 100644 index 0000000..74a7b92 Binary files /dev/null and b/assets/images/dc1.jpg differ diff --git a/assets/images/favicon-archive.png b/assets/images/favicon-archive.png new file mode 100644 index 0000000..13f24a3 Binary files /dev/null and b/assets/images/favicon-archive.png differ diff --git a/assets/images/gallery/01.jpg b/assets/images/gallery/01.jpg new file mode 100644 index 0000000..662fc1d Binary files /dev/null and b/assets/images/gallery/01.jpg differ diff --git a/assets/images/gallery/02.jpg b/assets/images/gallery/02.jpg new file mode 100644 index 0000000..22fb37f Binary files /dev/null and b/assets/images/gallery/02.jpg differ diff --git a/assets/images/gallery/03.jpg b/assets/images/gallery/03.jpg new file mode 100644 index 0000000..cea735f Binary files /dev/null and b/assets/images/gallery/03.jpg differ diff --git a/assets/images/gallery/04.jpg b/assets/images/gallery/04.jpg new file mode 100644 index 0000000..48d7c32 Binary files /dev/null and b/assets/images/gallery/04.jpg differ diff --git a/assets/images/gallery/05.jpg b/assets/images/gallery/05.jpg new file mode 100644 index 0000000..0987809 Binary files /dev/null and b/assets/images/gallery/05.jpg differ diff --git a/assets/images/gallery/06.jpg b/assets/images/gallery/06.jpg new file mode 100644 index 0000000..662fc1d Binary files /dev/null and b/assets/images/gallery/06.jpg differ diff --git a/assets/images/globe.png b/assets/images/globe.png new file mode 100644 index 0000000..aec6654 Binary files /dev/null and b/assets/images/globe.png differ diff --git a/assets/images/home.svg b/assets/images/home.svg new file mode 100644 index 0000000..164ca2f --- /dev/null +++ b/assets/images/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/image-placeholder.png b/assets/images/image-placeholder.png new file mode 100755 index 0000000..a61a0c0 Binary files /dev/null and b/assets/images/image-placeholder.png differ diff --git a/assets/images/kabelachterkant.jpg b/assets/images/kabelachterkant.jpg new file mode 100644 index 0000000..7961201 Binary files /dev/null and b/assets/images/kabelachterkant.jpg differ diff --git a/assets/images/logo-archive.png b/assets/images/logo-archive.png new file mode 100644 index 0000000..afee107 Binary files /dev/null and b/assets/images/logo-archive.png differ diff --git a/assets/images/logo-darkmode-archive.png b/assets/images/logo-darkmode-archive.png new file mode 100644 index 0000000..c85cca9 Binary files /dev/null and b/assets/images/logo-darkmode-archive.png differ diff --git a/assets/images/logo-darkmode.png b/assets/images/logo-darkmode.png new file mode 100644 index 0000000..6dee57c Binary files /dev/null and b/assets/images/logo-darkmode.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..6dee57c Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/mastodon.svg b/assets/images/mastodon.svg new file mode 100644 index 0000000..3d811ac --- /dev/null +++ b/assets/images/mastodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/no-search-found.png b/assets/images/no-search-found.png new file mode 100755 index 0000000..1e1e6e1 Binary files /dev/null and b/assets/images/no-search-found.png differ diff --git a/assets/images/og-image.png b/assets/images/og-image.png new file mode 100644 index 0000000..e31ac0e Binary files /dev/null and b/assets/images/og-image.png differ diff --git a/assets/images/rack.jpg b/assets/images/rack.jpg new file mode 100644 index 0000000..a963c26 Binary files /dev/null and b/assets/images/rack.jpg differ diff --git a/assets/images/service-1.png b/assets/images/service-1.png new file mode 100755 index 0000000..5842791 Binary files /dev/null and b/assets/images/service-1.png differ diff --git a/assets/images/service-2.png b/assets/images/service-2.png new file mode 100755 index 0000000..2cc116a Binary files /dev/null and b/assets/images/service-2.png differ diff --git a/assets/images/service-3.png b/assets/images/service-3.png new file mode 100755 index 0000000..cf690b7 Binary files /dev/null and b/assets/images/service-3.png differ diff --git a/assets/images/stepping-up.png b/assets/images/stepping-up.png new file mode 100644 index 0000000..1dbd9f1 Binary files /dev/null and b/assets/images/stepping-up.png differ diff --git a/assets/images/tvtoren1.jpg b/assets/images/tvtoren1.jpg new file mode 100644 index 0000000..0475d86 Binary files /dev/null and b/assets/images/tvtoren1.jpg differ diff --git a/assets/images/uitzicht.jpg b/assets/images/uitzicht.jpg new file mode 100644 index 0000000..aaf0ab8 Binary files /dev/null and b/assets/images/uitzicht.jpg differ diff --git a/assets/images/user.png b/assets/images/user.png new file mode 100644 index 0000000..810e426 Binary files /dev/null and b/assets/images/user.png differ diff --git a/assets/images/users-scaled.png b/assets/images/users-scaled.png new file mode 100644 index 0000000..519e40d Binary files /dev/null and b/assets/images/users-scaled.png differ diff --git a/assets/images/users.png b/assets/images/users.png new file mode 100644 index 0000000..e852fe7 Binary files /dev/null and b/assets/images/users.png differ diff --git a/assets/images/users.svg b/assets/images/users.svg new file mode 100644 index 0000000..5bf51a7 --- /dev/null +++ b/assets/images/users.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/website-new.png b/assets/images/website-new.png new file mode 100644 index 0000000..8440904 Binary files /dev/null and b/assets/images/website-new.png differ diff --git a/assets/scss/base.scss b/assets/scss/base.scss new file mode 100755 index 0000000..336b27d --- /dev/null +++ b/assets/scss/base.scss @@ -0,0 +1,59 @@ +html { + @apply text-base-sm md:text-base; +} + +body { + @apply bg-body font-primary font-normal leading-relaxed text-text; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + @apply font-secondary font-bold leading-tight text-dark; +} + +h1, +.h1 { + @apply text-h1-sm md:text-h1; +} + +h2, +.h2 { + @apply text-h2-sm md:text-h2; +} + +h3, +.h3 { + @apply text-h3-sm md:text-h3; +} + +h4, +.h4 { + @apply text-h4; +} + +h5, +.h5 { + @apply text-h5; +} + +h6, +.h6 { + @apply text-h6; +} + +b, +strong { + @apply font-semibold; +} + +code { + @apply after:border-none; +} + +blockquote > p { + @apply my-0 #{!important}; +} diff --git a/assets/scss/buttons.scss b/assets/scss/buttons.scss new file mode 100755 index 0000000..8b4c8b1 --- /dev/null +++ b/assets/scss/buttons.scss @@ -0,0 +1,15 @@ +.btn { + @apply inline-block rounded border border-transparent px-5 py-2 font-semibold capitalize transition; +} + +.btn-sm { + @apply rounded-sm px-4 py-1.5 text-sm; +} + +.btn-primary { + @apply border-primary bg-primary text-white; +} + +.btn-outline-primary { + @apply border-dark text-dark hover:bg-dark bg-transparent hover:text-white; +} diff --git a/assets/scss/components.scss b/assets/scss/components.scss new file mode 100755 index 0000000..97eb781 --- /dev/null +++ b/assets/scss/components.scss @@ -0,0 +1,74 @@ +main { + min-height: 70vh; +} + +// section style +.section { + @apply py-24 xl:py-28; + &-sm { + @apply py-16 xl:py-20; + } +} + +// container +.container { + @apply mx-auto px-4 2xl:max-w-[1320px]; +} + +// form style +.form-input { + @apply bg-theme-light text-dark placeholder:text-light focus:border-primary w-full rounded border-transparent px-6 py-4 focus:ring-transparent; +} + +.form-label { + @apply font-secondary text-dark mb-4 block text-xl font-normal; +} + +// social icons +.social-icons { + @apply space-x-4; + li { + @apply inline-block; + a { + @apply bg-primary flex h-9 w-9 items-center justify-center rounded text-center leading-9 text-white; + svg { + @apply h-5 w-5; + } + } + } +} + +// swiper pagination +.swiper-pagination-bullet { + @apply bg-theme-light h-2.5 w-2.5 opacity-100 mx-1.5 #{!important}; + + &-active { + @apply bg-primary h-4 w-4 #{!important}; + } +} + +// content style +.content { + @apply prose max-w-none; + @apply prose-headings:mb-[.3em] prose-headings:mt-[.6em] prose-headings:text-dark; + @apply prose-h1:text-h1-sm md:prose-h1:text-h1; + @apply prose-h2:text-h2-sm md:prose-h2:text-h2; + @apply prose-h3:text-h3-sm md:prose-h3:text-h3; + @apply prose-img:max-w-full prose-img:rounded; + @apply prose-hr:border-border; + @apply prose-p:text-base prose-p:text-text; + @apply prose-blockquote:rounded-lg prose-blockquote:border prose-blockquote:border-l-[10px] prose-blockquote:border-primary prose-blockquote:bg-theme-light prose-blockquote:px-8 prose-blockquote:py-10 prose-blockquote:font-secondary prose-blockquote:text-2xl prose-blockquote:not-italic prose-blockquote:text-dark; + @apply prose-pre:rounded-lg prose-pre:bg-theme-light; + @apply prose-code:px-1; + @apply prose-strong:text-dark; + @apply prose-a:text-text prose-a:underline hover:prose-a:text-primary; + @apply prose-li:text-text; + @apply prose-table:relative prose-table:overflow-hidden prose-table:rounded-lg prose-table:before:absolute prose-table:before:left-0 prose-table:before:top-0 prose-table:before:h-full prose-table:before:w-full prose-table:before:rounded-[inherit] prose-table:before:border prose-table:before:content-[""]; + @apply prose-thead:border-border prose-thead:bg-theme-light; + @apply prose-th:relative prose-th:z-10 prose-th:px-4 prose-th:py-[18px] prose-th:text-dark; + @apply prose-tr:border-border; + @apply prose-td:relative prose-td:z-10 prose-td:px-3 prose-td:py-[18px]; + .btn { + @apply no-underline hover:text-white #{!important}; + } +} diff --git a/assets/scss/custom.scss b/assets/scss/custom.scss new file mode 100755 index 0000000..ae59615 --- /dev/null +++ b/assets/scss/custom.scss @@ -0,0 +1,127 @@ +// Add your own custom styles here +.grid-container { + display: flex; + justify-content: space-between; + } + + .column { + flex-basis: calc(50% - 10px); /* Adjust width as necessary */ + } + + .list { + list-style-type: none; + padding: 0; + } + + .list-item { + margin-bottom: 10px; + } + + .link { + text-decoration: none; + color: inherit; + } + + .title { + font-weight: bold; + } + + .hr-list { + border: 0; + border-top: 1px solid #ccc; + margin-top: 5px; + margin-bottom: 5px; + } + + .list-item { + display: flex; + justify-content: space-between; + align-items: center; + } + + .content { + flex: 1; + } + + .link { + text-align: left; + } + + .time { + text-align: right; + } + + + .grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + } + + .grid-item { + + padding: 20px; + } + + .header-with-image { + display: flex; + align-items: flex-start; + } + + .header-with-image img { + margin-right: 10px; + max-width: 100px; /* Adjust as needed */ + max-height: 100px; /* Adjust as needed */ + align-items: center; + } + + .read-more-link { + color: #FF6E00; /* Use the variable defined in theme.json */ + } + + .center-wrapper { + display: flex; + justify-content: center; + align-items: center; + + } + + .grid-container-small { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + .hr-list { + margin-top: 0; + margin-bottom: 0; + margin-right: .5rem; + } + + .center-layout { + display: flex; + justify-content: center; + + + } + + .hr-list2 { + border: 20; + border-top: 1px solid #FF6E00; + margin-top: 5px; + margin-bottom: 5px; + } + + .header-with-image2 { + text-align: center; + } + + .header-with-image2 img { + display: inline-block; + + } + + .line { + border-top: 1px solid #FF6E00; /* Change color and thickness as needed */ + margin: 10px 0; /* Adjust spacing between the line and the divs */ +} \ No newline at end of file diff --git a/assets/scss/main.scss b/assets/scss/main.scss new file mode 100755 index 0000000..4bbaf78 --- /dev/null +++ b/assets/scss/main.scss @@ -0,0 +1,30 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + @import "base"; +} + +@layer components { + @import "components"; + @import "navigation"; + @import "buttons"; +} + +@layer utilities { + @import "utilities"; +} + +@import "search"; +@import "social-share"; +@import "gallery-slider"; +@import "images"; +@import "toc"; +@import "tab"; +@import "accordion"; +@import "modal"; +@import "notice"; + +@import "module-overrides"; +@import "custom"; diff --git a/assets/scss/module-overrides.scss b/assets/scss/module-overrides.scss new file mode 100644 index 0000000..8a12267 --- /dev/null +++ b/assets/scss/module-overrides.scss @@ -0,0 +1,57 @@ +// table of contents +.table-of-content { + @apply overflow-hidden rounded; +} + +// share icons +.share-icons { + .share-link { + @apply h-9 w-9 rounded leading-9; + @apply bg-primary hover:bg-primary; + } + .share-icon svg { + } +} + +// notice +.notice { + @apply rounded-lg; +} + +// tab +.tab { + @apply border-border overflow-hidden rounded-lg border; + &-nav { + @apply border-border bg-theme-light pl-4; + + &-item { + @apply text-dark px-8 text-lg #{!important}; + &.active { + @apply border-dark; + } + } + } + &-content { + &-panel { + @apply px-4 pt-0 #{!important}; + } + } +} + +// accordion +.accordion { + @apply border-border bg-theme-light mb-6 overflow-hidden rounded-lg border; + &-header { + @apply text-dark; + } +} + +// cookie consent +.cookie-box { + @apply rounded-lg #{!important}; +} + +// slider +.gallery-slider { + @apply ml-0 #{!important}; +} diff --git a/assets/scss/navigation.scss b/assets/scss/navigation.scss new file mode 100755 index 0000000..a97aa2c --- /dev/null +++ b/assets/scss/navigation.scss @@ -0,0 +1,87 @@ +// navbar toggler +input#nav-toggle:checked + label #show-button { + @apply hidden; +} + +input#nav-toggle:checked + label #hide-button { + @apply block; +} + +input#nav-toggle:checked ~ #nav-menu { + @apply block; +} + +.header { + @apply bg-body py-6; +} + +// navbar items +.navbar { + @apply relative flex flex-wrap items-center justify-between; +} + +.navbar-brand { + @apply text-dark text-xl font-semibold; + image { + @apply max-h-full max-w-full; + } +} + +.navbar-nav { + @apply text-center lg:text-left; +} + +// .nav-item { +// @apply mx-3; +// } + +.nav-link { + @apply text-dark hover:text-primary block p-3 cursor-pointer font-semibold transition lg:px-2 lg:py-3; +} + +.nav-dropdown { + @apply mr-0; + & > svg { + @apply pointer-events-none; + } + &.active { + .nav-dropdown-list { + @apply block; + } + } +} + +.nav-dropdown-list { + @apply bg-body z-10 min-w-[180px] rounded p-4 shadow hidden lg:invisible lg:absolute lg:block lg:opacity-0; +} + +.nav-dropdown-item { + @apply [&:not(:last-child)]:mb-2; +} + +.nav-dropdown-link { + @apply text-dark hover:text-primary block py-1 font-semibold transition; +} + +//theme-switcher +.theme-switcher { + @apply inline-flex; + + label { + @apply bg-border relative inline-block h-4 w-6 cursor-pointer rounded-2xl lg:w-10; + } + + input { + @apply absolute opacity-0; + } + + span { + @apply bg-dark absolute -top-1 left-0 flex h-6 w-6 items-center justify-center rounded-full transition-all duration-300; + } + + input:checked + label { + span { + @apply lg:left-4; + } + } +} diff --git a/assets/scss/utilities.scss b/assets/scss/utilities.scss new file mode 100755 index 0000000..7889b3a --- /dev/null +++ b/assets/scss/utilities.scss @@ -0,0 +1,20 @@ +.bg-gradient { + @apply bg-gradient-to-b from-[rgba(249,249,249,1)] from-[0.53%] to-white to-[83.28%]; +} + +.rounded-sm { + @apply rounded-[4px]; +} +.rounded { + @apply rounded-[6px]; +} +.rounded-lg { + @apply rounded-[12px]; +} +.rounded-xl { + @apply rounded-[16px]; +} + +.shadow { + box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05); +} diff --git a/content/default.nix b/content/default.nix new file mode 100644 index 0000000..0aa4dc3 --- /dev/null +++ b/content/default.nix @@ -0,0 +1,184 @@ +{ config, lib, ... }: +let + inherit (config) pages; + cfg = config; +in +{ + imports = lib.nixFiles ./.; + + collections.news.type = cfg.content-types.article; + collections.events.type = cfg.content-types.event; + + pages.index = + { config, link, ... }: + { + title = "Welcome to the Fediversity project"; + description = "Fediversity web site"; + summary = '' + This web site hosts up-to-date information about the the NGI Zero Fediversity project. + ''; + body = '' + ${pages.fediversity.summary} + + [Learn more about Fediversity](${link pages.fediversity}) + ''; + outputs.html = (cfg.templates.html.page config).override ( + _final: prev: { + html = { + head.title.text = "Fediversity"; + head.link.stylesheets = prev.html.head.link.stylesheets ++ [ + { href = "${link cfg.assets."index.css"}"; } + ]; + body.content = + let + to-section = + { + heading, + body, + attrs ? { }, + }: + { + section = { + heading.content = heading; + inherit attrs; + content = [ + (cfg.templates.html.markdown { + name = "${config.name}-${lib.slug heading}"; + inherit body; + }) + ]; + }; + }; + in + [ + (lib.head prev.html.body.content) + { + section = { + attrs = { }; + heading.content = config.title; + content = + [ + (cfg.templates.html.markdown { inherit (config) name body; }) + ] + ++ (map to-section [ + { + heading = "Fediversity grants"; + body = '' + ${pages.grants.summary} + + [Learn more about Fediversity grants](${link pages.grants}) + ''; + } + { + heading = "Consortium"; + body = '' + The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag. + + ${toString ( + map + (partner: '' + ### ${partner.title} + + ${partner.summary} + + [Read more about ${partner.title}](${link partner}) + '') + ( + with pages; + [ + nlnet + oid + tweag + nordunet + ] + ) + )} + ''; + } + { + heading = "Fediverse explained"; + body = '' + ${toString ( + map + (role: '' + ### ${role.title} + + ${role.summary} + + [Read more about ${role.title}](${link role}) + '') + ( + with pages; + [ + individuals + developers + european-commission + ] + ) + )} + ''; + } + ]); + }; + } + ] + ++ (map to-section [ + { + heading = "News"; + attrs = { + class = [ "collection" ]; + }; + body = + let + sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry); + in + lib.join "\n" ( + map (article: '' + - ${article.date} [${article.title}](${link article}) + '') sorted + ); + } + { + heading = "Events"; + attrs = { + class = [ "collection" ]; + }; + body = + let + sorted = with lib; reverseList (sortOn (entry: entry.start-date) cfg.collections.events.entry); + in + lib.join "\n" ( + map (article: '' + - ${article.start-date} [${article.title}](${link article}) + '') sorted + ); + } + ]); + }; + + } + ); + }; + + assets."index.css".path = + with lib; + builtins.toFile "index.css" '' + section h1, section h2, section h3 + { + text-align: center; + } + section h1 { + font-size: 3em; + } + section h2 { + font-size: 2.5em; + } + section h3 { + font-size: 1.5em; + } + section.collection h1 { + font-size: 2em; + text-align: left; + } + ''; +} diff --git a/content/developers.nix b/content/developers.nix new file mode 100644 index 0000000..5416d95 --- /dev/null +++ b/content/developers.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + pages.developers = { + title = "Developers"; + description = "Information for developers about the Fediversity project"; + summary = '' + As a developer building the next generation of social platforms, you are looking to make it easier to facilitate your customers to use your product. Fediversity can help. + ''; + body = '' + The Fediversity Project enables easy hosting for a wide variety of fediverse platforms, all based on NixOS. At the start, the project will support Mastodon, PixelFed,PeerTube, Matrix and Nexcloud, and the project is actively working to expand this offering. Other services that are offered are email (based on Stalwart) and domain registry. + + As part of the NGI Funding, the Fediversity Project also offers grants to developers to expand the ecosystem. The NLNet website has more information on how you as a developer can apply to grants, ranging from 5.000 to 50.000 euro's. + + If you are a developer of fediverse software, and would like to get your platform also offered for easy hosting as part of the Fediversity Project, please reach out to us. You can contact us HERE. + + As the project is based upon NixOS, we are actively supporting making fediverse projects available as nix packages. If your project is on the fence about this, please reach out. Nix packages make updating and maintaining fediverse projects a breeze! + ''; + }; +} diff --git a/content/european-commission.nix b/content/european-commission.nix new file mode 100644 index 0000000..79a0438 --- /dev/null +++ b/content/european-commission.nix @@ -0,0 +1,15 @@ +{ ... }: +{ + pages.european-commission = { + title = "European Commission"; + description = "Information about the Fediversity project for the grant providers"; + summary = '' + The Fediversity Project operates on a grant gratiously provided by the HORIZON fund by the EC. Learn more about the accountability of the project. + ''; + body = '' + The Fediversity project implements the visions outlined by the Next Generation Internet (NGI) initiative for an open internet in several ways. Most importantly, it helps with decentralisation of the internet, a core principle of the NGI, by making it easier for people to participate in the Open Social Web on their own terms. NGI's goal of empowering individuals in the digital sphere is helped by making it easy for them to set up their own servers and platforms. While a variety of Fediverse software exist, there are still barriers of entry for people. In order for people to be truly empowered, joining the fediverse needs to be as weasy as possible. Additionally, the Fediverse emphasises interoperability and openness, which are key concerns addressed by the NGI. + + Part of the values of openness and transparency of is that the Fediversity project is that all the deliverables of the projects are have a 'Public' Dissemination level. On this page an overview of all deliverables of the Fediversity project can be found. + ''; + }; +} diff --git a/content/events.nix b/content/events.nix new file mode 100644 index 0000000..6d92ffb --- /dev/null +++ b/content/events.nix @@ -0,0 +1,26 @@ +{ config, lib, ... }: +{ + pages.events = + { link, ... }: + rec { + title = "Events"; + description = "Events related to the Fediverse and NixOS"; + summary = description; + body = + with lib; + let + events = map ( + event: with lib; '' + ## [${event.title}](${link event}) + + ${event.start-date} ${ + optionalString (!isNull event.end-date && event.end-date != event.start-date) "to ${event.end-date}" + } in ${event.location} + '' + ) config.collections.events.entry; + in + '' + ${join "\n" events} + ''; + }; +} diff --git a/content/events/2024-11-zurich-zhf.nix b/content/events/2024-11-zurich-zhf.nix new file mode 100644 index 0000000..8f63458 --- /dev/null +++ b/content/events/2024-11-zurich-zhf.nix @@ -0,0 +1,25 @@ +{ config, ... }: +{ + collections.events.entry = + { link, ... }: + { + title = "NixOS 24.11 ZHF hackathon"; + name = "zhf-24-11"; + description = "NixOS 24.11 ZHF hackathon in Zürich"; + start-date = "2024-11-23"; + end-date = "2024-11-24"; + start-time = "10:00"; + end-time = "17:00"; + location = "OST Campus Rapperswil"; + body = '' + The biannual [Zürich NixOS ZHF hackathon](https://zurich.nix.ug/) has become somewhat of an institution for maintaining the tradition of preparing the upcoming NixOS release. + + The main goal of the two-day gathering is to bring down the number of build failures on the [continuous integration system Hydra](https://status.nixos.org/) before the release: ZHF stands for *Zero Hydra Failures*. + It also presents a great opportunity to learn Nix, squash bugs together, get to know each other, discuss current events, and make plans for the future. + + This is the greatest event in the series so far, with more than 40 participants from all over Europe, including many high-profile contributors and maintainers in the Nix ecosystem. + + [Fediversity engineers attended](${link config.collections.news.by-name.zhf-24-11}) to present prototypes and exchange ideas with other developers. + ''; + }; +} diff --git a/content/events/owc-annual-conference-2024.nix b/content/events/owc-annual-conference-2024.nix new file mode 100644 index 0000000..f44d520 --- /dev/null +++ b/content/events/owc-annual-conference-2024.nix @@ -0,0 +1,26 @@ +{ ... }: +{ + collections.events.entry = + { ... }: + { + title = "OW2con 2024"; + description = "OW2con is the annual European open source conference in Paris"; + start-date = "2024-06-11"; + end-date = "2024-06-12"; + start-time = "09:00"; + end-time = "18:00"; + location = "Paris-Chatillon"; + body = '' + OW2con is the European open source conference organized by OW2. + An international meeting of developpers, IT companies, academics and non-profit organizations, OW2con brings together the entire open source community, during two days of presentations ranging from tech topics to business and ethical issues of open source. + It also offers a unique opportunity to establish contact with peers through friendly networking sessions. + OW2con is [open](https://www.ngi.eu/event/open-source-community-annual-conference-2024/) to all, the event is free and all sessions are held in English. + + The OW2con’24 call for presentations is open. + This year we are giving the highlight on the theme of open source funding: + What are the current solutions for innovators, start-ups or ISVs to finance their development? + Private or public financing? + Are national and European public policies up to the challenges? + ''; + }; +} diff --git a/content/events/publicspaces-conference-2024.nix b/content/events/publicspaces-conference-2024.nix new file mode 100644 index 0000000..3f2af58 --- /dev/null +++ b/content/events/publicspaces-conference-2024.nix @@ -0,0 +1,20 @@ +{ ... }: +{ + collections.events.entry = + { ... }: + { + title = "PublicSpaces Conference 2024"; + description = "A conference by PublicSpaces, Taking Back the Internet."; + start-date = "2024-06-06"; + end-date = "2024-06-07"; + start-time = "09:00"; + end-time = "18:00"; + location = "Pakhuis de Zwijger - Amsterdam"; + body = '' + On June 6th and 7th, PublicSpaces and Waag Futurelab proudly present the fourth edition of the PublicSpaces conference under the theme 'Empowering the Internet'. + Held at Pakhuis de Zwijger, this two-day event will feature panels, keynotes, roundtable discussions, lectures, as well as art and cultural showcases, all aimed at collectively shaping the rules for a more inclusive internet. + Join us as we navigate towards a digital landscape where everyone has a voice. + For more information, check out the [website](https://publicspaces.net/2024/02/01/save-the-date-publicspaces-conferentie-2024/) + ''; + }; +} diff --git a/content/events/waag-state-internet-2024.nix b/content/events/waag-state-internet-2024.nix new file mode 100644 index 0000000..0c6d607 --- /dev/null +++ b/content/events/waag-state-internet-2024.nix @@ -0,0 +1,27 @@ +{ ... }: +{ + collections.events.entry = + { ... }: + { + title = "State of the Internet 2024"; + description = "The State of the Internet 2024 by Waag"; + start-date = "2024-05-16"; + end-date = "2024-05-16"; + start-time = "18:00"; + end-time = "20:00"; + location = "OBA Oosterdok - Amsterdam"; + body = '' + Join us at the State of the Internet 2024, where Waag Futurelab, alongside the Municipality of Amsterdam and the OBA, delves into the depths of the online realm. + Featuring Kim van Sparrentak, Member of the European Parliament, discussing Europe's efforts to regulate Big Tech and enhance digital rights. + Explore the impact of pivotal European laws like the GDPR and AI Act while celebrating 30 years of Waag Futurelab's dedication to democratizing technology access for all. + + The event takes place at: + + OBA Oosterdok
+ Oosterdokskade 143
+ 1011 DK Amsterdam + + Registration available [here](https://waag.org/nl/event/de-staat-van-het-internet-2024-met-kim-van-sparrentak/). + ''; + }; +} diff --git a/content/fediversity.nix b/content/fediversity.nix new file mode 100644 index 0000000..4e8f2cc --- /dev/null +++ b/content/fediversity.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + pages.fediversity = { + title = "Fediversity"; + description = "More information about the Fediversity project"; + summary = '' + The Fediversity Project is a comprehensive effort to bring easy-to-use, hosted cloud services that have service portability and personal freedom at their core to everyone. + ''; + body = '' + Fediversity is a comprehensive effort to bring easy-to-use, hosted cloud services with service portability and personal freedom at their core to everyone. It wants to provide everyone with high-quality, secure IT systems for everyday use. Without tracking, without exploitation, in a way that runs everywhere and scales effortlessly. Fediversity is based on NixOS, a disruptive Linux distribution with a unique approach to package and configuration management. Built on top of the Nix package manager, NixOS is completely declarative, makes upgrading systems reliable, and has many other advantages. Because it is reproducible, it is ideally suited for complex deployment scenario’s where consistent behaviour, stability and configurability matter. + + One such “complex” deployment scenario is running state-of-the-art services for the Fediverse, like PeerTube, Mastodon, Owncast or Lemmy — especially if you want to for instance add services like live chat or transcoding. But even running more traditional services like modern e-mail servers with possible whistles and bells can be daunting. The same holds for deploying a VPN, private cloud storage, wiki, etc. Fediversity will enable all of these use cases, and more — finally bringing these to the market in a way that is as conveient as using a hosted service. + + Fediversity is a pilot funded by the European Commission, building on many projects funding through the Next Generation Internet initiative. The results of the project should greatly simplify the creation and delivery of robust and secure services, on the web and beyond. + + Fediversity will deliver an ambitious development effort, but this is a vast domain with many more challenges than what any preconceived effort could tackle by itself. This is why we invite your contribution to help us reshape the state of play, and together create an open, trustworthy and reliable internet for all. + ''; + }; +} diff --git a/content/grants.nix b/content/grants.nix new file mode 100644 index 0000000..2dfe134 --- /dev/null +++ b/content/grants.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + pages.grants = { + title = "Grants"; + description = "How to apply for grants as part of the Fediversity Project"; + summary = '' + Fediversity will award 450 000 euro in small to medium-size R&D grants towards solutions that bring the next generation of social networks closer. We are seeking project proposals between 5.000 and 50.000 euro’s — which should get you on your way. + ''; + body = '' + Fediversity invites other people to join this ambitious development effort. It is a vast domain with many more challenges than what any preconceived effort could tackle by itself. This is why we invite your contribution to help us reshape the state of play, and together create an open, trustworthy and reliable internet for all. + + This is your opportunity to make a real difference. You tell us how your project can help Fediversity go harder, better, faster, stronger. In order to enable you to make such contributions, NLnet will award 450 000 euro in small to medium-size R&D grants towards solutions that bring the next generation of social networks closer. NLnet is seeking project proposals between 5.000 and 50.000 euro’s — which should get you on your way. + + Noteworthy fact: many projects which are to be deployed inside Fediversity were themselves bootstrapped on precisely such a grant from NGI, and now it is your turn. + + For more information on how to apply, check the NLnet website. + ''; + }; +} diff --git a/content/individuals.nix b/content/individuals.nix new file mode 100644 index 0000000..bf041d3 --- /dev/null +++ b/content/individuals.nix @@ -0,0 +1,20 @@ +{ ... }: +{ + pages.individuals = { + title = "Individuals"; + description = "Information about the project for regular people"; + summary = '' + Always be in control with your own data on social networks, whether that’s with Mastodon, PeerTube or Pixelfed: Fediversity makes it possible. + ''; + body = '' + The fediverse shows great potential in fundamentally rethinking how we approach the internet. It is a new way of thinking about how the internet can be a social web, and solves for the problems that the current Big Tech platforms have, while at the same time enabling a new wave of innovation and new ideas on the social web. + + - Giving people control of their data and social connections, allowing them to choose whatever platform and product they want. + - Giving people control of their privacy and their feeds, without black-box algorithms that decide for them what they get to see. + + The Fediversity project is working on making it easier for people to join the fediverse, and taking full control on their own online social presence. We make it easier for you to join the fediverse, giving you an easy way to select what you need. With one click, you can select whether you need photo sharing, microblogging, video sharing, blogging or simply email. You can get a domain name as well, so you can be up and running with a professional social presence on the new internet in without any effort. + + Fediversity is currently in development, and you can follow us on Mastodon for all the latest information. + ''; + }; +} diff --git a/content/navigation.nix b/content/navigation.nix new file mode 100644 index 0000000..5f7481b --- /dev/null +++ b/content/navigation.nix @@ -0,0 +1,49 @@ +{ config, ... }: +let + inherit (config) pages; +in +{ + menus.main = { + label = "Main"; + items = [ + { + page = pages.index // { + title = "Start"; + }; + } + { + menu.label = "For you"; + menu.items = map (page: { inherit page; }) ( + with pages; + [ + individuals + developers + european-commission + ] + ); + } + { + menu.label = "Consortium"; + menu.items = map (page: { inherit page; }) ( + with pages; + [ + nlnet + oid + tweag + nordunet + ] + ); + } + { page = pages.fediversity; } + { page = pages.grants; } + { page = pages.news; } + { page = pages.events; } + { + link = { + label = "Contact"; + url = "mailto:contact@fediversity.eu"; + }; + } + ]; + }; +} diff --git a/content/news.nix b/content/news.nix new file mode 100644 index 0000000..96d7c16 --- /dev/null +++ b/content/news.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: +{ + pages.news = + { link, ... }: + rec { + title = "News"; + description = "News about Fediversity"; + summary = description; + body = + with lib; + let + news = map (article: '' + ## [${article.title}](${link article}) + + ${article.date} by ${article.author} + + ${article.summary} + '') config.collections.news.entry; + in + '' + ${join "\n\n" news} + ''; + }; +} diff --git a/content/news/2024-11-zurich-zhf.nix b/content/news/2024-11-zurich-zhf.nix new file mode 100644 index 0000000..be331c0 --- /dev/null +++ b/content/news/2024-11-zurich-zhf.nix @@ -0,0 +1,24 @@ +{ config, ... }: +{ + collections.news.entry = + { link, ... }: + rec { + name = "zhf-24-11"; + title = "NixOS 24.11 release hackathon and workshop"; + description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon"; + date = "2024-11-28"; + author = "Valentin Gagarin"; + summary = '' + Fediversity engineers met in Zürich at a [NixOS 24.11 ZHF hackathon](${link config.collections.events.by-name.zhf-24-11}) to present prototypes and exchange ideas with the Nix community. + ''; + body = '' + ${summary} + + Robert held a lightning talk on the design of [NixOps4](https://github.com/nixops4/nixops4), which is currently in the prototype stage of development. + Before that, Nicolas had already shown an internal demonstration that NixOps4 is capable of deploying multiple NixOS services in the Fediversity test environment. + + In the afternoon, Robert, Valentin, and Koen got together with Eli from [Thymis](https://thymis.io) and Johannes from [Clan](https://clan.lol/) to walk each other through the architecture of their respective systems. + This was an extraordinarily fruitful encounter that helped us to identify overlaps and potential for future collaboration! + ''; + }; +} diff --git a/content/news/nordunet-conference.nix b/content/news/nordunet-conference.nix new file mode 100644 index 0000000..17ffefa --- /dev/null +++ b/content/news/nordunet-conference.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + collections.news.entry = { + title = "Nordunet Conference 2024"; + description = "Report from the NORDUnet Conference 2024"; + date = "2024-09-17"; + author = "Laurens Hof"; + summary = '' + Fediversity was represented in Bergen at the Nordunet Conference for 2024, with both the Internet Discourse Foundation and Nordunet themselves being present. + ''; + body = '' + Fediversity was represented in Bergen at the Nordunet Conference for 2024, with both the Internet Discourse Foundation and Nordunet themselves being present. This was a great opportunity for the different organisations in the consortium to meet with each other and exchange ideas. + + One of those new ideas that came out of the conference is to think about offering [EduMEET](https://edumeet.org/) as a part of Fediversity. EduMEET is an open source video conferencing platform that is build for and by the Research and Education community. EduMEET allows for the possibility of recording conference calls, but does not offer an easy place to host these recordings. PeerTube is already mature fediverse software that offers video hosting. Combining these two pieces of software in the offering to onboard public organisations can make it easier to offer a complete package for the organisations. It can potentially help lower the barrier of entry, while at the same time making it more attractive for public education organisations to start using fediverse software. + + Fediversity is now starting to explore if and how efforts with Nordunet to promote EduMEET can be combined with Fediversity's (and thus Nordunet!) project to promote the fediverse. + + Another aspect that came out of the conference is the possibility to use [Argus](https://openargus.org/) as a real-time monitoring tool as part of our hosting stack that we're building. How to do real-time monitoring was so far still unclear in our plans for building a Nix panel, but Argus might just be the open source tool we're looking for. + + It was exciting to meet so many people in the community that are all working towards building better digital systems for public organisations, and we're proud to contribute our small piece to a much larger puzzle. Hope to meet more of you all soon! + ''; + }; +} diff --git a/content/news/project-launch.nix b/content/news/project-launch.nix new file mode 100644 index 0000000..5d59a85 --- /dev/null +++ b/content/news/project-launch.nix @@ -0,0 +1,22 @@ +{ ... }: + +{ + collections.news.entry = + { ... }: + { + title = "Fediversity project publicly announced"; + description = "The Fediversity project has officially been announced"; + date = "2024-01-01"; + author = "Laurens Hof"; + summary = '' + We are pleased to introduce the launch of our new website dedicated to the Fediversity project. + ''; + body = '' + The Consortium behind the Fediversity project announces that the project has officially been started. NLnet, Tweag, NorduNet and the Open Internet Discourse Foundation are working together to build a new service for cloud hosters. + + Fediversity is a comprehensive effort to bring easy-to-use, hosted cloud services with service portability and personal freedom at their core to everyone. It wants to provide everyone with high-quality, secure IT systems for everyday use. Without tracking, without exploitation, in a way that runs everywhere and scales effortlessly. Fediversity is based on NixOS, a disruptive Linux distribution with a unique approach to package and configuration management. Built on top of the Nix package manager, NixOS is completely declarative, makes upgrading systems reliable, and has many other advantages. Because it is reproducible, it is ideally suited for complex deployment scenario's where consistent behaviour, stability and configurability matter. + + Fediversity has received funding from the European Union’s Horizon Europe research and innovation programme under grant agreement No. 101136078. + ''; + }; +} diff --git a/content/news/publicspaces-conference-2024.nix b/content/news/publicspaces-conference-2024.nix new file mode 100644 index 0000000..b3850ab --- /dev/null +++ b/content/news/publicspaces-conference-2024.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + collections.news.entry = { + title = "PublicSpaces Conference 2024"; + description = "Report from the PublicSpaces Conference 2024 - 'Take Back the Internet'"; + date = "2024-07-30"; + author = "Laurens Hof"; + summary = '' + PublicSpaces and Waag Futurelabs recently held their yearly conference in Amsterdam, titled ‘Taking Back the Internet’ + ''; + body = '' + PublicSpaces and Waag Futurelabs recently held their yearly conference in Amsterdam, titled 'Taking Back the Internet'. PublicSpaces is a network of public organisations fighting for an internet based on public values. The Fediversity Project attended, to share ideas, and learn more about how people and organisations think about an ethical internet. If you are interested, you can view all sessions [here](https://conference.publicspaces.net/en/archive/pubconf2024) (hosted on PeerTube!). + + Alexandra van Huffelen, who was Dutch Secretary of State of Digitalisation until last month, gave the opening talk to discuss digitalisation and public values. In the talk, van Huffelen said that the Netherlands has a prominent lead in the EU with the promotion of public values in the digital infrastructure. Van Huffelen has been a prominent supporter of open standards and decentralisation, and has pushed the usage of Mastodon within the Dutch government, which fits well with the goals and vision of the Fediversity project. Project Lead Koen de Jonge took the opportunity shortly before the talk to hand van Huffelen a Mastodon pin, which she proudly wore during her talk, as you can see in the header image! + + There were quite some other talks about the Fediverse as well, discussing how to move the space forward. The goal of the Fediversity Project is to provide the technological infrastructure that makes it easier for people to join an open, free and fair social internet. The strength of Fediversity is in our technological capabilities, making the infrastructure more accessible. For our project to be successful, we also need a social infrastructure, that teaches people what it is and how it is beneficial for them, and how to get them on board. We also need public organisations to lead by example. Both of these social aspects of growing the fediverse were on full display during the PublicSpaces conference, and there is a real enthusiasm in growing the social internet. Fediversity is a strong supporter of organisations like PublicSpaces; while organisations like PublicSpaces help facilitate people and organisations with their thinking about why they should join the fediverse, and which steps should they take, Fediversity can provide the technological infrastructure that makes it all as easy as possible. + ''; + }; +} diff --git a/content/news/tech-session.nix b/content/news/tech-session.nix new file mode 100644 index 0000000..ebb8dd8 --- /dev/null +++ b/content/news/tech-session.nix @@ -0,0 +1,19 @@ +{ ... }: +{ + collections.news.entry = { + title = "Fediversity tech session"; + description = "Fediversity tech session - NixOS and Kubernetes"; + date = "2024-08-05"; + author = "Laurens Hof"; + summary = '' + Recently Fediversity hosted a tech session on NixOS and Kubernetes. We invited people within the community to discuss some design considerations of the Fediversity project with us. + ''; + body = '' + Recently Fediversity hosted a tech session on NixOS and Kubernetes. We invited people within the community to discuss some design considerations of the Fediversity project with us. + + One of the core ideas of Fediversity is to build on top of NixOS. NixOS makes upgrading system reliable, and complex deployment reproducable. One of the goals of the Fediversity project that provides an interesting challenge is to help people move away from the cloud hyperscalers. Offering our project on Kubernetes offers easy integration with the storage platforms of the hyperscalers. Easy integration with the hyperscalers is an explicit anti-goal of Fediversity, but we're not sure if we can offer all the functionality with NixOS yet. + + You can check out our entire conversation right here. + ''; + }; +} diff --git a/content/news/website-launch.nix b/content/news/website-launch.nix new file mode 100644 index 0000000..2dae814 --- /dev/null +++ b/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"; + summary = '' + We are pleased to introduce the launch of our new website dedicated to the Fediversity project. + ''; + 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/content/partners/nlnet.nix b/content/partners/nlnet.nix new file mode 100644 index 0000000..b7aaf10 --- /dev/null +++ b/content/partners/nlnet.nix @@ -0,0 +1,16 @@ +{ ... }: +{ + pages.nlnet = { + title = "NLnet"; + description = "Details about the NLnet Foundation"; + summary = '' + NLnet supports organisations and people who contribute to an open internet for all. They fund projects that help fix the internet through open hardware, open software, open standards, open science and open data. + ''; + body = '' + The NLnet Foundation supports organisations and people who contribute to an open internet for all. NLnet funds projects that help fix the internet through open hardware, open software, open standards, open science and open data. After its historical contribution to the early internet in Europe in the 1980’s, NLnet has been financially supporting the open internet since 1997. + + NLnet provides grants to free and open source projects between 5.000 and 50.000 euro with the possibility to scale up. Funding is open to anyone: organisations of any type and individuals. Within NGI Fediversity, NLnet facilitates the open calls for third-party funding and contributes to outreach and dissemination. + ''; + + }; +} diff --git a/content/partners/nordunet.nix b/content/partners/nordunet.nix new file mode 100644 index 0000000..3864711 --- /dev/null +++ b/content/partners/nordunet.nix @@ -0,0 +1,15 @@ +{ ... }: +{ + pages.nordunet = { + title = "NORDUnet"; + description = "Details about NORDUnet"; + summary = '' + NORDUnet is a collaboration of the National Research and Education Networks (NREN) of the Nordic countries. + ''; + body = '' + NORDUnet connects universities and research institutions across Denmark, Finland, Iceland, Norway, and Sweden. It enables collaboration, data sharing, and access to online resources for academic and research purposes. + + Fun fact: the website of NORDUnet, nordu.net is the oldest still active domain on the internet. + ''; + }; +} diff --git a/content/partners/oid.nix b/content/partners/oid.nix new file mode 100644 index 0000000..54c4cfb --- /dev/null +++ b/content/partners/oid.nix @@ -0,0 +1,21 @@ +{ ... }: +{ + + pages.oid = { + title = "Open Internet Discourse Foundation"; + description = "Details about the Open Internet Discourse Foundation"; + summary = '' + The Open Internet Discourse Foundation (OID) is founded on the belief that everyone deserves the freedom to express themselves and use the internet without constraints, and is committed to help build a better internet where individuals can truly be who they are. + ''; + body = '' + The three pillars that are at the core of the OID Foundation: + + OID believes in the fundamental right of individuals to privacy, self-determination, and freedom of expression. + Building sustainably. The OID Foundation believes that the internet is crucial infrastructure for society, and as such, should be build from the perspective that software projects can exist and be maintained for a long time; decades, not years. + Transparancy. The OID Foundation takes the commitment to openness seriously, and strives to use open software in all aspects. + In order to realise our vision, we need a healthy, functional and open internet. This is where OID comes in, working on the infrastructure that powers the internet in a way that promotes it’s values. + + OID Foundation is rooted in constructive optimism, believing in tackling challenges head-on with a positive outlook, viewing each obstacle as an opportunity for improvement. As a practical example of the long-term vision is the work on NixOS that the OID is doing, where the reproducibility and long-term maintainability of NixOS’s package management help with an open and sustainable internet. + ''; + }; +} diff --git a/content/partners/tweag.nix b/content/partners/tweag.nix new file mode 100644 index 0000000..5d72c64 --- /dev/null +++ b/content/partners/tweag.nix @@ -0,0 +1,13 @@ +{ ... }: +{ + pages.tweag = { + title = "Tweag"; + description = "Details about Tweag"; + summary = '' + Tweag is the open source program office (OSPO) of Modus Create, and has extensive experience working with Nix, and many people at the forefront of the Nix community are Tweagers + ''; + body = '' + Tweag is the open source program office (OSPO) of Modus Create, a global digital consulting firm that helps enterprises build competitive advantage through digital innovation. Tweagers are leading contributors to several open source projects — from functional programming languages to cross-platform frameworks. Tweag has extensive experience working with Nix, and many people at the forefront of the Nix community are Tweagers. + ''; + }; +} diff --git a/content_/_index.md b/content_/_index.md new file mode 100755 index 0000000..baba528 --- /dev/null +++ b/content_/_index.md @@ -0,0 +1,89 @@ +--- +# Banner +banner: + title: "Welcome to the Fediversity Project" + content: "" + # image: "/images/checkbox-illustration-scaled.png" + image: "/images/home.svg" + button: + enable: true + label: "For You" + link: "/individuals" + +# Features +features3: + - title: "Consortium" + image: "/images/users.svg" + content: "The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag." + button: + enable: false + label: "Learn more" + link: "" + + - title: "NLnet" + image: "/images/users.svg" + content: "NLnet supports organisations and people who contribute to an open internet for all. They fund projects that help fix the internet through open hardware, open software, open standards, open science and open data." + button: + enable: true + label: "Learn more" + link: "/nlnet" + + - title: "Open Internet Discourse" + image: "/images/users.svg" + content: "The Open Internet Discourse Foundation (OID) is founded on the belief that everyone deserves the freedom to express themselves and use the internet without constraints, and is committed to help build a better internet where individuals can truly be who they are." + button: + enable: true + label: "Learn more" + link: "/oid" + + - title: "Tweag" + image: "/images/users.svg" + content: "Tweag is the open source program office (OSPO) of Modus Create, and has extensive experience working with Nix, and many people at the forefront of the Nix community are Tweagers." + button: + enable: true + label: "Learn more" + link: "/tweag" + + - title: "NORDUnet" + image: "/images/users.svg" + content: "NORDUnet is a collaboration of the National Research and Education Networks of the Nordic countries." + button: + enable: true + label: "Learn more" + link: "/nordunet" + +features: + - title: "Fediversity Grants" + image: "/images/stepping-up.png" + content: "" + button: + enable: true + label: "Learn more" + link: "/grants" + +features2: + - title: "Individuals" + image: "/images/user.png" + content: "Always be in control with your own data on social networks, whether that's with Mastodon, PeerTube or Pixelfed: Fediversity makes it possible." + button: + enable: true + label: "Learn more" + link: "/individuals" + + - title: "Developers" + image: "/images/code.png" + content: "As a developer building the next generation of social platforms, you are looking to make it easier to facilitate your customers to use your product. Fediversity can help." + button: + enable: true + label: "Learn more" + link: "/developers" + + - title: "European Commission" + image: "/images/globe.png" + content: "The Fediversity Project operates on a grant gratiously provided by the HORIZON fund by the EC. Learn more about the accountability of the project." + button: + enable: true + label: "Learn more" + link: "/ec" + +--- diff --git a/content_/authors/_index.md b/content_/authors/_index.md new file mode 100644 index 0000000..62eae44 --- /dev/null +++ b/content_/authors/_index.md @@ -0,0 +1,3 @@ +--- +title: "Authors" +--- diff --git a/content_/authors/laurens-hof.md b/content_/authors/laurens-hof.md new file mode 100644 index 0000000..ab98ec7 --- /dev/null +++ b/content_/authors/laurens-hof.md @@ -0,0 +1,12 @@ +--- +title: Laurens Hof +email: laurens@procolix.com +image: "/images/avatar.png" +description: storyteller +social: + - name: github + icon: fa-brands fa-github + link: https://github.com +--- + +Story teller for the Fediversity Project. diff --git a/content_/contact/_index.md b/content_/contact/_index.md new file mode 100644 index 0000000..cfccf0d --- /dev/null +++ b/content_/contact/_index.md @@ -0,0 +1,18 @@ +--- +title: "Contact" +meta_title: "" +description: "How to contact us" +draft: false +--- + +Please feel free to reach out to us via email at the following addresses for any inquiries or assistance. We’re here to help and eager to hear from you! + +For generic questions about the Fediversity project: + +contact@fediversity.eu +For questions about the administrative side of the Fediversity Project: + +fediversity@nlnet.nl +If you have questions about the funding rounds that are part of the Fediversity project, you can find out more information on the NLnet website: + +https://nlnet.nl/fediversity/guideforapplicants/ \ No newline at end of file diff --git a/content_/pages/privacy-policy.md b/content_/pages/privacy-policy.md new file mode 100644 index 0000000..87aff88 --- /dev/null +++ b/content_/pages/privacy-policy.md @@ -0,0 +1,12 @@ +--- +title: "Privacy" +# meta title +meta_title: "" +# meta description +description: "Privacy Policy" +# save as draft +draft: false +--- + +The Fediversity website does not track you, and does not process any of your data. + diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..258d1ff --- /dev/null +++ b/default.nix @@ -0,0 +1,102 @@ +{ + sources ? import ../npins, + system ? builtins.currentSystem, + pkgs ? import sources.nixpkgs { + inherit system; + config = { }; + overlays = [ ]; + }, + lib ? import "${sources.nixpkgs}/lib", +}: +let + lib' = + final: prev: + let + new = import ./lib.nix { lib = final; }; + in + new // { types = prev.recursiveUpdate prev.types new.types; }; + lib'' = lib.extend lib'; + +in +rec { + lib = lib''; + result = lib.evalModules { + modules = [ + ./structure + ./content + ./presentation + { + _module.args = { + inherit pkgs; + }; + } + ]; + }; + + inherit (result.config) build; + + shell = + let + run-tests = pkgs.writeShellApplication { + name = "run-tests"; + text = + with pkgs; + with lib; + '' + ${getExe nix-unit} ${toString ./tests.nix} "$@" + ''; + }; + test-loop = pkgs.writeShellApplication { + name = "test-loop"; + text = + with pkgs; + with lib; + '' + ${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix} + ''; + }; + devmode = pkgs.devmode.override { + buildArgs = "${toString ./.} -A build --show-trace"; + open = "/index.html"; + }; + in + pkgs.mkShellNoCC { + packages = [ + pkgs.npins + run-tests + test-loop + devmode + ]; + }; + + inherit sources pkgs; + tests = + with pkgs; + with lib; + let + source = fileset.toSource { + root = ../.; + fileset = fileset.unions [ + ./default.nix + ./tests.nix + ./lib.nix + ../npins + ]; + }; + in + runCommand "run-tests" + { + buildInputs = [ pkgs.nix ]; + } + '' + export HOME="$(realpath .)" + # HACK: nix-unit initialises its own entire Nix, so it needs a store to operate on, + # but since we're in a derivation, we can't fetch sources, so copy Nixpkgs manually here. + # `''${sources.nixpkgs}` resolves to `-source`, + # adding it verbatim will result in --source, so rename it first + cp -r ${sources.nixpkgs} source + nix-store --add --store "$HOME" source + ${getExe nix-unit} --gc-roots-dir "$HOME" --store "$HOME" ${source}/website/tests.nix "$@" + touch $out + ''; +} diff --git a/images/screenshot.png b/images/screenshot.png new file mode 100644 index 0000000..62d9852 Binary files /dev/null and b/images/screenshot.png differ diff --git a/images/tn.png b/images/tn.png new file mode 100644 index 0000000..6765469 Binary files /dev/null and b/images/tn.png differ diff --git a/layouts/404.html b/layouts/404.html new file mode 100755 index 0000000..e038668 --- /dev/null +++ b/layouts/404.html @@ -0,0 +1,21 @@ +{{ define "main" }} +
+
+
+
+ 404 +

Page not found

+
+

+ The page you are looking for might have been removed, had its name + changed, or is temporarily unavailable. +

+
+ + Back to home + +
+
+
+
+{{ end }} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100755 index 0000000..f1ceaab --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,39 @@ + + + + + {{ partial "essentials/head.html" . }} + + + {{ partialCached "essentials/style.html" . }} + + + + + {{ if hugo.IsProduction }} {{ partialCached "preloader.html" . }} {{ + partialCached "gtm-noscript.html" . }} {{ else }} {{ partial + "preloader.html" . }} + + + {{ partial "components/tw-size-indicator.html" . }} {{ end }} + + + {{ partial "essentials/header.html" . }} {{ partial "search-modal.html" + (dict "Context" . ) }} + +
{{ block "main" . }}{{ end }}
+ + + {{ partial "essentials/footer.html" . }} + + + {{ partialCached "essentials/script.html" . }} + + diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100755 index 0000000..b4b2b9d --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,16 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+ +
+
+{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100755 index 0000000..a3b5b19 --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,12 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+
+
+
{{ .Content }}
+
+
+
+
+{{ end }} diff --git a/layouts/_default/taxonomy.html b/layouts/_default/taxonomy.html new file mode 100755 index 0000000..8ad4a15 --- /dev/null +++ b/layouts/_default/taxonomy.html @@ -0,0 +1,20 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+
+ +
+
+ {{ range .Data.Pages }} +
+ {{ partial "components/blog-card" . }} +
+ {{ end }} +
+
+
+
+
+{{ end }} + diff --git a/layouts/_default/terms.html b/layouts/_default/terms.html new file mode 100755 index 0000000..93cc45c --- /dev/null +++ b/layouts/_default/terms.html @@ -0,0 +1,32 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+ +
+
+{{ end }} diff --git a/layouts/about/list.html b/layouts/about/list.html new file mode 100644 index 0000000..b3e4016 --- /dev/null +++ b/layouts/about/list.html @@ -0,0 +1,14 @@ +{{ define "main" }} +
+
+
+
+ {{ partial "image" (dict "Src" .Params.image "Alt" .Title "Class" + "mx-auto mb-6" "Size" "200x200") }} +

{{ .Title }}

+
{{ .Content }}
+
+
+
+
+{{ end }} diff --git a/layouts/authors/list.html b/layouts/authors/list.html new file mode 100644 index 0000000..fda2b3d --- /dev/null +++ b/layouts/authors/list.html @@ -0,0 +1,14 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+
+ {{ range .RegularPages }} +
+ {{ partial "components/author-card" . }} +
+ {{ end }} +
+
+
+{{ end }} diff --git a/layouts/authors/single.html b/layouts/authors/single.html new file mode 100755 index 0000000..49802c9 --- /dev/null +++ b/layouts/authors/single.html @@ -0,0 +1,46 @@ +{{ define "main" }} +
+
+
+
+ {{ $image:= .Params.image }} {{ if $image }} {{ partial "image" (dict + "Src" $image "Alt" .Title "Class" "mx-auto" "Size" "200x200") }} {{ else + if .Params.Email }} + {{ .Title }} + {{ end }} +

{{ .Title }}

+
{{ .Content }}
+ +
+
+ +
+ {{ $filterByAuthor := where site.RegularPages "Params.author" "==" .Title + }} {{ range $filterByAuthor }} +
+ {{ partial "components/blog-card" . }} +
+ {{ end }} +
+
+
+{{ end }} diff --git a/layouts/blog/list.html b/layouts/blog/list.html new file mode 100644 index 0000000..b899d21 --- /dev/null +++ b/layouts/blog/list.html @@ -0,0 +1,22 @@ +{{ define "main" }} {{ partial "page-header" . }} + +
+
+
+ + +
+ {{ $paginator:= .Paginate .RegularPages }} {{ range $paginator.Pages + }} +
+ {{ partial "components/blog-card" . }} +
+ {{ end }} +
+ {{ partial "components/pagination.html" . }} + + +
+
+
+{{ end }} diff --git a/layouts/blog/single.html b/layouts/blog/single.html new file mode 100644 index 0000000..975de70 --- /dev/null +++ b/layouts/blog/single.html @@ -0,0 +1,68 @@ +{{ define "main" }} +
+
+
+
+ + {{ $image:= .Params.image }} {{ if $image }} +
+
+

{{ .Title }}

+ +
{{ .Content }}
+ +
+
+
+ {{ partial "image" (dict "Src" $image "Alt" .Title "Class" "w-full rounded") }} +
+
+
+ {{ end }} + + + + + +
+ {{ $tags:= .Params.tags }} {{ if $tags }} +
+
{{ i18n "tags" }} :
+ +
+ {{ end }} + +
+ + +
+
+ + +
+
+{{ end }} diff --git a/layouts/contact/list.html b/layouts/contact/list.html new file mode 100755 index 0000000..f6ee6fc --- /dev/null +++ b/layouts/contact/list.html @@ -0,0 +1,64 @@ +{{ define "main" }} {{ partial "page-header" . }} + + +
+
+
+
+
{{ .Content }}
+
+
+
+
+ + + +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ +{{ end }} \ No newline at end of file diff --git a/layouts/events/list.html b/layouts/events/list.html new file mode 100644 index 0000000..f0657e1 --- /dev/null +++ b/layouts/events/list.html @@ -0,0 +1,31 @@ +{{ define "main" }} {{ partial "page-header" . }} +
+
    + {{ if gt (len (where .Site.RegularPages "Section" "events")) 0 }} +
    +
    + Image +

    Events

    +
    +
      + {{ range (first 5 (where .Site.RegularPages "Section" "events").ByDate.Reverse) }} +
    • + +
      + + +
      +
    • +
      + {{ end }} +
    + +
    + {{ end }} +
+ +
+ +{{ end }} diff --git a/layouts/events/single.html b/layouts/events/single.html new file mode 100644 index 0000000..dcd5155 --- /dev/null +++ b/layouts/events/single.html @@ -0,0 +1,73 @@ +{{ define "main" }} +
+
+
+
+ + {{ $image:= .Params.image }} {{ if $image }} +
+
+

{{ .Title }}

+
    + +
  • + + Start: {{ .Params.datetime_start }} +
  • +
    +
  • + + End: {{ .Params.datetime_end }} +
  • +
    +
  • + + Location: {{ .Params.location }} +
  • + + +
+
{{ .Content }}
+ +
+
+
+ {{ partial "image" (dict "Src" $image "Alt" .Title "Class" "w-full rounded") }} +
+
+
+ {{ end }} + + + + + +
+ {{ $tags:= .Params.tags }} {{ if $tags }} +
+
{{ i18n "tags" }} :
+ +
+ {{ end }} + +
+ + +
+
+ + +
+
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100755 index 0000000..2667b3e --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,208 @@ +{{ define "main" }} + +{{ with .Params.banner }} +
+
+
+
+

{{ .title | markdownify }}

+

{{ .content | markdownify }}

+ {{ with .button }} {{ if .enable }} + + {{ .label }} + + + {{ end }} {{ end }} +
+
+ {{ partial "image" (dict "Src" .image "Alt" "Banner image" "Loading" + "eager" "Class" "mx-auto lg:!max-w-[800px]" "DisplayXL" "800x" ) }} +
+
+
+
+{{ end }} + + + +

Consortium

+ + +
+
+ {{ range $i, $e := .Params.features3 }} + +
+ +
+ Image +
{{ .title | markdownify }}
+
+ + +

{{ .content | markdownify }} + {{ with .button }} {{ if .enable }} + + Read more + + + {{ end }} {{ end }} +

+
+ {{ end }} +
+
+ + + + +{{ range $i, $e:= .Params.features }} +
+
+
+
+ {{ partial "image" (dict "Src" .image "Alt" "feature image" "DisplayXL" + "520x" "DisplayLG" "425x" "DisplayMD" "360x") }} +
+
+

{{ .title | markdownify }}

+

{{ .content | markdownify }}

+
    + {{ range .bulletpoints }} +
  • + + {{ . | markdownify }} +
  • + {{ end }} +
+ {{ with .button }} {{ if .enable }} + + {{ .label }} + + + {{ end }} {{ end }} +
+
+
+
+{{ end }} + + + + + + +

Fediverse Explained

+ + +
+
+ {{ range $i, $e := .Params.features2 }} +
+
+ Image +

{{ .title | markdownify }}

+
+ +

{{ .content | markdownify }}

+ {{ with .button }} {{ if .enable }} + + {{ .label }} + + + {{ end }} {{ end }} +
+ {{ end }} +
+
+ + +
+
+
+ + +{{ if gt (len (where .Site.RegularPages "Section" "blog")) 0 }} +
+ +
+
+
+
    + {{ if gt (len (where .Site.RegularPages "Section" "events")) 0 }} +
    +
    + Image +

    Events

    +
    +
      + {{ range (first 5 (where .Site.RegularPages "Section" "events").ByDate.Reverse) }} +
    • + +
      + + +
      +
    • +
      + {{ end }} +
    + {{ end }} +
+
+
+
    + {{ if gt (len (where .Site.RegularPages "Section" "blog")) 0 }} +
    +
    + Image +

    Latest News

    +
    +
      + {{ range (first 5 (where .Site.RegularPages "Section" "blog").ByDate.Reverse) }} +
    • + +
      + + +
      +
    • +
      + {{ end }} +
    + {{ end }} +
+
+
+
+
+{{ end }} + + +{{ end }} diff --git a/layouts/partials/call-to-action.html b/layouts/partials/call-to-action.html new file mode 100644 index 0000000..931be79 --- /dev/null +++ b/layouts/partials/call-to-action.html @@ -0,0 +1,25 @@ + +{{ with site.GetPage "sections/call-to-action" }} {{ if .Params.enable }} +
+
+
+
+
+ {{ partial "image" (dict "Src" .image "Alt" "call to action" "Class" + "w-full") }} +
+
+

{{ .Title | markdownify }}

+

{{ .Params.description | markdownify }}

+ {{ with .Params.button }} {{ if .enable }} + + {{ .label }} + + {{ end }} {{ end }} +
+
+
+
+
+{{ end }} {{ end }} + diff --git a/layouts/partials/components/author-card.html b/layouts/partials/components/author-card.html new file mode 100755 index 0000000..6d59c04 --- /dev/null +++ b/layouts/partials/components/author-card.html @@ -0,0 +1,27 @@ +
+ {{ $image:= .Params.image }} {{ if $image }} {{ partial "image" (dict "Src" + $image "Alt" .Title "Class" "mx-auto mb-6 rounded" "size" "120x120") }} {{ + else if .Params.Email }} + {{ .Title }} + {{ end }} +

+ {{ .Title }} +

+

{{ .Summary }}

+ +
diff --git a/layouts/partials/components/blog-card.html b/layouts/partials/components/blog-card.html new file mode 100644 index 0000000..5fd8d17 --- /dev/null +++ b/layouts/partials/components/blog-card.html @@ -0,0 +1,30 @@ +
+ {{ $image:= .Params.image }} {{ if $image }} {{ partial "image" (dict "Src" $image "Alt" .Title "Class" "w-full rounded") }} {{ end }} +

+ {{ .Title }} +

+ {{ $categories:= .Params.categories }} {{ if $categories }} + + {{ end }} +

{{ .Summary }}

+ + {{ i18n "read_more" }} + +
diff --git a/layouts/partials/components/breadcrumb.html b/layouts/partials/components/breadcrumb.html new file mode 100644 index 0000000..0780009 --- /dev/null +++ b/layouts/partials/components/breadcrumb.html @@ -0,0 +1,25 @@ +{{ $context := .Context }} {{ $class := .Class }} {{ $base := +site.Home.Permalink }} + + diff --git a/layouts/partials/components/language-switcher.html b/layouts/partials/components/language-switcher.html new file mode 100644 index 0000000..95f0707 --- /dev/null +++ b/layouts/partials/components/language-switcher.html @@ -0,0 +1,25 @@ + +{{ $class := .Class }} {{ $context := .Context }} {{ $pageLang := $context.Lang +}} {{ $base:= urls.Parse site.Home.Permalink }} {{ $siteLanguages := +site.Home.AllTranslations }} {{ $pageLink := replace (replace +$context.RelPermalink (add $pageLang "/") "") $base.Path "/" }} {{ if +$context.IsTranslated }} + +{{ end }} diff --git a/layouts/partials/components/pagination.html b/layouts/partials/components/pagination.html new file mode 100755 index 0000000..23928cf --- /dev/null +++ b/layouts/partials/components/pagination.html @@ -0,0 +1,133 @@ +{{ $paginator := .Paginator }} + +{{ $adjacent_links := 2 }} + +{{ $max_links := (add (mul $adjacent_links 2) 1) }} + +{{ $lower_limit := (add $adjacent_links 1) }} + +{{ $upper_limit := (sub $paginator.TotalPages $adjacent_links) }} + +{{ if gt $paginator.TotalPages 1 }} + +{{ end }} diff --git a/layouts/partials/components/theme-switcher.html b/layouts/partials/components/theme-switcher.html new file mode 100644 index 0000000..54d6075 --- /dev/null +++ b/layouts/partials/components/theme-switcher.html @@ -0,0 +1,65 @@ + +{{ $class := .Class }} {{ if site.Params.theme_switcher }} +
+ + +
+ + + +{{ end }} diff --git a/layouts/partials/components/tw-size-indicator.html b/layouts/partials/components/tw-size-indicator.html new file mode 100644 index 0000000..4ac7e1e --- /dev/null +++ b/layouts/partials/components/tw-size-indicator.html @@ -0,0 +1,10 @@ +
+ all + + + + + +
diff --git a/layouts/partials/essentials/footer.html b/layouts/partials/essentials/footer.html new file mode 100755 index 0000000..792179d --- /dev/null +++ b/layouts/partials/essentials/footer.html @@ -0,0 +1,65 @@ +
+
+
+ +
+
    + {{ range site.Menus.footer }} +
  • + {{ .Name }} +
  • + {{ end }} +
+
+
+ +
+
+
+
+
+

{{ site.Params.copyright | markdownify }}

+
+
+
diff --git a/layouts/partials/essentials/head.html b/layouts/partials/essentials/head.html new file mode 100755 index 0000000..9dae7a0 --- /dev/null +++ b/layouts/partials/essentials/head.html @@ -0,0 +1,51 @@ + + + + + + + + + +{{ partialCached "favicon" . }} + + +{{ partialCached "manifest" . }} + + +{{ partialCached "site-verifications.html" . }} + + +{{ partial "basic-seo.html" . }} + + +{{ partialCached "custom-script.html" . }} + + +{{ if and site.Config.Services.GoogleAnalytics.ID (ne +site.Config.Services.GoogleAnalytics.ID "G-MEASUREMENT_ID") }} {{ template +"_internal/google_analytics.html" . }} {{ end }} + + +{{ partialCached "gtm.html" . }} + + +{{ partial "search-index.html" . }} + + +{{/* {{ partialCached "matomo-analytics.html" . }} */}} + + +{{/* {{ partialCached "baidu-analytics.html" . }} */}} + + +{{/* {{ partialCached "plausible-analytics.html" . }} */}} + + +{{/* {{ partialCached "counter-analytics.html" . }} */}} + + +{{/* {{ partialCached "crisp-chat.html" . }} */}} diff --git a/layouts/partials/essentials/header.html b/layouts/partials/essentials/header.html new file mode 100755 index 0000000..9e05694 --- /dev/null +++ b/layouts/partials/essentials/header.html @@ -0,0 +1,152 @@ +
+ +
diff --git a/layouts/partials/essentials/script.html b/layouts/partials/essentials/script.html new file mode 100755 index 0000000..9474a24 --- /dev/null +++ b/layouts/partials/essentials/script.html @@ -0,0 +1,45 @@ + +{{ $scripts := slice }} {{ $scriptsLazy := slice }} {{ range +site.Params.plugins.js }} {{ if findRE "^http" .link }} + +{{ else }} {{ if not .lazy }} {{ $scripts = $scripts | append (resources.Get +.link) }} {{ else }} {{ $scriptsLazy = $scriptsLazy | append (resources.Get +.link) }} {{ end }} {{ end }} {{ end }} + + +{{ $scripts = $scripts | append (resources.Get "js/main.js") }} {{ $scripts = +$scripts | resources.Concat "js/script.js" }} {{ $scriptsLazy = $scriptsLazy | +resources.Concat "js/script-lazy.js" }} {{ if hugo.IsProduction }} {{ $scripts = +$scripts | minify | fingerprint }} {{ $scriptsLazy = $scriptsLazy | minify | +fingerprint }} {{ end }} {{/* scripts */}} + + +{{/* scripts lazy */}} + + + +{{ partialCached "pwa.html" . }} + + +{{ partialCached "cookie-consent.html" . }} + + +{{ partialCached "adsense-script.html" . }} diff --git a/layouts/partials/essentials/style.html b/layouts/partials/essentials/style.html new file mode 100755 index 0000000..a496dab --- /dev/null +++ b/layouts/partials/essentials/style.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + +{{ $pf:= site.Data.theme.fonts.font_family.primary }} {{ $sf:= +site.Data.theme.fonts.font_family.secondary }} + + + + + +{{ $styles := slice }} {{ $stylesLazy := slice }} {{ range +site.Params.plugins.css }} {{ if findRE "^http" .link }} + +{{ else }} {{ if not .lazy }} {{ $styles = $styles | append (resources.Get +.link) }} {{ else }} {{ $stylesLazy = $stylesLazy | append (resources.Get .link) +}} {{ end }} {{ end }} {{ end }} {{/* main style */}} {{ $styles = $styles | +append (resources.Get "scss/main.scss" | toCSS) }} {{ $styles = $styles | +resources.Concat "css/style.css" }} {{ $styles = $styles | resources.PostCSS }} +{{ $stylesLazy = $stylesLazy | resources.Concat "css/style-lazy.css" }} {{ +$stylesLazy = $stylesLazy | resources.PostCSS }} {{ if hugo.IsProduction }} {{ +$styles = $styles | resources.ExecuteAsTemplate "css/style.css" . | minify | +fingerprint | resources.PostProcess }} {{ $stylesLazy = $stylesLazy | +resources.ExecuteAsTemplate "css/style-lazy.css" . | minify | fingerprint | +resources.PostProcess }} {{ else }} {{ $styles = $styles | +resources.ExecuteAsTemplate "css/style.css" . }} {{ $stylesLazy = $stylesLazy | +resources.ExecuteAsTemplate "css/style-lazy.css" . }} {{ end }} {{/* styles */}} + + +{{/* styles lazy */}} + diff --git a/layouts/partials/page-header.html b/layouts/partials/page-header.html new file mode 100755 index 0000000..02f0751 --- /dev/null +++ b/layouts/partials/page-header.html @@ -0,0 +1,10 @@ +
+
+
+

{{ i18n (printf "%s" (lower .Title)) | default .Title | title }}

+ {{ partial "components/breadcrumb" (dict "Context" . "Class" "mt-6") }} +
+
+
diff --git a/layouts/partials/widgets/categories.html b/layouts/partials/widgets/categories.html new file mode 100755 index 0000000..c475041 --- /dev/null +++ b/layouts/partials/widgets/categories.html @@ -0,0 +1,23 @@ + +{{ if isset site.Taxonomies "categories" }} {{ if not (eq (len +site.Taxonomies.categories) 0) }} +
+
{{ i18n "categories" }}
+
+ +
+
+{{ end }} {{ end }} diff --git a/layouts/partials/widgets/tags.html b/layouts/partials/widgets/tags.html new file mode 100755 index 0000000..731ee48 --- /dev/null +++ b/layouts/partials/widgets/tags.html @@ -0,0 +1,23 @@ + +{{ if isset site.Taxonomies "tags" }} {{ if not (eq (len site.Taxonomies.tags) +0) }} +
+
{{ i18n "tags" }}
+
+ +
+
+{{ end }} {{ end }} diff --git a/layouts/partials/widgets/widget-wrapper.html b/layouts/partials/widgets/widget-wrapper.html new file mode 100755 index 0000000..867b2d8 --- /dev/null +++ b/layouts/partials/widgets/widget-wrapper.html @@ -0,0 +1 @@ +{{ range .Widgets }} {{ partial ( print "widgets/" . ) $.Scope }} {{ end }} diff --git a/lib.nix b/lib.nix new file mode 100644 index 0000000..4ca8d77 --- /dev/null +++ b/lib.nix @@ -0,0 +1,226 @@ +{ lib }: +rec { + template = + g: f: x: + let + base = f x; + result = g base; + in + result + // { + override = + new: + let + base' = + if lib.isFunction new then + lib.recursiveUpdate base (new base' base) + else + lib.recursiveUpdate base new; + result' = g base'; + in + result' + // { + override = new: (template g (_: base') x).override new; + }; + }; + + /** + Recursively replace occurrences of `from` with `to` within `string` + + Example: + + replaceStringRec "--" "-" "hello-----world" + => "hello-world" + */ + replaceStringsRec = + from: to: string: + let + replaced = lib.replaceStrings [ from ] [ to ] string; + in + if replaced == string then string else replaceStringsRec from to replaced; + + /** + 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); + in + trimHyphens (replaceStringsRec "--" "-" replaced); + + squash = replaceStringsRec "\n\n" "\n"; + + /** + Trim trailing spaces and squash non-leading spaces + */ + trim = + string: + let + trimLine = + line: + with lib; + let + # separate leading spaces from the rest + parts = split "(^ *)" line; + spaces = head (elemAt parts 1); + rest = elemAt parts 2; + # drop trailing spaces + body = head (split " *$" rest); + in + if body == "" then "" else spaces + replaceStringsRec " " " " body; + in + join "\n" (map trimLine (splitLines string)); + + join = lib.concatStringsSep; + + splitLines = s: with builtins; filter (x: !isList x) (split "\n" s); + + indent = + prefix: s: + with lib.lists; + let + lines = splitLines s; + in + join "\n" ([ (head lines) ] ++ (map (x: if x == "" then x else "${prefix}${x}") (tail lines))); + + relativePath = + path1': path2': + let + inherit (lib.path) subpath; + inherit (lib) + lists + length + take + drop + min + max + ; + + path1 = subpath.components path1'; + prefix1 = take (length path1 - 1) path1; + path2 = subpath.components path2'; + prefix2 = take (length path2 - 1) path2; + + commonPrefixLength = + with lists; + findFirstIndex (i: i.fst != i.snd) (min (length prefix1) (length prefix2)) ( + zipLists prefix1 prefix2 + ); + + depth = max 0 (length prefix1 - commonPrefixLength); + + relativeComponents = + with lists; + [ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2); + in + join "/" relativeComponents; + + /** + Recursively list all Nix files from a directory, except the top-level `default.nix` + + Useful for module system `imports` from a top-level module. + * + */ + nixFiles = + dir: + with lib.fileset; + toList (difference (fileFilter ({ hasExt, ... }: hasExt "nix") dir) (dir + "/default.nix")); + + types = rec { + # arbitrarily nested attribute set where the leaves are of type `type` + # NOTE: this works for anything but attribute sets! + recursiveAttrs = + type: + with lib.types; + # NOTE: due to how `either` works, the first match is significant, + # so if `type` happens to be an attrset, the typecheck will consider + # `type`, not `attrsOf` + attrsOf (either type (recursiveAttrs type)); + + # collection of unnamed items that can be added to item-wise, i.e. without wrapping the item in a list + collection = + elemType: + let + unparenthesize = class: class == "noun"; + desc = type: types.optionDescriptionPhrase unparenthesize type; + desc' = + type: + let + typeDesc = lib.types.optionDescriptionPhrase unparenthesize type; + in + if type.descriptionClass == "noun" then typeDesc + "s" else "many instances of ${typeDesc}"; + in + lib.types.mkOptionType { + name = "collection"; + description = "separately specified ${desc elemType} for a collection of ${desc' elemType}"; + merge = + loc: defs: + map ( + def: + elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [ + { + inherit (def) file; + value = def.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; + }; + + listOfUnique = + elemType: + let + baseType = lib.types.listOf elemType; + in + baseType + // { + merge = + loc: defs: + let + # Keep track of which definition each value came from + defsWithValues = map ( + def: + map (v: { + inherit (def) file; + value = v; + }) def.value + ) defs; + flatDefs = lib.flatten defsWithValues; + + # Check for duplicates while preserving source info + seen = builtins.foldl' ( + acc: def: + if lib.lists.any (v: v.value == def.value) acc then + throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}" + else + acc ++ [ def ] + ) [ ] flatDefs; + in + map (def: def.value) seen; + }; + }; +} diff --git a/presentation/default.nix b/presentation/default.nix new file mode 100644 index 0000000..8a20230 --- /dev/null +++ b/presentation/default.nix @@ -0,0 +1,101 @@ +{ + config, + options, + lib, + pkgs, + ... +}: +let + inherit (lib) + mkOption + types + ; +in +{ + imports = lib.nixFiles ./.; + + options.templates = mkOption { + description = '' + Collection of named helper functions for conversion different structured representations which can be rendered to a string + ''; + type = with types; recursiveAttrs (functionTo (either str attrs)); + }; + + options.files = mkOption { + description = '' + Files that make up the site, mapping from output path to contents + + Add more files to the output by assigning to this attribute set. + ''; + type = with types; attrsOf path; + }; + + 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; + }; + + # TODO: this is an artefact of exploration; needs to be adapted to actual use + config.templates.table-of-contents = + { config, ... }: + let + outline = + { ... }: + { + options = { + value = mkOption { + # null denotes root + type = with types; nullOr (either str (listOf (attrTag categories.phrasing))); + subsections = mkOption { + type = with types; listOf (submodule outline); + default = + with lib; + map + # TODO: go into depth manually here, + # we don't want to pollute the DOM implementation + (c: (lib.head (attrValues c)).outline) + (filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content); + }; + }; + __toString = mkOption { + type = with types; functionTo str; + # TODO: convert to HTML + default = + self: + lib.squash '' + ${if isNull self.value then "root" else self.value} + ${if self.subsections != [ ] then " " + lib.indent " " (lib.join "\n" self.subsections) else ""} + ''; + }; + }; + }; + in + { + options.outline = mkOption { + type = types.submodule outline; + default = { + value = null; + subsections = + with lib; + map (c: (lib.head (attrValues c)).outline) ( + filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content + ); + }; + }; + }; +} diff --git a/presentation/dom.nix b/presentation/dom.nix new file mode 100644 index 0000000..3946758 --- /dev/null +++ b/presentation/dom.nix @@ -0,0 +1,859 @@ +/** + A strongly typed module system implementation of the Document Object Model (DOM) + + Based on the WHATWG's HTML Living Standard https://html.spec.whatwg.org (CC-BY 4.0) + Inspired by https://github.com/knupfer/type-of-html by @knupfer (BSD-3-Clause) + Similar work from the OCaml ecosystem: https://github.com/ocsigen/tyxml +*/ +{ config, lib, ... }: + +let + inherit (lib) mkOption types; + inherit (types) submodule; + + # https://html.spec.whatwg.org/multipage/dom.html#content-models + # https://html.spec.whatwg.org/multipage/dom.html#kinds-of-content + content-categories = [ + "none" # https://html.spec.whatwg.org/multipage/dom.html#the-nothing-content-model + "text" # https://html.spec.whatwg.org/multipage/dom.html#text-content + "metadata" # https://html.spec.whatwg.org/multipage/dom.html#metadata-content + "flow" # https://html.spec.whatwg.org/multipage/dom.html#flow-content + "sectioning" # https://html.spec.whatwg.org/multipage/dom.html#sectioning-content + "heading" # https://html.spec.whatwg.org/multipage/dom.html#heading-content + "phrasing" # https://html.spec.whatwg.org/multipage/dom.html#phrasing-content + "embedded" # https://html.spec.whatwg.org/multipage/dom.html#embedded-content-2 + "interactive" # https://html.spec.whatwg.org/multipage/dom.html#interactive-content + "palpable" # https://html.spec.whatwg.org/multipage/dom.html#palpable-content + "scripting" # https://html.spec.whatwg.org/multipage/dom.html#script-supporting-elements + ]; + + # base type for all DOM elements + element = + { ... }: + { + # TODO: add fields for upstream documentation references + # TODO: programmatically generate documentation + options = with lib; { + categories = mkOption { + type = types.listOfUnique (types.enum content-categories); + }; + __toString = mkOption { + internal = true; + type = with types; functionTo str; + }; + }; + }; + + # options with types for all the defined DOM elements + element-types = lib.mapAttrs (_name: value: mkOption { type = submodule value; }) elements; + + # attrset of categories, where values are module options with the type of the + # elements that belong to these categories + categories = + with lib; + genAttrs content-categories ( + category: + (mapAttrs (_: e: mkOption { type = submodule e; }) + # HACK: don't evaluate the submodule types, just grab the config directly + # TODO: we may want to do this properly and loop `categories` through the top-level `config` + ( + filterAttrs ( + _: e: + elem category + (e { + name = "dummy"; + config = { }; + }).config.categories + ) elements + ) + ) + ); + + global-attrs = lib.mapAttrs (_name: value: mkOption value) { + class = { + type = with types; listOf nonEmptyStr; + default = [ ]; + }; + hidden = { + type = types.bool; + default = false; + }; + id = { + # TODO: would be cool if we could enforce unique IDs per document + type = with types; nullOr nonEmptyStr; + default = null; + }; + lang = { + # TODO: https://www.rfc-editor.org/rfc/rfc5646.html + type = with types; nullOr str; + default = null; + }; + style = { + # TODO: CSS type ;..) + type = with types; nullOr str; + default = null; + }; + title = { + type = with types; nullOr lines; + default = null; + }; + # TODO: more global attributes + # https://html.spec.whatwg.org/#global-attributes + # https://html.spec.whatwg.org/#attr-aria-* + # https://html.spec.whatwg.org/multipage/microdata.html#encoding-microdata + }; + + # all possible attributes to `` elements. + # since not all of them apply to each `rel=` type, the separate implementations can pick from this collection + link-attrs = lib.mapAttrs (_name: value: mkOption value) { + href = { + # TODO: implement https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:attr-link-href-3 + # TODO: https://url.spec.whatwg.org/#valid-url-string + type = types.nonEmptyStr; + }; + media = { + # TODO: https://drafts.csswg.org/mediaqueries/#media + # it's awsome we have that standard, but ugh so much work + # ;..S + # Clay seems to do it right: https://github.com/sebastiaanvisser/clay + type = with types; nullOr str; + default = null; + }; + integrity = { + # TODO: implement https://w3c.github.io/webappsec-subresource-integrity/ + type = with types; nullOr str; + default = null; + }; + # TODO: more attributes + # https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:concept-element-attributes + }; + + # TODO: not sure where to put these, since so far they apply to multiple elements, + # but have the same properties for all of them + attrs = lib.mapAttrs (_name: value: mkOption value) { + # TODO: investigate: `href` may be coupled with other attributes such as `target` or `hreflang`, this could simplify things + href = { + # TODO: https://url.spec.whatwg.org/#valid-url-string + # ;..O + type = types.str; + }; + target = { + # https://html.spec.whatwg.org/multipage/document-sequences.html#valid-navigable-target-name-or-keyword + type = + let + is-valid-target = + s: + let + inherit (lib) match; + has-lt = s: match ".*<.*" s != null; + has-tab-or-newline = s: match ".*[\t\n].*" s != null; + has-valid-start = s: match "^[^_].*$" s != null; + in + has-valid-start s && !(has-lt s && has-tab-or-newline s); + in + with types; + either (enum [ + "_blank" + "_self" + "_parent" + "_top" + ]) (types.addCheck str is-valid-target); + }; + }; + + mkAttrs = + attrs: + with lib; + mkOption { + type = submodule { + options = global-attrs // attrs; + }; + default = { }; + }; + + print-attrs = + with lib; + attrs: + # TODO: figure out how let attributes know how to print themselves without polluting the interface + let + result = trim ( + join " " ( + mapAttrsToList + # TODO: this needs to be smarter for boolean attributes + # where the value must be written out explicitly. + # probably the attribute itself should have its own `__toString`. + ( + name: value: + if isBool value then + if value then name else "" + # TODO: some attributes must be explicitly empty + else + optionalString (toString value != "") ''${name}="${toString value}"'' + ) + attrs + ) + ); + in + if attrs == null then throw "wat" else optionalString (stringLength result > 0) " " + result; + + print-element = + name: attrs: content: + with lib; + # TODO: be smarter about content to save some space and repetition at the call sites + squash (trim '' + <${name}${print-attrs attrs}> + ${lib.indent " " content} + + ''); + + print-element' = name: attrs: "<${name}${print-attrs attrs}>"; + + toString-unwrap = + e: + with lib; + if isAttrs e then + toString (head (attrValues e)) + else if isList e then + toString (map toString-unwrap e) + else + e; + + elements = rec { + document = + { ... }: + { + imports = [ element ]; + options = { + inherit (element-types) html; + attrs = mkAttrs { }; + }; + + config.categories = [ ]; + config.__toString = self: '' + + ${self.html} + ''; + }; + + html = + { name, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + inherit (element-types) head body; + }; + + config.categories = [ ]; + config.__toString = + self: + print-element name self.attrs '' + ${self.head} + ${self.body} + ''; + }; + + head = + { name, ... }: + { + imports = [ element ]; + options = with lib; { + attrs = mkAttrs { }; + # https://html.spec.whatwg.org/multipage/semantics.html#the-head-element:concept-element-content-model + # XXX: this doesn't implement the iframe srcdoc semantics + # as those have questionable value and would complicate things a bit. + # it should be possible though, by passing a flag via module arguments. + inherit (element-types) title; + base = mkOption { + type = with types; nullOr (submodule base); + default = null; + }; + # https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset + meta.charset = mkOption { + # TODO: create programmatically from https://encoding.spec.whatwg.org/encodings.json + type = types.enum [ + "utf-8" + ]; + default = "utf-8"; + }; + # https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#viewport_width_and_screen_width + # this should not exist and no one should ever have to think about it + meta.viewport = mkOption { + type = submodule ( + { ... }: + { + # TODO: figure out how to render only non-default values + options = { + width = mkOption { + type = with types; either (ints.between 1 10000) (enum [ "device-width" ]); + default = "device-width"; # not default by standard + }; + height = mkOption { + type = with types; either (ints.between 1 10000) (enum [ "device-height" ]); + default = "device-height"; # not default by standard (but seems to work if you don't set it) + }; + initial-scale = mkOption { + type = types.numbers.between 0.1 10; + default = 1; + }; + minimum-scale = mkOption { + type = types.numbers.between 0.1 10; + # TODO: render only as many digits as needed + default = 0.1; + }; + maximum-scale = mkOption { + type = types.numbers.between 0.1 10; + default = 10; + }; + user-scalable = mkOption { + type = types.bool; + default = true; + }; + interactive-widget = mkOption { + type = types.enum [ + "resizes-visual" + "resizes-content" + "overlays-content" + ]; + default = "resizes-visual"; + }; + }; + } + ); + default = { }; + }; + + meta.authors = mkOption { + type = with types; listOf str; + default = [ ]; + }; + meta.description = mkOption { + type = with types; nullOr str; + default = null; + }; + # TODO: this one has more internal structure, e.g with hreflang + # TODO: print in output + link.canonical = mkOption { + type = with types; nullOr str; + default = null; + }; + link.stylesheets = mkOption { + type = types.listOf (submodule stylesheet); + default = [ ]; + }; + + # TODO: figure out `meta` elements + # https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element:concept-element-attributes + # https://html.spec.whatwg.org/multipage/semantics.html#other-metadata-names + }; + + config.categories = [ ]; + config.__toString = + self: + with lib; + print-element name self.attrs '' + ${self.title} + ${with lib; optionalString (!isNull self.base) self.base} + + + ${ + # https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-x-ua-compatible + "" + } + + + + + + ${print-element' "meta" { + name = "viewport"; + content = "${join ", " ( + mapAttrsToList (name: value: "${name}=${toString value}") self.meta.viewport + )}"; + }} + + ${join "\n" ( + map ( + author: + print-element' "meta" { + name = "author"; + content = "${author}"; + } + ) self.meta.authors + )} + + ${join "\n" ( + map ( + stylesheet: + print-element' "link" ( + { + rel = "stylesheet"; + } + // (removeAttrs stylesheet [ + "categories" + "__toString" + ]) + ) + ) self.link.stylesheets + )} + ''; + }; + + title = + { name, ... }: + { + imports = [ element ]; + options.attrs = mkAttrs { }; + options.text = mkOption { + type = types.str; + }; + config.categories = [ "metadata" ]; + config.__toString = self: "<${name}${print-attrs self.attrs}>${self.text}"; + + }; + + base = + { ... }: + { + imports = [ element ]; + # TODO: "A base element must have either an href attribute, a target attribute, or both." + options = global-attrs // { + inherit (attrs) href target; + }; + config.categories = [ "metadata" ]; + config.__toString = self: ""; + }; + + link = + { ... }: + { + imports = [ element ]; + options = global-attrs // { + # TODO: more attributes + # https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:concept-element-attributes + inherit (attrs) href; + # XXX: there are variants of `rel` for `link`, `a`/`area`, and `form` + rel = mkOption { + # https://html.spec.whatwg.org/multipage/semantics.html#attr-link-rel + type = + with types; + listOfUnique str (enum + # TODO: work out link types in detail, there are lots of additional constraints + # https://html.spec.whatwg.org/multipage/links.html#linkTypes + [ + "alternate" + "dns-prefetch" + "expect" + "help" + "icon" + "license" + "manifest" + "modulepreload" + "next" + "pingback" + "preconnect" + "prefetch" + "preload" + "prev" + "privacy-policy" + "search" + "terms-of-service" + ]); + }; + }; + # TODO: figure out how to make body-ok `link` elements + # https://html.spec.whatwg.org/multipage/semantics.html#allowed-in-the-body + config.categories = [ "metadata" ]; + config.__toString = self: ""; + }; + + # is implemented separately because it can be used both in `` and `` + # semantically it's a standalone thing but syntactically happens to be subsumed under `` + stylesheet = + { config, ... }: + { + imports = [ element ]; + options = global-attrs // { + type = mkOption { + # TODO: this must be a valid MIME type string, which is a bit involved. + # the syntax is explicated here: https://mimesniff.spec.whatwg.org/#mime-type-writing + # but the spec refers to RFC9110: https://www.rfc-editor.org/rfc/rfc9110#name-media-type + # all registered MIME types: https://www.iana.org/assignments/top-level-media-types/top-level-media-types.xhtml + # XXX: if nothing is specified, "text/css" is assumed. + # https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:link-type-stylesheet-2 + # there's no specification on what else could be there, and it's questionable whether setting anything else even makes sense. + # in practice, browsers seem to ignore anything but "text/css", so we may as well not care at all. + type = with types; nullOr str; + default = null; + }; + # https://html.spec.whatwg.org/multipage/semantics.html#attr-link-disabled + disabled = mkOption { + type = types.bool; + default = false; + }; + # TODO: implement the rest of the stylesheet attributes + # https://html.spec.whatwg.org/#link-type-stylesheet + inherit (link-attrs) href media integrity; + }; + # https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:body-ok + config.categories = [ + "metadata" + "phrasing" + ]; + config.__toString = + self: + print-attrs ( + removeAttrs self [ + "categories" + "__toString" + ] + ); + }; + + body = + { config, name, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + content = mkOption { + type = + with types; + let + # Type check that ensures spec-compliant section hierarchy + # https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines-2:concept-heading-7 + with-section-constraints = + baseType: + baseType + // { + merge = + loc: defs: + with lib; + let + find-and-attach = + def: + let + process-with-depth = + depth: content: + map ( + x: + if isAttrs x && x ? section then + x + // { + section = x.section // { + heading-level = depth; + content = process-with-depth (depth + 1) (x.section.content or [ ]); + }; + } + else + x + ) content; + + find-with-depth = + depth: content: + let + sections = map (v: { + inherit (def) file; + value = v; + depth = depth; + }) (filter (x: isAttrs x && x ? section) content); + subsections = concatMap ( + x: + if isAttrs x && x ? section && x.section ? content then + find-with-depth (depth + 1) x.section.content + else + [ ] + ) content; + in + sections ++ subsections; + + in + { + inherit def; + processed = process-with-depth 1 def.value; + validation = find-with-depth 1 def.value; + }; + + processed = map find-and-attach defs; + all-sections = flatten (map (p: p.validation) processed); + too-deep = filter (sec: sec.depth > 6) all-sections; + in + if too-deep != [ ] then + throw '' + The option `${lib.options.showOption loc}` has sections nested too deeply: + ${concatMapStrings ( + sec: " - depth ${toString sec.depth} section in ${toString sec.file}\n" + ) too-deep} + Section hierarchy must not be deeper than 6 levels.'' + else + baseType.merge loc (map (p: p.def // { value = p.processed; }) processed); + }; + in + with-section-constraints + # TODO: find a reasonable cut-off for where to place raw content + (listOf (either str (attrTag categories.flow))); + default = [ ]; + }; + }; + + config.categories = [ ]; + config.__toString = + self: with lib; print-element name self.attrs (join "\n" (map toString-unwrap self.content)); + }; + + section = + { config, name, ... }: + { + imports = [ element ]; + options = { + # setting to an attribute set will wrap the section in `
` + attrs = mkOption { + type = + with types; + nullOr (submodule { + options = global-attrs; + }); + default = null; + }; + heading = mkOption { + # XXX: while there are no explicit rules on whether sections should contain headers, + # sections should have content that would be listed in an outline. + # + # https://html.spec.whatwg.org/multipage/sections.html#use-div-for-wrappers + # + # such an outline is rather meaningless without headings for navigation, + # which is why we enforce headings in sections. + # arguably, and this is encoded here, a section *is defined* by its heading. + type = + with types; + submodule ( + { config, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + # setting to an attribute set will wrap the section in `
` + hgroup.attrs = mkOption { + type = + with types; + nullOr (submodule { + options = global-attrs; + }); + default = with lib; if (config.before == [ ] && config.after == [ ]) then null else { }; + }; + # https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element + before = mkOption { + type = with types; listOf (attrTag ({ inherit (element-types) p; } // categories.scripting)); + default = [ ]; + }; + content = mkOption { + # https://html.spec.whatwg.org/multipage/sections.html#the-h1,-h2,-h3,-h4,-h5,-and-h6-elements + type = with types; either str (listOf (attrTag categories.phrasing)); + }; + after = mkOption { + type = with types; listOf (attrTag ({ inherit (element-types) p; } // categories.scripting)); + default = [ ]; + }; + }; + } + ); + }; + # https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines + content = mkOption { + type = with types; listOf (either str (attrTag categories.flow)); + default = [ ]; + }; + }; + options.heading-level = mkOption { + # XXX: this will proudly fail if the invariant is violated, + # but the error message will be inscrutable + type = with types; ints.between 1 6; + internal = true; + }; + config = { + categories = [ + "flow" + "sectioning" + "palpable" + ]; + __toString = + self: + with lib; + let + n = toString config.heading-level; + heading = ''${self.heading.content}''; + hgroup = + with lib; + print-element "hgroup" self.heading.hgroup.attrs (squash '' + ${optionalString (!isNull self.heading.before) (toString-unwrap self.heading.before)} + ${heading} + ${optionalString (!isNull self.heading.after) (toString-unwrap self.heading.after)} + ''); + content = + (if isNull self.heading.hgroup.attrs then heading else hgroup) + + join "\n" (map toString-unwrap self.content); + in + if !isNull self.attrs then print-element name self.attrs content else content; + }; + }; + + p = + { name, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + content = mkOption { + type = with types; either str (listOf (attrTag categories.phrasing)); + }; + }; + config.categories = [ + "flow" + "palpable" + ]; + config.__toString = self: print-element name self.attrs (toString self.content); + }; + + dl = + { config, name, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + content = mkOption { + type = + with types; + listOf ( + submodule ( + { ... }: + { + options = { + # TODO: wrap in `
` if set + div.attrs = mkOption { + type = + with types; + nullOr (submodule { + options = global-attrs; + }); + default = null; + }; + before = mkOption { + type = with types; listOf (attrTag categories.scripting); + default = [ ]; + }; + terms = mkOption { + type = with types; nonEmptyListOf (submodule dt); + }; + between = mkOption { + type = with types; listOf (attrTag categories.scripting); + default = [ ]; + }; + descriptions = mkOption { + type = with types; nonEmptyListOf (submodule dd); + }; + after = mkOption { + type = with types; listOf (attrTag categories.scripting); + default = [ ]; + }; + }; + } + ) + ); + }; + }; + # XXX: here we can't express the spec requirement that `dl` is palpable if the list of term-description-pairs is nonempty. + # the reason is that we have to specify a child's *type* in the parent, but being palpable is a property of the value in this case. + # and while the module system does have some dependent typing capabilities, we can't say "the type is X but only if its value has property Y". + # but since the "palpable" category isn't used in any structural requirement in the spec, this is not a loss of fidelity on our side. + # TODO: the whole notion of content categories may be a red herring for this implementation after all, reconsider it. + # it does help to concisely express type constraints on an element's children, but it seems that most of the categories in the spec can be ignored entirely in this implementation. + # the cleanup task would be to identify which categories are really helpful, and document the rationale for using that mechanism as well as the specific choice of categories to keep. + config.categories = [ "flow" ]; + config.__toString = + self: + with lib; + let + content = map ( + entry: + let + list = squash '' + ${join "\n" entry.before} + ${join "\n" entry.terms} + ${join "\n" entry.between} + ${join "\n" entry.descriptions} + ${join "\n" entry.after} + ''; + in + if !isNull entry.div.attrs then print-element "div" entry.div.attrs list else list + ) self.content; + in + print-element name self.attrs (join "\n" content); + }; + + dt = + { config, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + dt = mkOption { + type = + with types; + either str ( + submodule ( + attrTag ( + # TODO: test + with lib; + removeAttrs + (filterAttrs ( + _name: value: + !any ( + c: + elem c [ + "sectioning" + "heading" + ] + ) value.categories + ) categories.flow) + [ + "header" + "footer" + ] + ) + ) + ); + }; + }; + config.categories = [ ]; + config.__toString = self: print-element "dt" self.attrs self.dt; + }; + + dd = + { config, ... }: + { + imports = [ element ]; + options = { + attrs = mkAttrs { }; + dd = mkOption { + type = with types; either str (submodule (attrTag categories.flow)); + }; + }; + config.categories = [ ]; + config.__toString = self: print-element "dd" self.attrs self.dd; + }; + }; +in +{ + imports = [ element ]; + options = { + inherit (element-types) html; + }; + + config.categories = [ ]; + config.__toString = self: '' + + ${self.html} + ''; +} diff --git a/presentation/favicon.png b/presentation/favicon.png new file mode 100644 index 0000000..2bf02a9 Binary files /dev/null and b/presentation/favicon.png differ diff --git a/presentation/ngi-fediversity.svg b/presentation/ngi-fediversity.svg new file mode 100644 index 0000000..8640e92 --- /dev/null +++ b/presentation/ngi-fediversity.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/style.css b/presentation/style.css new file mode 100644 index 0000000..2bb22d5 --- /dev/null +++ b/presentation/style.css @@ -0,0 +1,222 @@ +:root { + /* XXX: maybe use light-dark() once it's more widely supported */ + color-scheme: light dark; + --highlight: rgb(255, 110, 0); + --background: white; + --text-color: black; + --shadow: rgba(0,0,0,0.1); + scrollbar-gutter: stable; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: black; + --text-color: #f0f0f0; + --shadow: rgba(255,255,255,0.2); + } +} + +body { + font-family: Heebo, sans-serif; + color: var(--text-color); + background-color: var(--background); + padding: 1em; +} + +section { + max-width: 50em; + margin: auto; + margin-top: 1em; +} + +h1, h2, h3, h4, h5, h6 { + font-family: Signika, sans-serif; + margin-top: 0; +} + +h1 { + font-size: 2em; +} + +header > nav { + font-family: Signika, sans-serif; + margin-bottom: 2em; +} + +a:visited, +a +{ + color: var(--highlight); + text-decoration: none; +} + +header a:visited, +header a +{ + color: var(--text-color); +} + +header a { + text-decoration: none; +} + +header a:hover, +header li:hover +{ + color: var(--highlight); +} + +header nav ul { + padding: 0; +} + +header > nav ul li { + list-style-type: none; +} + +header > nav > ul { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 60em; + margin: auto; +} + +header > nav > ul > li:first-child a { + content: url('ngi-fediversity.svg'); + display: inline-block; + height: 2em; + margin-right: 5vw; +} + +header > nav > ul > li:last-child > a { + border: 1pt solid var(--text-color); + padding: 0.5em 1em 0.5em 1em; + border-radius: 3pt; + margin-left: 5vw; +} + +header > nav > ul > li:last-child:hover > a { + color: var(--background); + background-color: var(--highlight); + border-color: var(--background); +} + +header > nav > ul > li > details > nav { + position: absolute; + /*top: 2em;*/ + background: var(--background); + min-width: max-content; + margin-top: 1em; + padding: 1em; + box-shadow: 0 0 1em var(--shadow); + z-index: 0; +} + +header > nav > ul > li > details { + display: block; + /*padding: 1em 0;*/ + cursor: pointer; +} + +header > nav > ul > li > details[open] > summary { + color: var(--highlight); +} + +header > nav > ul > li > details > nav ul li { + padding: 0.25em 0; +} + +#menu-toggle, +#menu-toggle + label { + display: none; + appearance: none; +} + +@media (max-width: 50em) { + #menu-toggle:checked ~ nav > ul > li { + display: block; + } + + #menu-toggle ~ label { + position: absolute; + right: 0; + padding: 0.5em; + cursor: pointer; + display: block; + } + + .menu-close, + .menu-open { + cursor: pointer; + } + .menu-close { display: none; } + #menu-toggle:checked + label .menu-close { display: block; } + #menu-toggle:checked + label .menu-open { display: none; } + + header > nav { + margin-bottom: 1em; + }; + + header > nav > ul > li:not(:first-child) { + display: none; + } + + header > nav > ul { + flex-direction: column; + } + + header > nav > ul > li { + margin: 0; + padding: 0; + text-align: center; + font-size: 1.3em; + } + + header > nav > ul > li > details{ + /* compensate for collapse triangle */ + margin-left: -1rem; + } + + header > nav > ul > li > details > nav { + position: relative; + margin: 0; + padding: 0 0 0.5em 0; + box-shadow: none; + /* compensate back for container's collapse triangle compensation */ + margin-left: 1rem; + } + + header > nav > ul > li > details > nav ul li { + padding: 0; + font-size: 1.15rem; + } + + header > nav > ul::before { + content: ""; + display: flex; + justify-content: space-between; + align-items: center; + } + + header > nav > ul > li:first-child { + display: block; + } + + header > nav > ul > li:first-child a { + margin: 0 0 0.5em 0; + height: 2.5em; + } + + header > nav > ul > li:last-child { + margin: 1em 0 0 0; + } + + header > nav > ul > li:last-child a { + margin: 0; + } + + header { + position: relative; + } +} diff --git a/presentation/style.nix b/presentation/style.nix new file mode 100644 index 0000000..df65ff3 --- /dev/null +++ b/presentation/style.nix @@ -0,0 +1,82 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + config.assets."style.css".path = ./style.css; + config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg; + # TODO: auto-generate a bunch from SVG + config.assets."favicon.png".path = ./favicon.png; + config.assets."fonts.css".path = + with lib; + builtins.toFile "fonts.css" ( + join "\n" ( + map + (font: '' + @font-face { + font-family: '${font.name}'; + font-style: normal; + font-weight: ${toString font.weight}; + src: url(/${head config.assets.${font.file}.locations}) format('woff2'); + } + '') + ( + (crossLists (name: file: weight: { inherit name file weight; }) [ + [ "Signika" ] + [ + "signika-extended.woff2" + "signika.woff2" + ] + [ + 500 + 700 + ] + ]) + ++ (crossLists (name: file: weight: { inherit name file weight; }) [ + [ "Heebo" ] + [ + "heebo-extended.woff2" + "heebo.woff2" + ] + [ + 400 + 600 + ] + ]) + ) + ) + ); + + # TODO: get directly from https://github.com/google/fonts + # and compress with https://github.com/fonttools/fonttools + config.assets."signika-extended.woff2" = { + path = pkgs.fetchurl { + url = "https://fonts.gstatic.com/s/signika/v25/vEFO2_JTCgwQ5ejvMV0Ox_Kg1UwJ0tKfX6bOjM7sfA.woff2"; + hash = "sha256-6xM7cHYlTKNf1b0gpqhPJjwOoZfxx9+u1e4JPYG2lKk="; + name = "signika-extended.woff2"; + }; + }; + config.assets."signika.woff2" = { + path = pkgs.fetchurl { + url = "https://fonts.gstatic.com/s/signika/v25/vEFO2_JTCgwQ5ejvMV0Ox_Kg1UwJ0tKfX6bBjM4.woff2"; + hash = "sha256-Yu0kGT3seb8Qtulu84wvY6nLyPXsRBO/JvTD2BQBtHg="; + name = "signika.woff2"; + }; + }; + config.assets."heebo-extended.woff2" = { + path = pkgs.fetchurl { + url = "https://fonts.gstatic.com/s/heebo/v26/NGS6v5_NC0k9P9H2TbE.woff2"; + hash = "sha256-lk3+fFEqYWbHHGyXkdhKnOOMGS9m5ZbbxQcRQCSlxDE="; + name = "heebo-extended.woff2"; + }; + }; + config.assets."heebo.woff2" = { + path = pkgs.fetchurl { + url = "https://fonts.gstatic.com/s/heebo/v26/NGS6v5_NC0k9P9H4TbFzsQ.woff2"; + hash = "sha256-JWnjYlbcNsg6KCJnRRjECL2HnZGJOBTMtdutWBNza4Q="; + name = "heebo.woff2"; + }; + }; +} diff --git a/presentation/templates.nix b/presentation/templates.nix new file mode 100644 index 0000000..4fcd88b --- /dev/null +++ b/presentation/templates.nix @@ -0,0 +1,87 @@ +{ + config, + lib, + pkgs, + ... +}: + +{ + config.templates.html = { + dom = + document: + let + eval = lib.evalModules { + class = "DOM"; + modules = [ + document + (import ./dom.nix) + ]; + }; + in + { + __toString = _: toString eval.config; + value = eval.config; + }; + + markdown = + { name, body }: + let + commonmark = + pkgs.runCommand "${name}.html" + { + buildInputs = [ pkgs.cmark ]; + } + '' + cmark ${builtins.toFile "${name}.md" body} > $out + ''; + in + builtins.readFile commonmark; + nav = + { menu, page }: + let + render-item = + item: + if item ? menu then + '' +
  • ${item.menu.label} + ${lib.indent " " (item.menu.outputs.html page)} +
  • + '' + else if item ? page then + ''
  • ${item.page.title}
  • '' + else + ''
  • ${item.link.label}
  • ''; + in + '' + + ''; + }; + + config.templates.files = + fs: + with lib; + foldl' + # TODO: create static redirects from `tail .locations` + ( + acc: elem: + acc + // + (mapAttrs' ( + type: value: { + name = head elem.locations + optionalString (type != "") ".${type}"; + value = + if isStorePath value then + value + else + builtins.toFile (elem.name + optionalString (type != "") ".${type}") (toString value); + } + )) + elem.outputs + ) + { } + fs; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a6bdf20 --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./. { }).shell diff --git a/structure/article.nix b/structure/article.nix new file mode 100644 index 0000000..68694a5 --- /dev/null +++ b/structure/article.nix @@ -0,0 +1,62 @@ +{ + config, + options, + lib, + ... +}: +let + inherit (lib) + mkOption + types + ; + cfg = config; +in +{ + content-types.article = + { config, collection, ... }: + { + imports = [ cfg.content-types.page ]; + options = { + collection = mkOption { + description = "Collection this article belongs to"; + type = options.collections.type.nestedTypes.elemType; + default = collection; + }; + date = mkOption { + description = "Publication date"; + type = with types; str; + default = null; + }; + author = mkOption { + description = "Page author"; + type = with types; either str (nonEmptyListOf str); + default = null; + }; + }; + config.name = with lib; mkDefault (slug config.title); + config.outputs.html = lib.mkForce ( + (cfg.templates.html.page config).override ( + _final: prev: { + html = { + # TODO: make authors always a list + head.meta.authors = if lib.isList config.author then config.author else [ config.author ]; + body.content = + with lib; + map ( + e: + if isAttrs e && e ? section then + recursiveUpdate e { + section.heading = { + before = [ { p.content = "Published ${config.date}"; } ]; + after = [ { p.content = "Written by ${config.author}"; } ]; + }; + } + else + e + ) prev.html.body.content; + }; + } + ) + ); + }; +} diff --git a/structure/assets.nix b/structure/assets.nix new file mode 100644 index 0000000..a30e5f2 --- /dev/null +++ b/structure/assets.nix @@ -0,0 +1,44 @@ +{ config, lib, ... }: +let + inherit (lib) + mkOption + types + ; + cfg = config; +in +{ + options.assets = mkOption { + description = '' + Collection of assets, i.e. static files that can be linked to from within documents + ''; + type = + with types; + attrsOf ( + submodule ( + { config, ... }: + { + imports = [ cfg.content-types.document ]; + options.path = mkOption { + type = types.path; + }; + config.outputs."" = if lib.isStorePath config.path then config.path else "${config.path}"; + } + ) + ); + default = { }; + }; + + config.files = + with lib; + let + flatten = + attrs: + mapAttrsToList ( + _name: value: + # HACK: we somehow have to distinguish a module value from regular attributes. + # arbitrary choice: the outputs attribute + if value ? outputs then value else mapAttrsToList value + ) attrs; + in + cfg.templates.files (flatten cfg.assets); +} diff --git a/structure/collections.nix b/structure/collections.nix new file mode 100644 index 0000000..4eb32db --- /dev/null +++ b/structure/collections.nix @@ -0,0 +1,99 @@ +{ + config, + options, + lib, + ... +}: + +let + inherit (lib) + mkOption + types + ; + cfg = config; +in + +{ + options.collections = mkOption { + description = '' + 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, ... }: + { + options = { + type = mkOption { + description = "Type of entries in the collection"; + type = types.deferredModule; + }; + name = mkOption { + description = "Symbolic name, used as a human-readable identifier"; + type = types.str; + default = name; + }; + prefixes = mkOption { + description = '' + List of historic output locations for files in the collection + + The first element is the canonical location. + All other elements are used to create redirects to the canonical location. + + The default entry is the symbolic name of the collection. + When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden. + ''; + type = with types; nonEmptyListOf str; + example = [ "." ]; + default = [ config.name ]; + }; + entry = mkOption { + description = "An entry in the collection"; + type = + with types; + collection (submodule ({ + imports = [ config.type ]; + _module.args.collection = config; + process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls; + })); + }; + by-name = mkOption { + description = "Entries accessible by symbolic name"; + type = with types; attrsOf attrs; + default = + with lib; + listToAttrs ( + map (e: { + name = e.name; + value = e; + }) config.entry + ); + }; + }; + } + ) + ); + }; + + config.files = + with lib; + let + collections = concatMap (collection: collection.entry) (attrValues config.collections); + in + cfg.templates.files collections; +} diff --git a/structure/default.nix b/structure/default.nix new file mode 100644 index 0000000..225a912 --- /dev/null +++ b/structure/default.nix @@ -0,0 +1,98 @@ +{ + config, + options, + lib, + ... +}: +let + inherit (lib) + mkOption + types + ; +in +{ + imports = lib.nixFiles ./.; + + options.content-types = mkOption { + description = "Content types"; + type = with types; attrsOf deferredModule; + }; + + config.content-types.document = + { + name, + config, + options, + link, + ... + }: + { + config._module.args.link = config.link; + options = { + name = mkOption { + description = "Symbolic name, used as a human-readable identifier"; + type = types.str; + default = name; + }; + locations = mkOption { + description = '' + List of historic output locations for the resulting file + + Elements are relative paths to output files, without suffix. + The suffix will be added depending on output file type. + + The first element is the canonical location. + All other elements are used to create redirects to the canonical location. + + The default entry is the symbolic name of the document. + When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden. + ''; + type = with types; nonEmptyListOf str; + apply = config.process-locations; + example = [ + "about/overview" + "index" + ]; + default = [ config.name ]; + }; + process-locations = mkOption { + description = "Function to post-process the output locations of contained document"; + type = types.functionTo options.locations.type; + default = lib.id; + }; + link = mkOption { + description = "Helper function for transparent linking to other pages"; + type = with types; functionTo attrs; + # TODO: we may want links to other representations, + # and currently the mapping of output types to output file + # names is soft. + default = + with lib; + target: + let + path = relativePath (head config.locations) (head target.locations); + links = mapAttrs ( + type: _output: path + optionalString (type != "") ".${type}" + # ^^^^^^^^^^^^ + # convention for raw files + ) target.outputs; + in + if length (attrValues links) == 0 then + throw "no output to link to for '${target.name}'" + else if length (attrValues links) == 1 then + links + // { + __toString = _: head (attrValues links); + } + else + links; + }; + outputs = mkOption { + description = '' + Representations of the document in different formats + ''; + type = with types; attrsOf (either attrs pathInStore); + }; + }; + }; +} diff --git a/structure/event.nix b/structure/event.nix new file mode 100644 index 0000000..2d72904 --- /dev/null +++ b/structure/event.nix @@ -0,0 +1,96 @@ +{ + config, + options, + lib, + ... +}: +let + inherit (lib) + mkOption + types + ; + cfg = config; +in +{ + content-types.event = + { config, collection, ... }: + { + imports = [ cfg.content-types.page ]; + options = { + collection = mkOption { + description = "Collection this event belongs to"; + type = options.collections.type.nestedTypes.elemType; + default = collection; + }; + start-date = mkOption { + description = "Start date of the event"; + type = with types; str; + }; + start-time = mkOption { + description = "Start time of the event"; + type = with types; str; + default = null; + }; + end-date = mkOption { + description = "End date of the event"; + type = with types; str; + default = null; + }; + end-time = mkOption { + description = "End time of the event"; + type = with types; str; + default = null; + }; + location = mkOption { + description = "Location of the event"; + type = with types; str; + }; + }; + config.name = with lib; mkDefault (slug config.title); + config.summary = lib.mkDefault config.description; + config.outputs.html = lib.mkForce ( + (cfg.templates.html.page config).override ( + _final: prev: { + html.body.content = + with lib; + map ( + e: + if isAttrs e && e ? section then + recursiveUpdate e { + section.content = [ + { + dl.content = + [ + { + terms = [ { dt = "Location"; } ]; + descriptions = [ { dd = config.location; } ]; + } + { + terms = [ { dt = "Start"; } ]; + descriptions = [ + { + dd = config.start-date + lib.optionalString (!isNull config.start-time) " ${config.start-time}"; + } + ]; + } + ] + ++ lib.optional (!isNull config.end-date) { + terms = [ { dt = "End"; } ]; + descriptions = [ + { + dd = config.end-date + lib.optionalString (!isNull config.end-time) " ${config.end-time}"; + } + ]; + }; + } + ] ++ e.section.content; + } + else + e + ) prev.html.body.content; + + } + ) + ); + }; +} diff --git a/structure/navigation.nix b/structure/navigation.nix new file mode 100644 index 0000000..80112e7 --- /dev/null +++ b/structure/navigation.nix @@ -0,0 +1,88 @@ +{ + config, + options, + lib, + ... +}: +let + inherit (lib) + mkOption + types + ; + cfg = config; + subtype = + baseModule: + types.submodule [ + baseModule + { + _module.freeformType = types.attrs; + # XXX: this is supposed to be used with a finished value, + # and we don't want to process locations again. + process-locations = lib.mkForce lib.id; + } + ]; +in +{ + options.menus = mkOption { + description = '' + Collection navigation menus + ''; + type = with types; attrsOf (submodule config.content-types.navigation); + }; + + config.content-types.named-link = + { ... }: + { + options = { + label = mkOption { + description = "Link label"; + type = types.str; + }; + url = mkOption { + description = "Link URL"; + type = types.str; + }; + }; + }; + + config.content-types.navigation = + { name, config, ... }: + { + 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 = subtype cfg.content-types.page; }; + link = mkOption { type = submodule cfg.content-types.named-link; }; + }); + }; + outputs = mkOption { + description = '' + Representations of the navigation structure in different formats + + It must be a function that takes the page on which the navigation is to be shown, such that relative links get computed correctly. + ''; + type = with types; attrsOf (functionTo str); + default.html = + page: + cfg.templates.html.nav { + menu = config; + inherit page; + }; + }; + }; + }; +} diff --git a/structure/page.nix b/structure/page.nix new file mode 100644 index 0000000..14ff619 --- /dev/null +++ b/structure/page.nix @@ -0,0 +1,92 @@ +{ config, lib, ... }: +let + inherit (lib) + mkOption + types + ; + cfg = config; +in +{ + # TODO: enable i18n, e.g. via a nested attribute for language-specific content + options.pages = mkOption { + description = '' + Collection of pages on the site + ''; + type = with types; attrsOf (submodule config.content-types.page); + }; + + config.files = with lib; cfg.templates.files (attrValues config.pages); + + config.content-types.page = + { name, config, ... }: + { + imports = [ cfg.content-types.document ]; + options = { + title = mkOption { + description = "Page title"; + type = types.str; + default = name; + }; + 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; + }; + }; + + config.outputs.html = cfg.templates.html.page config; + }; + + config.templates.html.page = lib.template cfg.templates.html.dom (page: { + html = { + head = { + title.text = page.title; + meta.description = page.description; + link.canonical = lib.head page.locations; + link.stylesheets = [ + # TODO: allow enabling preload with a flag + { href = "${page.link cfg.assets."style.css"}"; } + { href = "${page.link cfg.assets."fonts.css"}"; } + ]; + }; + body.content = [ + '' +
    + + + ${lib.indent " " (cfg.menus.main.outputs.html page)} +
    + '' + { + section = { + attrs = { }; + heading.content = page.title; + content = [ + (cfg.templates.html.markdown { inherit (page) name body; }) + ]; + }; + } + ]; + }; + }); +} diff --git a/tests.nix b/tests.nix new file mode 100644 index 0000000..00f32bb --- /dev/null +++ b/tests.nix @@ -0,0 +1,42 @@ +# tests written for running with `nix-unit` +# https://github.com/nix-community/nix-unit +let + inherit (import ./. { }) lib; +in +{ + test-relativePath = + with lib; + let + testData = [ + { + from = "bar"; + to = "baz"; + expected = "./baz"; + } + { + from = "foo/bar"; + to = "foo/baz"; + expected = "./baz"; + } + { + from = "foo"; + to = "bar/baz"; + expected = "./bar/baz"; + } + { + from = "foo/bar"; + to = "baz"; + expected = "./../baz"; + } + { + from = "foo/bar/baz"; + to = "foo"; + expected = "./../../foo"; + } + ]; + in + { + expr = map (case: relativePath case.from case.to) testData; + expected = map (case: case.expected) testData; + }; +}